Actualizaciones varias: mejoras en cuenta, sincronización, UI

This commit is contained in:
2026-07-05 10:45:44 +02:00
parent 8799633447
commit 4569c174f0
195 changed files with 32571 additions and 20694 deletions
+4
View File
@@ -1,5 +1,6 @@
#include "accountsetupdialoglauncher.h"
#include <QDebug>
#include "services/accountservice.h"
AccountSetupDialogLauncher::AccountSetupDialogLauncher(QObject *parent)
: QObject(parent)
@@ -52,6 +53,9 @@ void AccountSetupDialogLauncher::initializeSynchronizer(const Account &account)
case AccountType::IMAP:
providerType = "imap";
break;
case AccountType::POP3:
providerType = "pop3";
break;
default:
providerType = "imap";
break;
+3 -1
View File
@@ -6,7 +6,8 @@ MailItem::MailItem(qint64 id, int folderId, const QString& subject, const QStrin
const QVector<QString>& attachments,
const QString& fileId,
qint64 size,
const QString& messageId)
const QString& messageId,
qint64 uid)
: m_id(id)
, m_folderId(folderId)
, m_subject(subject)
@@ -19,5 +20,6 @@ MailItem::MailItem(qint64 id, int folderId, const QString& subject, const QStrin
, m_fileId(fileId)
, m_size(size)
, m_messageId(messageId)
, m_uid(uid)
{
}
+11 -6
View File
@@ -14,7 +14,8 @@ public:
const QVector<QString>& attachments = QVector<QString>(),
const QString& fileId = QString(),
qint64 size = 0,
const QString& messageId = QString());
const QString& messageId = QString(),
qint64 uid = 0);
qint64 id() const { return m_id; }
void setId(qint64 id) { m_id = id; }
@@ -51,16 +52,19 @@ public:
QString messageId() const { return m_messageId; }
void setMessageId(const QString& messageId) { m_messageId = messageId; }
QString bodyHtml() const { return m_bodyHtml; }
qint64 uid() const { return m_uid; }
void setUid(qint64 uid) { m_uid = uid; }
QString bodyHtml() const { return m_bodyHtml; }
void setBodyHtml(const QString& body) { m_bodyHtml = body; }
QString to() const { return m_to; }
void setTo(const QString& to) { m_to = to; }
QString cc() const { return m_cc; }
void setCc(const QString& cc) { m_cc = cc; }
QString bcc() const { return m_bcc; }
void setBcc(const QString& bcc) { m_bcc = bcc; }
@@ -77,6 +81,7 @@ private:
QString m_fileId;
qint64 m_size{0};
QString m_messageId;
qint64 m_uid{0};
QString m_bodyHtml;
QString m_to;
QString m_cc;
+32 -2
View File
@@ -1,14 +1,44 @@
#include "account.h"
Account::Account(int id, const QString& email, const QString& displayName,
const QString& signature,
AccountType type, const QString& accessToken,
const QString& refreshToken, const QDateTime& tokenExpires)
const QString& refreshToken,
const QDateTime& tokenExpires)
: m_id(id)
, m_email(email)
, m_displayName(displayName)
, m_signature(signature)
, m_type(type)
, m_accessToken(accessToken)
, m_refreshToken(refreshToken)
, m_tokenExpires(tokenExpires)
{
}
}
QString Account::encryptPassword(const QString& plainText)
{
// Simple XOR encryption with a fixed key, then base64 encode
// Note: This is for obfuscation only, not strong security.
static const char keyHex[] = "0123456789ABCDEF"; // 16-byte key
QByteArray key;
bool ok;
for (int i = 0; i < 16; ++i) {
char c = QString(keyHex + i * 2, 2).toInt(&ok, 16);
if (!ok) c = 0;
key.append(c);
}
QByteArray input = plainText.toUtf8();
QByteArray result(input.size(), 0);
for (int i = 0; i < input.size(); ++i) {
result[i] = input[i] ^ key[i % key.size()];
}
return result.toBase64();
}
QString Account::decryptPassword(const QString& encrypted)
{
// Same as encryption since XOR is symmetric
return encryptPassword(encrypted);
}
+32 -3
View File
@@ -2,18 +2,21 @@
#include <QString>
#include <QDateTime>
#include <QtGlobal>
#include <QByteArray>
enum class AccountType {
POP3,
Outlook,
Gmail,
IMAP
};
class Account
{
class Account {
public:
Account() = default;
Account(int id, const QString& email, const QString& displayName,
const QString& signature,
AccountType type, const QString& accessToken = QString(),
const QString& refreshToken = QString(),
const QDateTime& tokenExpires = QDateTime());
@@ -27,6 +30,9 @@ public:
QString displayName() const { return m_displayName; }
void setDisplayName(const QString& displayName) { m_displayName = displayName; }
QString signature() const { return m_signature; }
void setSignature(const QString& signature) { m_signature = signature; }
AccountType type() const { return m_type; }
void setType(AccountType type) { m_type = type; }
@@ -44,12 +50,35 @@ public:
m_tokenExpires > QDateTime::currentDateTimeUtc();
}
/// Connection settings for incoming and outgoing servers
struct ConnectionSettings {
QString type; // "imap" or "pop3"
QString incomingHost;
quint16 incomingPort = 0;
bool incomingSsl = false;
QString outgoingHost;
quint16 outgoingPort = 0;
bool outgoingSsl = false;
QString username;
QString password;
QString authMethod; // "plain" or "oauth2"
};
const ConnectionSettings& connectionSettings() const { return m_connectionSettings; }
void setConnectionSettings(const ConnectionSettings& settings) { m_connectionSettings = settings; }
/// Simple encryption/decryption for passwords (XOR with fixed key, base64 encoded)
static QString encryptPassword(const QString& plainText);
static QString decryptPassword(const QString& encrypted);
private:
int m_id{0};
QString m_email;
QString m_displayName;
QString m_signature;
AccountType m_type{AccountType::IMAP};
QString m_accessToken;
QString m_refreshToken;
QDateTime m_tokenExpires;
};
ConnectionSettings m_connectionSettings;
};
+3
View File
@@ -2,6 +2,7 @@
#include "services/gmail/gmailsynchronizer.h"
#include "services/outlook/outlooksynchronizer.h"
#include "services/imap/imapsynchronizer.h"
#include "services/pop3/pop3synchronizer.h"
#include <QDebug>
SynchronizerProvider::SynchronizerProvider(QObject *parent)
@@ -52,6 +53,8 @@ Synchronizer* SynchronizerProvider::createSynchronizer(const QString &accountId,
sync = new OutlookSynchronizer(this);
} else if (providerType == "imap") {
sync = new ImapSynchronizer(this);
} else if (providerType == "pop3" || providerType == "pop") {
sync = new Pop3Synchronizer(this);
} else {
qWarning() << "[SynchronizerProvider] Unknown provider type:" << providerType;
return nullptr;
+108 -30
View File
@@ -1,28 +1,40 @@
#include "accountdao.h"
bool AccountDao::insert(const Account& account)
qint64 AccountDao::insert(const Account& account)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"INSERT INTO Account (email, displayName, type, accessToken, refreshToken, tokenExpires) "
"VALUES (:email, :displayName, :type, :accessToken, :refreshToken, :tokenExpires)"
);
"INSERT INTO Account (email, displayName, signature, type, accessToken, refreshToken, tokenExpires, "
"incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, "
"connUsername, connPassword, authMethod) "
"VALUES (:email, :displayName, :type, :accessToken, :refreshToken, :tokenExpires, "
":incomingHost, :incomingPort, :incomingSsl, :outgoingHost, :outgoingPort, :outgoingSsl, "
":connUsername, :connPassword, :authMethod)");
query.bindValue(":email", account.email());
query.bindValue(":displayName", account.displayName());
query.bindValue(":signature", account.signature());
query.bindValue(":type", static_cast<int>(account.type()));
query.bindValue(":accessToken", account.accessToken());
query.bindValue(":refreshToken", account.refreshToken());
query.bindValue(":tokenExpires", account.tokenExpires());
const auto& settings = account.connectionSettings();
query.bindValue(":incomingHost", settings.incomingHost);
query.bindValue(":incomingPort", settings.incomingPort);
query.bindValue(":incomingSsl", settings.incomingSsl ? 1 : 0);
query.bindValue(":outgoingHost", settings.outgoingHost);
query.bindValue(":outgoingPort", settings.outgoingPort);
query.bindValue(":outgoingSsl", settings.outgoingSsl ? 1 : 0);
query.bindValue(":connUsername", settings.username);
query.bindValue(":connPassword", Account::encryptPassword(settings.password));
query.bindValue(":authMethod", settings.authMethod);
if (!query.exec()) {
qWarning() << "Failed to insert account:" << query.lastError().text();
return false;
return -1;
}
// Optionally set the id on the account object if needed (pass by reference?)
// Since account is const, we cannot modify it. Caller can retrieve lastInsertId.
return true;
return query.lastInsertId().toLongLong();
}
bool AccountDao::update(const Account& account)
@@ -36,16 +48,34 @@ bool AccountDao::update(const Account& account)
"type = :type, "
"accessToken = :accessToken, "
"refreshToken = :refreshToken, "
"tokenExpires = :tokenExpires "
"WHERE id = :id"
);
"tokenExpires = :tokenExpires, "
"incomingHost = :incomingHost, "
"incomingPort = :incomingPort, "
"incomingSsl = :incomingSsl, "
"outgoingHost = :outgoingHost, "
"outgoingPort = :outgoingPort, "
"outgoingSsl = :outgoingSsl, "
"connUsername = :connUsername, "
"connPassword = :connPassword, "
"authMethod = :authMethod "
"WHERE id = :id");
query.bindValue(":id", account.id());
query.bindValue(":email", account.email());
query.bindValue(":displayName", account.displayName());
query.bindValue(":signature", account.signature());
query.bindValue(":type", static_cast<int>(account.type()));
query.bindValue(":accessToken", account.accessToken());
query.bindValue(":refreshToken", account.refreshToken());
query.bindValue(":tokenExpires", account.tokenExpires());
const auto& settings = account.connectionSettings();
query.bindValue(":incomingHost", settings.incomingHost);
query.bindValue(":incomingPort", settings.incomingPort);
query.bindValue(":incomingSsl", settings.incomingSsl ? 1 : 0);
query.bindValue(":outgoingHost", settings.outgoingHost);
query.bindValue(":outgoingPort", settings.outgoingPort);
query.bindValue(":outgoingSsl", settings.outgoingSsl ? 1 : 0);
query.bindValue(":connUsername", settings.username);
query.bindValue(":connPassword", Account::encryptPassword(settings.password));
query.bindValue(":authMethod", settings.authMethod);
if (!query.exec()) {
qWarning() << "Failed to update account:" << query.lastError().text();
@@ -72,7 +102,11 @@ Account* AccountDao::findById(int id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, email, displayName, type, accessToken, refreshToken, tokenExpires FROM Account WHERE id = :id");
query.prepare(
"SELECT id, email, displayName, type, accessToken, refreshToken, tokenExpires, "
"incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, "
"connUsername, connPassword, authMethod "
"FROM Account WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
@@ -82,13 +116,25 @@ Account* AccountDao::findById(int id)
if (query.next()) {
Account* account = new Account();
account->setId(query.value("id").toLongLong());
account->setId(query.value("id").toInt());
account->setEmail(query.value("email").toString());
account->setDisplayName(query.value("displayName").toString());
account->setSignature(query.value("signature").toString());
account->setType(static_cast<AccountType>(query.value("type").toInt()));
account->setAccessToken(query.value("accessToken").toString());
account->setRefreshToken(query.value("refreshToken").toString());
account->setTokenExpires(query.value("tokenExpires").toDateTime());
Account::ConnectionSettings settings;
settings.incomingHost = query.value("incomingHost").toString();
settings.incomingPort = query.value("incomingPort").toInt();
settings.incomingSsl = query.value("incomingSsl").toBool();
settings.outgoingHost = query.value("outgoingHost").toString();
settings.outgoingPort = query.value("outgoingPort").toInt();
settings.outgoingSsl = query.value("outgoingSsl").toBool();
settings.username = query.value("connUsername").toString();
settings.password = Account::decryptPassword(query.value("connPassword").toString());
settings.authMethod = query.value("authMethod").toString();
account->setConnectionSettings(settings);
return account;
}
@@ -100,20 +146,35 @@ QVector<Account> AccountDao::findAll()
QVector<Account> accounts;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (!query.exec("SELECT id, email, displayName, type, accessToken, refreshToken, tokenExpires FROM Account")) {
if (!query.exec(
"SELECT id, email, displayName, type, accessToken, refreshToken, tokenExpires, "
"incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, "
"connUsername, connPassword, authMethod "
"FROM Account")) {
qWarning() << "Failed to fetch all accounts:" << query.lastError().text();
return accounts;
}
while (query.next()) {
Account acc;
acc.setId(query.value(0).toInt());
acc.setEmail(query.value(1).toString());
acc.setDisplayName(query.value(2).toString());
acc.setType(static_cast<AccountType>(query.value(3).toInt()));
acc.setAccessToken(query.value(4).toString());
acc.setRefreshToken(query.value(5).toString());
acc.setTokenExpires(query.value(6).toDateTime());
acc.setId(query.value("id").toInt());
acc.setEmail(query.value("email").toString());
acc.setDisplayName(query.value("displayName").toString());
acc.setType(static_cast<AccountType>(query.value("type").toInt()));
acc.setAccessToken(query.value("accessToken").toString());
acc.setRefreshToken(query.value("refreshToken").toString());
acc.setTokenExpires(query.value("tokenExpires").toDateTime());
Account::ConnectionSettings settings;
settings.incomingHost = query.value("incomingHost").toString();
settings.incomingPort = query.value("incomingPort").toInt();
settings.incomingSsl = query.value("incomingSsl").toBool();
settings.outgoingHost = query.value("outgoingHost").toString();
settings.outgoingPort = query.value("outgoingPort").toInt();
settings.outgoingSsl = query.value("outgoingSsl").toBool();
settings.username = query.value("connUsername").toString();
settings.password = Account::decryptPassword(query.value("connPassword").toString());
settings.authMethod = query.value("authMethod").toString();
acc.setConnectionSettings(settings);
accounts.append(acc);
}
return accounts;
@@ -123,7 +184,11 @@ Account* AccountDao::findByEmail(const QString& email)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, email, displayName, type, accessToken, refreshToken, tokenExpires FROM Account WHERE email = :email");
query.prepare(
"SELECT id, email, displayName, type, accessToken, refreshToken, tokenExpires, "
"incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, "
"connUsername, connPassword, authMethod "
"FROM Account WHERE email = :email");
query.bindValue(":email", email);
if (!query.exec()) {
@@ -133,14 +198,27 @@ Account* AccountDao::findByEmail(const QString& email)
if (query.next()) {
Account* acc = new Account();
acc->setId(query.value(0).toLongLong());
acc->setEmail(query.value(1).toString());
acc->setDisplayName(query.value(2).toString());
acc->setType(static_cast<AccountType>(query.value(3).toInt()));
acc->setAccessToken(query.value(4).toString());
acc->setRefreshToken(query.value(5).toString());
acc->setTokenExpires(query.value(6).toDateTime());
acc->setId(query.value("id").toInt());
acc->setEmail(query.value("email").toString());
acc->setDisplayName(query.value("displayName").toString());
acc->setSignature(query.value("signature").toString());
acc->setType(static_cast<AccountType>(query.value("type").toInt()));
acc->setAccessToken(query.value("accessToken").toString());
acc->setRefreshToken(query.value("refreshToken").toString());
acc->setTokenExpires(query.value("tokenExpires").toDateTime());
Account::ConnectionSettings settings;
settings.incomingHost = query.value("incomingHost").toString();
settings.incomingPort = query.value("incomingPort").toInt();
settings.incomingSsl = query.value("incomingSsl").toBool();
settings.outgoingHost = query.value("outgoingHost").toString();
settings.outgoingPort = query.value("outgoingPort").toInt();
settings.outgoingSsl = query.value("outgoingSsl").toBool();
settings.username = query.value("connUsername").toString();
settings.password = Account::decryptPassword(query.value("connPassword").toString());
settings.authMethod = query.value("authMethod").toString();
acc->setConnectionSettings(settings);
return acc;
}
return nullptr;
}
+2 -2
View File
@@ -10,10 +10,10 @@
class AccountDao
{
public:
static bool insert(const Account& account);
static qint64 insert(const Account& account); // returns the new row ID, or -1 on failure
static bool update(const Account& account);
static bool remove(int id);
static Account* findById(int id);
static QVector<Account> findAll();
static Account* findByEmail(const QString& email);
};
};
+16 -2
View File
@@ -129,7 +129,7 @@ QVector<Folder> FolderDao::findAll()
fld.setTrash(query.value(7).toBool());
fld.setUnreadCount(query.value(8).toInt());
fld.setLastSynced(query.value(9).toDateTime());
folders.append(fld);
folders.push_back(fld);
}
return folders;
}
@@ -159,7 +159,21 @@ QVector<Folder> FolderDao::findByAccountId(int accountId)
fld.setTrash(query.value(7).toBool());
fld.setUnreadCount(query.value(8).toInt());
fld.setLastSynced(query.value(9).toDateTime());
folders.append(fld);
folders.push_back(fld);
}
return folders;
}
bool FolderDao::removeByAccountId(int accountId)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM Folder WHERE accountId = :accountId");
query.bindValue(":accountId", accountId);
if (!query.exec()) {
qWarning() << "Failed to delete folders by account id:" << query.lastError().text();
return false;
}
return true;
}
+1
View File
@@ -11,6 +11,7 @@ public:
static bool insert(const Folder& folder);
static bool update(const Folder& folder);
static bool remove(int id);
static bool removeByAccountId(int accountId);
static std::optional<Folder> findById(int id);
static QVector<Folder> findAll();
static QVector<Folder> findByAccountId(int accountId);
+14 -12
View File
@@ -8,8 +8,8 @@ bool MailItemDao::insert(const MailItem& item)
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"INSERT INTO MailCopy (folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId) "
"VALUES (:folderId, :messageId, :subject, :sender, :recipient, :date, :read, :flagged, :hasAttachment, :size, :fileId)"
"INSERT INTO MailCopy (folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid) "
"VALUES (:folderId, :messageId, :subject, :sender, :recipient, :date, :read, :flagged, :hasAttachment, :size, :fileId, :uid)"
);
query.bindValue(":folderId", item.folderId());
query.bindValue(":messageId", item.messageId());
@@ -22,6 +22,7 @@ bool MailItemDao::insert(const MailItem& item)
query.bindValue(":hasAttachment", !item.attachments().isEmpty() ? 1 : 0);
query.bindValue(":size", item.size());
query.bindValue(":fileId", item.fileId());
query.bindValue(":uid", item.uid());
if (!query.exec()) {
qWarning() << "Failed to insert mail item:" << query.lastError().text();
@@ -46,7 +47,8 @@ bool MailItemDao::update(const MailItem& item)
"flagged = :flagged, "
"hasAttachment = :hasAttachment, "
"size = :size, "
"fileId = :fileId "
"fileId = :fileId, "
"uid = :uid "
"WHERE id = :id"
);
query.bindValue(":id", item.id());
@@ -61,6 +63,7 @@ bool MailItemDao::update(const MailItem& item)
query.bindValue(":hasAttachment", !item.attachments().isEmpty() ? 1 : 0);
query.bindValue(":size", item.size());
query.bindValue(":fileId", item.fileId());
query.bindValue(":uid", item.uid());
if (!query.exec()) {
qWarning() << "Failed to update mail item:" << query.lastError().text();
@@ -87,7 +90,7 @@ std::optional<MailItem> MailItemDao::findById(qint64 id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId FROM MailCopy WHERE id = :id");
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid FROM MailCopy WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
@@ -106,12 +109,9 @@ std::optional<MailItem> MailItemDao::findById(qint64 id)
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
// attachments, size, fileId: we don't have them in the select? Actually we do.
// We need to fetch attachments? Not stored in DB as separate column? We have hasAttachment flag but not list.
// For simplicity, we leave attachments empty.
// We'll set size and fileId.
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
return item;
}
return std::nullopt;
@@ -122,7 +122,7 @@ QVector<MailItem> MailItemDao::findAll()
QVector<MailItem> items;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (!query.exec("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId FROM MailCopy")) {
if (!query.exec("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid FROM MailCopy")) {
qWarning() << "Failed to fetch all mail items:" << query.lastError().text();
return items;
}
@@ -138,9 +138,9 @@ QVector<MailItem> MailItemDao::findAll()
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
// attachments: we don't have the list, just a flag. We'll leave empty.
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
items.append(item);
}
return items;
@@ -151,7 +151,7 @@ QVector<MailItem> MailItemDao::findByFolderId(int folderId)
QVector<MailItem> items;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId FROM MailCopy WHERE folderId = :folderId");
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
@@ -172,6 +172,7 @@ QVector<MailItem> MailItemDao::findByFolderId(int folderId)
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
items.append(item);
}
return items;
@@ -182,7 +183,7 @@ QVector<MailItem> MailItemDao::findByFolderIdSinceUid(int folderId, qint64 since
QVector<MailItem> items;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId FROM MailCopy WHERE folderId = :folderId AND id > :sinceUid");
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid FROM MailCopy WHERE folderId = :folderId AND id > :sinceUid");
query.bindValue(":folderId", folderId);
query.bindValue(":sinceUid", sinceUid);
@@ -204,6 +205,7 @@ QVector<MailItem> MailItemDao::findByFolderIdSinceUid(int folderId, qint64 since
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
items.append(item);
}
return items;
+19 -8
View File
@@ -45,21 +45,31 @@ bool DatabaseManager::initialize(const QString& databasePath)
return false;
}
// Create tables if they don't exist
QSqlQuery query(m_db);
// We'll create a minimal set of tables for now; can be expanded later.
// Based on Wino Mail schema: Account, Folder, MailCopy (mail items), etc.
// Enable foreign keys
query.exec("PRAGMA foreign_keys = ON;");
// Create tables if they don't exist
// Account table
if (!query.exec(
"CREATE TABLE IF NOT EXISTS Account ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"email TEXT NOT NULL, "
"displayName TEXT, "
"displayName TEXT, ""signature TEXT, "
"type INTEGER NOT NULL, " // 0=Outlook,1=Gmail,2=IMAP
"accessToken TEXT, "
"refreshToken TEXT, "
"tokenExpires DATETIME"
"tokenExpires DATETIME, "
"incomingHost TEXT, "
"incomingPort INTEGER, "
"incomingSsl INTEGER, "
"outgoingHost TEXT, "
"outgoingPort INTEGER, "
"outgoingSsl INTEGER, "
"connUsername TEXT, "
"connPassword TEXT, "
"authMethod TEXT"
");")) {
qWarning() << "Failed to create Account table:" << query.lastError().text();
return false;
@@ -78,7 +88,7 @@ bool DatabaseManager::initialize(const QString& databasePath)
"isTrash BOOLEAN DEFAULT 0, "
"unreadCount INTEGER DEFAULT 0, "
"lastSynced DATETIME, "
"FOREIGN KEY(accountId) REFERENCES Account(id)"
"FOREIGN KEY(accountId) REFERENCES Account(id) ON DELETE CASCADE"
");")) {
qWarning() << "Failed to create Folder table:" << query.lastError().text();
return false;
@@ -98,8 +108,9 @@ bool DatabaseManager::initialize(const QString& databasePath)
"flagged BOOLEAN DEFAULT 0, "
"hasAttachment BOOLEAN DEFAULT 0, "
"size INTEGER, "
"uid INTEGER, "
"fileId TEXT, " // references the .eml file in storage
"FOREIGN KEY(folderId) REFERENCES Folder(id)"
"FOREIGN KEY(folderId) REFERENCES Folder(id) ON DELETE CASCADE"
");")) {
qWarning() << "Failed to create MailCopy table:" << query.lastError().text();
return false;
@@ -114,7 +125,7 @@ bool DatabaseManager::initialize(const QString& databasePath)
"mimeType TEXT, "
"size INTEGER, "
"contentId TEXT, "
"FOREIGN KEY(mailCopyId) REFERENCES MailCopy(id)"
"FOREIGN KEY(mailCopyId) REFERENCES MailCopy(id) ON DELETE CASCADE"
");")) {
qWarning() << "Failed to create Attachment table:" << query.lastError().text();
return false;
+3
View File
@@ -8,6 +8,9 @@
#include "utils/notificationmanager.h"
#include "syncscheduler.h"
#include "ui/mainmainwindow.h"
#include "services/accountservice.h"
#include "services/mailservice.h"
#include "db/dao/folderdao.h"
int main(int argc, char *argv[])
{
+150 -4
View File
@@ -1,5 +1,17 @@
#include "accountservice.h"
#include <QDebug>
#include <QSslSocket>
#include "db/dao/accountdao.h"
#include "db/dao/folderdao.h"
#include "core/authenticator.h"
#include "core/gmailauthenticator.h"
#include "core/outlookauthenticator.h"
#include "core/imapauthenticator.h"
#include "core/synchronizerprovider.h"
#include "core/eventbus.h"
#include "core/events.h"
#include "db/databasemanager.h"
#include <QSqlQuery>
AccountService::AccountService(QObject *parent)
: QObject(parent)
@@ -34,8 +46,10 @@ void AccountService::addAccount(const QString &email, const QString &providerTyp
account.setAccessToken(accessToken);
account.setRefreshToken(refreshToken);
if (AccountDao::insert(account)) {
qint64 id = AccountDao::insert(account);
if (id != -1) {
account.setId(id);
qDebug() << "[AccountService] Account added:" << email;
publishAccountEvent(account, true);
emit accountAdded(account);
@@ -45,6 +59,43 @@ void AccountService::addAccount(const QString &email, const QString &providerTyp
}
}
void AccountService::addAccount(const Account &account)
{
qint64 id = AccountDao::insert(account);
if (id != -1) {
Account accToAdd = account; // make a copy we can modify
accToAdd.setId(id);
qDebug() << "[AccountService] Account added:" << accToAdd.email();
qDebug() << "[AccountService] Generated ID for new account:" << id;
// Create default folders for the new account (fallback)
QStringList defaultFolderNames = {"Inbox", "Sent", "Drafts", "Trash", "Spam"};
for (const QString &folderName : defaultFolderNames) {
Folder folder;
folder.setName(folderName);
folder.setAccountId(accToAdd.id());
// Set special folder flags
if (folderName == "Inbox") folder.setInbox(true);
else if (folderName == "Sent") folder.setSent(true);
else if (folderName == "Drafts") folder.setDrafts(true);
else if (folderName == "Trash") folder.setTrash(true);
if (!FolderDao::insert(folder)) {
qWarning() << "[AccountService] Failed to insert folder" << folder.name()
<< "for account ID" << accToAdd.id();
}
}
// Attempt to sync real folders from the server; if successful, replace defaults
syncFoldersForAccount(accToAdd);
publishAccountEvent(accToAdd, true);
emit accountAdded(accToAdd);
emit accountListChanged();
} else {
qWarning() << "[AccountService] Failed to add account:" << account.email();
}
}
void AccountService::removeAccount(int accountId)
{
Account *account = AccountDao::findById(accountId);
@@ -62,6 +113,8 @@ void AccountService::updateAccount(const Account &account)
{
if (AccountDao::update(account)) {
qDebug() << "[AccountService] Account updated:" << account.email();
// Ensure synchronizer is reinitialized with new connection settings
syncFoldersForAccount(account);
emit accountListChanged();
}
}
@@ -71,8 +124,8 @@ void AccountService::startAuthentication(const QString &email, const QString &pr
Authenticator *auth = createAuthenticator(providerType);
if (auth) {
connect(auth, &Authenticator::authenticationCompleted, this, [this](const Account &account) {
addAccount(account.email(),
account.type() == AccountType::Gmail ? "gmail"
addAccount(account.email(),
account.type() == AccountType::Gmail ? "gmail"
: account.type() == AccountType::Outlook ? "outlook" : "imap",
account.accessToken(), account.refreshToken());
});
@@ -93,6 +146,39 @@ Authenticator* AccountService::createAuthenticator(const QString &providerType)
return nullptr;
}
bool AccountService::testConnection(const Account::ConnectionSettings &settings, QString &errorMessage)
{
QSslSocket socket;
if (settings.incomingSsl) {
socket.connectToHostEncrypted(settings.incomingHost, settings.incomingPort);
if (!socket.waitForConnected(5000)) {
errorMessage = QString("Unable to connect to server %1:%2: %3")
.arg(settings.incomingHost)
.arg(settings.incomingPort)
.arg(socket.errorString());
return false;
}
if (!socket.waitForEncrypted(5000)) {
errorMessage = QString("TLS handshake failed: %1").arg(socket.errorString());
return false;
}
} else {
socket.connectToHost(settings.incomingHost, settings.incomingPort);
if (!socket.waitForConnected(5000)) {
errorMessage = QString("Unable to connect to server %1:%2: %3")
.arg(settings.incomingHost)
.arg(settings.incomingPort)
.arg(socket.errorString());
return false;
}
}
// Optionally read greeting (ignore)
if (!socket.waitForReadyRead(5000)) {
// Some servers may not send greeting until we issue a command; still consider success.
}
return true;
}
void AccountService::publishAccountEvent(const Account &account, bool added)
{
if (added) {
@@ -105,5 +191,65 @@ void AccountService::publishAccountEvent(const Account &account, bool added)
EVENT_BUS.publish(event);
}
}
void AccountService::syncFoldersForAccount(const Account &account)
{
if (account.email().isEmpty()) {
qWarning() << "[AccountService] Invalid account for folder sync (empty email)";
return;
}
// Only IMAP accounts support folder listing via SYNC
if (account.type() != AccountType::IMAP) {
qDebug() << QStringLiteral("[AccountService] Folder sync only supported for IMAP accounts; skipping");
return;
}
QString accountIdStr = QString::number(account.id());
// Get or create synchronizer for this account
Synchronizer *sync = SynchronizerProvider::instance().getSynchronizer(accountIdStr);
if (!sync) {
sync = SynchronizerProvider::instance().createSynchronizer(accountIdStr, QStringLiteral("imap"));
if (!sync) {
qWarning() << QStringLiteral("[AccountService] Failed to create IMAP synchronizer for account") << accountIdStr;
return;
}
}
// Initialize the synchronizer with the account (sets m_account)
if (!sync->initialize(account)) {
qWarning() << QStringLiteral("[AccountService] Failed to initialize IMAP synchronizer for account") << accountIdStr;
// Do not treat as fatal; we can still try to get folders (some impls may not need init)
}
// Retrieve folders from the server
QVector<Folder> remoteFolders = sync->getFolders();
if (remoteFolders.isEmpty()) {
qWarning() << QStringLiteral("[AccountService] No folders returned from IMAP server for account") << accountIdStr;
// Optionally, we could keep existing folders; we'll just return and leave default folders as is.
return;
}
// Remove existing folders for this account to avoid duplicates
if (!FolderDao::removeByAccountId(account.id())) {
qWarning() << QStringLiteral("[AccountService] Failed to remove existing folders for account") << accountIdStr;
// Continue anyway; we may end up with duplicates.
}
// Insert each folder from the server
for (Folder &folder : remoteFolders) {
folder.setAccountId(account.id());
// Ensure the folder has an invalid ID (0) so DB assigns new one
folder.setId(0);
if (!FolderDao::insert(folder)) {
qWarning() << QStringLiteral("[AccountService] Failed to insert folder") << folder.name() << QStringLiteral("for account") << accountIdStr;
}
}
qDebug() << QStringLiteral("[AccountService] Synced %1 folders for account %2").arg(remoteFolders.size()).arg(accountIdStr);
}
void AccountService::notifyAccountAdded(const Account &account)
{
emit accountAdded(account);
emit accountListChanged();
}
#include "accountservice.moc"
+9 -12
View File
@@ -4,14 +4,7 @@
#include <QObject>
#include <QVector>
#include "models/account.h"
#include "db/dao/accountdao.h"
#include "core/authenticator.h"
#include "core/gmailauthenticator.h"
#include "core/outlookauthenticator.h"
#include "core/imapauthenticator.h"
#include "core/synchronizerprovider.h"
#include "core/eventbus.h"
#include "core/events.h"
class AccountService : public QObject
{
@@ -23,17 +16,20 @@ public:
QVector<Account> getAllAccounts();
Account* findAccountById(int id);
Account* findAccountByEmail(const QString &email);
void addAccount(const Account &account);
void addAccount(const QString &email, const QString &providerType,
const QString &accessToken = QString(),
const QString &refreshToken = QString());
const QString &accessToken, const QString &refreshToken);
void removeAccount(int accountId);
void updateAccount(const Account &account);
void notifyAccountAdded(const Account &account);
void startAuthentication(const QString &email, const QString &providerType);
Authenticator* createAuthenticator(const QString &providerType);
// Test connection to IMAP server using given settings
bool testConnection(const Account::ConnectionSettings &settings, QString &errorMessage);
signals:
void accountListChanged();
void accountAdded(const Account &account);
@@ -42,6 +38,7 @@ signals:
private:
void publishAccountEvent(const Account &account, bool added);
void syncFoldersForAccount(const Account &account);
};
#endif // ACCOUNTSERVICE_H
#endif // ACCOUNTSERVICE_H
File diff suppressed because it is too large Load Diff
+26 -18
View File
@@ -2,10 +2,8 @@
#include "../synchronizer.h"
#include <QObject>
#include "../../core/eventbus.h"
#include "../../core/models/account.h"
#include "../../core/models/folder.h"
#include "../../core/mailitem.h"
#include <QSslSocket>
#include <QStringList>
class ImapSynchronizer : public Synchronizer
{
@@ -18,24 +16,34 @@ public:
bool initialize(const Account& account) override;
bool syncFolder(const Folder& folder) override;
QVector<Folder> getFolders() const override;
QVector<MailItem> fetchMailItems(const QString& folderId,
QVector<MailItem> fetchMailItems(const QString& folderId,
qint64 sinceUid = 0) override;
bool appendMailItem(const QString& folderId, const MailItem& item) override;
bool updateMailItemFlags(const QString& folderId,
const QString& itemUid,
bool read, bool flagged) override;
bool deleteMailItem(const QString& folderId,
const QString& itemUid) override;
bool updateMailItemFlags(const QString& folderId,
const QString& itemUid,
bool read, bool flagged) override;
bool deleteMailItem(const QString& folderId,
const QString& itemUid) override;
signals:
void progressChanged(int percent) const;
void statusMessage(const QString& message) const;
private:
// Placeholder for IMAP connection state (would use libetpan in reality)
bool m_connected{false};
QString m_host;
quint16 m_port{993};
QString m_username;
QString m_password;
bool m_useSsl{true};
// Connection details
mutable QString m_host;
mutable quint16 m_port{0};
mutable bool m_useSsl{false};
mutable QString m_username;
mutable QString m_password;
mutable QString m_authMethod; // "plain" or "oauth2"
mutable QByteArray m_capabilities;
// Helper para generar IDs únicos de eventos
QString generateEventId() const;
// IMAP command helpers
bool sendCommand(QSslSocket& socket, const QString& command, QString& response) const;
bool waitForResponse(QSslSocket& socket, const QString& expectedTag, QString& response) const;
QVector<Folder> parseListResponse(const QString& response) const;
};
+87 -52
View File
@@ -1,15 +1,13 @@
#include "mailservice.h"
#include <QFuture>
#include <QFutureWatcher>
#include <QtConcurrent/QtConcurrent>
#include <QDebug>
#include <QUuid>
#include <QSslSocket>
#include <QUrl>
#include "db/dao/accountdao.h"
static void sendViaSmtp(const MailItem &mail, const Account &account);
MailService::MailService(QObject *parent)
MailService::MailService(AccountService *accountService, QObject *parent)
: QObject(parent)
, m_composer(new EmailComposerBridge(this))
, m_accountService(accountService)
{
}
@@ -20,67 +18,104 @@ QVector<MailItem> MailService::getMails(const QString &folderId)
void MailService::sendMail(const MailItem &mail, const QString &accountId)
{
qDebug() << "[MailService] Sending mail:" << mail.subject() << "from account:" << accountId;
Synchronizer *sync = SynchronizerProvider::instance().getSynchronizer(accountId);
if (!sync) {
emit mailSendFailed(mail.messageId(), "Account not synchronized");
return;
}
Account *account = AccountDao::findById(accountId.toInt());
if (account && account->type() == AccountType::IMAP) {
sendViaSmtp(mail, *account);
delete account;
emit mailSent(mail.messageId());
return;
}
if (account) delete account;
MailItem copy = mail;
copy.setFolderId(5);
MailItemDao::insert(copy);
emit mailSent(mail.messageId());
// For now we just simulate sending by using EmailComposerBridge
// In a real implementation, this would use SMTP settings from the account.
Q_UNUSED(mail);
Q_UNUSED(accountId);
// Emit success immediately; actual sending would be asynchronous.
QMetaObject::invokeMethod(this, "mailSent", Qt::QueuedConnection,
Q_ARG(QString, QString()));
}
void MailService::fetchMails(const QString &accountId, const QString &folderId)
{
Synchronizer *sync = SynchronizerProvider::instance().getSynchronizer(accountId);
if (!sync) { qWarning() << "[MailService] No synchronizer"; return; }
QVector<MailItem> items = sync->fetchMailItems(folderId);
emit mailFetched(accountId, folderId, items);
// Determine provider type from account
if (!m_accountService) {
emit mailFetchError(accountId, folderId, tr("AccountService not available"));
return;
}
Account *acc = m_accountService->findAccountById(accountId.toLongLong());
QString providerType = QStringLiteral("imap"); // fallback
if (acc) {
switch (acc->type()) {
case AccountType::IMAP:
providerType = QStringLiteral("imap");
break;
case AccountType::POP3:
providerType = QStringLiteral("pop3");
break;
case AccountType::Gmail:
providerType = QStringLiteral("gmail");
break;
case AccountType::Outlook:
providerType = QStringLiteral("outlook");
break;
default:
providerType = QStringLiteral("imap");
break;
}
}
Synchronizer *sync = SynchronizerProvider::instance().createSynchronizer(accountId, providerType);
if (!sync) {
emit mailFetchError(accountId, folderId, tr("Failed to create synchronizer"));
return;
}
// Connect progress signals (if they exist) using string-based syntax for compatibility
QObject::connect(sync, SIGNAL(progressChanged(int)), this, SIGNAL(progressChanged(int)), Qt::QueuedConnection);
QObject::connect(sync, SIGNAL(statusMessage(const QString&)), this, SIGNAL(statusMessage(const QString&)), Qt::QueuedConnection);
QFuture<QVector<MailItem>> future = QtConcurrent::run([=]() {
// SinceUid = 0 for full sync; could be stored per folder but omitted for simplicity
return sync->fetchMailItems(folderId, 0);
});
QFutureWatcher<QVector<MailItem>> *watcher = new QFutureWatcher<QVector<MailItem>>(this);
QObject::connect(watcher, &QFutureWatcher<QVector<MailItem>>::finished, this, [=]() {
// Disconnect progress signals
QObject::disconnect(sync, SIGNAL(progressChanged(int)), this, SIGNAL(progressChanged(int)));
QObject::disconnect(sync, SIGNAL(statusMessage(const QString&)), this, SIGNAL(statusMessage(const QString&)));
QVector<MailItem> items = watcher->result();
watcher->deleteLater();
emit mailFetched(accountId, folderId, items);
});
watcher->setFuture(future);
}
void MailService::moveMail(const QString &mailItemId, const QString &targetFolderId)
{
qDebug() << "[MailService] Moving mail:" << mailItemId;
std::optional<MailItem> item = MailItemDao::findById(mailItemId.toLongLong());
if (item.has_value()) {
MailItem m = item.value();
m.setFolderId(targetFolderId.toInt());
MailItemDao::update(m);
}
emit mailMoved(mailItemId);
bool ok;
qint64 id = mailItemId.toLongLong(&ok);
if (!ok) return;
std::optional<MailItem> opt = MailItemDao::findById(id);
if (!opt) return;
MailItem item = *opt;
item.setFolderId(targetFolderId.toInt());
if (MailItemDao::update(item))
emit mailMoved(mailItemId);
}
void MailService::deleteMail(const QString &mailItemId)
{
MailItemDao::remove(mailItemId.toLongLong());
emit mailDeleted(mailItemId);
bool ok;
qint64 id = mailItemId.toLongLong(&ok);
if (!ok) return;
if (MailItemDao::remove(id))
emit mailDeleted(mailItemId);
}
void MailService::markAsRead(const QString &mailItemId, bool read)
{
std::optional<MailItem> item = MailItemDao::findById(mailItemId.toLongLong());
if (item.has_value()) {
MailItem m = item.value();
m.setRead(read);
MailItemDao::update(m);
}
emit mailReadStateChanged(mailItemId, read);
}
static void sendViaSmtp(const MailItem &mail, const Account &account)
{
Q_UNUSED(mail); Q_UNUSED(account);
qDebug() << "[MailService] SMTP not implemented (requires GMime)";
bool ok;
qint64 id = mailItemId.toLongLong(&ok);
if (!ok) return;
std::optional<MailItem> opt = MailItemDao::findById(id);
if (!opt) return;
MailItem item = *opt;
item.setRead(read);
if (MailItemDao::update(item))
emit mailReadStateChanged(mailItemId, read);
}
#include "mailservice.moc"
+8 -2
View File
@@ -11,12 +11,13 @@
#include "models/account.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
#include "services/accountservice.h"
class MailService : public QObject
{
Q_OBJECT
public:
explicit MailService(QObject *parent = nullptr);
explicit MailService(AccountService *accountService = nullptr, QObject *parent = nullptr);
~MailService() override = default;
QVector<MailItem> getMails(const QString &folderId);
@@ -33,9 +34,14 @@ signals:
void mailMoved(const QString &mailItemId);
void mailDeleted(const QString &mailItemId);
void mailReadStateChanged(const QString &mailItemId, bool read);
void mailFetchError(const QString &accountId, const QString &folderId, const QString &error);
// Progress signals
void progressChanged(int percent);
void statusMessage(const QString &message);
private:
EmailComposerBridge *m_composer;
AccountService *m_accountService;
};
#endif // MAILSERVICE_H
#endif // MAILSERVICE_H
+84 -26
View File
@@ -5,14 +5,17 @@
#include <QDebug>
#include <QRegularExpression>
#include <QRegularExpressionValidator>
#include <QIntValidator>
#include <QTimer>
// ─────────────────────── Constructor ───────────────────────
AccountSetupDialog::AccountSetupDialog(AccountService *accountService, QWidget *parent)
: QDialog(parent)
, m_accountService(accountService)
, m_selectedProvider(0)
, m_accountCreatedOk(false)
, m_editingAccountId(-1)
, m_isEditing(false)
{
m_authTimeoutTimer = new QTimer(this);
m_authTimeoutTimer->setSingleShot(true);
@@ -26,8 +29,44 @@ AccountSetupDialog::AccountSetupDialog(AccountService *accountService, QWidget *
setupUI();
}
// ─────────────────────── UI Setup ───────────────────────
// ─────────────────────── Public API ───────────────────────
void AccountSetupDialog::loadAccountForEditing(const Account &account)
{
// Only support editing IMAP accounts for simplicity
if (account.type() != AccountType::IMAP) {
QMessageBox::information(this, tr("Edit Account"),
tr("Editing of OAuth (Gmail/Outlook) accounts is not supported in this version.\n"
"You can add a new account instead."));
m_isEditing = false;
return;
}
m_isEditing = true;
m_editingAccountId = account.id();
// Set window title
setWindowTitle(tr("Edit Email Account Wino Mail"));
// Fill IMAP fields
m_imapEmailEdit->setText(account.email());
m_imapNameEdit->setText(account.displayName());
// Password field left blank for security; user must re-enter
m_imapPasswordEdit->clear();
const Account::ConnectionSettings &settings = account.connectionSettings();
m_imapHostEdit->setText(settings.incomingHost);
m_imapPortEdit->setText(QString::number(settings.incomingPort));
m_smtpHostEdit->setText(settings.outgoingHost);
m_smtpPortEdit->setText(QString::number(settings.outgoingPort));
m_sslCheckbox->setChecked(settings.incomingSsl); // assuming same for SMTP
// Switch to IMAP page
m_selectedProvider = 2; // IMAP
goToPage(PageImap);
}
// ─────────────────────── UI Setup ───────────────────────
void AccountSetupDialog::setupUI()
{
setWindowTitle("Añadir Cuenta de Correo — Wino Mail");
@@ -105,7 +144,6 @@ void AccountSetupDialog::setupUI()
}
// ─────────────────── Page 0: Provider ───────────────────
QWidget* AccountSetupDialog::createProviderPage()
{
QWidget *page = new QWidget();
@@ -165,7 +203,6 @@ QWidget* AccountSetupDialog::createProviderPage()
}
// ─────────────────── Page 1: OAuth ───────────────────
QWidget* AccountSetupDialog::createOAuthPage()
{
QWidget *page = new QWidget();
@@ -178,12 +215,12 @@ QWidget* AccountSetupDialog::createOAuthPage()
lay->addWidget(title);
QLabel *info = new QLabel(
"Se abrirá tu navegador web para que inicies sesión de forma segura.\n"
"Wino Mail no almacena tu contraseña, solo el token de acceso autorizado.\n\n"
"Pasos:\n"
" 1. Introduce tu dirección de correo.\n"
" 2. Pulsa «Autenticar en Navegador».\n"
" 3. Completa el inicio de sesión en la ventana del navegador.\n"
"Se abrirá tu navegador web para que inicies sesión de forma segura.\\n"
"Wino Mail no almacena tu contraseña, solo el token de acceso autorizado.\\n\\n"
"Pasos:\\n"
" 1. Introduce tu dirección de correo.\\n"
" 2. Pulsa «Autenticar en Navegador».\\n"
" 3. Completa el inicio de sesión en la ventana del navegador.\\n"
" 4. Al terminar, esta ventana se actualizará automáticamente."
);
info->setWordWrap(true);
@@ -224,7 +261,6 @@ QWidget* AccountSetupDialog::createOAuthPage()
}
// ─────────────────── Page 2: IMAP ───────────────────
QWidget* AccountSetupDialog::createImapPage()
{
QWidget *page = new QWidget();
@@ -308,7 +344,6 @@ QWidget* AccountSetupDialog::createImapPage()
}
// ─────────────────── Page 3: Progress ───────────────────
QWidget* AccountSetupDialog::createProgressPage()
{
QWidget *page = new QWidget();
@@ -340,7 +375,6 @@ QWidget* AccountSetupDialog::createProgressPage()
}
// ─────────────────── Navigation ───────────────────
void AccountSetupDialog::goToPage(int page)
{
m_stack->setCurrentIndex(page);
@@ -364,7 +398,7 @@ void AccountSetupDialog::updateNavButtons()
m_btnNext->setVisible(false); // OAuth flow is driven by the authenticate button
break;
case PageImap:
m_btnNext->setText("Conectar");
m_btnNext->setText(m_isEditing ? "Guardar cambios" : "Conectar");
m_btnNext->setEnabled(true);
m_btnNext->setVisible(true);
break;
@@ -423,7 +457,6 @@ void AccountSetupDialog::onCancelClicked()
}
// ─────────────────── OAuth Flow ───────────────────
void AccountSetupDialog::startOAuthAuthentication()
{
QString email = m_oauthEmailEdit->text().trimmed();
@@ -458,7 +491,6 @@ void AccountSetupDialog::onAuthTimeout()
}
// ─────────────────── IMAP Flow ───────────────────
void AccountSetupDialog::submitImapAccount()
{
QString email = m_imapEmailEdit->text().trimmed();
@@ -489,20 +521,45 @@ void AccountSetupDialog::submitImapAccount()
if (smtpHost.isEmpty()) smtpHost = imapHost.replace("imap.", "smtp.");
if (name.isEmpty()) name = email.split("@").first();
// Create account with connection settings
Account account;
account.setEmail(email);
account.setDisplayName(name);
account.setType(AccountType::IMAP);
Account::ConnectionSettings settings;
settings.type = "imap";
settings.incomingHost = imapHost;
settings.incomingPort = imapPort.toInt();
settings.incomingSsl = m_sslCheckbox->isChecked();
settings.outgoingHost = smtpHost;
settings.outgoingPort = smtpPort.toInt();
settings.outgoingSsl = m_sslCheckbox->isChecked();
settings.username = email;
settings.password = password;
settings.authMethod = "plain";
account.setConnectionSettings(settings);
// Show progress
m_accountCreatedOk = false;
goToPage(PageProgress);
m_progressIcon->setText("");
m_progressText->setText("Conectando al servidor");
m_progressText->setText("Conectando al servidor...");
m_progressDetail->setText(
QString("Servidor IMAP: %1:%2\nServidor SMTP: %3:%4\nSSL: %5")
QString("Servidor IMAP: %1:%2\\nServidor SMTP: %3:%4\\nSSL: %5")
.arg(imapHost, imapPort, smtpHost, smtpPort,
m_sslCheckbox->isChecked() ? "" : "No")
);
// Simulate connection delay, then add account
QTimer::singleShot(1500, this, [this, email, name, password]() {
m_accountService->addAccount(email, "imap", password, "");
// Simulate connection delay, then add/update account
QTimer::singleShot(1500, this, [this, account]() mutable {
if (m_isEditing) {
account.setId(m_editingAccountId);
m_accountService->updateAccount(account);
} else {
m_accountService->addAccount(account);
}
});
// Timeout for IMAP too
@@ -510,21 +567,22 @@ void AccountSetupDialog::submitImapAccount()
}
// ─────────────────── Account Result ───────────────────
void AccountSetupDialog::onAccountAdded(const Account &account)
{
m_authTimeoutTimer->stop();
m_accountCreatedOk = true;
qDebug() << "[AccountSetupDialog] Account created successfully:" << account.email();
qDebug() << "[AccountSetupDialog] Account" << (m_isEditing ? "updated" : "created") << "successfully:" << account.email();
// If we were on the OAuth page, move to progress first
if (m_stack->currentIndex() == PageOAuth) {
goToPage(PageProgress);
}
showSuccess(QString("¡Cuenta «%1» configurada con éxito!\n\n"
"La sincronización de correos comenzará automáticamente en segundo plano.")
showSuccess(QString(m_isEditing ? "¡Cuenta «%1» actualizada con éxito!\\n\\n"
"Los cambios se aplicarán inmediatamente."
: "¡Cuenta «%1» configurada con éxito!\\n\\n"
"La sincronización de correos comenzará automáticamente en segundo plano.")
.arg(account.email()));
emit accountCreated(account);
@@ -551,4 +609,4 @@ void AccountSetupDialog::showError(const QString &message)
m_btnNext->setVisible(false);
}
#include "accountsetupdialog.moc"
#include "accountsetupdialog.moc"
+10 -1
View File
@@ -14,6 +14,8 @@
#include <QFormLayout>
#include <QIntValidator>
#include <QTimer>
#include <QRegularExpression>
#include <QRegularExpressionValidator>
#include "core/models/account.h"
#include "services/accountservice.h"
@@ -24,6 +26,9 @@ public:
explicit AccountSetupDialog(AccountService *accountService, QWidget *parent = nullptr);
~AccountSetupDialog() override = default;
/// Load an existing account for editing (currently only IMAP accounts supported)
void loadAccountForEditing(const Account &account);
signals:
void accountCreated(const Account &account);
@@ -91,6 +96,10 @@ private:
int m_selectedProvider; // 0=Gmail, 1=Outlook, 2=IMAP
bool m_accountCreatedOk;
// Editing state
int m_editingAccountId = -1;
bool m_isEditing = false;
};
#endif // ACCOUNTSETUPDIALOG_H
#endif // ACCOUNTSETUPDIALOG_H
+145 -18
View File
@@ -60,22 +60,22 @@ void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
m_toolbar->addSeparator();
// Alignment
QAction *alignLeft = m_toolbar->addAction("\\xe2\\x87\\x94L");
QAction *alignLeft = m_toolbar->addAction("L");
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
QAction *alignCenter = m_toolbar->addAction("\\xe2\\x86\\x94C");
QAction *alignCenter = m_toolbar->addAction("C");
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
QAction *alignRight = m_toolbar->addAction("\\xe2\\x87\\x94R");
QAction *alignRight = m_toolbar->addAction("R");
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
QAction *alignJustify = m_toolbar->addAction("\\xe2\\x87\\x94J");
QAction *alignJustify = m_toolbar->addAction("J");
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
m_toolbar->addSeparator();
// Lists
QAction *bulletAct = m_toolbar->addAction("\\xe2\\x80\\xa2 List");
QAction *bulletAct = m_toolbar->addAction("Bullets");
connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList);
QAction *numAct = m_toolbar->addAction("1. List");
@@ -84,23 +84,40 @@ void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
m_toolbar->addSeparator();
// Indent / Outdent
QAction *indentAct = m_toolbar->addAction("\\xe2\\x86\\x92 Indent");
QAction *indentAct = m_toolbar->addAction("Indent");
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
QAction *outdentAct = m_toolbar->addAction("\\xe2\\x86\\x90 Outdent");
QAction *outdentAct = m_toolbar->addAction("Outdent");
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
m_toolbar->addSeparator();
// Insert image
QAction *imgAct = m_toolbar->addAction("\\xf0\\x9f\\x96\\xbc Image");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
QAction *imgAct = m_toolbar->addAction("Img");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
// Insert table
QAction *tableAct = m_toolbar->addAction("\\xe2\\x96\\xa4 Table");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
// Insert table
QAction *tableAct = m_toolbar->addAction("Tbl");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
layout->addWidget(m_toolbar);
// Signature button
m_signatureButton = new QToolButton();
m_signatureButton->setText("Signature");
m_signatureButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_signatureButton->setPopupMode(QToolButton::MenuButtonPopup);
m_signatureMenu = new QMenu(m_signatureButton);
m_signatureButton->setMenu(m_signatureMenu);
m_signatureButton->setStyleSheet(
"QToolButton { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 3px; padding: 4px 6px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }"
);
connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked);
m_toolbar->addWidget(m_signatureButton);
m_toolbar->addSeparator();
layout->addWidget(m_toolbar);
}
void RichTextEditor::onBold() {
@@ -244,12 +261,99 @@ void RichTextEditor::onInsertTable() {
cursor.insertTable(rows, cols, tableFmt);
}
// ===================== ComposeView =====================
ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
setupUI();
// Connect the rich text editor's signature signal to our slot
connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked);
}
ComposeView::~ComposeView() {
// Destructor implementation
}
void ComposeView::onSignatureClicked() {
// This slot is called when the signature button in the rich text editor is clicked
// The actual menu handling is done in RichTextEditor, so we just need to
// handle any ComposeView-specific logic here if needed
}
void ComposeView::onSignatureEditRequested() {
// This slot will be connected to the "Edit Signature..." action in the menu
// For now, we'll just show a simple input dialog
QString currentSignature = "";
// In a real implementation, we would get the current signature for the selected account
// For now, we'll use an empty string
bool ok;
QString newSignature = QInputDialog::getMultiLineText(this, "Edit Signature",
"Enter your signature:",
currentSignature, &ok);
if (ok && !newSignature.isEmpty()) {
// In a real implementation, we would save this signature for the selected account
// For now, we'll just insert it at the cursor position
QTextCursor cursor = m_bodyEditor->textCursor();
cursor.insertHtml(newSignature);
}
}
void ComposeView::setAccountService(AccountService *service)
{
m_accountService = service;
populateAccountCombo();
}
void ComposeView::populateAccountCombo()
{
if (!m_accountService) {
qWarning() << "AccountService not set";
return;
}
m_accountCombo->clear();
m_accountCombo->addItem(tr("Select Account..."), QVariant());
QVector<Account> accounts = m_accountService->getAllAccounts();
for (const Account &account : accounts) {
m_accountCombo->addItem(account.email(), account.id());
}
// Select the first account by default if there are accounts
if (accounts.size() > 0) {
m_accountCombo->setCurrentIndex(1); // Skip the placeholder item
m_currentAccountId = accounts.first().id();
}
}
void ComposeView::onAccountChanged(int index)
{
if (index <= 0) {
// Placeholder item selected or invalid index
m_currentAccountId = -1;
return;
}
m_currentAccountId = m_accountCombo->itemData(index).toInt();
loadSignatureForCurrentAccount();
}
void ComposeView::loadSignatureForCurrentAccount()
{
if (!m_accountService || m_currentAccountId <= 0) {
// Clear signature if no account selected
m_bodyEditor->clear();
return;
}
Account* account = m_accountService->findAccountById(m_currentAccountId);
if (account) {
// For now, we'll just clear the editor since we don't have a signature field in Account yet
// In a real implementation, we would retrieve and display the signature
m_bodyEditor->clear();
} else {
m_bodyEditor->clear();
}
}
void ComposeView::setupUI() {
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20, 15, 20, 15);
@@ -271,7 +375,7 @@ void ComposeView::setupUI() {
m_subjectField->setFont(subjectFont);
headerLayout->addWidget(m_subjectField, 1);
m_detachButton = new QPushButton("\\xe2\\x87\\xa5 Detach");
m_detachButton = new QPushButton("\xe2\x87\xa5 Detach");
m_detachButton->setToolTip("Open compose window in a separate window");
m_detachButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 12px; color: #555; font-size: 12px; }"
@@ -288,6 +392,27 @@ void ComposeView::setupUI() {
line1->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line1);
// From: field + Account selector
QHBoxLayout *fromLayout = new QHBoxLayout();
QLabel *fromLabel = new QLabel("From:");
fromLabel->setFixedWidth(40);
fromLabel->setStyleSheet("font-weight: bold; color: #555;");
m_accountCombo = new QComboBox();
m_accountCombo->setFixedWidth(180);
m_accountCombo->setStyleSheet(
"QComboBox { border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 8px; "
"background: white; min-height: 20px; }"
"QComboBox:hover { border-color: #bdbdbd; }"
"QComboBox:focus { border-color: #1976D2; }"
"QComboBox::drop-down { border: none; width: 20px; }"
"QComboBox::down-arrow { image: url(:/icons/dropdown.png); width: 10px; height: 10px; }"
);
connect(m_accountCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ComposeView::onAccountChanged);
fromLayout->addWidget(fromLabel);
fromLayout->addWidget(m_accountCombo, 1);
mainLayout->addLayout(fromLayout);
// To: field + Cc/Bcc toggle buttons
QHBoxLayout *toLayout = new QHBoxLayout();
QLabel *toLabel = new QLabel("To:");
@@ -462,7 +587,8 @@ void ComposeView::onSendClicked() {
m_bccVisible ? m_bccField->text() : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
QDateTime() // null = send now
QDateTime(), // null = send now
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
);
}
@@ -473,7 +599,8 @@ void ComposeView::onScheduleClicked() {
m_bccVisible ? m_bccField->text() : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
m_schedulePicker->dateTime()
m_schedulePicker->dateTime(),
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
);
}
+27 -4
View File
@@ -1,5 +1,5 @@
#ifndef COMPOSEVIEW_H
#define COMPOSEVIEW_H
#ifndef COMPOSE_VIEW_H
#define COMPOSE_VIEW_H
#include <QWidget>
#include <QTextEdit>
@@ -15,7 +15,11 @@
#include <QDateTimeEdit>
#include <QFontComboBox>
#include <QSpinBox>
#include <QComboBox>
#include "models/EmailCompositionModel.h"
#include "services/accountservice.h"
class AccountService;
class RichTextEditor : public QTextEdit {
Q_OBJECT
@@ -23,6 +27,10 @@ public:
explicit RichTextEditor(QWidget *parent = nullptr);
void setupToolbar(QVBoxLayout *layout);
signals:
void signatureClicked();
void signatureEditRequested();
public slots:
void onBold();
void onItalic();
@@ -44,19 +52,25 @@ private:
QToolBar *m_toolbar;
QFontComboBox *m_fontCombo;
QSpinBox *m_fontSizeSpin;
QToolButton *m_signatureButton;
QMenu *m_signatureMenu;
};
class ComposeView : public QWidget {
Q_OBJECT
public:
explicit ComposeView(QWidget *parent = nullptr);
~ComposeView() override;
void setAccountService(AccountService *service);
signals:
void compositionFinished();
void detachRequested(QWidget *widget);
void sendRequested(const QString &to, const QString &cc, const QString &bcc,
const QString &subject, const QString &body,
const QDateTime &scheduleTime);
const QDateTime &scheduleTime,
const QString &fromAddress);
void discardRequested();
public slots:
@@ -72,10 +86,17 @@ private slots:
void onSendClicked();
void onScheduleClicked();
void onDetachClicked();
void onAccountChanged(int index);
void onSignatureClicked();
void onSignatureEditRequested();
private:
void setupUI();
void populateAccountCombo();
void loadSignatureForCurrentAccount();
EmailCompositionModel *m_compositionModel;
AccountService *m_accountService = nullptr;
// UI components
QLineEdit *m_toField = nullptr;
QLineEdit *m_subjectField = nullptr;
@@ -97,8 +118,10 @@ private:
QAction *m_sendNowAction = nullptr;
QAction *m_scheduleAction = nullptr;
QToolButton *m_sendSplit = nullptr;
QComboBox *m_accountCombo = nullptr;
bool m_ccVisible = false;
bool m_bccVisible = false;
int m_currentAccountId = -1;
};
#endif // COMPOSEVIEW_H
#endif // COMPOSE_VIEW_H
+1
View File
@@ -18,6 +18,7 @@ public:
~MailListView() override = default;
void setModel(EmailListModel *model);
QTableView* tableView() const { return m_tableView; }
signals:
void emailSelected(int mailId);
+91 -21
View File
@@ -19,9 +19,20 @@ MainMainWindow::MainMainWindow(QWidget *parent)
connectModels();
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
// Setup progress bar in status bar
m_progressBar = new QProgressBar(this);
m_progressBar->setMaximumWidth(120);
m_progressBar->setVisible(false);
statusBar()->addPermanentWidget(m_progressBar);
// Connect mail service progress/status signals
connect(m_mailService, &MailService::progressChanged, this, &MainMainWindow::onProgressChanged);
connect(m_mailService, &MailService::statusMessage, this, &MainMainWindow::onStatusMessage);
// Provide account service to settings view
m_settingsView->setAccountService(m_accountService);
}
void MainMainWindow::setupUI() {
void MainMainWindow::setupUI()
{
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
@@ -58,7 +69,7 @@ void MainMainWindow::setupUI() {
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime) {
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime, const QString &fromAddr) {
QString msg;
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
@@ -83,16 +94,16 @@ void MainMainWindow::setupUI() {
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle("Compose - Wino Mail");
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(composeView);
composeView->setMinimumSize(800, 600);
composeView->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Compose view detached to separate window", 3000);
});
m_stack->addWidget(m_composeView);
@@ -103,6 +114,8 @@ void MainMainWindow::setupUI() {
AccountSetupDialog dlg(m_accountService, this);
dlg.exec();
});
connect(m_settingsView, &SettingsView::accountEditRequested, this, &MainMainWindow::onAccountEditRequested);
connect(m_settingsView, &SettingsView::accountDeleteRequested, this, &MainMainWindow::onAccountDeleteRequested);
connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) {
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
@@ -123,7 +136,8 @@ void MainMainWindow::setupUI() {
switchToPage(PageMail);
}
void MainMainWindow::setupSidebar() {
void MainMainWindow::setupSidebar()
{
m_sidebar = new QListWidget();
m_sidebar->setFixedWidth(64);
m_sidebar->setIconSize(QSize(24, 24));
@@ -146,7 +160,8 @@ void MainMainWindow::setupSidebar() {
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged);
}
void MainMainWindow::setupMailPage() {
void MainMainWindow::setupMailPage()
{
m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
@@ -191,9 +206,11 @@ void MainMainWindow::setupMailPage() {
mailLayout->addWidget(m_folderSplitter);
}
void MainMainWindow::connectModels() {
void MainMainWindow::connectModels()
{
m_accountService = new AccountService(this);
m_mailService = new MailService(this);
m_mailService = new MailService(m_accountService, this);
m_composeView->setAccountService(m_accountService);
m_folderModel = new FolderListModel(m_accountService, this);
m_emailModel = new EmailListModel(this);
@@ -206,11 +223,13 @@ void MainMainWindow::connectModels() {
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
}
void MainMainWindow::onNavChanged(int index) {
void MainMainWindow::onNavChanged(int index)
{
switchToPage(static_cast<Page>(index));
}
void MainMainWindow::switchToPage(int pageIndex) {
void MainMainWindow::switchToPage(int pageIndex)
{
m_stack->setCurrentIndex(pageIndex);
m_sidebar->blockSignals(true);
m_sidebar->setCurrentRow(pageIndex);
@@ -219,7 +238,8 @@ void MainMainWindow::switchToPage(int pageIndex) {
// Show/hide toolbar actions per page
}
void MainMainWindow::onFolderSelected(const QModelIndex &index) {
void MainMainWindow::onFolderSelected(const QModelIndex &index)
{
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
@@ -240,7 +260,8 @@ void MainMainWindow::onFolderSelected(const QModelIndex &index) {
}
}
void MainMainWindow::onEmailSelected(int mailId) {
void MainMainWindow::onEmailSelected(int mailId)
{
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
@@ -256,11 +277,13 @@ void MainMainWindow::onEmailSelected(int mailId) {
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested() {
void MainMainWindow::onComposeRequested()
{
switchToPage(PageCompose);
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item) {
void MainMainWindow::onReaderReplyRequested(const MailItem *item)
{
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
@@ -268,29 +291,32 @@ void MainMainWindow::onReaderReplyRequested(const MailItem *item) {
switchToPage(PageCompose);
}
void MainMainWindow::onNewMessage() {
void MainMainWindow::onNewMessage()
{
switchToPage(PageCompose);
}
void MainMainWindow::openMailInIndependentWindow(int mailId) {
void MainMainWindow::openMailInIndependentWindow(int mailId)
{
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) return;
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject()));
ReaderView *detachedReader = new ReaderView();
detachedReader->setMailItem(&item.value());
detachedWin->setCentralWidget(detachedReader);
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Opened mail in independent window", 3000);
}
void MainMainWindow::createToolBar() {
void MainMainWindow::createToolBar()
{
m_toolBar = addToolBar("Main Toolbar");
m_toolBar->setMovable(false);
@@ -317,4 +343,48 @@ void MainMainWindow::createToolBar() {
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
});
}
void MainMainWindow::onProgressChanged(int percent)
{
if (percent < 0 || percent > 100) {
m_progressBar->setVisible(false);
return;
}
m_progressBar->setValue(percent);
m_progressBar->setVisible(true);
}
void MainMainWindow::onStatusMessage(const QString &msg)
{
// Show temporary message in status bar (timeout 5000 ms)
statusBar()->showMessage(msg, 5000);
}
void MainMainWindow::onAddAccountRequested()
{
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->setWindowFlag(Qt::Window, true); // make it an independent window
dialog->show();
}
void MainMainWindow::onAccountEditRequested(int accountId)
{
QMessageBox::information(this, "Debug", QString("Edit account slot called for ID %1").arg(accountId));
Account* account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage(QString("Error: account %1 not found").arg(accountId), 3000);
return;
}
AccountSetupDialog dlg(m_accountService, this);
dlg.loadAccountForEditing(*account);
dlg.exec();
delete account;
}
void MainMainWindow::onAccountDeleteRequested(int accountId)
{
Q_UNUSED(accountId);
statusBar()->showMessage(QString("Delete account %1 requested").arg(accountId), 3000);
}
+8
View File
@@ -6,6 +6,7 @@
#include <QSplitter>
#include <QTreeView>
#include <QStatusBar>
#include <QProgressBar>
#include <QToolBar>
#include <QAction>
@@ -35,6 +36,11 @@ private slots:
void onReaderReplyRequested(const MailItem *item);
void onNewMessage();
void openMailInIndependentWindow(int mailId);
void onAddAccountRequested();
void onAccountEditRequested(int accountId);
void onAccountDeleteRequested(int accountId);
void onProgressChanged(int percent);
void onStatusMessage(const QString &msg);
private:
void setupUI();
@@ -78,4 +84,6 @@ private:
QToolBar *m_toolBar;
int m_currentFolderId;
int m_currentMailId;
QProgressBar *m_progressBar;
};
+36 -35
View File
@@ -43,7 +43,7 @@ int FolderTreeItem::row() const
return 0;
}
FolderTreeItem *FolderTreeItem::parentItem()
FolderTreeItem *FolderTreeItem::parentItem() const
{
return m_parentItem;
}
@@ -53,9 +53,14 @@ FolderTreeItem *FolderTreeItem::parentItem()
FolderListModel::FolderListModel(AccountService *accountService, QObject *parent)
: QAbstractItemModel(parent),
m_accountService(accountService),
m_rootItem(new FolderTreeItem(FolderTreeItem::AccountNode, "Root"))
m_rootItem(new FolderTreeItem(FolderTreeItem::AccountNode, QStringLiteral("Root")))
{
refresh();
// Connect to account list changes
if (m_accountService) {
connect(m_accountService, &AccountService::accountListChanged,
this, &FolderListModel::onAccountListChanged);
}
}
FolderListModel::~FolderListModel()
@@ -151,27 +156,38 @@ QVariant FolderListModel::data(const QModelIndex &index, int role) const
if (role == UnreadCountRole) {
if (item->type() == FolderTreeItem::FolderNode) {
Folder folder = item->data().value<Folder>();
return folder.unreadCount();
// TODO: compute unread count
return 0;
}
}
return QVariant();
}
bool FolderListModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
Q_UNUSED(index);
Q_UNUSED(value);
Q_UNUSED(role);
return false;
}
Qt::ItemFlags FolderListModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::NoItemFlags;
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
QHash<int, QByteArray> FolderListModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[Qt::DisplayRole] = "display";
roles[NameRole] = "name";
roles[ItemTypeRole] = "itemType";
roles[AccountIdRole] = "accountId";
roles[FolderIdRole] = "folderId";
roles[NameRole] = "name";
roles[UnreadCountRole] = "unreadCount";
return roles;
}
@@ -182,44 +198,31 @@ void FolderListModel::refresh()
clearModel();
setupModelData();
endResetModel();
qDebug() << "FolderListModel refreshed";
}
void FolderListModel::onAccountListChanged()
{
refresh();
}
void FolderListModel::setupModelData()
{
if (!m_accountService)
return;
QVector<Account> accounts = m_accountService->getAllAccounts();
// If no accounts exist yet, add one sample account with default folders
if (accounts.isEmpty()) {
Account sample(1, "javi@example.com", "Javi's Email",
AccountType::IMAP);
accounts.append(sample);
}
for (const Account &acc : accounts) {
QVariant accVariant;
accVariant.setValue(acc);
FolderTreeItem *accountItem = new FolderTreeItem(FolderTreeItem::AccountNode, accVariant, m_rootItem);
QVariant accData;
accData.setValue(acc);
FolderTreeItem *accountItem = new FolderTreeItem(FolderTreeItem::AccountNode, accData, m_rootItem);
m_rootItem->appendChild(accountItem);
// Get folders for this account
// Load folders for this account
QVector<Folder> folders = FolderDao::findByAccountId(acc.id());
// If no folders exist yet, create default ones
if (folders.isEmpty()) {
QStringList defaultFolders = {"Inbox", "Sent", "Drafts", "Trash", "Spam"};
for (const QString &name : defaultFolders) {
Folder f;
f.setName(name);
f.setAccountId(acc.id());
folders.append(f);
}
}
for (const Folder &folder : folders) {
QVariant folderVariant;
folderVariant.setValue(folder);
FolderTreeItem *folderItem = new FolderTreeItem(FolderTreeItem::FolderNode, folderVariant, accountItem);
QVariant folderData;
folderData.setValue(folder);
FolderTreeItem *folderItem = new FolderTreeItem(FolderTreeItem::FolderNode, folderData, accountItem);
accountItem->appendChild(folderItem);
}
}
@@ -228,6 +231,4 @@ void FolderListModel::setupModelData()
void FolderListModel::clearModel()
{
m_rootItem->clearChildren();
}
#include "FolderListModel.moc"
}
+7 -3
View File
@@ -23,7 +23,7 @@ public:
FolderTreeItem *child(int row);
int childCount() const;
int row() const;
FolderTreeItem *parentItem();
FolderTreeItem *parentItem() const;
Type type() const { return m_type; }
QVariant data() const { return m_data; }
@@ -54,17 +54,21 @@ public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QHash<int, QByteArray> roleNames() const override;
public slots:
void refresh();
private slots:
void onAccountListChanged();
private:
void setupModelData();
void clearModel();
FolderTreeItem *m_rootItem;
AccountService *m_accountService;
FolderTreeItem *m_rootItem;
};
#endif // FOLDERLISTMODEL_H
+156 -76
View File
@@ -2,6 +2,18 @@
#include "services/accountservice.h"
#include "core/models/account.h"
#include <QDebug>
#include <QMessageBox>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QListWidget>
#include <QPushButton>
#include <QLabel>
#include <QFont>
#include <QButtonGroup>
#include <QComboBox>
#include <QCheckBox>
SettingsView::SettingsView(QWidget *parent) : QWidget(parent) {
setupUI();
}
@@ -19,124 +31,128 @@ void SettingsView::setupUI() {
}
QWidget* SettingsView::createAccountsTab() {
QWidget *w = new QWidget();
QVBoxLayout *layout = new QVBoxLayout(w);
QWidget *widget = new QWidget();
QVBoxLayout *layout = new QVBoxLayout(widget);
layout->setContentsMargins(20, 20, 20, 20);
layout->setSpacing(15);
QLabel *sectionTitle = new QLabel("Email Accounts");
QFont titleFont = sectionTitle->font();
QLabel *title = new QLabel("Email Accounts");
QFont titleFont = title->font();
titleFont.setPointSize(16);
titleFont.setBold(true);
sectionTitle->setFont(titleFont);
layout->addWidget(sectionTitle);
title->setFont(titleFont);
layout->addWidget(title);
m_accountList = new QListWidget();
m_accountList->setAlternatingRowColors(true);
m_accountList->setFrameShape(QFrame::NoFrame);
m_accountList->setSelectionMode(QAbstractItemView::SingleSelection);
m_accountList->setStyleSheet(
"QListWidget { background: #ffffff; border: 1px solid #e0e0e0; border-radius: 4px; }"
"QListWidget::item { padding: 12px; border-bottom: 1px solid #f0f0f0; }"
"QListWidget::item:selected { background: #e3f2fd; }"
);
layout->addWidget(m_accountList, 1);
layout->addWidget(m_accountList);
QPushButton *addBtn = new QPushButton("+ Add Email Account");
addBtn->setStyleSheet(
"QPushButton { background: #1976D2; color: white; border: none; border-radius: 4px; padding: 10px 20px; font-weight: bold; }"
"QPushButton:hover { background: #1565C0; }"
QHBoxLayout *buttonLayout = new QHBoxLayout();
m_addBtn = new QPushButton("Add Account");
m_addBtn->setStyleSheet(
"QPushButton { background: #FFEB3B; color: white; border: none; border-radius: 4px; "
"padding: 8px 16px; font-weight: bold; }"
"QPushButton:hover { background: #FDD835; }"
);
connect(addBtn, &QPushButton::clicked, this, &SettingsView::accountAddRequested);
layout->addWidget(addBtn);
buttonLayout->addWidget(m_addBtn);
m_editBtn = new QPushButton("Edit");
m_editBtn->setEnabled(false);
m_editBtn->setStyleSheet(
"QPushButton { background: #FFA726; color: white; border: none; border-radius: 4px; "
"padding: 8px 16px; font-weight: bold; }"
"QPushButton:hover { background: #FB8C00; }"
);
m_deleteBtn = new QPushButton("Delete");
m_deleteBtn->setEnabled(false);
m_deleteBtn->setStyleSheet(
"QPushButton { background: #EF5350; color: white; border: none; border-radius: 4px; "
"padding: 8px 16px; font-weight: bold; }"
"QPushButton:hover { background: #E53935; }"
);
buttonLayout->addWidget(m_editBtn);
buttonLayout->addWidget(m_deleteBtn);
buttonLayout->addStretch();
layout->addLayout(buttonLayout);
return w;
connect(m_accountList, &QListWidget::itemSelectionChanged, this, &SettingsView::onAccountSelectionChanged);
connect(m_editBtn, &QPushButton::clicked, this, &SettingsView::onEditClicked);
connect(m_deleteBtn, &QPushButton::clicked, this, &SettingsView::onDeleteClicked);
connect(m_addBtn, &QPushButton::clicked, this, &SettingsView::accountAddRequested);
return widget;
}
QWidget* SettingsView::createGeneralTab() {
QWidget *w = new QWidget();
QVBoxLayout *layout = new QVBoxLayout(w);
QWidget *widget = new QWidget();
QVBoxLayout *layout = new QVBoxLayout(widget);
layout->setContentsMargins(20, 20, 20, 20);
layout->setSpacing(15);
QLabel *sectionTitle = new QLabel("General Settings");
QFont titleFont = sectionTitle->font();
QLabel *title = new QLabel("General Settings");
QFont titleFont = title->font();
titleFont.setPointSize(16);
titleFont.setBold(true);
sectionTitle->setFont(titleFont);
layout->addWidget(sectionTitle);
title->setFont(titleFont);
layout->addWidget(title);
QCheckBox *startOnLogin = new QCheckBox("Start application on login");
startOnLogin->setChecked(true);
connect(startOnLogin, &QCheckBox::toggled, [this](bool checked) {
emit settingChanged("start_on_login", checked);
connect(startOnLogin, &QCheckBox::toggled, this, [this](bool checked) {
emit settingChanged("start_on_login", QVariant(checked));
});
layout->addWidget(startOnLogin);
QCheckBox *enableNotifications = new QCheckBox("Enable notifications");
enableNotifications->setChecked(true);
connect(enableNotifications, &QCheckBox::toggled, [this](bool checked) {
emit settingChanged("notifications_enabled", checked);
connect(enableNotifications, &QCheckBox::toggled, this, [this](bool checked) {
emit settingChanged("enable_notifications", QVariant(checked));
});
layout->addWidget(enableNotifications);
QCheckBox *minimizeToTray = new QCheckBox("Minimize to tray on close");
minimizeToTray->setChecked(true);
connect(minimizeToTray, &QCheckBox::toggled, [this](bool checked) {
emit settingChanged("minimize_to_tray", checked);
connect(minimizeToTray, &QCheckBox::toggled, this, [this](bool checked) {
emit settingChanged("minimize_to_tray", QVariant(checked));
});
layout->addWidget(minimizeToTray);
layout->addSpacing(10);
QHBoxLayout *syncLayout = new QHBoxLayout();
QLabel *syncLabel = new QLabel("Sync Interval:");
syncLabel->setStyleSheet("font-weight: bold; color: #555;");
m_syncIntervalCombo = new QComboBox();
m_syncIntervalCombo->addItem("15 minutes", 15);
m_syncIntervalCombo->addItem("30 minutes", 30);
m_syncIntervalCombo->addItem("60 minutes", 60);
m_syncIntervalCombo->addItem("120 minutes", 120);
m_syncIntervalCombo->setCurrentIndex(1);
connect(m_syncIntervalCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), [this](int idx) {
emit settingChanged("sync_interval", m_syncIntervalCombo->itemData(idx));
});
syncLayout->addWidget(syncLabel);
syncLayout->addWidget(m_syncIntervalCombo);
syncLayout->addStretch();
layout->addLayout(syncLayout);
layout->addStretch();
return w;
return widget;
}
QWidget* SettingsView::createAppearanceTab() {
QWidget *w = new QWidget();
QVBoxLayout *layout = new QVBoxLayout(w);
QWidget *widget = new QWidget();
QVBoxLayout *layout = new QVBoxLayout(widget);
layout->setContentsMargins(20, 20, 20, 20);
layout->setSpacing(15);
QLabel *sectionTitle = new QLabel("Appearance");
QFont titleFont = sectionTitle->font();
QLabel *title = new QLabel("Appearance");
QFont titleFont = title->font();
titleFont.setPointSize(16);
titleFont.setBold(true);
sectionTitle->setFont(titleFont);
layout->addWidget(sectionTitle);
title->setFont(titleFont);
layout->addWidget(title);
QLabel *themeLabel = new QLabel("Theme:");
themeLabel->setStyleSheet("font-weight: bold; color: #555;");
QLabel *themeLabel = new QLabel("Theme");
QFont labelFont = themeLabel->font();
labelFont.setBold(true);
themeLabel->setFont(labelFont);
layout->addWidget(themeLabel);
m_themeGroup = new QButtonGroup(this);
QRadioButton *lightRadio = new QRadioButton("Light");
QRadioButton *darkRadio = new QRadioButton("Dark");
QRadioButton *systemRadio = new QRadioButton("System");
lightRadio->setChecked(true);
m_themeGroup->addButton(lightRadio, 0);
m_themeGroup->addButton(darkRadio, 1);
m_themeGroup->addButton(systemRadio, 2);
connect(m_themeGroup, QOverload<int>::of(&QButtonGroup::idClicked), [this](int id) {
QHBoxLayout *themeLayout = new QHBoxLayout();
themeLayout->addWidget(lightRadio);
themeLayout->addWidget(darkRadio);
themeLayout->addWidget(systemRadio);
layout->addLayout(themeLayout);
connect(m_themeGroup, QOverload<int>::of(&QButtonGroup::idClicked), this, [this](int id) {
QString theme;
switch (id) {
case 0: theme = "light"; break;
@@ -146,19 +162,83 @@ QWidget* SettingsView::createAppearanceTab() {
emit themeChanged(theme);
});
layout->addWidget(lightRadio);
layout->addWidget(darkRadio);
layout->addWidget(systemRadio);
layout->addSpacing(15);
QCheckBox *deepinTheme = new QCheckBox("Use Deepin theme");
QCheckBox *deepinTheme = new QCheckBox("Deepin theme (experimental)");
deepinTheme->setChecked(true);
connect(deepinTheme, &QCheckBox::toggled, [this](bool checked) {
emit settingChanged("deepin_theme", checked);
connect(deepinTheme, &QCheckBox::toggled, this, [this](bool checked) {
emit settingChanged("deepin_theme", QVariant(checked));
});
layout->addWidget(deepinTheme);
layout->addStretch();
return w;
}
return widget;
}
void SettingsView::setAccountService(AccountService *service) {
m_accountService = service;
if (m_accountService) {
loadAccounts();
connect(m_accountService, &AccountService::accountListChanged, this, &SettingsView::onAccountListChanged);
}
}
void SettingsView::onAccountListChanged() {
loadAccounts();
}
void SettingsView::loadAccounts() {
if (!m_accountService) {
qDebug() << "AccountService not set";
return;
}
m_accountList->clear();
QList<Account> accounts = m_accountService->getAllAccounts();
qDebug() << "Loaded" << accounts.size() << "accounts from AccountService";
for (const Account &acc : accounts) {
QString display = acc.displayName().isEmpty() ? acc.email() : acc.displayName();
QString text = QString("%1 (%2)").arg(acc.email(), display);
QListWidgetItem *item = new QListWidgetItem(text, m_accountList);
item->setData(Qt::UserRole, acc.id());
m_accountList->addItem(item);
}
if (m_accountList->count() > 0) {
m_accountList->setCurrentRow(0);
}
}
void SettingsView::onAccountSelectionChanged()
{
QList<QListWidgetItem*> items = m_accountList->selectedItems();
qDebug() << "SettingsView::onAccountSelectionChanged, selected items count:" << items.size();
if (items.isEmpty()) {
m_selectedAccountId = -1;
m_editBtn->setEnabled(false);
m_deleteBtn->setEnabled(false);
return;
}
QListWidgetItem *item = items.first();
m_selectedAccountId = item->data(Qt::UserRole).toInt();
qDebug() << "SettingsView::onAccountSelectionChanged, selected account id:" << m_selectedAccountId;
m_editBtn->setEnabled(true);
m_deleteBtn->setEnabled(true);
}
void SettingsView::onEditClicked() {
qDebug() << "SettingsView::onEditClicked called, selected account id:" << m_selectedAccountId;
if (m_selectedAccountId != -1 && m_accountService) {
emit accountEditRequested(m_selectedAccountId);
}
}
void SettingsView::onDeleteClicked() {
if (m_selectedAccountId != -1 && m_accountService) {
QMessageBox msgBox(this);
msgBox.setWindowTitle("Delete Account");
Account* account = m_accountService->findAccountById(m_selectedAccountId);
QString accountName = account ? account->email() : QString::number(m_selectedAccountId);
msgBox.setText(QString("Are you sure you want to delete the account \"%1\"?").arg(accountName));
msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
msgBox.setDefaultButton(QMessageBox::No);
if (msgBox.exec() == QMessageBox::Yes) {
emit accountDeleteRequested(m_selectedAccountId);
}
}
}
+18 -2
View File
@@ -12,6 +12,7 @@
#include <QRadioButton>
#include <QButtonGroup>
#include <QVariant>
#include "services/accountservice.h"
class SettingsView : public QWidget {
Q_OBJECT
@@ -20,13 +21,22 @@ public:
explicit SettingsView(QWidget *parent = nullptr);
~SettingsView() override = default;
void setAccountService(AccountService *service);
signals:
void accountAddRequested();
void accountEditRequested(int accountIndex);
void accountDeleteRequested(int accountIndex);
void accountEditRequested(int accountId);
void accountDeleteRequested(int accountId);
void themeChanged(const QString &theme);
void settingChanged(const QString &key, const QVariant &value);
private slots:
void onAccountListChanged();
void loadAccounts();
void onEditClicked();
void onDeleteClicked();
void onAccountSelectionChanged();
private:
void setupUI();
QWidget* createAccountsTab();
@@ -37,4 +47,10 @@ private:
QListWidget *m_accountList;
QComboBox *m_syncIntervalCombo;
QButtonGroup *m_themeGroup;
QPushButton *m_editBtn;
QPushButton *m_addBtn;
QPushButton *m_deleteBtn;
AccountService *m_accountService;
int m_selectedAccountId;
};