diff --git a/src/core/accountsetupdialoglauncher.cpp b/src/core/accountsetupdialoglauncher.cpp index 1023813..4f2381c 100644 --- a/src/core/accountsetupdialoglauncher.cpp +++ b/src/core/accountsetupdialoglauncher.cpp @@ -17,24 +17,26 @@ void AccountSetupDialogLauncher::setupAccountManually(const Account &account) { qDebug() << "[AccountSetupDialogLauncher] Setting up account:" << account.email(); - // Save to database - bool created = AccountDao::insert(account); - if (!created) { + // Save to database and retain the generated id for the synchronizer/event. + const qint64 id = AccountDao::insert(account); + if (id < 0) { qWarning() << "[AccountSetupDialogLauncher] Failed to save account to DB"; emit accountSetupFailed("Failed to save account"); return; } + Account storedAccount = account; + storedAccount.setId(static_cast(id)); // Initialize synchronizer - initializeSynchronizer(account); + initializeSynchronizer(storedAccount); // Publish event WinoMail::Events::AccountAddedEvent event; - event.account = account; + event.account = storedAccount; EVENT_BUS.publish(event); qDebug() << "[AccountSetupDialogLauncher] Account created successfully:" << account.email(); - emit accountSetupCompleted(account); + emit accountSetupCompleted(storedAccount); } void AccountSetupDialogLauncher::initializeSynchronizer(const Account &account) diff --git a/src/core/emailmanager.cpp b/src/core/emailmanager.cpp index d9c7373..13817f3 100644 --- a/src/core/emailmanager.cpp +++ b/src/core/emailmanager.cpp @@ -1,6 +1,9 @@ #include "emailmanager.h" #include #include +#include +#include "db/dao/mailitemdao.h" +#include "services/mimestorage.h" EmailManager::EmailManager(QObject *parent) : QObject(parent) @@ -9,29 +12,33 @@ EmailManager::EmailManager(QObject *parent) MailItem EmailManager::getMailItemById(qint64 id) const { - Q_UNUSED(id); - qDebug() << "[EmailManager] getMailItemById (stub)"; - return MailItem(); + const auto item = MailItemDao::findById(id); + return item.value_or(MailItem()); } QString EmailManager::getStorageDirectory() const { - qDebug() << "[EmailManager] getStorageDirectory (stub)"; - return QString(); + return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/mails"); } QString EmailManager::getEmailHtmlById(qint64 id) const { - Q_UNUSED(id); - qDebug() << "[EmailManager] getEmailHtmlById (stub)"; - return QString("

GMime not available

"); + const auto item = MailItemDao::findById(id); + if (!item) return QString(); + if (!item->bodyHtml().isEmpty()) return item->bodyHtml(); + if (item->fileId().isEmpty()) return QString(); + MimeStorageService storage; + ParsedMimeMessage parsed; + return storage.parseMessage(storage.readEmlFile(item->fileId()), parsed) ? parsed.bodyHtml : QString(); } QString EmailManager::convertEmlToHtml(const QString& emlFilePath) const { - Q_UNUSED(emlFilePath); - qDebug() << "[EmailManager] convertEmlToHtml (stub)"; - return QString(); + QFile file(emlFilePath); + if (!file.open(QIODevice::ReadOnly)) return QString(); + MimeStorageService storage; + ParsedMimeMessage parsed; + return storage.parseMessage(file.readAll(), parsed) ? parsed.bodyHtml : QString(); } bool EmailManager::sendEmail(const QString& to, const QString& subject, const QString& body) @@ -39,8 +46,7 @@ bool EmailManager::sendEmail(const QString& to, const QString& subject, const QS Q_UNUSED(to); Q_UNUSED(subject); Q_UNUSED(body); - qDebug() << "[EmailManager] sendEmail (stub)"; return false; } -#include "emailmanager.moc" \ No newline at end of file +#include "emailmanager.moc" diff --git a/src/core/gmailauthenticator.cpp b/src/core/gmailauthenticator.cpp index 86e3300..0f9e82b 100644 --- a/src/core/gmailauthenticator.cpp +++ b/src/core/gmailauthenticator.cpp @@ -4,9 +4,7 @@ #include #include -// Gmail OAuth2 configuration -static const char* GMAIL_CLIENT_ID = "your-gmail-client-id.apps.googleusercontent.com"; -static const char* GMAIL_CLIENT_SECRET = "your-gmail-client-secret"; +// Configure the OAuth application outside the source tree. static const char* GMAIL_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"; static const char* GMAIL_TOKEN_URL = "https://oauth2.googleapis.com/token"; static const char* GMAIL_SCOPES = "https://www.googleapis.com/auth/gmail.modify https://www.googleapis.com/auth/gmail.readonly email profile"; @@ -15,8 +13,8 @@ GmailAuthenticator::GmailAuthenticator(QObject *parent) : Authenticator(parent) , m_callbackServer(new OAuthCallbackServer(8080, this)) { - m_clientId = GMAIL_CLIENT_ID; - m_clientSecret = GMAIL_CLIENT_SECRET; + m_clientId = qEnvironmentVariable("WINO_GMAIL_CLIENT_ID"); + m_clientSecret = qEnvironmentVariable("WINO_GMAIL_CLIENT_SECRET"); m_authEndpoint = GMAIL_AUTH_URL; m_tokenEndpoint = GMAIL_TOKEN_URL; m_scopes = GMAIL_SCOPES; @@ -31,6 +29,10 @@ GmailAuthenticator::GmailAuthenticator(QObject *parent) void GmailAuthenticator::authenticate(const QString &email) { + if (m_clientId.isEmpty() || m_clientSecret.isEmpty()) { + emit authenticationFailed("Configure WINO_GMAIL_CLIENT_ID and WINO_GMAIL_CLIENT_SECRET first"); + return; + } m_email = email; m_redirectUri = m_callbackServer->redirectUri(); qDebug() << "[GmailAuthenticator] Starting authentication for:" << email; @@ -93,4 +95,4 @@ void GmailAuthenticator::onTokenReply(QNetworkReply *reply) { reply->deleteLater(); } -#include "gmailauthenticator.moc" \ No newline at end of file +#include "gmailauthenticator.moc" diff --git a/src/core/mailitem.h b/src/core/mailitem.h index 58f48d6..f5f43bd 100644 --- a/src/core/mailitem.h +++ b/src/core/mailitem.h @@ -3,6 +3,7 @@ #include #include #include +#include class MailItem { @@ -68,6 +69,12 @@ public: QString bcc() const { return m_bcc; } void setBcc(const QString& bcc) { m_bcc = bcc; } + // The original message is kept transiently while it is being persisted. + // It is intentionally not stored in SQLite; MimeStorageService writes it + // verbatim to the account/folder mail store. + QByteArray rawMime() const { return m_rawMime; } + void setRawMime(const QByteArray& rawMime) { m_rawMime = rawMime; } + private: qint64 m_id{0}; int m_folderId{0}; @@ -86,4 +93,5 @@ private: QString m_to; QString m_cc; QString m_bcc; -}; \ No newline at end of file + QByteArray m_rawMime; +}; diff --git a/src/core/models/account.cpp b/src/core/models/account.cpp index 31829f8..6ac0b4f 100644 --- a/src/core/models/account.cpp +++ b/src/core/models/account.cpp @@ -1,4 +1,35 @@ #include "account.h" +#include +#include +#include + +#ifdef Q_OS_WIN +#include +#include +#endif + +namespace { + +QByteArray localPasswordKey() +{ + QByteArray seed = QSysInfo::machineUniqueId(); + seed.append('|').append(qgetenv("USERNAME")); + seed.append('|').append(qgetenv("USER")); + seed.append('|').append(QCoreApplication::organizationName().toUtf8()); + seed.append('|').append(QCoreApplication::applicationName().toUtf8()); + return QCryptographicHash::hash(seed, QCryptographicHash::Sha256); +} + +QString decryptLegacyPassword(const QString &encrypted) +{ + const QByteArray data = QByteArray::fromBase64(encrypted.toUtf8()); + static const QByteArray key = QByteArray::fromHex("0123456789ABCDEF0123456789ABCDEF"); + QByteArray result(data.size(), 0); + for (int i = 0; i < data.size(); ++i) result[i] = data[i] ^ key.at(i % key.size()); + return QString::fromUtf8(result); +} + +} Account::Account(int id, const QString& email, const QString& displayName, const QString& signature, @@ -18,27 +49,47 @@ Account::Account(int id, const QString& email, const QString& displayName, 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::fromLatin1(keyHex + i * 2, 2).toInt(&ok, 16); - if (!ok) c = 0; - key.append(c); + const QByteArray input = plainText.toUtf8(); +#ifdef Q_OS_WIN + DATA_BLOB source{static_cast(input.size()), reinterpret_cast(const_cast(input.constData()))}; + DATA_BLOB protectedData{}; + if (CryptProtectData(&source, L"Wino Mail password", nullptr, nullptr, nullptr, + CRYPTPROTECT_UI_FORBIDDEN, &protectedData)) { + const QByteArray output(reinterpret_cast(protectedData.pbData), protectedData.cbData); + LocalFree(protectedData.pbData); + return QStringLiteral("dpapi:") + QString::fromLatin1(output.toBase64()); } - - QByteArray input = plainText.toUtf8(); +#endif + const QByteArray key = localPasswordKey(); QByteArray result(input.size(), 0); - for (int i = 0; i < input.size(); ++i) { - result[i] = input[i] ^ key[i % key.size()]; - } - return result.toBase64(); + for (int i = 0; i < input.size(); ++i) result[i] = input[i] ^ key.at(i % key.size()); + return QStringLiteral("local:") + QString::fromLatin1(result.toBase64()); } QString Account::decryptPassword(const QString& encrypted) { - // Same as encryption since XOR is symmetric - return encryptPassword(encrypted); + if (encrypted.startsWith(QStringLiteral("dpapi:"))) { +#ifdef Q_OS_WIN + const QByteArray data = QByteArray::fromBase64(encrypted.mid(6).toLatin1()); + DATA_BLOB source{static_cast(data.size()), reinterpret_cast(const_cast(data.constData()))}; + DATA_BLOB plain{}; + if (CryptUnprotectData(&source, nullptr, nullptr, nullptr, nullptr, + CRYPTPROTECT_UI_FORBIDDEN, &plain)) { + const QString result = QString::fromUtf8(reinterpret_cast(plain.pbData), plain.cbData); + LocalFree(plain.pbData); + return result; + } +#endif + return QString(); + } + if (encrypted.startsWith(QStringLiteral("local:"))) { + const QByteArray data = QByteArray::fromBase64(encrypted.mid(6).toLatin1()); + const QByteArray key = localPasswordKey(); + QByteArray result(data.size(), 0); + for (int i = 0; i < data.size(); ++i) result[i] = data[i] ^ key.at(i % key.size()); + return QString::fromUtf8(result); + } + // Read passwords written by older versions once; all new writes use the + // user-bound envelope above. + return decryptLegacyPassword(encrypted); } diff --git a/src/core/models/account.h b/src/core/models/account.h index 787c4ae..d4ba758 100644 --- a/src/core/models/account.h +++ b/src/core/models/account.h @@ -67,7 +67,8 @@ public: 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) + /// Protects passwords with the Windows user-bound data protection API. + /// Non-Windows builds use a machine/user-derived compatibility envelope. static QString encryptPassword(const QString& plainText); static QString decryptPassword(const QString& encrypted); diff --git a/src/core/models/account.h.backup b/src/core/models/account.h.backup new file mode 100644 index 0000000..1687f56 --- /dev/null +++ b/src/core/models/account.h.backup @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include +#include + +enum class AccountType { + POP3, + Outlook, + Gmail, + IMAP +}; + +class Account +{ +public: + Account() = default; + Account(int id, const QString& email, const QString& displayName, + AccountType type, const QString& accessToken = QString(), + const QString& refreshToken = QString(), + const QDateTime& tokenExpires = QDateTime()); + + int id() const { return m_id; } + void setId(int id) { m_id = id; } + + QString email() const { return m_email; } + void setEmail(const QString& email) { m_email = email; } + + QString displayName() const { return m_displayName; } + void setDisplayName(const QString& displayName) { m_displayName = displayName; } + + AccountType type() const { return m_type; } + void setType(AccountType type) { m_type = type; } + + QString accessToken() const { return m_accessToken; } + void setAccessToken(const QString& token) { m_accessToken = token; } + + QString refreshToken() const { return m_refreshToken; } + void setRefreshToken(const QString& token) { m_refreshToken = token; } + + QDateTime tokenExpires() const { return m_tokenExpires; } + void setTokenExpires(const QDateTime& expires) { m_tokenExpires = expires; } + + bool isTokenValid() const { + return !m_accessToken.isEmpty() && m_tokenExpires.isValid() && + 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; + AccountType m_type{AccountType::IMAP}; + QString m_accessToken; + QString m_refreshToken; + QDateTime m_tokenExpires; + ConnectionSettings m_connectionSettings; +}; \ No newline at end of file diff --git a/src/core/models/account.h.backup2 b/src/core/models/account.h.backup2 new file mode 100644 index 0000000..13f3e6b --- /dev/null +++ b/src/core/models/account.h.backup2 @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include +#include + +enum class AccountType { + POP3, + Outlook, + Gmail, + IMAP +}; + +class Account +{ +public: + Account() = default; + Account(int id, const QStringAccount(int id, const QString& email, const QString& displayName, email, const QStringAccount(int id, const QString& email, const QString& displayName, displayName, const QStringAccount(int id, const QString& email, const QString& displayName, signature, + AccountType type, const QString& accessToken = QString(), + const QString& refreshToken = QString(), + const QDateTime& tokenExpires = QDateTime()); + + int id() const { return m_id; } + void setId(int id) { m_id = id; } + + QString email() const { return m_email; } + void setEmail(const QString& email) { m_email = email; } + + 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; } + + QString accessToken() const { return m_accessToken; } + void setAccessToken(const QString& token) { m_accessToken = token; } + + QString refreshToken() const { return m_refreshToken; } + void setRefreshToken(const QString& token) { m_refreshToken = token; } + + QDateTime tokenExpires() const { return m_tokenExpires; } + void setTokenExpires(const QDateTime& expires) { m_tokenExpires = expires; } + + bool isTokenValid() const { + return !m_accessToken.isEmpty() && m_tokenExpires.isValid() && + 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; + QString m_signature; + AccountType m_type{AccountType::IMAP}; + QString m_accessToken; + QString m_refreshToken; + QDateTime m_tokenExpires; + ConnectionSettings m_connectionSettings; +}; \ No newline at end of file diff --git a/src/core/outlookauthenticator.cpp b/src/core/outlookauthenticator.cpp index fc5449e..5950817 100644 --- a/src/core/outlookauthenticator.cpp +++ b/src/core/outlookauthenticator.cpp @@ -4,9 +4,7 @@ #include #include -// Microsoft Graph OAuth2 configuration -static const char* OUTLOOK_CLIENT_ID = "your-outlook-client-id"; -static const char* OUTLOOK_CLIENT_SECRET = "your-outlook-client-secret"; +// Configure the OAuth application outside the source tree. static const char* OUTLOOK_AUTH_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; static const char* OUTLOOK_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; static const char* OUTLOOK_SCOPES = "offline_access Mail.ReadWrite Mail.Send User.Read"; @@ -15,8 +13,8 @@ OutlookAuthenticator::OutlookAuthenticator(QObject *parent) : Authenticator(parent) , m_callbackServer(new OAuthCallbackServer(8080, this)) { - m_clientId = OUTLOOK_CLIENT_ID; - m_clientSecret = OUTLOOK_CLIENT_SECRET; + m_clientId = qEnvironmentVariable("WINO_OUTLOOK_CLIENT_ID"); + m_clientSecret = qEnvironmentVariable("WINO_OUTLOOK_CLIENT_SECRET"); m_authEndpoint = OUTLOOK_AUTH_URL; m_tokenEndpoint = OUTLOOK_TOKEN_URL; m_scopes = OUTLOOK_SCOPES; @@ -31,6 +29,10 @@ OutlookAuthenticator::OutlookAuthenticator(QObject *parent) void OutlookAuthenticator::authenticate(const QString &email) { + if (m_clientId.isEmpty() || m_clientSecret.isEmpty()) { + emit authenticationFailed("Configure WINO_OUTLOOK_CLIENT_ID and WINO_OUTLOOK_CLIENT_SECRET first"); + return; + } m_email = email; m_redirectUri = m_callbackServer->redirectUri(); qDebug() << "[OutlookAuthenticator] Starting authentication for:" << email; @@ -85,4 +87,4 @@ void OutlookAuthenticator::onTokenReply(QNetworkReply *reply) { reply->deleteLater(); } -#include "outlookauthenticator.moc" \ No newline at end of file +#include "outlookauthenticator.moc" diff --git a/src/db/dao/accountdao.cpp b/src/db/dao/accountdao.cpp index 747877f..c12e5f1 100644 --- a/src/db/dao/accountdao.cpp +++ b/src/db/dao/accountdao.cpp @@ -2,7 +2,7 @@ qint64 AccountDao::insert(const Account& account) { - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); query.prepare( "INSERT INTO Account (email, displayName, signature, type, accessToken, refreshToken, tokenExpires, " @@ -39,12 +39,13 @@ qint64 AccountDao::insert(const Account& account) bool AccountDao::update(const Account& account) { - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); query.prepare( "UPDATE Account SET " "email = :email, " "displayName = :displayName, " + "signature = :signature, " "type = :type, " "accessToken = :accessToken, " "refreshToken = :refreshToken, " @@ -66,6 +67,7 @@ bool AccountDao::update(const Account& account) query.bindValue(":type", static_cast(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); @@ -86,7 +88,7 @@ bool AccountDao::update(const Account& account) bool AccountDao::remove(int id) { - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); query.prepare("DELETE FROM Account WHERE id = :id"); query.bindValue(":id", id); @@ -100,10 +102,10 @@ bool AccountDao::remove(int id) Account* AccountDao::findById(int id) { - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); query.prepare( - "SELECT id, email, displayName, type, accessToken, refreshToken, tokenExpires, " + "SELECT id, email, displayName, signature, type, accessToken, refreshToken, tokenExpires, " "incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, " "connUsername, connPassword, authMethod " "FROM Account WHERE id = :id"); @@ -144,10 +146,10 @@ Account* AccountDao::findById(int id) QVector AccountDao::findAll() { QVector accounts; - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); if (!query.exec( - "SELECT id, email, displayName, type, accessToken, refreshToken, tokenExpires, " + "SELECT id, email, displayName, signature, type, accessToken, refreshToken, tokenExpires, " "incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, " "connUsername, connPassword, authMethod " "FROM Account")) { @@ -160,6 +162,7 @@ QVector AccountDao::findAll() 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(query.value("type").toInt())); acc.setAccessToken(query.value("accessToken").toString()); acc.setRefreshToken(query.value("refreshToken").toString()); @@ -182,10 +185,10 @@ QVector AccountDao::findAll() Account* AccountDao::findByEmail(const QString& email) { - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); query.prepare( - "SELECT id, email, displayName, type, accessToken, refreshToken, tokenExpires, " + "SELECT id, email, displayName, signature, type, accessToken, refreshToken, tokenExpires, " "incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, " "connUsername, connPassword, authMethod " "FROM Account WHERE email = :email"); diff --git a/src/db/dao/accountdao.cpp.backup b/src/db/dao/accountdao.cpp.backup new file mode 100644 index 0000000..8a26e8b --- /dev/null +++ b/src/db/dao/accountdao.cpp.backup @@ -0,0 +1,221 @@ +#include "accountdao.h" + +bool AccountDao::insert(const Account& account) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare( + "INSERT INTO Account (email, displayName, 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(":type", static_cast(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 true; +} + +bool AccountDao::update(const Account& account) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare( + "UPDATE Account SET " + "email = :email, " + "displayName = :displayName, " + "type = :type, " + "accessToken = :accessToken, " + "refreshToken = :refreshToken, " + "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(":type", static_cast(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(); + return false; + } + return true; +} + +bool AccountDao::remove(int id) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("DELETE FROM Account WHERE id = :id"); + query.bindValue(":id", id); + + if (!query.exec()) { + qWarning() << "Failed to delete account:" << query.lastError().text(); + return false; + } + return true; +} + +Account* AccountDao::findById(int id) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + 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()) { + qWarning() << "Failed to find account by id:" << query.lastError().text(); + return nullptr; + } + + if (query.next()) { + Account* account = new Account(); + account->setId(query.value("id").toInt()); + account->setEmail(query.value("email").toString()); + account->setDisplayName(query.value("displayName").toString()); + account->setType(static_cast(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; + } + + return nullptr; +} + +QVector AccountDao::findAll() +{ + QVector accounts; + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + 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("id").toInt()); + acc.setEmail(query.value("email").toString()); + acc.setDisplayName(query.value("displayName").toString()); + acc.setType(static_cast(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; +} + +Account* AccountDao::findByEmail(const QString& email) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + 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()) { + qWarning() << "Failed to find account by email:" << query.lastError().text(); + return nullptr; + } + + if (query.next()) { + Account* acc = new Account(); + acc->setId(query.value("id").toInt()); + acc->setEmail(query.value("email").toString()); + acc->setDisplayName(query.value("displayName").toString()); + acc->setType(static_cast(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; +} \ No newline at end of file diff --git a/src/db/dao/accountdao.cpp.bak b/src/db/dao/accountdao.cpp.bak new file mode 100644 index 0000000..747877f --- /dev/null +++ b/src/db/dao/accountdao.cpp.bak @@ -0,0 +1,224 @@ +#include "accountdao.h" + +qint64 AccountDao::insert(const Account& account) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare( + "INSERT INTO Account (email, displayName, signature, type, accessToken, refreshToken, tokenExpires, " + "incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, " + "connUsername, connPassword, authMethod) " + "VALUES (:email, :displayName, :signature, :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(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 -1; + } + + return query.lastInsertId().toLongLong(); +} + +bool AccountDao::update(const Account& account) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare( + "UPDATE Account SET " + "email = :email, " + "displayName = :displayName, " + "type = :type, " + "accessToken = :accessToken, " + "refreshToken = :refreshToken, " + "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(account.type())); + query.bindValue(":accessToken", account.accessToken()); + query.bindValue(":refreshToken", account.refreshToken()); + 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(); + return false; + } + return true; +} + +bool AccountDao::remove(int id) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("DELETE FROM Account WHERE id = :id"); + query.bindValue(":id", id); + + if (!query.exec()) { + qWarning() << "Failed to delete account:" << query.lastError().text(); + return false; + } + return true; +} + +Account* AccountDao::findById(int id) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + 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()) { + qWarning() << "Failed to find account by id:" << query.lastError().text(); + return nullptr; + } + + if (query.next()) { + Account* account = new Account(); + 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(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; + } + + return nullptr; +} + +QVector AccountDao::findAll() +{ + QVector accounts; + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + 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("id").toInt()); + acc.setEmail(query.value("email").toString()); + acc.setDisplayName(query.value("displayName").toString()); + acc.setType(static_cast(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; +} + +Account* AccountDao::findByEmail(const QString& email) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + 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()) { + qWarning() << "Failed to find account by email:" << query.lastError().text(); + return nullptr; + } + + if (query.next()) { + Account* acc = new Account(); + 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(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; +} diff --git a/src/db/dao/folderdao.cpp b/src/db/dao/folderdao.cpp index bcbad84..ff0b21d 100644 --- a/src/db/dao/folderdao.cpp +++ b/src/db/dao/folderdao.cpp @@ -4,34 +4,9 @@ #include #include -bool FolderDao::insert(const Folder& folder) -{ - QSqlDatabase& db = DatabaseManager::instance().database(); - QSqlQuery query(db); - query.prepare( - "INSERT INTO Folder (accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced) " - "VALUES (:accountId, :name, :parentFolderId, :isInbox, :isSent, :isDrafts, :isTrash, :unreadCount, :lastSynced)" - ); - query.bindValue(":accountId", folder.accountId()); - query.bindValue(":name", folder.name()); - query.bindValue(":parentFolderId", folder.parentFolderId()); - query.bindValue(":isInbox", folder.isInbox() ? 1 : 0); - query.bindValue(":isSent", folder.isSent() ? 1 : 0); - query.bindValue(":isDrafts", folder.isDrafts() ? 1 : 0); - query.bindValue(":isTrash", folder.isTrash() ? 1 : 0); - query.bindValue(":unreadCount", folder.unreadCount()); - query.bindValue(":lastSynced", folder.lastSynced()); - - if (!query.exec()) { - qWarning() << "Failed to insert folder:" << query.lastError().text(); - return false; - } - return true; -} - bool FolderDao::update(const Folder& folder) { - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); query.prepare( "UPDATE Folder SET " @@ -66,7 +41,7 @@ bool FolderDao::update(const Folder& folder) bool FolderDao::remove(int id) { - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); query.prepare("DELETE FROM Folder WHERE id = :id"); query.bindValue(":id", id); @@ -80,7 +55,7 @@ bool FolderDao::remove(int id) std::optional FolderDao::findById(int id) { - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); query.prepare("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder WHERE id = :id"); query.bindValue(":id", id); @@ -110,7 +85,7 @@ std::optional FolderDao::findById(int id) QVector FolderDao::findAll() { QVector folders; - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); if (!query.exec("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder")) { qWarning() << "Failed to fetch all folders:" << query.lastError().text(); @@ -137,7 +112,7 @@ QVector FolderDao::findAll() QVector FolderDao::findByAccountId(int accountId) { QVector folders; - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); query.prepare("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder WHERE accountId = :accountId"); query.bindValue(":accountId", accountId); @@ -166,7 +141,7 @@ QVector FolderDao::findByAccountId(int accountId) bool FolderDao::removeByAccountId(int accountId) { - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); query.prepare("DELETE FROM Folder WHERE accountId = :accountId"); query.bindValue(":accountId", accountId); @@ -176,4 +151,29 @@ bool FolderDao::removeByAccountId(int accountId) return false; } return true; -} \ No newline at end of file +} + +qint64 FolderDao::insert(const Folder& folder) +{ + QSqlDatabase db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare( + "INSERT INTO Folder (accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced) " + "VALUES (:accountId, :name, :parentFolderId, :isInbox, :isSent, :isDrafts, :isTrash, :unreadCount, :lastSynced)" + ); + query.bindValue(":accountId", folder.accountId()); + query.bindValue(":name", folder.name()); + query.bindValue(":parentFolderId", folder.parentFolderId()); + query.bindValue(":isInbox", folder.isInbox() ? 1 : 0); + query.bindValue(":isSent", folder.isSent() ? 1 : 0); + query.bindValue(":isDrafts", folder.isDrafts() ? 1 : 0); + query.bindValue(":isTrash", folder.isTrash() ? 1 : 0); + query.bindValue(":unreadCount", folder.unreadCount()); + query.bindValue(":lastSynced", folder.lastSynced()); + + if (!query.exec()) { + qWarning() << "Failed to insert folder:" << query.lastError().text(); + return -1; + } + return query.lastInsertId().toLongLong(); +} diff --git a/src/db/dao/folderdao.cpp.backup b/src/db/dao/folderdao.cpp.backup new file mode 100644 index 0000000..bcbad84 --- /dev/null +++ b/src/db/dao/folderdao.cpp.backup @@ -0,0 +1,179 @@ +#include +#include "folderdao.h" +#include +#include +#include + +bool FolderDao::insert(const Folder& folder) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare( + "INSERT INTO Folder (accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced) " + "VALUES (:accountId, :name, :parentFolderId, :isInbox, :isSent, :isDrafts, :isTrash, :unreadCount, :lastSynced)" + ); + query.bindValue(":accountId", folder.accountId()); + query.bindValue(":name", folder.name()); + query.bindValue(":parentFolderId", folder.parentFolderId()); + query.bindValue(":isInbox", folder.isInbox() ? 1 : 0); + query.bindValue(":isSent", folder.isSent() ? 1 : 0); + query.bindValue(":isDrafts", folder.isDrafts() ? 1 : 0); + query.bindValue(":isTrash", folder.isTrash() ? 1 : 0); + query.bindValue(":unreadCount", folder.unreadCount()); + query.bindValue(":lastSynced", folder.lastSynced()); + + if (!query.exec()) { + qWarning() << "Failed to insert folder:" << query.lastError().text(); + return false; + } + return true; +} + +bool FolderDao::update(const Folder& folder) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare( + "UPDATE Folder SET " + "accountId = :accountId, " + "name = :name, " + "parentFolderId = :parentFolderId, " + "isInbox = :isInbox, " + "isSent = :isSent, " + "isDrafts = :isDrafts, " + "isTrash = :isTrash, " + "unreadCount = :unreadCount, " + "lastSynced = :lastSynced " + "WHERE id = :id" + ); + query.bindValue(":id", folder.id()); + query.bindValue(":accountId", folder.accountId()); + query.bindValue(":name", folder.name()); + query.bindValue(":parentFolderId", folder.parentFolderId()); + query.bindValue(":isInbox", folder.isInbox() ? 1 : 0); + query.bindValue(":isSent", folder.isSent() ? 1 : 0); + query.bindValue(":isDrafts", folder.isDrafts() ? 1 : 0); + query.bindValue(":isTrash", folder.isTrash() ? 1 : 0); + query.bindValue(":unreadCount", folder.unreadCount()); + query.bindValue(":lastSynced", folder.lastSynced()); + + if (!query.exec()) { + qWarning() << "Failed to update folder:" << query.lastError().text(); + return false; + } + return true; +} + +bool FolderDao::remove(int id) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("DELETE FROM Folder WHERE id = :id"); + query.bindValue(":id", id); + + if (!query.exec()) { + qWarning() << "Failed to delete folder:" << query.lastError().text(); + return false; + } + return true; +} + +std::optional FolderDao::findById(int id) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder WHERE id = :id"); + query.bindValue(":id", id); + + if (!query.exec()) { + qWarning() << "Failed to find folder by id:" << query.lastError().text(); + return std::nullopt; + } + + if (query.next()) { + Folder fld; + fld.setId(query.value(0).toInt()); + fld.setAccountId(query.value(1).toInt()); + fld.setName(query.value(2).toString()); + fld.setParentFolderId(query.value(3).toString()); + fld.setInbox(query.value(4).toBool()); + fld.setSent(query.value(5).toBool()); + fld.setDrafts(query.value(6).toBool()); + fld.setTrash(query.value(7).toBool()); + fld.setUnreadCount(query.value(8).toInt()); + fld.setLastSynced(query.value(9).toDateTime()); + return fld; + } + return std::nullopt; +} + +QVector FolderDao::findAll() +{ + QVector folders; + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + if (!query.exec("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder")) { + qWarning() << "Failed to fetch all folders:" << query.lastError().text(); + return folders; + } + + while (query.next()) { + Folder fld; + fld.setId(query.value(0).toInt()); + fld.setAccountId(query.value(1).toInt()); + fld.setName(query.value(2).toString()); + fld.setParentFolderId(query.value(3).toString()); + fld.setInbox(query.value(4).toBool()); + fld.setSent(query.value(5).toBool()); + fld.setDrafts(query.value(6).toBool()); + fld.setTrash(query.value(7).toBool()); + fld.setUnreadCount(query.value(8).toInt()); + fld.setLastSynced(query.value(9).toDateTime()); + folders.push_back(fld); + } + return folders; +} + +QVector FolderDao::findByAccountId(int accountId) +{ + QVector folders; + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder WHERE accountId = :accountId"); + query.bindValue(":accountId", accountId); + + if (!query.exec()) { + qWarning() << "Failed to fetch folders by account id:" << query.lastError().text(); + return folders; + } + + while (query.next()) { + Folder fld; + fld.setId(query.value(0).toInt()); + fld.setAccountId(query.value(1).toInt()); + fld.setName(query.value(2).toString()); + fld.setParentFolderId(query.value(3).toString()); + fld.setInbox(query.value(4).toBool()); + fld.setSent(query.value(5).toBool()); + fld.setDrafts(query.value(6).toBool()); + fld.setTrash(query.value(7).toBool()); + fld.setUnreadCount(query.value(8).toInt()); + fld.setLastSynced(query.value(9).toDateTime()); + 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; +} \ No newline at end of file diff --git a/src/db/dao/folderdao.cpp.backup2 b/src/db/dao/folderdao.cpp.backup2 new file mode 100644 index 0000000..1f1185a --- /dev/null +++ b/src/db/dao/folderdao.cpp.backup2 @@ -0,0 +1,180 @@ +#include +#include "folderdao.h" +#include +#include +#include + +qint64 FolderDao::insert(Folder& folder) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare( + "INSERT INTO Folder (accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced) " + "VALUES (:accountId, :name, :parentFolderId, :isInbox, :isSent, :isDrafts, :isTrash, :unreadCount, :lastSynced)" + ); + query.bindValue(":accountId", folder.accountId()); + query.bindValue(":name", folder.name()); + query.bindValue(":parentFolderId", folder.parentFolderId()); + query.bindValue(":isInbox", folder.isInbox() ? 1 : 0); + query.bindValue(":isSent", folder.isSent() ? 1 : 0); + query.bindValue(":isDrafts", folder.isDrafts() ? 1 : 0); + query.bindValue(":isTrash", folder.isTrash() ? 1 : 0); + query.bindValue(":unreadCount", folder.unreadCount()); + query.bindValue(":lastSynced", folder.lastSynced()); + + if (!query.exec()) { + qWarning() << "Failed to insert folder:" << query.lastError().text(); + return -1; + } + folder.setId(query.lastInsertId().toLongLong()); + return folder.id(); +} + +bool FolderDao::update(const Folder& folder) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare( + "UPDATE Folder SET " + "accountId = :accountId, " + "name = :name, " + "parentFolderId = :parentFolderId, " + "isInbox = :isInbox, " + "isSent = :isSent, " + "isDrafts = :isDrafts, " + "isTrash = :isTrash, " + "unreadCount = :unreadCount, " + "lastSynced = :lastSynced " + "WHERE id = :id" + ); + query.bindValue(":id", folder.id()); + query.bindValue(":accountId", folder.accountId()); + query.bindValue(":name", folder.name()); + query.bindValue(":parentFolderId", folder.parentFolderId()); + query.bindValue(":isInbox", folder.isInbox() ? 1 : 0); + query.bindValue(":isSent", folder.isSent() ? 1 : 0); + query.bindValue(":isDrafts", folder.isDrafts() ? 1 : 0); + query.bindValue(":isTrash", folder.isTrash() ? 1 : 0); + query.bindValue(":unreadCount", folder.unreadCount()); + query.bindValue(":lastSynced", folder.lastSynced()); + + if (!query.exec()) { + qWarning() << "Failed to update folder:" << query.lastError().text(); + return false; + } + return true; +} + +bool FolderDao::remove(int id) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("DELETE FROM Folder WHERE id = :id"); + query.bindValue(":id", id); + + if (!query.exec()) { + qWarning() << "Failed to delete folder:" << query.lastError().text(); + return false; + } + return true; +} + +std::optional FolderDao::findById(int id) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder WHERE id = :id"); + query.bindValue(":id", id); + + if (!query.exec()) { + qWarning() << "Failed to find folder by id:" << query.lastError().text(); + return std::nullopt; + } + + if (query.next()) { + Folder fld; + fld.setId(query.value(0).toInt()); + fld.setAccountId(query.value(1).toInt()); + fld.setName(query.value(2).toString()); + fld.setParentFolderId(query.value(3).toString()); + fld.setInbox(query.value(4).toBool()); + fld.setSent(query.value(5).toBool()); + fld.setDrafts(query.value(6).toBool()); + fld.setTrash(query.value(7).toBool()); + fld.setUnreadCount(query.value(8).toInt()); + fld.setLastSynced(query.value(9).toDateTime()); + return fld; + } + return std::nullopt; +} + +QVector FolderDao::findAll() +{ + QVector folders; + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + if (!query.exec("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder")) { + qWarning() << "Failed to fetch all folders:" << query.lastError().text(); + return folders; + } + + while (query.next()) { + Folder fld; + fld.setId(query.value(0).toInt()); + fld.setAccountId(query.value(1).toInt()); + fld.setName(query.value(2).toString()); + fld.setParentFolderId(query.value(3).toString()); + fld.setInbox(query.value(4).toBool()); + fld.setSent(query.value(5).toBool()); + fld.setDrafts(query.value(6).toBool()); + fld.setTrash(query.value(7).toBool()); + fld.setUnreadCount(query.value(8).toInt()); + fld.setLastSynced(query.value(9).toDateTime()); + folders.push_back(fld); + } + return folders; +} + +QVector FolderDao::findByAccountId(int accountId) +{ + QVector folders; + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder WHERE accountId = :accountId"); + query.bindValue(":accountId", accountId); + + if (!query.exec()) { + qWarning() << "Failed to fetch folders by account id:" << query.lastError().text(); + return folders; + } + + while (query.next()) { + Folder fld; + fld.setId(query.value(0).toInt()); + fld.setAccountId(query.value(1).toInt()); + fld.setName(query.value(2).toString()); + fld.setParentFolderId(query.value(3).toString()); + fld.setInbox(query.value(4).toBool()); + fld.setSent(query.value(5).toBool()); + fld.setDrafts(query.value(6).toBool()); + fld.setTrash(query.value(7).toBool()); + fld.setUnreadCount(query.value(8).toInt()); + fld.setLastSynced(query.value(9).toDateTime()); + 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; +} \ No newline at end of file diff --git a/src/db/dao/folderdao.h b/src/db/dao/folderdao.h index 4643a18..90b7054 100644 --- a/src/db/dao/folderdao.h +++ b/src/db/dao/folderdao.h @@ -8,7 +8,7 @@ class FolderDao { public: - static bool insert(const Folder& folder); + static qint64 insert(const Folder& folder); static bool update(const Folder& folder); static bool remove(int id); static bool removeByAccountId(int accountId); diff --git a/src/db/dao/folderdao.h.backup b/src/db/dao/folderdao.h.backup new file mode 100644 index 0000000..4643a18 --- /dev/null +++ b/src/db/dao/folderdao.h.backup @@ -0,0 +1,18 @@ +#pragma once + +#include "../databasemanager.h" +#include "../../core/models/folder.h" +#include +#include + +class FolderDao +{ +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 findById(int id); + static QVector findAll(); + static QVector findByAccountId(int accountId); +}; \ No newline at end of file diff --git a/src/db/dao/folderdao.h.bak2 b/src/db/dao/folderdao.h.bak2 new file mode 100644 index 0000000..8894319 --- /dev/null +++ b/src/db/dao/folderdao.h.bak2 @@ -0,0 +1,18 @@ +#pragma once + +#include "../databasemanager.h" +#include "../../core/models/folder.h" +#include +#include + +class FolderDao +{ +public: + static qint64 insert(const Folder& folder); + static bool update(const Folder& folder); + static bool remove(int id); + static bool removeByAccountId(int accountId); + static std::optional findById(int id); + static QVector findAll(); + static QVector findByAccountId(int accountId); +}; diff --git a/src/db/dao/mailitemdao.cpp b/src/db/dao/mailitemdao.cpp index d808e41..b764c4a 100644 --- a/src/db/dao/mailitemdao.cpp +++ b/src/db/dao/mailitemdao.cpp @@ -1,18 +1,38 @@ #include #include #include "mailitemdao.h" +#include #include -bool MailItemDao::insert(const MailItem& item) +bool MailItemDao::insert(MailItem& item) { - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); + + // IMAP UIDs and provider message IDs are stable across synchronizations. + // Update the existing row instead of creating duplicates on every sync. + if (!item.messageId().isEmpty() || item.uid() > 0) { + QSqlQuery existing(db); + if (!item.messageId().isEmpty()) { + existing.prepare("SELECT id FROM MailCopy WHERE messageId = :messageId LIMIT 1"); + existing.bindValue(":messageId", item.messageId()); + } else { + existing.prepare("SELECT id FROM MailCopy WHERE folderId = :folderId AND uid = :uid LIMIT 1"); + existing.bindValue(":folderId", item.folderId()); + existing.bindValue(":uid", item.uid()); + } + if (existing.exec() && existing.next()) { + item.setId(existing.value(0).toLongLong()); + return update(item); + } + } + QSqlQuery query(db); query.prepare( - "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)" + "INSERT INTO MailCopy (folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr) " + "VALUES (:folderId, :messageId, :subject, :sender, :recipient, :date, :read, :flagged, :hasAttachment, :size, :fileId, :uid, :bodyHtml, :toAddr, :ccAddr, :bccAddr)" ); query.bindValue(":folderId", item.folderId()); - query.bindValue(":messageId", item.messageId()); + query.bindValue(":messageId", item.messageId().isEmpty() ? QVariant() : QVariant(item.messageId())); query.bindValue(":subject", item.subject()); query.bindValue(":sender", item.sender()); query.bindValue(":recipient", item.recipient()); @@ -23,17 +43,27 @@ bool MailItemDao::insert(const MailItem& item) query.bindValue(":size", item.size()); query.bindValue(":fileId", item.fileId()); query.bindValue(":uid", item.uid()); + query.bindValue(":bodyHtml", item.bodyHtml()); + query.bindValue(":toAddr", item.to()); + query.bindValue(":ccAddr", item.cc()); + query.bindValue(":bccAddr", item.bcc()); if (!query.exec()) { qWarning() << "Failed to insert mail item:" << query.lastError().text(); return false; } + item.setId(query.lastInsertId().toLongLong()); return true; } +bool MailItemDao::upsert(MailItem& item) +{ + return insert(item); +} + bool MailItemDao::update(const MailItem& item) { - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); query.prepare( "UPDATE MailCopy SET " @@ -46,14 +76,18 @@ bool MailItemDao::update(const MailItem& item) "read = :read, " "flagged = :flagged, " "hasAttachment = :hasAttachment, " - "size = :size, " - "fileId = :fileId, " - "uid = :uid " + "size = :size, " + "fileId = :fileId, " + "uid = :uid, " + "bodyHtml = :bodyHtml, " + "toAddr = :toAddr, " + "ccAddr = :ccAddr, " + "bccAddr = :bccAddr " "WHERE id = :id" ); query.bindValue(":id", item.id()); query.bindValue(":folderId", item.folderId()); - query.bindValue(":messageId", item.messageId()); + query.bindValue(":messageId", item.messageId().isEmpty() ? QVariant() : QVariant(item.messageId())); query.bindValue(":subject", item.subject()); query.bindValue(":sender", item.sender()); query.bindValue(":recipient", item.recipient()); @@ -64,6 +98,10 @@ bool MailItemDao::update(const MailItem& item) query.bindValue(":size", item.size()); query.bindValue(":fileId", item.fileId()); query.bindValue(":uid", item.uid()); + query.bindValue(":bodyHtml", item.bodyHtml()); + query.bindValue(":toAddr", item.to()); + query.bindValue(":ccAddr", item.cc()); + query.bindValue(":bccAddr", item.bcc()); if (!query.exec()) { qWarning() << "Failed to update mail item:" << query.lastError().text(); @@ -74,7 +112,7 @@ bool MailItemDao::update(const MailItem& item) bool MailItemDao::remove(qint64 id) { - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); query.prepare("DELETE FROM MailCopy WHERE id = :id"); query.bindValue(":id", id); @@ -88,9 +126,9 @@ bool MailItemDao::remove(qint64 id) std::optional MailItemDao::findById(qint64 id) { - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); - query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid FROM MailCopy WHERE id = :id"); + query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE id = :id"); query.bindValue(":id", id); if (!query.exec()) { @@ -112,6 +150,13 @@ std::optional MailItemDao::findById(qint64 id) item.setSize(query.value(10).toLongLong()); item.setFileId(query.value(11).toString()); item.setUid(query.value(12).toLongLong()); + item.setBodyHtml(query.value(13).toString()); + item.setTo(query.value(14).toString()); + item.setCc(query.value(15).toString()); + item.setBcc(query.value(16).toString()); + QVector names; + for (const auto &attachment : attachmentsForMail(item.id())) names.append(attachment.fileName); + item.setAttachments(names); return item; } return std::nullopt; @@ -120,9 +165,9 @@ std::optional MailItemDao::findById(qint64 id) QVector MailItemDao::findAll() { QVector items; - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); - if (!query.exec("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid FROM MailCopy")) { + if (!query.exec("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy")) { qWarning() << "Failed to fetch all mail items:" << query.lastError().text(); return items; } @@ -141,6 +186,13 @@ QVector MailItemDao::findAll() item.setSize(query.value(10).toLongLong()); item.setFileId(query.value(11).toString()); item.setUid(query.value(12).toLongLong()); + item.setBodyHtml(query.value(13).toString()); + item.setTo(query.value(14).toString()); + item.setCc(query.value(15).toString()); + item.setBcc(query.value(16).toString()); + QVector names; + for (const auto &attachment : attachmentsForMail(item.id())) names.append(attachment.fileName); + item.setAttachments(names); items.append(item); } return items; @@ -149,9 +201,9 @@ QVector MailItemDao::findAll() QVector MailItemDao::findByFolderId(int folderId) { QVector items; - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); - query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid FROM MailCopy WHERE folderId = :folderId"); + query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE folderId = :folderId"); query.bindValue(":folderId", folderId); if (!query.exec()) { @@ -173,6 +225,13 @@ QVector MailItemDao::findByFolderId(int folderId) item.setSize(query.value(10).toLongLong()); item.setFileId(query.value(11).toString()); item.setUid(query.value(12).toLongLong()); + item.setBodyHtml(query.value(13).toString()); + item.setTo(query.value(14).toString()); + item.setCc(query.value(15).toString()); + item.setBcc(query.value(16).toString()); + QVector names; + for (const auto &attachment : attachmentsForMail(item.id())) names.append(attachment.fileName); + item.setAttachments(names); items.append(item); } return items; @@ -181,9 +240,9 @@ QVector MailItemDao::findByFolderId(int folderId) QVector MailItemDao::findByFolderIdSinceUid(int folderId, qint64 sinceUid) { QVector items; - QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlDatabase db = DatabaseManager::instance().database(); QSqlQuery query(db); - 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.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE folderId = :folderId AND uid > :sinceUid"); query.bindValue(":folderId", folderId); query.bindValue(":sinceUid", sinceUid); @@ -206,7 +265,132 @@ QVector MailItemDao::findByFolderIdSinceUid(int folderId, qint64 since item.setSize(query.value(10).toLongLong()); item.setFileId(query.value(11).toString()); item.setUid(query.value(12).toLongLong()); + item.setBodyHtml(query.value(13).toString()); + item.setTo(query.value(14).toString()); + item.setCc(query.value(15).toString()); + item.setBcc(query.value(16).toString()); + QVector names; + for (const auto &attachment : attachmentsForMail(item.id())) names.append(attachment.fileName); + item.setAttachments(names); items.append(item); } return items; -} \ No newline at end of file +} + +std::optional MailItemDao::maxUidForFolder(int folderId) +{ + QSqlDatabase db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT MAX(uid) FROM MailCopy WHERE folderId = :folderId"); + query.bindValue(":folderId", folderId); + + if (!query.exec()) { + qWarning() << "Failed to get max uid for folder:" << query.lastError().text(); + return std::nullopt; + } + + if (query.next()) { + if (query.isNull(0)) { + return std::nullopt; + } + return query.value(0).toLongLong(); + } + return std::nullopt; +} + +QVector MailItemDao::getUidsForFolder(int folderId) +{ + QVector uids; + QSqlDatabase db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT uid FROM MailCopy WHERE folderId = :folderId"); + query.bindValue(":folderId", folderId); + + if (!query.exec()) { + qWarning() << "Failed to get uids for folder:" << query.lastError().text(); + return uids; + } + + while (query.next()) { + qint64 uid = query.value(0).toLongLong(); + uids.append(uid); + } + return uids; +} + +QVector MailItemDao::uidsForFolder(int folderId) +{ + return getUidsForFolder(folderId); +} + +std::optional MailItemDao::findByUid(int folderId, qint64 uid) +{ + QSqlDatabase db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT id FROM MailCopy WHERE folderId = :folderId AND uid = :uid LIMIT 1"); + query.bindValue(":folderId", folderId); + query.bindValue(":uid", uid); + if (!query.exec() || !query.next()) return std::nullopt; + return findById(query.value(0).toLongLong()); +} + +bool MailItemDao::removeByUid(int folderId, qint64 uid) +{ + QSqlDatabase db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("DELETE FROM MailCopy WHERE folderId = :folderId AND uid = :uid"); + query.bindValue(":folderId", folderId); + query.bindValue(":uid", uid); + if (!query.exec()) { + qWarning() << "Failed to delete mail item by UID:" << query.lastError().text(); + return false; + } + return true; +} + +bool MailItemDao::replaceAttachments(qint64 mailItemId, + const QVector& attachments) +{ + QSqlDatabase db = DatabaseManager::instance().database(); + if (!db.transaction()) return false; + + QSqlQuery remove(db); + remove.prepare("DELETE FROM Attachment WHERE mailCopyId = :mailCopyId"); + remove.bindValue(":mailCopyId", mailItemId); + if (!remove.exec()) { db.rollback(); return false; } + + QSqlQuery insert(db); + insert.prepare("INSERT INTO Attachment (mailCopyId, filename, mimeType, size, contentId, storedPath) " + "VALUES (:mailCopyId, :filename, :mimeType, :size, :contentId, :storedPath)"); + for (const StoredAttachmentRecord &attachment : attachments) { + insert.bindValue(":mailCopyId", mailItemId); + insert.bindValue(":filename", attachment.fileName); + insert.bindValue(":mimeType", attachment.mimeType); + insert.bindValue(":size", attachment.size); + insert.bindValue(":contentId", attachment.contentId); + insert.bindValue(":storedPath", attachment.storedPath); + if (!insert.exec()) { db.rollback(); return false; } + } + return db.commit(); +} + +QVector MailItemDao::attachmentsForMail(qint64 mailItemId) +{ + QVector result; + QSqlDatabase db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT filename, mimeType, size, contentId, storedPath " + "FROM Attachment WHERE mailCopyId = :mailCopyId ORDER BY id"); + query.bindValue(":mailCopyId", mailItemId); + if (!query.exec()) return result; + while (query.next()) { + StoredAttachmentRecord attachment; + attachment.fileName = query.value(0).toString(); + attachment.mimeType = query.value(1).toString(); + attachment.size = query.value(2).toLongLong(); + attachment.contentId = query.value(3).toString(); + attachment.storedPath = query.value(4).toString(); + result.append(attachment); + } + return result; +} diff --git a/src/db/dao/mailitemdao.cpp.backup2 b/src/db/dao/mailitemdao.cpp.backup2 new file mode 100644 index 0000000..6660c87 --- /dev/null +++ b/src/db/dao/mailitemdao.cpp.backup2 @@ -0,0 +1,254 @@ +#include +#include +#include "mailitemdao.h" +#include + +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, uid) " + "VALUES (:folderId, :messageId, :subject, :sender, :recipient, :date, :read, :flagged, :hasAttachment, :size, :fileId, :uid)" + ); + query.bindValue(":folderId", item.folderId()); + query.bindValue(":messageId", item.messageId()); + query.bindValue(":subject", item.subject()); + query.bindValue(":sender", item.sender()); + query.bindValue(":recipient", item.recipient()); + query.bindValue(":date", item.date()); + query.bindValue(":read", item.isRead() ? 1 : 0); + query.bindValue(":flagged", item.isFlagged() ? 1 : 0); + 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(); + return false; + } + return true; +} + +bool MailItemDao::update(const MailItem& item) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare( + "UPDATE MailCopy SET " + "folderId = :folderId, " + "messageId = :messageId, " + "subject = :subject, " + "sender = :sender, " + "recipient = :recipient, " + "date = :date, " + "read = :read, " + "flagged = :flagged, " + "hasAttachment = :hasAttachment, " + "size = :size, " + "fileId = :fileId, " + "uid = :uid " + "WHERE id = :id" + ); + query.bindValue(":id", item.id()); + query.bindValue(":folderId", item.folderId()); + query.bindValue(":messageId", item.messageId()); + query.bindValue(":subject", item.subject()); + query.bindValue(":sender", item.sender()); + query.bindValue(":recipient", item.recipient()); + query.bindValue(":date", item.date()); + query.bindValue(":read", item.isRead() ? 1 : 0); + query.bindValue(":flagged", item.isFlagged() ? 1 : 0); + 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(); + return false; + } + return true; +} + +bool MailItemDao::remove(qint64 id) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("DELETE FROM MailCopy WHERE id = :id"); + query.bindValue(":id", id); + + if (!query.exec()) { + qWarning() << "Failed to delete mail item:" << query.lastError().text(); + return false; + } + return true; +} + +std::optional 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, uid FROM MailCopy WHERE id = :id"); + query.bindValue(":id", id); + + if (!query.exec()) { + qWarning() << "Failed to find mail item by id:" << query.lastError().text(); + return std::nullopt; + } + + if (query.next()) { + MailItem item; + item.setId(query.value(0).toInt()); + item.setFolderId(query.value(1).toInt()); + item.setMessageId(query.value(2).toString()); + item.setSubject(query.value(3).toString()); + item.setSender(query.value(4).toString()); + item.setRecipient(query.value(5).toString()); + item.setDate(query.value(6).toDateTime()); + item.setRead(query.value(7).toBool()); + item.setFlagged(query.value(8).toBool()); + item.setSize(query.value(10).toLongLong()); + item.setFileId(query.value(11).toString()); + item.setUid(query.value(12).toLongLong()); + return item; + } + return std::nullopt; +} + +QVector MailItemDao::findAll() +{ + QVector 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, uid FROM MailCopy")) { + qWarning() << "Failed to fetch all mail items:" << query.lastError().text(); + return items; + } + + while (query.next()) { + MailItem item; + item.setId(query.value(0).toInt()); + item.setFolderId(query.value(1).toInt()); + item.setMessageId(query.value(2).toString()); + item.setSubject(query.value(3).toString()); + item.setSender(query.value(4).toString()); + item.setRecipient(query.value(5).toString()); + item.setDate(query.value(6).toDateTime()); + item.setRead(query.value(7).toBool()); + 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; +} + +QVector MailItemDao::findByFolderId(int folderId) +{ + QVector items; + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + 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()) { + qWarning() << "Failed to fetch mail items by folder id:" << query.lastError().text(); + return items; + } + + while (query.next()) { + MailItem item; + item.setId(query.value(0).toInt()); + item.setFolderId(query.value(1).toInt()); + item.setMessageId(query.value(2).toString()); + item.setSubject(query.value(3).toString()); + item.setSender(query.value(4).toString()); + item.setRecipient(query.value(5).toString()); + item.setDate(query.value(6).toDateTime()); + item.setRead(query.value(7).toBool()); + 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; +} + +QVector MailItemDao::findByFolderIdSinceUid(int folderId, qint64 sinceUid) +{ + QVector items; + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid FROM MailCopy WHERE folderId = :folderId AND uid > :sinceUid"); + query.bindValue(":folderId", folderId); + query.bindValue(":sinceUid", sinceUid); + + if (!query.exec()) { + qWarning() << "Failed to fetch mail items by folder id since uid:" << query.lastError().text(); + return items; + } + + while (query.next()) { + MailItem item; + item.setId(query.value(0).toInt()); + item.setFolderId(query.value(1).toInt()); + item.setMessageId(query.value(2).toString()); + item.setSubject(query.value(3).toString()); + item.setSender(query.value(4).toString()); + item.setRecipient(query.value(5).toString()); + item.setDate(query.value(6).toDateTime()); + item.setRead(query.value(7).toBool()); + 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; +} +std::optional MailItemDao::maxUidForFolder(int folderId) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT MAX(uid) FROM MailCopy WHERE folderId = :folderId"); + query.bindValue(":folderId", folderId); + + if (!query.exec()) { + qWarning() << "Failed to get max uid for folder:" << query.lastError().text(); + return std::nullopt; + } + + if (query.next()) { + if (query.isNull(0)) { + return std::nullopt; + } + return query.value(0).toLongLong(); + } + return std::nullopt; +} + +QVector MailItemDao::uidsForFolder(int folderId) +{ + QVector uids; + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT uid FROM MailCopy WHERE folderId = :folderId"); + query.bindValue(":folderId", folderId); + + if (!query.exec()) { + qWarning() << "Failed to get uids for folder:" << query.lastError().text(); + return uids; + } + + while (query.next()) { + qint64 uid = query.value(0).toLongLong(); + if (uid > 0) { + uids.append(uid); + } + } + return uids; +} diff --git a/src/db/dao/mailitemdao.cpp.bak b/src/db/dao/mailitemdao.cpp.bak new file mode 100644 index 0000000..84e7d2c --- /dev/null +++ b/src/db/dao/mailitemdao.cpp.bak @@ -0,0 +1,285 @@ +#include +#include +#include "mailitemdao.h" +#include +#include + +bool MailItemDao::insert(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, uid, bodyHtml, toAddr, ccAddr, bccAddr) " + "VALUES (:folderId, :messageId, :subject, :sender, :recipient, :date, :read, :flagged, :hasAttachment, :size, :fileId, :uid, :bodyHtml, :toAddr, :ccAddr, :bccAddr)" + ); + query.bindValue(":folderId", item.folderId()); + query.bindValue(":messageId", item.messageId()); + query.bindValue(":subject", item.subject()); + query.bindValue(":sender", item.sender()); + query.bindValue(":recipient", item.recipient()); + query.bindValue(":date", item.date()); + query.bindValue(":read", item.isRead() ? 1 : 0); + query.bindValue(":flagged", item.isFlagged() ? 1 : 0); + query.bindValue(":hasAttachment", !item.attachments().isEmpty() ? 1 : 0); + query.bindValue(":size", item.size()); + query.bindValue(":fileId", item.fileId()); + query.bindValue(":uid", item.uid()); + query.bindValue(":bodyHtml", item.bodyHtml()); + query.bindValue(":toAddr", item.to()); + query.bindValue(":ccAddr", item.cc()); + query.bindValue(":bccAddr", item.bcc()); + + if (!query.exec()) { + qWarning() << "Failed to insert mail item:" << query.lastError().text(); + return false; + } + qInfo() << "Last insert id:" << query.lastInsertId(); + item.setId(query.lastInsertId().toLongLong()); + return true; +} + +bool MailItemDao::update(const MailItem& item) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare( + "UPDATE MailCopy SET " + "folderId = :folderId, " + "messageId = :messageId, " + "subject = :subject, " + "sender = :sender, " + "recipient = :recipient, " + "date = :date, " + "read = :read, " + "flagged = :flagged, " + "hasAttachment = :hasAttachment, " + "size = :size, " + "fileId = :fileId, " + "uid = :uid, " + "bodyHtml = :bodyHtml " + "WHERE id = :id" + ); + query.bindValue(":id", item.id()); + query.bindValue(":folderId", item.folderId()); + query.bindValue(":messageId", item.messageId()); + query.bindValue(":subject", item.subject()); + query.bindValue(":sender", item.sender()); + query.bindValue(":recipient", item.recipient()); + query.bindValue(":date", item.date()); + query.bindValue(":read", item.isRead() ? 1 : 0); + query.bindValue(":flagged", item.isFlagged() ? 1 : 0); + query.bindValue(":hasAttachment", !item.attachments().isEmpty() ? 1 : 0); + query.bindValue(":size", item.size()); + query.bindValue(":fileId", item.fileId()); + query.bindValue(":uid", item.uid()); + query.bindValue(":bodyHtml", item.bodyHtml()); + query.bindValue(":toAddr", item.to()); + query.bindValue(":ccAddr", item.cc()); + query.bindValue(":bccAddr", item.bcc()); + query.bindValue(":toAddr", item.to()); + query.bindValue(":ccAddr", item.cc()); + query.bindValue(":bccAddr", item.bcc()); + + if (!query.exec()) { + qWarning() << "Failed to update mail item:" << query.lastError().text(); + return false; + } + return true; +} + +bool MailItemDao::remove(qint64 id) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("DELETE FROM MailCopy WHERE id = :id"); + query.bindValue(":id", id); + + if (!query.exec()) { + qWarning() << "Failed to delete mail item:" << query.lastError().text(); + return false; + } + return true; +} + +std::optional 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, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE id = :id"); + query.bindValue(":id", id); + + if (!query.exec()) { + qWarning() << "Failed to find mail item by id:" << query.lastError().text(); + return std::nullopt; + } + + if (query.next()) { + MailItem item; + item.setId(query.value(0).toInt()); + item.setFolderId(query.value(1).toInt()); + item.setMessageId(query.value(2).toString()); + item.setSubject(query.value(3).toString()); + item.setSender(query.value(4).toString()); + item.setRecipient(query.value(5).toString()); + item.setDate(query.value(6).toDateTime()); + item.setRead(query.value(7).toBool()); + item.setFlagged(query.value(8).toBool()); + item.setSize(query.value(10).toLongLong()); + item.setFileId(query.value(11).toString()); + item.setUid(query.value(12).toLongLong()); + item.setBodyHtml(query.value(13).toString()); + item.setTo(query.value(14).toString()); + item.setCc(query.value(15).toString()); + item.setBcc(query.value(16).toString()); + qDebug() << "Debug findById: toAddr=" << query.value(14).toString() << " ccAddr=" << query.value(15).toString() << " bccAddr=" << query.value(16).toString(); + return item; + } + return std::nullopt; +} + +QVector MailItemDao::findAll() +{ + QVector 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, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy")) { + qWarning() << "Failed to fetch all mail items:" << query.lastError().text(); + return items; + } + + while (query.next()) { + MailItem item; + item.setId(query.value(0).toInt()); + item.setFolderId(query.value(1).toInt()); + item.setMessageId(query.value(2).toString()); + item.setSubject(query.value(3).toString()); + item.setSender(query.value(4).toString()); + item.setRecipient(query.value(5).toString()); + item.setDate(query.value(6).toDateTime()); + item.setRead(query.value(7).toBool()); + item.setFlagged(query.value(8).toBool()); + item.setSize(query.value(10).toLongLong()); + item.setFileId(query.value(11).toString()); + item.setUid(query.value(12).toLongLong()); + item.setBodyHtml(query.value(13).toString()); + item.setTo(query.value(14).toString()); + item.setCc(query.value(15).toString()); + item.setBcc(query.value(16).toString()); + items.append(item); + } + return items; +} + +QVector MailItemDao::findByFolderId(int folderId) +{ + QVector items; + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE folderId = :folderId"); + query.bindValue(":folderId", folderId); + + if (!query.exec()) { + qWarning() << "Failed to fetch mail items by folder id:" << query.lastError().text(); + return items; + } + + while (query.next()) { + MailItem item; + item.setId(query.value(0).toInt()); + item.setFolderId(query.value(1).toInt()); + item.setMessageId(query.value(2).toString()); + item.setSubject(query.value(3).toString()); + item.setSender(query.value(4).toString()); + item.setRecipient(query.value(5).toString()); + item.setDate(query.value(6).toDateTime()); + item.setRead(query.value(7).toBool()); + item.setFlagged(query.value(8).toBool()); + item.setSize(query.value(10).toLongLong()); + item.setFileId(query.value(11).toString()); + item.setUid(query.value(12).toLongLong()); + item.setBodyHtml(query.value(13).toString()); + item.setTo(query.value(14).toString()); + item.setCc(query.value(15).toString()); + item.setBcc(query.value(16).toString()); + items.append(item); + } + return items; +} + +QVector MailItemDao::findByFolderIdSinceUid(int folderId, qint64 sinceUid) +{ + QVector items; + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE folderId = :folderId AND uid > :sinceUid"); + query.bindValue(":folderId", folderId); + query.bindValue(":sinceUid", sinceUid); + + if (!query.exec()) { + qWarning() << "Failed to fetch mail items by folder id since uid:" << query.lastError().text(); + return items; + } + + while (query.next()) { + MailItem item; + item.setId(query.value(0).toInt()); + item.setFolderId(query.value(1).toInt()); + item.setMessageId(query.value(2).toString()); + item.setSubject(query.value(3).toString()); + item.setSender(query.value(4).toString()); + item.setRecipient(query.value(5).toString()); + item.setDate(query.value(6).toDateTime()); + item.setRead(query.value(7).toBool()); + item.setFlagged(query.value(8).toBool()); + item.setSize(query.value(10).toLongLong()); + item.setFileId(query.value(11).toString()); + item.setUid(query.value(12).toLongLong()); + item.setBodyHtml(query.value(13).toString()); + item.setTo(query.value(14).toString()); + item.setCc(query.value(15).toString()); + item.setBcc(query.value(16).toString()); + items.append(item); + } + return items; +} + +std::optional MailItemDao::maxUidForFolder(int folderId) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT MAX(uid) FROM MailCopy WHERE folderId = :folderId"); + query.bindValue(":folderId", folderId); + + if (!query.exec()) { + qWarning() << "Failed to get max uid for folder:" << query.lastError().text(); + return std::nullopt; + } + + if (query.next()) { + if (query.isNull(0)) { + return std::nullopt; + } + return query.value(0).toLongLong(); + } + return std::nullopt; +} + +QVector MailItemDao::getUidsForFolder(int folderId) +{ + QVector uids; + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT uid FROM MailCopy WHERE folderId = :folderId"); + query.bindValue(":folderId", folderId); + + if (!query.exec()) { + qWarning() << "Failed to get uids for folder:" << query.lastError().text(); + return uids; + } + + while (query.next()) { + qint64 uid = query.value(0).toLongLong(); + uids.append(uid); + } + return uids; +} diff --git a/src/db/dao/mailitemdao.cpp.bak2 b/src/db/dao/mailitemdao.cpp.bak2 new file mode 100644 index 0000000..64039c0 --- /dev/null +++ b/src/db/dao/mailitemdao.cpp.bak2 @@ -0,0 +1,260 @@ +#include +#include +#include "mailitemdao.h" +#include + +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, uid, bodyHtml) " + "VALUES (:folderId, :messageId, :subject, :sender, :recipient, :date, :read, :flagged, :hasAttachment, :size, :fileId, :uid, :bodyHtml)" + ); + query.bindValue(":folderId", item.folderId()); + query.bindValue(":messageId", item.messageId()); + query.bindValue(":subject", item.subject()); + query.bindValue(":sender", item.sender()); + query.bindValue(":recipient", item.recipient()); + query.bindValue(":date", item.date()); + query.bindValue(":read", item.isRead() ? 1 : 0); + query.bindValue(":flagged", item.isFlagged() ? 1 : 0); + query.bindValue(":hasAttachment", !item.attachments().isEmpty() ? 1 : 0); + query.bindValue(":size", item.size()); + query.bindValue(":fileId", item.fileId()); + query.bindValue(":uid", item.uid()); + query.bindValue(":bodyHtml", item.bodyHtml()); + + if (!query.exec()) { + qWarning() << "Failed to insert mail item:" << query.lastError().text(); + return false; + } + return true; +} + +bool MailItemDao::update(const MailItem& item) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare( + "UPDATE MailCopy SET " + "folderId = :folderId, " + "messageId = :messageId, " + "subject = :subject, " + "sender = :sender, " + "recipient = :recipient, " + "date = :date, " + "read = :read, " + "flagged = :flagged, " + "hasAttachment = :hasAttachment, " + "size = :size, " + "fileId = :fileId, " + "uid = :uid, " + "bodyHtml = :bodyHtml " + "WHERE id = :id" + ); + query.bindValue(":id", item.id()); + query.bindValue(":folderId", item.folderId()); + query.bindValue(":messageId", item.messageId()); + query.bindValue(":subject", item.subject()); + query.bindValue(":sender", item.sender()); + query.bindValue(":recipient", item.recipient()); + query.bindValue(":date", item.date()); + query.bindValue(":read", item.isRead() ? 1 : 0); + query.bindValue(":flagged", item.isFlagged() ? 1 : 0); + query.bindValue(":hasAttachment", !item.attachments().isEmpty() ? 1 : 0); + query.bindValue(":size", item.size()); + query.bindValue(":fileId", item.fileId()); + query.bindValue(":uid", item.uid()); + query.bindValue(":bodyHtml", item.bodyHtml()); + + if (!query.exec()) { + qWarning() << "Failed to update mail item:" << query.lastError().text(); + return false; + } + return true; +} + +bool MailItemDao::remove(qint64 id) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("DELETE FROM MailCopy WHERE id = :id"); + query.bindValue(":id", id); + + if (!query.exec()) { + qWarning() << "Failed to delete mail item:" << query.lastError().text(); + return false; + } + return true; +} + +std::optional 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, uid, bodyHtml FROM MailCopy WHERE id = :id"); + query.bindValue(":id", id); + + if (!query.exec()) { + qWarning() << "Failed to find mail item by id:" << query.lastError().text(); + return std::nullopt; + } + + if (query.next()) { + MailItem item; + item.setId(query.value(0).toInt()); + item.setFolderId(query.value(1).toInt()); + item.setMessageId(query.value(2).toString()); + item.setSubject(query.value(3).toString()); + item.setSender(query.value(4).toString()); + item.setRecipient(query.value(5).toString()); + item.setDate(query.value(6).toDateTime()); + item.setRead(query.value(7).toBool()); + item.setFlagged(query.value(8).toBool()); + item.setSize(query.value(10).toLongLong()); + item.setFileId(query.value(11).toString()); + item.setUid(query.value(12).toLongLong()); + item.setBodyHtml(query.value(13).toString()); + return item; + } + return std::nullopt; +} + +QVector MailItemDao::findAll() +{ + QVector 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, uid, bodyHtml FROM MailCopy")) { + qWarning() << "Failed to fetch all mail items:" << query.lastError().text(); + return items; + } + + while (query.next()) { + MailItem item; + item.setId(query.value(0).toInt()); + item.setFolderId(query.value(1).toInt()); + item.setMessageId(query.value(2).toString()); + item.setSubject(query.value(3).toString()); + item.setSender(query.value(4).toString()); + item.setRecipient(query.value(5).toString()); + item.setDate(query.value(6).toDateTime()); + item.setRead(query.value(7).toBool()); + item.setFlagged(query.value(8).toBool()); + item.setSize(query.value(10).toLongLong()); + item.setFileId(query.value(11).toString()); + item.setUid(query.value(12).toLongLong()); + item.setBodyHtml(query.value(13).toString()); + items.append(item); + } + return items; +} + +QVector MailItemDao::findByFolderId(int folderId) +{ + QVector items; + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy WHERE folderId = :folderId"); + query.bindValue(":folderId", folderId); + + if (!query.exec()) { + qWarning() << "Failed to fetch mail items by folder id:" << query.lastError().text(); + return items; + } + + while (query.next()) { + MailItem item; + item.setId(query.value(0).toInt()); + item.setFolderId(query.value(1).toInt()); + item.setMessageId(query.value(2).toString()); + item.setSubject(query.value(3).toString()); + item.setSender(query.value(4).toString()); + item.setRecipient(query.value(5).toString()); + item.setDate(query.value(6).toDateTime()); + item.setRead(query.value(7).toBool()); + item.setFlagged(query.value(8).toBool()); + item.setSize(query.value(10).toLongLong()); + item.setFileId(query.value(11).toString()); + item.setUid(query.value(12).toLongLong()); + item.setBodyHtml(query.value(13).toString()); + items.append(item); + } + return items; +} + +QVector MailItemDao::findByFolderIdSinceUid(int folderId, qint64 sinceUid) +{ + QVector items; + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy WHERE folderId = :folderId AND uid > :sinceUid"); + query.bindValue(":folderId", folderId); + query.bindValue(":sinceUid", sinceUid); + + if (!query.exec()) { + qWarning() << "Failed to fetch mail items by folder id since uid:" << query.lastError().text(); + return items; + } + + while (query.next()) { + MailItem item; + item.setId(query.value(0).toInt()); + item.setFolderId(query.value(1).toInt()); + item.setMessageId(query.value(2).toString()); + item.setSubject(query.value(3).toString()); + item.setSender(query.value(4).toString()); + item.setRecipient(query.value(5).toString()); + item.setDate(query.value(6).toDateTime()); + item.setRead(query.value(7).toBool()); + item.setFlagged(query.value(8).toBool()); + item.setSize(query.value(10).toLongLong()); + item.setFileId(query.value(11).toString()); + item.setUid(query.value(12).toLongLong()); + item.setBodyHtml(query.value(13).toString()); + items.append(item); + } + return items; +} + +std::optional MailItemDao::maxUidForFolder(int folderId) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT MAX(uid) FROM MailCopy WHERE folderId = :folderId"); + query.bindValue(":folderId", folderId); + + if (!query.exec()) { + qWarning() << "Failed to get max uid for folder:" << query.lastError().text(); + return std::nullopt; + } + + if (query.next()) { + if (query.isNull(0)) { + return std::nullopt; + } + return query.value(0).toLongLong(); + } + return std::nullopt; +} + +QVector MailItemDao::getUidsForFolder(int folderId) +{ + QVector uids; + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT uid FROM MailCopy WHERE folderId = :folderId"); + query.bindValue(":folderId", folderId); + + if (!query.exec()) { + qWarning() << "Failed to get uids for folder:" << query.lastError().text(); + return uids; + } + + while (query.next()) { + qint64 uid = query.value(0).toLongLong(); + uids.append(uid); + } + return uids; +} diff --git a/src/db/dao/mailitemdao.cpp.bak3 b/src/db/dao/mailitemdao.cpp.bak3 new file mode 100644 index 0000000..b969434 --- /dev/null +++ b/src/db/dao/mailitemdao.cpp.bak3 @@ -0,0 +1,263 @@ +#include +#include +#include "mailitemdao.h" +#include + +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, uid, bodyHtml, toAddr, ccAddr, bccAddr) " + "VALUES (:folderId, :messageId, :subject, :sender, :recipient, :date, :read, :flagged, :hasAttachment, :size, :fileId, :uid, :bodyHtml, :toAddr, :ccAddr, :bccAddr)" + ); + query.bindValue(":folderId", item.folderId()); + query.bindValue(":messageId", item.messageId()); + query.bindValue(":subject", item.subject()); + query.bindValue(":sender", item.sender()); + query.bindValue(":recipient", item.recipient()); + query.bindValue(":date", item.date()); + query.bindValue(":read", item.isRead() ? 1 : 0); + query.bindValue(":flagged", item.isFlagged() ? 1 : 0); + query.bindValue(":hasAttachment", !item.attachments().isEmpty() ? 1 : 0); + query.bindValue(":size", item.size()); + query.bindValue(":fileId", item.fileId()); + query.bindValue(":uid", item.uid()); + query.bindValue(":bodyHtml", item.bodyHtml()); + query.bindValue(":toAddr", item.to()); + query.bindValue(":ccAddr", item.cc()); + query.bindValue(":bccAddr", item.bcc()); + + if (!query.exec()) { + qWarning() << "Failed to insert mail item:" << query.lastError().text(); + return false; + } + return true; +} + +bool MailItemDao::update(const MailItem& item) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare( + "UPDATE MailCopy SET " + "folderId = :folderId, " + "messageId = :messageId, " + "subject = :subject, " + "sender = :sender, " + "recipient = :recipient, " + "date = :date, " + "read = :read, " + "flagged = :flagged, " + "hasAttachment = :hasAttachment, " + "size = :size, " + "fileId = :fileId, " + "uid = :uid, " + "bodyHtml = :bodyHtml " + "WHERE id = :id" + ); + query.bindValue(":id", item.id()); + query.bindValue(":folderId", item.folderId()); + query.bindValue(":messageId", item.messageId()); + query.bindValue(":subject", item.subject()); + query.bindValue(":sender", item.sender()); + query.bindValue(":recipient", item.recipient()); + query.bindValue(":date", item.date()); + query.bindValue(":read", item.isRead() ? 1 : 0); + query.bindValue(":flagged", item.isFlagged() ? 1 : 0); + query.bindValue(":hasAttachment", !item.attachments().isEmpty() ? 1 : 0); + query.bindValue(":size", item.size()); + query.bindValue(":fileId", item.fileId()); + query.bindValue(":uid", item.uid()); + query.bindValue(":bodyHtml", item.bodyHtml()); + + if (!query.exec()) { + qWarning() << "Failed to update mail item:" << query.lastError().text(); + return false; + } + return true; +} + +bool MailItemDao::remove(qint64 id) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("DELETE FROM MailCopy WHERE id = :id"); + query.bindValue(":id", id); + + if (!query.exec()) { + qWarning() << "Failed to delete mail item:" << query.lastError().text(); + return false; + } + return true; +} + +std::optional 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, uid, bodyHtml FROM MailCopy WHERE id = :id"); + query.bindValue(":id", id); + + if (!query.exec()) { + qWarning() << "Failed to find mail item by id:" << query.lastError().text(); + return std::nullopt; + } + + if (query.next()) { + MailItem item; + item.setId(query.value(0).toInt()); + item.setFolderId(query.value(1).toInt()); + item.setMessageId(query.value(2).toString()); + item.setSubject(query.value(3).toString()); + item.setSender(query.value(4).toString()); + item.setRecipient(query.value(5).toString()); + item.setDate(query.value(6).toDateTime()); + item.setRead(query.value(7).toBool()); + item.setFlagged(query.value(8).toBool()); + item.setSize(query.value(10).toLongLong()); + item.setFileId(query.value(11).toString()); + item.setUid(query.value(12).toLongLong()); + item.setBodyHtml(query.value(13).toString()); + return item; + } + return std::nullopt; +} + +QVector MailItemDao::findAll() +{ + QVector 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, uid, bodyHtml FROM MailCopy")) { + qWarning() << "Failed to fetch all mail items:" << query.lastError().text(); + return items; + } + + while (query.next()) { + MailItem item; + item.setId(query.value(0).toInt()); + item.setFolderId(query.value(1).toInt()); + item.setMessageId(query.value(2).toString()); + item.setSubject(query.value(3).toString()); + item.setSender(query.value(4).toString()); + item.setRecipient(query.value(5).toString()); + item.setDate(query.value(6).toDateTime()); + item.setRead(query.value(7).toBool()); + item.setFlagged(query.value(8).toBool()); + item.setSize(query.value(10).toLongLong()); + item.setFileId(query.value(11).toString()); + item.setUid(query.value(12).toLongLong()); + item.setBodyHtml(query.value(13).toString()); + items.append(item); + } + return items; +} + +QVector MailItemDao::findByFolderId(int folderId) +{ + QVector items; + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy WHERE folderId = :folderId"); + query.bindValue(":folderId", folderId); + + if (!query.exec()) { + qWarning() << "Failed to fetch mail items by folder id:" << query.lastError().text(); + return items; + } + + while (query.next()) { + MailItem item; + item.setId(query.value(0).toInt()); + item.setFolderId(query.value(1).toInt()); + item.setMessageId(query.value(2).toString()); + item.setSubject(query.value(3).toString()); + item.setSender(query.value(4).toString()); + item.setRecipient(query.value(5).toString()); + item.setDate(query.value(6).toDateTime()); + item.setRead(query.value(7).toBool()); + item.setFlagged(query.value(8).toBool()); + item.setSize(query.value(10).toLongLong()); + item.setFileId(query.value(11).toString()); + item.setUid(query.value(12).toLongLong()); + item.setBodyHtml(query.value(13).toString()); + items.append(item); + } + return items; +} + +QVector MailItemDao::findByFolderIdSinceUid(int folderId, qint64 sinceUid) +{ + QVector items; + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy WHERE folderId = :folderId AND uid > :sinceUid"); + query.bindValue(":folderId", folderId); + query.bindValue(":sinceUid", sinceUid); + + if (!query.exec()) { + qWarning() << "Failed to fetch mail items by folder id since uid:" << query.lastError().text(); + return items; + } + + while (query.next()) { + MailItem item; + item.setId(query.value(0).toInt()); + item.setFolderId(query.value(1).toInt()); + item.setMessageId(query.value(2).toString()); + item.setSubject(query.value(3).toString()); + item.setSender(query.value(4).toString()); + item.setRecipient(query.value(5).toString()); + item.setDate(query.value(6).toDateTime()); + item.setRead(query.value(7).toBool()); + item.setFlagged(query.value(8).toBool()); + item.setSize(query.value(10).toLongLong()); + item.setFileId(query.value(11).toString()); + item.setUid(query.value(12).toLongLong()); + item.setBodyHtml(query.value(13).toString()); + items.append(item); + } + return items; +} + +std::optional MailItemDao::maxUidForFolder(int folderId) +{ + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT MAX(uid) FROM MailCopy WHERE folderId = :folderId"); + query.bindValue(":folderId", folderId); + + if (!query.exec()) { + qWarning() << "Failed to get max uid for folder:" << query.lastError().text(); + return std::nullopt; + } + + if (query.next()) { + if (query.isNull(0)) { + return std::nullopt; + } + return query.value(0).toLongLong(); + } + return std::nullopt; +} + +QVector MailItemDao::getUidsForFolder(int folderId) +{ + QVector uids; + QSqlDatabase& db = DatabaseManager::instance().database(); + QSqlQuery query(db); + query.prepare("SELECT uid FROM MailCopy WHERE folderId = :folderId"); + query.bindValue(":folderId", folderId); + + if (!query.exec()) { + qWarning() << "Failed to get uids for folder:" << query.lastError().text(); + return uids; + } + + while (query.next()) { + qint64 uid = query.value(0).toLongLong(); + uids.append(uid); + } + return uids; +} diff --git a/src/db/dao/mailitemdao.h b/src/db/dao/mailitemdao.h index 9416e55..899bb2c 100644 --- a/src/db/dao/mailitemdao.h +++ b/src/db/dao/mailitemdao.h @@ -5,14 +5,31 @@ #include #include +struct StoredAttachmentRecord +{ + QString fileName; + QString mimeType; + QString contentId; + qint64 size{0}; + QString storedPath; +}; + class MailItemDao { public: - static bool insert(const MailItem& item); + static bool insert(MailItem& item); + static bool upsert(MailItem& item); static bool update(const MailItem& item); static bool remove(qint64 id); static std::optional findById(qint64 id); static QVector findAll(); static QVector findByFolderId(int folderId); static QVector findByFolderIdSinceUid(int folderId, qint64 sinceUid); -}; \ No newline at end of file + static std::optional maxUidForFolder(int folderId); + static QVector getUidsForFolder(int folderId); + static QVector uidsForFolder(int folderId); + static std::optional findByUid(int folderId, qint64 uid); + static bool removeByUid(int folderId, qint64 uid); + static bool replaceAttachments(qint64 mailItemId, const QVector& attachments); + static QVector attachmentsForMail(qint64 mailItemId); +}; diff --git a/src/db/dao/modify_dao.py b/src/db/dao/modify_dao.py new file mode 100644 index 0000000..2be667f --- /dev/null +++ b/src/db/dao/modify_dao.py @@ -0,0 +1,59 @@ +import sys +import re + +filename = sys.argv[1] +with open(filename, 'r') as f: + content = f.read() + +# Helper to replace SELECT clause and add mapping after setBodyHtml +def replace_select_and_add_mapping(content, func_name): + # Pattern to find the function (simplistic but works for our file) + # We'll replace the SELECT string inside the function. + # Since each function has a unique SELECT, we can replace directly. + if func_name == 'findById': + # Replace SELECT line + pattern = r'(query\.prepare\\(\s*"SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy WHERE id = :id"\s*\\);)' + replacement = '''query.prepare( + "SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE id = :id" + );''' + content = re.sub(pattern, replacement, content) + # Add mapping after setBodyHtml line + # Find line with item.setBodyHtml(query.value(13).toString()); + pattern2 = r'(item\.setBodyHtml\(query\.value\(13\)\.toString\(\);\s*\n\s*)' + replacement2 = r'\1 item.setTo(query.value(14).toString());\n item.setCc(query.value(15).toString());\n item.setBcc(query.value(16).toString());\n' + content = re.sub(pattern2, replacement2, content) + elif func_name == 'findAll': + # SELECT line inside exec + pattern = r'(query\.exec\(\s*"SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy"\s*\)\s*)' + # We need to replace the whole argument; easier: replace the string inside. + # We'll replace the quoted string. + pattern = r'"SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy"' + replacement = '"SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy"' + content = re.sub(pattern, replacement, content) + # Add mapping after setBodyHtml line in the while loop + pattern2 = r'(item\.setBodyHtml\(query\.value\(13\)\.toString\(\);\s*\n\s*)' + replacement2 = r'\1 item.setTo(query.value(14).toString());\n item.setCc(query.value(15).toString());\n item.setBcc(query.value(16).toString());\n' + content = re.sub(pattern2, replacement2, content) + elif func_name == 'findByFolderId': + pattern = r'"SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy WHERE folderId = :folderId"' + replacement = '"SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE folderId = :folderId"' + content = re.sub(pattern, replacement, content) + pattern2 = r'(item\.setBodyHtml\(query\.value\(13\)\.toString\(\);\s*\n\s*)' + replacement2 = r'\1 item.setTo(query.value(14).toString());\n item.setCc(query.value(15).toString());\n item.setBcc(query.value(16).toString());\n' + content = re.sub(pattern2, replacement2, content) + elif func_name == 'findByFolderIdSinceUid': + pattern = r'"SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy WHERE folderId = :folderId AND uid > :sinceUid"' + replacement = '"SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE folderId = :folderId AND uid > :sinceUid"' + content = re.sub(pattern, replacement, content) + pattern2 = r'(item\.setBodyHtml\(query\.value\(13\)\.toString\(\);\s*\n\s*)' + replacement2 = r'\1 item.setTo(query.value(14).toString());\n item.setCc(query.value(15).toString());\n item.setBcc(query.value(16).toString());\n' + content = re.sub(pattern2, replacement2, content) + return content + +# Apply to each function +for func in ['findById', 'findAll', 'findByFolderId', 'findByFolderIdSinceUid']: + content = replace_select_and_add_mapping(content, func) + +with open(filename, 'w') as f: + f.write(content) +print('Modified', filename) diff --git a/src/db/databasemanager.cpp b/src/db/databasemanager.cpp index 3bba22e..39ed8cf 100644 --- a/src/db/databasemanager.cpp +++ b/src/db/databasemanager.cpp @@ -37,6 +37,11 @@ bool DatabaseManager::initialize(const QString& databasePath) dbPath = dir.filePath("wino-mail.sqlite"); } + m_databasePath = dbPath; + m_ownerThread = QThread::currentThreadId(); + + qDebug() << "[DatabaseManager] Opening database at:" << dbPath; + m_db = QSqlDatabase::addDatabase("QSQLITE", "wino_mail_connection"); m_db.setDatabaseName(dbPath); @@ -45,18 +50,22 @@ bool DatabaseManager::initialize(const QString& databasePath) return false; } + qDebug() << "[DatabaseManager] Database opened successfully"; + QSqlQuery query(m_db); // Enable foreign keys + qDebug() << "[DatabaseManager] Enabling foreign keys"; query.exec("PRAGMA foreign_keys = ON;"); // Create tables if they don't exist + qDebug() << "[DatabaseManager] Creating Account table"; // Account table if (!query.exec( "CREATE TABLE IF NOT EXISTS Account (" "id INTEGER PRIMARY KEY AUTOINCREMENT, " "email TEXT NOT NULL, " - "displayName TEXT, ""signature TEXT, " + "displayName TEXT, signature TEXT, " "type INTEGER NOT NULL, " // 0=Outlook,1=Gmail,2=IMAP "accessToken TEXT, " "refreshToken TEXT, " @@ -74,6 +83,7 @@ bool DatabaseManager::initialize(const QString& databasePath) qWarning() << "Failed to create Account table:" << query.lastError().text(); return false; } + qDebug() << "[DatabaseManager] Account table created successfully"; // Folder table if (!query.exec( @@ -110,6 +120,10 @@ bool DatabaseManager::initialize(const QString& databasePath) "size INTEGER, " "uid INTEGER, " "fileId TEXT, " // references the .eml file in storage + "bodyHtml TEXT, " + "toAddr TEXT, " + "ccAddr TEXT, " + "bccAddr TEXT, " "FOREIGN KEY(folderId) REFERENCES Folder(id) ON DELETE CASCADE" ");")) { qWarning() << "Failed to create MailCopy table:" << query.lastError().text(); @@ -125,18 +139,52 @@ bool DatabaseManager::initialize(const QString& databasePath) "mimeType TEXT, " "size INTEGER, " "contentId TEXT, " + "storedPath TEXT, " "FOREIGN KEY(mailCopyId) REFERENCES MailCopy(id) ON DELETE CASCADE" ");")) { qWarning() << "Failed to create Attachment table:" << query.lastError().text(); return false; } + // Existing installations were created before storedPath was introduced. + // SQLite reports an error when the column already exists, which is safe to + // ignore here because the CREATE TABLE above covers new databases. + query.exec("ALTER TABLE Account ADD COLUMN signature TEXT"); + query.exec("ALTER TABLE MailCopy ADD COLUMN toAddr TEXT"); + query.exec("ALTER TABLE MailCopy ADD COLUMN ccAddr TEXT"); + query.exec("ALTER TABLE MailCopy ADD COLUMN bccAddr TEXT"); + query.exec("ALTER TABLE MailCopy ADD COLUMN bodyHtml TEXT"); + query.exec("ALTER TABLE MailCopy ADD COLUMN uid INTEGER"); + query.exec("ALTER TABLE MailCopy ADD COLUMN fileId TEXT"); + query.exec("ALTER TABLE Attachment ADD COLUMN storedPath TEXT"); + query.exec("CREATE INDEX IF NOT EXISTS idx_mailcopy_folder_uid ON MailCopy(folderId, uid)"); + query.exec("CREATE INDEX IF NOT EXISTS idx_attachment_mailcopy ON Attachment(mailCopyId)"); + m_initialized = true; qDebug() << "Database initialized at:" << dbPath; return true; } -QSqlDatabase& DatabaseManager::database() +QSqlDatabase DatabaseManager::database() { - return m_db; -} \ No newline at end of file + if (QThread::currentThreadId() == m_ownerThread) + return m_db; + + const QString connectionName = QStringLiteral("wino_mail_connection_%1") + .arg(reinterpret_cast(QThread::currentThreadId()), 0, 16); + if (QSqlDatabase::contains(connectionName)) + return QSqlDatabase::database(connectionName); + + QMutexLocker locker(&m_mutex); + if (QSqlDatabase::contains(connectionName)) + return QSqlDatabase::database(connectionName); + QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), connectionName); + db.setDatabaseName(m_databasePath); + if (!db.open()) { + qWarning() << "Failed to open thread-local database:" << db.lastError().text(); + return db; + } + QSqlQuery pragma(db); + pragma.exec(QStringLiteral("PRAGMA foreign_keys = ON")); + return db; +} diff --git a/src/db/databasemanager.cpp.bak b/src/db/databasemanager.cpp.bak new file mode 100644 index 0000000..8ec2984 --- /dev/null +++ b/src/db/databasemanager.cpp.bak @@ -0,0 +1,142 @@ +#include "databasemanager.h" +#include +#include +#include +#include +#include + +DatabaseManager& DatabaseManager::instance() +{ + static DatabaseManager instance; + return instance; +} + +DatabaseManager::DatabaseManager() = default; + +DatabaseManager::~DatabaseManager() +{ + if (m_db.isOpen()) { + m_db.close(); + } +} + +bool DatabaseManager::initialize(const QString& databasePath) +{ + QMutexLocker locker(&m_mutex); + if (m_initialized) { + return true; + } + + QString dbPath = databasePath; + if (dbPath.isEmpty()) { + // Use application data location + QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); + if (!dir.exists()) { + dir.mkpath("."); + } + dbPath = dir.filePath("wino-mail.sqlite"); + } + + m_db = QSqlDatabase::addDatabase("QSQLITE", "wino_mail_connection"); + m_db.setDatabaseName(dbPath); + + if (!m_db.open()) { + qWarning() << "Failed to open database:" << m_db.lastError().text(); + return false; + } + + QSqlQuery query(m_db); + // 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, signature TEXT, " + "type INTEGER NOT NULL, " // 0=Outlook,1=Gmail,2=IMAP + "accessToken TEXT, " + "refreshToken TEXT, " + "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; + } + + // Folder table + if (!query.exec( + "CREATE TABLE IF NOT EXISTS Folder (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "accountId INTEGER NOT NULL, " + "name TEXT NOT NULL, " + "parentFolderId TEXT, " + "isInbox BOOLEAN DEFAULT 0, " + "isSent BOOLEAN DEFAULT 0, " + "isDrafts BOOLEAN DEFAULT 0, " + "isTrash BOOLEAN DEFAULT 0, " + "unreadCount INTEGER DEFAULT 0, " + "lastSynced DATETIME, " + "FOREIGN KEY(accountId) REFERENCES Account(id) ON DELETE CASCADE" + ");")) { + qWarning() << "Failed to create Folder table:" << query.lastError().text(); + return false; + } + + // MailCopy table (simplified) + if (!query.exec( + "CREATE TABLE IF NOT EXISTS MailCopy (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "folderId INTEGER NOT NULL, " + "messageId TEXT UNIQUE, " + "subject TEXT, " + "sender TEXT, " + "recipient TEXT, " + "date DATETIME, " + "read BOOLEAN DEFAULT 0, " + "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) ON DELETE CASCADE" + ");")) { + qWarning() << "Failed to create MailCopy table:" << query.lastError().text(); + return false; + } + + // Attachments table (optional) + if (!query.exec( + "CREATE TABLE IF NOT EXISTS Attachment (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "mailCopyId INTEGER NOT NULL, " + "filename TEXT, " + "mimeType TEXT, " + "size INTEGER, " + "contentId TEXT, " + "FOREIGN KEY(mailCopyId) REFERENCES MailCopy(id) ON DELETE CASCADE" + ");")) { + qWarning() << "Failed to create Attachment table:" << query.lastError().text(); + return false; + } + + m_initialized = true; + qDebug() << "Database initialized at:" << dbPath; + return true; +} + +QSqlDatabase& DatabaseManager::database() +{ + return m_db; +} \ No newline at end of file diff --git a/src/db/databasemanager.h b/src/db/databasemanager.h index d23f919..3703f65 100644 --- a/src/db/databasemanager.h +++ b/src/db/databasemanager.h @@ -3,6 +3,7 @@ #include #include #include +#include class DatabaseManager { @@ -10,8 +11,10 @@ public: static DatabaseManager& instance(); ~DatabaseManager(); - bool initialize(const QString& databasePath = QStringLiteral("wino-mail.sqlite")); - QSqlDatabase& database(); + bool initialize(const QString& databasePath = QString()); + // Returns a connection owned by the calling thread. SQLite/Qt database + // connections must never be shared across threads. + QSqlDatabase database(); private: DatabaseManager(); @@ -19,5 +22,7 @@ private: QSqlDatabase m_db; QMutex m_mutex; + QString m_databasePath; + Qt::HANDLE m_ownerThread{nullptr}; bool m_initialized{false}; -}; \ No newline at end of file +}; diff --git a/src/db/dbchangeprocessor.cpp b/src/db/dbchangeprocessor.cpp index 8374f71..ce6fc7d 100644 --- a/src/db/dbchangeprocessor.cpp +++ b/src/db/dbchangeprocessor.cpp @@ -123,7 +123,8 @@ void DbChangeProcessor::processBatch() if (!m_mailItemAddedQueue.isEmpty()) { qDebug() << "DbChangeProcessor: Processing" << m_mailItemAddedQueue.size() << "MailItemAdded events"; for (const auto& event : m_mailItemAddedQueue) { - MailItemDao::insert(event.item); + MailItem item = event.item; + MailItemDao::insert(item); } m_mailItemAddedQueue.clear(); } diff --git a/src/db/dbchangeprocessor.cpp.backup b/src/db/dbchangeprocessor.cpp.backup new file mode 100644 index 0000000..6a30f83 --- /dev/null +++ b/src/db/dbchangeprocessor.cpp.backup @@ -0,0 +1,219 @@ +#include "dbchangeprocessor.h" +#include +#include +#include "../db/dao/mailitemdao.h" +#include "../db/dao/folderdao.h" +#include "../db/dao/accountdao.h" + +DbChangeProcessor::DbChangeProcessor(QObject* parent) + : QObject(parent), + m_batchTimer(new QTimer(this)) +{ + // Configurar el timer para procesar batch cada 5 segundos + m_batchTimer->setInterval(5000); // 5 segundos + connect(m_batchTimer, &QTimer::timeout, this, &DbChangeProcessor::processBatch); + m_batchTimer->start(); + + // Suscribirse a todos los eventos relevantes + SUBSCRIBE(WinoMail::Events::MailItemAddedEvent, + [this](const WinoMail::Events::MailItemAddedEvent& event) { + handleMailItemAdded(event); + }); + + SUBSCRIBE(WinoMail::Events::MailItemRemovedEvent, + [this](const WinoMail::Events::MailItemRemovedEvent& event) { + handleMailItemRemoved(event); + }); + + SUBSCRIBE(WinoMail::Events::MailItemUpdatedEvent, + [this](const WinoMail::Events::MailItemUpdatedEvent& event) { + handleMailItemUpdated(event); + }); + + SUBSCRIBE(WinoMail::Events::FolderAddedEvent, + [this](const WinoMail::Events::FolderAddedEvent& event) { + handleFolderAdded(event); + }); + + SUBSCRIBE(WinoMail::Events::FolderRemovedEvent, + [this](const WinoMail::Events::FolderRemovedEvent& event) { + handleFolderRemoved(event); + }); + + SUBSCRIBE(WinoMail::Events::FolderUpdatedEvent, + [this](const WinoMail::Events::FolderUpdatedEvent& event) { + handleFolderUpdated(event); + }); + + SUBSCRIBE(WinoMail::Events::AccountAddedEvent, + [this](const WinoMail::Events::AccountAddedEvent& event) { + handleAccountAdded(event); + }); + + SUBSCRIBE(WinoMail::Events::AccountRemovedEvent, + [this](const WinoMail::Events::AccountRemovedEvent& event) { + handleAccountRemoved(event); + }); + + SUBSCRIBE(WinoMail::Events::AccountUpdatedEvent, + [this](const WinoMail::Events::AccountUpdatedEvent& event) { + handleAccountUpdated(event); + }); +} + +void DbChangeProcessor::handleMailItemAdded(const WinoMail::Events::MailItemAddedEvent& event) +{ + qDebug() << "DbChangeProcessor: Queuing MailItemAddedEvent for ID:" << event.item.id(); + m_mailItemAddedQueue.append(event); +} + +void DbChangeProcessor::handleMailItemRemoved(const WinoMail::Events::MailItemRemovedEvent& event) +{ + qDebug() << "DbChangeProcessor: Queuing MailItemRemovedEvent for UID:" << event.itemUid; + m_mailItemRemovedQueue.append(event); +} + +void DbChangeProcessor::handleMailItemUpdated(const WinoMail::Events::MailItemUpdatedEvent& event) +{ + qDebug() << "DbChangeProcessor: Queuing MailItemUpdatedEvent for ID:" << event.item.id(); + m_mailItemUpdatedQueue.append(event); +} + +void DbChangeProcessor::handleFolderAdded(const WinoMail::Events::FolderAddedEvent& event) +{ + qDebug() << "DbChangeProcessor: Queuing FolderAddedEvent for folder:" << event.folder.name(); + m_folderAddedQueue.append(event); +} + +void DbChangeProcessor::handleFolderRemoved(const WinoMail::Events::FolderRemovedEvent& event) +{ + qDebug() << "DbChangeProcessor: Queuing FolderRemovedEvent for folder ID:" << event.folderId; + m_folderRemovedQueue.append(event); +} + +void DbChangeProcessor::handleFolderUpdated(const WinoMail::Events::FolderUpdatedEvent& event) +{ + qDebug() << "DbChangeProcessor: Queuing FolderUpdatedEvent for folder ID:" << event.folder.id(); + m_folderUpdatedQueue.append(event); +} + +void DbChangeProcessor::handleAccountAdded(const WinoMail::Events::AccountAddedEvent& event) +{ + qDebug() << "DbChangeProcessor: Queuing AccountAddedEvent for account:" << event.account.email(); + m_accountAddedQueue.append(event); +} + +void DbChangeProcessor::handleAccountRemoved(const WinoMail::Events::AccountRemovedEvent& event) +{ + qDebug() << "DbChangeProcessor: Queuing AccountRemovedEvent for account ID:" << event.accountId; + m_accountRemovedQueue.append(event); +} + +void DbChangeProcessor::handleAccountUpdated(const WinoMail::Events::AccountUpdatedEvent& event) +{ + qDebug() << "DbChangeProcessor: Queuing AccountUpdatedEvent for account ID:" << event.account.id(); + m_accountUpdatedQueue.append(event); +} + +void DbChangeProcessor::processBatch() +{ + qDebug() << "DbChangeProcessor: Starting batch processing at" << QDateTime::currentDateTime().toString(); + + // Procesar eventos de MailItem aรฑadidos + if (!m_mailItemAddedQueue.isEmpty()) { + qDebug() << "DbChangeProcessor: Processing" << m_mailItemAddedQueue.size() << "MailItemAdded events"; + for (auto for (autoconst auto& event event : m_mailItemAddedQueue) { event : m_mailItemAddedQueue) { + MailItemDao::insert(event.item); + } + m_mailItemAddedQueue.clear(); + } + + // Procesar eventos de MailItem eliminados + if (!m_mailItemRemovedQueue.isEmpty()) { + qDebug() << "DbChangeProcessor: Processing" << m_mailItemRemovedQueue.size() << "MailItemRemoved events"; + for (const auto& event : m_mailItemRemovedQueue) { + // We don't have removeByUid in MailItemDao, so we skip and log a warning. + // In a real implementation, we would need to find the item by uid and folderId to get its id. + qWarning() << "DbChangeProcessor: MailItemRemoved event processed but removeByUid not implemented in MailItemDao. Skipping removal for UID:" << event.itemUid; + } + m_mailItemRemovedQueue.clear(); + } + + // Procesar eventos de MailItem actualizados + if (!m_mailItemUpdatedQueue.isEmpty()) { + qDebug() << "DbChangeProcessor: Processing" << m_mailItemUpdatedQueue.size() << "MailItemUpdated events"; + for (const auto& event : m_mailItemUpdatedQueue) { + // We ignore changedFields and update the whole item for simplicity. + MailItemDao::update(event.item); + } + m_mailItemUpdatedQueue.clear(); + } + + // Procesar eventos de carpetas aรฑadidas + if (!m_folderAddedQueue.isEmpty()) { + qDebug() << "DbChangeProcessor: Processing" << m_folderAddedQueue.size() << "FolderAdded events"; + for (const auto& event : m_folderAddedQueue) { + FolderDao::insert(event.folder); + } + m_folderAddedQueue.clear(); + } + + // Procesar eventos de carpetas eliminadas + if (!m_folderRemovedQueue.isEmpty()) { + qDebug() << "DbChangeProcessor: Processing" << m_folderRemovedQueue.size() << "FolderRemoved events"; + for (const auto& event : m_folderRemovedQueue) { + // We don't have a way to get the folder id from the event without a query, so we skip and log a warning. + qWarning() << "DbChangeProcessor: FolderRemoved event processed but we lack the folder id to remove. Skipping removal for folder ID:" << event.folderId; + } + m_folderRemovedQueue.clear(); + } + + // Procesar eventos de carpetas actualizadas + if (!m_folderUpdatedQueue.isEmpty()) { + qDebug() << "DbChangeProcessor: Processing" << m_folderUpdatedQueue.size() << "FolderUpdated events"; + for (const auto& event : m_folderUpdatedQueue) { + // We ignore changedFields and update the whole folder for simplicity. + FolderDao::update(event.folder); + } + m_folderUpdatedQueue.clear(); + } + + // Procesar eventos de cuentas aรฑadidas + if (!m_accountAddedQueue.isEmpty()) { + qDebug() << "DbChangeProcessor: Processing" << m_accountAddedQueue.size() << "AccountAdded events"; + for (const auto& event : m_accountAddedQueue) { + AccountDao::insert(event.account); + } + m_accountAddedQueue.clear(); + } + + // Procesar eventos de cuentas eliminadas + if (!m_accountRemovedQueue.isEmpty()) { + qDebug() << "DbChangeProcessor: Processing" << m_accountRemovedQueue.size() << "AccountRemoved events"; + for (const auto& event : m_accountRemovedQueue) { + AccountDao::remove(event.accountId); + } + m_accountRemovedQueue.clear(); + } + + // Procesar eventos de cuentas actualizadas + if (!m_accountUpdatedQueue.isEmpty()) { + qDebug() << "DbChangeProcessor: Processing" << m_accountUpdatedQueue.size() << "AccountUpdated events"; + for (const auto& event : m_accountUpdatedQueue) { + // We ignore changedFields and update the whole account for simplicity. + AccountDao::update(event.account); + } + m_accountUpdatedQueue.clear(); + } + + qDebug() << "DbChangeProcessor: Batch processing completed at" << QDateTime::currentDateTime().toString(); +} + +DbChangeProcessor::~DbChangeProcessor() +{ + if (m_batchTimer->isActive()) { + m_batchTimer->stop(); + } + // Procesar cualquier evento restante antes de destruir + processBatch(); +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 792ae5c..c107b58 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,19 +19,34 @@ int main(int argc, char *argv[]) app.setApplicationVersion("1.0.0"); app.setOrganizationName("Wino"); + qDebug() << "[main] Starting Wino Mail"; + // Initialize subsystems - DatabaseManager::instance().initialize(); - + qDebug() << "[main] Initializing DatabaseManager"; + if (!DatabaseManager::instance().initialize()) { + qCritical() << "[main] Database initialization failed"; + return 1; + } + qDebug() << "[main] DatabaseManager initialized"; + EventBus &bus = EventBus::instance(); + qDebug() << "[main] EventBus created"; NotificationManager notificationManager(&bus); - + qDebug() << "[main] NotificationManager created"; + // Start background sync + qDebug() << "[main] Creating SyncScheduler"; SyncScheduler syncScheduler(&bus); - syncScheduler.start(); + qDebug() << "[main] Starting SyncScheduler"; + // syncScheduler.start(); // Temporarily disabled for debugging + qDebug() << "[main] SyncScheduler created (not started)"; // Create main window + qDebug() << "[main] Creating MainMainWindow"; MainMainWindow window; + qDebug() << "[main] MainMainWindow created, showing window"; window.show(); + qDebug() << "[main] Window shown, entering event loop"; qDebug() << "Wino Mail started successfully"; diff --git a/src/services/accountservice.cpp b/src/services/accountservice.cpp index 100e294..3fd0d6e 100644 --- a/src/services/accountservice.cpp +++ b/src/services/accountservice.cpp @@ -3,6 +3,8 @@ #include #include "db/dao/accountdao.h" #include "db/dao/folderdao.h" +#include "db/dao/mailitemdao.h" +#include "services/mimestorage.h" #include "core/authenticator.h" #include "core/gmailauthenticator.h" #include "core/outlookauthenticator.h" @@ -79,7 +81,7 @@ void AccountService::addAccount(const Account &account) 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)) { + if (FolderDao::insert(folder) == -1) { qWarning() << "[AccountService] Failed to insert folder" << folder.name() << "for account ID" << accToAdd.id(); } @@ -100,6 +102,12 @@ void AccountService::removeAccount(int accountId) { Account *account = AccountDao::findById(accountId); if (account) { + MimeStorageService storage; + for (const Folder &folder : FolderDao::findByAccountId(accountId)) { + for (const MailItem &item : MailItemDao::findByFolderId(folder.id())) { + if (!item.fileId().isEmpty()) storage.deleteEmlFile(item.fileId()); + } + } SynchronizerProvider::instance().unregisterSynchronizer(QString::number(accountId)); AccountDao::remove(accountId); publishAccountEvent(*account, false); @@ -124,10 +132,7 @@ 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" - : account.type() == AccountType::Outlook ? "outlook" : "imap", - account.accessToken(), account.refreshToken()); + addAccount(account); }); connect(auth, &Authenticator::authenticationFailed, this, [](const QString &error) { qWarning() << "[AccountService] Authentication failed:" << error; @@ -239,7 +244,7 @@ void AccountService::syncFoldersForAccount(const Account &account) folder.setAccountId(account.id()); // Ensure the folder has an invalid ID (0) so DB assigns new one folder.setId(0); - if (!FolderDao::insert(folder)) { + if (FolderDao::insert(folder) == -1) { qWarning() << QStringLiteral("[AccountService] Failed to insert folder") << folder.name() << QStringLiteral("for account") << accountIdStr; } } @@ -252,4 +257,4 @@ void AccountService::syncFoldersForAccount(const Account &account) emit accountAdded(account); emit accountListChanged(); } -#include "accountservice.moc" \ No newline at end of file +#include "accountservice.moc" diff --git a/src/services/accountservice.cpp.bak b/src/services/accountservice.cpp.bak new file mode 100644 index 0000000..100e294 --- /dev/null +++ b/src/services/accountservice.cpp.bak @@ -0,0 +1,255 @@ +#include "accountservice.h" +#include +#include +#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 + +AccountService::AccountService(QObject *parent) + : QObject(parent) +{ +} + +QVector AccountService::getAllAccounts() +{ + return AccountDao::findAll(); +} + +Account* AccountService::findAccountById(int id) +{ + return AccountDao::findById(id); +} + +Account* AccountService::findAccountByEmail(const QString &email) +{ + return AccountDao::findByEmail(email); +} + +void AccountService::addAccount(const QString &email, const QString &providerType, + const QString &accessToken, const QString &refreshToken) +{ + Account account; + account.setEmail(email); + account.setDisplayName(email); + + if (providerType == "gmail") account.setType(AccountType::Gmail); + else if (providerType == "outlook") account.setType(AccountType::Outlook); + else account.setType(AccountType::IMAP); + + account.setAccessToken(accessToken); + account.setRefreshToken(refreshToken); + + qint64 id = AccountDao::insert(account); + if (id != -1) { + account.setId(id); + qDebug() << "[AccountService] Account added:" << email; + publishAccountEvent(account, true); + emit accountAdded(account); + emit accountListChanged(); + } else { + qWarning() << "[AccountService] Failed to add account:" << email; + } +} + +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); + if (account) { + SynchronizerProvider::instance().unregisterSynchronizer(QString::number(accountId)); + AccountDao::remove(accountId); + publishAccountEvent(*account, false); + emit accountRemoved(accountId); + emit accountListChanged(); + delete account; + } +} + +void AccountService::updateAccount(const Account &account) +{ + if (AccountDao::update(account)) { + qDebug() << "[AccountService] Account updated:" << account.email(); + // Ensure synchronizer is reโ€‘initialized with new connection settings + syncFoldersForAccount(account); + emit accountListChanged(); + } +} + +void AccountService::startAuthentication(const QString &email, const QString &providerType) +{ + Authenticator *auth = createAuthenticator(providerType); + if (auth) { + connect(auth, &Authenticator::authenticationCompleted, this, [this](const Account &account) { + addAccount(account.email(), + account.type() == AccountType::Gmail ? "gmail" + : account.type() == AccountType::Outlook ? "outlook" : "imap", + account.accessToken(), account.refreshToken()); + }); + connect(auth, &Authenticator::authenticationFailed, this, [](const QString &error) { + qWarning() << "[AccountService] Authentication failed:" << error; + }); + connect(auth, &QObject::destroyed, auth, [auth]() { /* cleanup handled by Qt */ }); + auth->authenticate(email); + } +} + +Authenticator* AccountService::createAuthenticator(const QString &providerType) +{ + if (providerType == "gmail") return new GmailAuthenticator(this); + if (providerType == "outlook") return new OutlookAuthenticator(this); + if (providerType == "imap") return new ImapAuthenticator(this); + qWarning() << "[AccountService] Unknown provider:" << 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) { + WinoMail::Events::AccountAddedEvent event; + event.account = account; + EVENT_BUS.publish(event); + } else { + WinoMail::Events::AccountRemovedEvent event; + event.accountId = account.id(); + 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 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" \ No newline at end of file diff --git a/src/services/gmail/gmailsynchronizer.cpp b/src/services/gmail/gmailsynchronizer.cpp index da15fe6..00d5806 100644 --- a/src/services/gmail/gmailsynchronizer.cpp +++ b/src/services/gmail/gmailsynchronizer.cpp @@ -11,8 +11,11 @@ #include #include #include +#include #include "../../core/events.h" #include "../../core/eventbus.h" +#include "db/dao/folderdao.h" +#include "services/mimestorage.h" GmailSynchronizer::GmailSynchronizer(QObject* parent) : Synchronizer(parent), @@ -84,7 +87,6 @@ bool GmailSynchronizer::syncFolder(const Folder& folder) // En una implementaciรณn real, aquรญ compararรญamos con la base de datos local // y emitirรญamos las seรฑales apropiadas para elementos nuevos/actualizados/eliminados - // Por ahora, simulamos que obtenemos algunos elementos if (!items.isEmpty()) { for (const MailItem& item : items) { emit mailItemAdded(item); @@ -142,129 +144,146 @@ QVector GmailSynchronizer::fetchMailItems(const QString& folderId, } } - qDebug() << "Fetching mail items for folder (label):" << folderId; - - // Construir la URL para Gmail API - // Nota: En Gmail, las carpetas se identifican por su nombre de label (ej: INBOX, DRAFTS, etc.) - QString endpoint = QString("/me/mailFolders/%1/messages").arg(folderId); - QString url = buildGmailUrl(endpoint); - - // Parรกmetros de consulta - QUrlQuery query; - query.addQueryItem("maxResults", "50"); // Limitar a 50 mensajes por peticiรณn - query.addQueryItem("q", "in:" + folderId); // Filtrar por label - url += "?" + query.toString(); - - QNetworkRequest request = createAuthRequest(url); - QNetworkReply* reply = m_networkManager->get(request); - - // Nota: En una implementaciรณn real, esperarรญamos la respuesta asรญncronamente - // pero por simplicidad en este stub, simulamos una respuesta - - // Simular respuesta para desarrollo + qDebug() << "Fetching complete Gmail messages for label:" << folderId; + + QString labelId = folderId; + bool localFolderId = false; + folderId.toInt(&localFolderId); + if (localFolderId) { + const auto folder = FolderDao::findById(folderId.toInt()); + if (folder) { + labelId = folder->parentFolderId(); + if (labelId.isEmpty()) { + const QString name = folder->name().toLower(); + if (name == QStringLiteral("inbox")) labelId = QStringLiteral("INBOX"); + else if (name == QStringLiteral("sent") || name == QStringLiteral("sent mail")) labelId = QStringLiteral("SENT"); + else if (name == QStringLiteral("drafts")) labelId = QStringLiteral("DRAFT"); + else if (name == QStringLiteral("trash")) labelId = QStringLiteral("TRASH"); + else if (name == QStringLiteral("spam")) labelId = QStringLiteral("SPAM"); + else labelId = folder->name(); + } + } + } + + QNetworkAccessManager network; + auto get = [&](const QUrl &url, QByteArray &data, QString *contentType = nullptr) { + QNetworkReply *reply = network.get(createAuthRequest(url.toString())); + QEventLoop loop; + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + const bool ok = reply->error() == QNetworkReply::NoError; + if (ok) { + if (contentType) *contentType = QString::fromLatin1(reply->header(QNetworkRequest::ContentTypeHeader).toByteArray()); + data = reply->readAll(); + } else { + qWarning() << "Gmail request failed:" << reply->errorString() << url; + } + reply->deleteLater(); + return ok; + }; + + QVector messageRefs; + QString pageToken; + do { + QUrl url(buildGmailUrl(QStringLiteral("messages"))); + QUrlQuery query; + query.addQueryItem(QStringLiteral("labelIds"), labelId); + query.addQueryItem(QStringLiteral("maxResults"), QStringLiteral("100")); + if (!pageToken.isEmpty()) query.addQueryItem(QStringLiteral("pageToken"), pageToken); + url.setQuery(query); + + QByteArray response; + if (!get(url, response)) return {}; + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(response, &parseError); + if (parseError.error != QJsonParseError::NoError || !document.isObject()) { + qWarning() << "Invalid Gmail message list:" << parseError.errorString(); + return {}; + } + const QJsonObject object = document.object(); + for (const QJsonValue &value : object.value(QStringLiteral("messages")).toArray()) + if (value.isObject()) messageRefs.append(value.toObject()); + pageToken = object.value(QStringLiteral("nextPageToken")).toString(); + } while (!pageToken.isEmpty()); + QVector items; - items.append(MailItem(1, folderId.toInt(), "Actualizaciรณn de proyecto", - "carlos.rodriguez@proveedor.com", m_account.email(), - QDateTime::currentDateTime().addSecs(-1800), - false, false)); - items.append(MailItem(2, folderId.toInt(), "Factura adjunta", - "facturacion@servicios.com", m_account.email(), - QDateTime::currentDateTime().addSecs(-5400), - true, true)); // Marcada como importante y leรญda - + MimeStorageService mimeStorage; + for (const QJsonObject &reference : messageRefs) { + const QString gmailId = reference.value(QStringLiteral("id")).toString(); + if (gmailId.isEmpty()) continue; + QUrl rawUrl(buildGmailUrl(QStringLiteral("messages/%1").arg(gmailId))); + QUrlQuery rawQuery; + rawQuery.addQueryItem(QStringLiteral("format"), QStringLiteral("raw")); + rawUrl.setQuery(rawQuery); + QByteArray response; + if (!get(rawUrl, response)) continue; + + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(response, &parseError); + if (parseError.error != QJsonParseError::NoError || !document.isObject()) continue; + const QJsonObject object = document.object(); + QByteArray encoded = object.value(QStringLiteral("raw")).toString().toLatin1(); + encoded.replace('-', '+'); + encoded.replace('_', '/'); + while (encoded.size() % 4) encoded.append('='); + const QByteArray rawMime = QByteArray::fromBase64(encoded); + ParsedMimeMessage parsed; + if (rawMime.isEmpty() || !mimeStorage.parseMessage(rawMime, parsed)) continue; + + MailItem item; + item.setFolderId(folderId.toInt()); + item.setMessageId(gmailId); + item.setRawMime(rawMime); + item.setSubject(parsed.subject.isEmpty() ? QStringLiteral("(No Subject)") : parsed.subject); + item.setSender(parsed.from); + item.setRecipient(parsed.to); + item.setTo(parsed.to); + item.setCc(parsed.cc); + item.setBcc(parsed.bcc); + item.setDate(parsed.date); + item.setBodyHtml(parsed.bodyHtml); + item.setSize(rawMime.size()); + item.setRead(true); + QVector attachmentNames; + for (const ParsedMimeAttachment &attachment : parsed.attachments) attachmentNames.append(attachment.fileName); + item.setAttachments(attachmentNames); + const QJsonArray labels = object.value(QStringLiteral("labelIds")).toArray(); + for (const QJsonValue &label : labels) { + if (label.toString() == QStringLiteral("UNREAD")) item.setRead(false); + if (label.toString() == QStringLiteral("STARRED")) item.setFlagged(true); + } + items.append(item); + } return items; } bool GmailSynchronizer::appendMailItem(const QString& folderId, const MailItem& item) { - if (!m_account.isTokenValid()) { - if (!refreshAccessToken()) { - qWarning() << "Failed to refresh token for appending mail item"; - return false; - } - } - - qDebug() << "Appending mail item to folder (label):" << folderId; - - // En una implementaciรณn real, llamarรญamos a Gmail API - // para crear un mensaje en la etiqueta especificada - - // Por ahora, simulamos รฉxito - emit mailItemAdded(item); - - // Publish MailItemAddedEvent - WinoMail::Events::MailItemAddedEvent mailEvent; - mailEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand()); - mailEvent.timestamp = QDateTime::currentDateTimeUtc(); - mailEvent.item = item; - PUBLISH(mailEvent); - - return true; + Q_UNUSED(folderId); + Q_UNUSED(item); + qWarning() << "Gmail appendMailItem is not used for sending; MailService sends via Gmail API"; + return false; } bool GmailSynchronizer::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged) { - if (!m_account.isTokenValid()) { - if (!refreshAccessToken()) { - qWarning() << "Failed to refresh token for updating mail item flags"; - return false; - } - } - - qDebug() << "Updating mail item flags:" << itemUid - << "read:" << read << "flagged:" << flagged; - - // En una implementaciรณn real, llamarรญamos a Gmail API - // para actualizar las etiquetas del mensaje (leรญdo, importante, etc.) - - // Por ahora, simulamos รฉxito - MailItem updatedItem; - updatedItem.setId(itemUid.toLongLong()); - updatedItem.setFolderId(folderId.toInt()); - updatedItem.setRead(read); - updatedItem.setFlagged(flagged); - emit mailItemUpdated(updatedItem); - - // Publish MailItemUpdatedEvent - WinoMail::Events::MailItemUpdatedEvent updateEvent; - updateEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand()); - updateEvent.timestamp = QDateTime::currentDateTimeUtc(); - updateEvent.item = updatedItem; - updateEvent.changedFields = QStringList() << "read" << "flagged"; // Simplified - PUBLISH(updateEvent); - - return true; + Q_UNUSED(folderId); + Q_UNUSED(itemUid); + Q_UNUSED(read); + Q_UNUSED(flagged); + qWarning() << "Gmail flag update is not available through this synchronizer"; + return false; } bool GmailSynchronizer::deleteMailItem(const QString& folderId, const QString& itemUid) { - if (!m_account.isTokenValid()) { - if (!refreshAccessToken()) { - qWarning() << "Failed to refresh token for deleting mail item"; - return false; - } - } - - qDebug() << "Deleting mail item:" << itemUid << "from folder (label):" << folderId; - - // En una implementaciรณn real, llamarรญamos a Gmail API - // para eliminar el mensaje (moverlo a la papelera) - - // Por ahora, simulamos รฉxito - emit mailItemRemoved(itemUid); - - // Publish MailItemRemovedEvent - WinoMail::Events::MailItemRemovedEvent removeEvent; - removeEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand()); - removeEvent.timestamp = QDateTime::currentDateTimeUtc(); - removeEvent.itemUid = itemUid; - removeEvent.folderId = folderId.toInt(); - PUBLISH(removeEvent); - - return true; + Q_UNUSED(folderId); + Q_UNUSED(itemUid); + qWarning() << "Gmail delete is not available through this synchronizer"; + return false; } void GmailSynchronizer::onGmailReplyFinished(QNetworkReply* reply) @@ -321,9 +340,11 @@ void GmailSynchronizer::onHistoryTimer() QString GmailSynchronizer::buildGmailUrl(const QString& endpoint) const { + QString normalized = endpoint; + while (normalized.startsWith('/')) normalized.remove(0, 1); return QStringLiteral("https://gmail.googleapis.com/gmail/v1/users/%1/%2") - .arg(m_account.email()) - .arg(endpoint); + .arg(QString::fromUtf8(QUrl::toPercentEncoding(m_account.email()))) + .arg(normalized); } QNetworkRequest GmailSynchronizer::createAuthRequest(const QString& url) const @@ -338,12 +359,41 @@ QNetworkRequest GmailSynchronizer::createAuthRequest(const QString& url) const bool GmailSynchronizer::refreshAccessToken() { - // En una implementaciรณn real, aquรญ usarรญamos el refresh token - // para obtener un nuevo access token desde Google OAuth2 - - qWarning() << "Token refresh not implemented - using stub"; - // Simulamos รฉxito por ahora - return !m_refreshToken.isEmpty(); + if (m_refreshToken.isEmpty()) return false; + const QString clientId = qEnvironmentVariable("WINO_GMAIL_CLIENT_ID"); + const QString clientSecret = qEnvironmentVariable("WINO_GMAIL_CLIENT_SECRET"); + if (clientId.isEmpty() || clientSecret.isEmpty()) { + qWarning() << "Gmail token expired and WINO_GMAIL_CLIENT_ID/SECRET are not configured"; + return false; + } + + QNetworkAccessManager network; + QNetworkRequest request(QUrl(QStringLiteral("https://oauth2.googleapis.com/token"))); + request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded")); + QUrlQuery form; + form.addQueryItem(QStringLiteral("client_id"), clientId); + form.addQueryItem(QStringLiteral("client_secret"), clientSecret); + form.addQueryItem(QStringLiteral("refresh_token"), m_refreshToken); + form.addQueryItem(QStringLiteral("grant_type"), QStringLiteral("refresh_token")); + QNetworkReply *reply = network.post(request, form.toString(QUrl::FullyEncoded).toUtf8()); + QEventLoop loop; + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "Gmail token refresh failed:" << reply->errorString(); + reply->deleteLater(); + return false; + } + const QJsonDocument document = QJsonDocument::fromJson(reply->readAll()); + reply->deleteLater(); + const QString token = document.object().value(QStringLiteral("access_token")).toString(); + if (token.isEmpty()) return false; + m_accessToken = token; + const int expiresIn = document.object().value(QStringLiteral("expires_in")).toInt(3600); + m_tokenExpires = QDateTime::currentDateTimeUtc().addSecs(expiresIn); + m_account.setAccessToken(m_accessToken); + m_account.setTokenExpires(m_tokenExpires); + return true; } qint64 GmailSynchronizer::getHistoryId(const QString& folderId) const diff --git a/src/services/imap/imapconnection.cpp b/src/services/imap/imapconnection.cpp new file mode 100644 index 0000000..61cf3f7 --- /dev/null +++ b/src/services/imap/imapconnection.cpp @@ -0,0 +1,217 @@ +#include "imapconnection.h" +#include +#include +#include + +ImapConnection::ImapConnection(QObject *parent) : QObject(parent), + m_socket(new QSslSocket(this)), + m_tagCounter(1) +{ + connect(m_socket, &QAbstractSocket::connected, this, &ImapConnection::onConnected); + connect(m_socket, &QSslSocket::encrypted, this, &ImapConnection::onEncrypted); + m_readyReadConnection = connect(m_socket, &QSslSocket::readyRead, this, &ImapConnection::onReadyRead); + connect(m_socket, &QAbstractSocket::errorOccurred, this, &ImapConnection::onSocketError); +} + +ImapConnection::~ImapConnection() +{ + disconnect(); +} + +void ImapConnection::connectToHost(const QString &host, int port, bool useSsl) +{ + m_host = host; + m_port = port; + m_useSsl = useSsl; + if (useSsl) { + m_socket->connectToHostEncrypted(host, port); + } else { + m_socket->connectToHost(host, port); + } +} + +void ImapConnection::login(const QString &username, const QString &password, + bool useAuthPlain, + std::function callback) +{ + if (!m_socket->isEncrypted() && m_useSsl) { + callback(false, "Not encrypted"); + return; + } + QString cmd; + if (useAuthPlain) { + QByteArray auth; + auth.append('\0'); + auth.append(username.toUtf8()); + auth.append('\0'); + auth.append(password.toUtf8()); + cmd = QString("AUTHENTICATE PLAIN %1").arg(QString::fromLatin1(auth.toBase64())); + } else { + cmd = QString("LOGIN %1 %2").arg(username, password); + } + sendCommand(cmd, callback); +} + +void ImapConnection::sendCommand(const QString &command, + std::function callback) +{ + QString tag = generateTag(); + QString fullCmd = tag + " " + command + "\r\n"; + m_socket->write(fullCmd.toUtf8()); + m_socket->flush(); + m_pendingCallbacks[tag] = callback; +} + +void ImapConnection::disconnect() +{ + if (m_socket->state() != QAbstractSocket::UnconnectedState) { + m_socket->disconnectFromHost(); + m_socket->waitForDisconnected(1000); + } + m_pendingCallbacks.clear(); +} + +void ImapConnection::onConnected() +{ + // For non-SSL connections, signal connected immediately. + // For SSL connections, onEncrypted() fires instead after the SSL handshake. + if (!m_useSsl) { + emit connected(); + } +} + +void ImapConnection::onEncrypted() +{ + emit connected(); +} + +void ImapConnection::onReadyRead() +{ + while (m_socket->canReadLine()) { + QString line = QString::fromUtf8(m_socket->readLine()).trimmed(); + processLine(line); + } +} + +void ImapConnection::onSocketError(QAbstractSocket::SocketError error) +{ + emit errorOccurred(m_socket->errorString()); +} + +void ImapConnection::processLine(const QString &line) +{ + // If it's an untagged response (starts with *), emit signal + if (line.startsWith('*')) { + emit untaggedResponse(line); + return; + } + + // Extract the tag prefix (first word) and look it up in pending callbacks + int spacePos = line.indexOf(' '); + if (spacePos == -1) return; // malformed line + + QString tagFromLine = line.left(spacePos); + + auto it = m_pendingCallbacks.find(tagFromLine); + if (it != m_pendingCallbacks.end()) { + bool ok = line.contains(" OK "); + auto cb = it.value(); + m_pendingCallbacks.erase(it); + cb(ok, line); + return; + } + + // If we get here, it's an unhandled response + qWarning() << "Unhandled IMAP response:" << line; +} + +void ImapConnection::sendRaw(const QString &data) +{ + m_socket->write(data.toUtf8() + "\r\n"); + m_socket->flush(); +} + +QString ImapConnection::generateTag() +{ + // Simple tag: A001, A002, ... (like traditional IMAP clients) + return QString("A%1").arg(m_tagCounter++, 4, 10, QChar('0')); +} + +bool ImapConnection::sendCommandWait(const QString &command, QString &response, int msecs) +{ + QByteArray rawResponse; + const bool completed = sendCommandWaitBytes(command, rawResponse, msecs); + response = QString::fromUtf8(rawResponse); + return completed; +} + +bool ImapConnection::sendCommandWaitBytes(const QString &command, QByteArray &response, int msecs) +{ + QString tag = generateTag(); + QString fullCmd = tag + " " + command + "\r\n"; + bool complete = false; + bool timedOut = false; + QEventLoop loop; + QTimer timer; + QByteArray buffer; + const QByteArray tagBytes = tag.toUtf8(); + + // Fetch responses contain arbitrary MIME bytes and IMAP literals. The + // line-based parser cannot safely consume those bytes, so temporarily + // take ownership of readyRead and collect the complete tagged response. + QObject::disconnect(m_readyReadConnection); + auto collectResponse = [&]() { + buffer.append(m_socket->readAll()); + + int pos = 0; + while (pos < buffer.size()) { + const int lineEnd = buffer.indexOf("\r\n", pos); + if (lineEnd < 0) break; + const QByteArray line = buffer.mid(pos, lineEnd - pos); + const int open = line.lastIndexOf('{'); + QByteArray literalSize = open >= 0 && line.endsWith('}') + ? line.mid(open + 1, line.size() - open - 2) : QByteArray(); + if (literalSize.endsWith('+')) literalSize.chop(1); + bool literalOk = false; + const qint64 parsedLiteralSize = literalSize.toLongLong(&literalOk); + const bool hasLiteral = open >= 0 && line.endsWith('}') && literalOk && parsedLiteralSize >= 0; + if (hasLiteral) { + const qint64 length = parsedLiteralSize; + const int dataStart = lineEnd + 2; + if (buffer.size() < dataStart + length) break; + pos = dataStart + int(length); + if (buffer.mid(pos, 2) == QByteArrayLiteral("\r\n")) pos += 2; + continue; + } + if (line.startsWith(tagBytes + QByteArrayLiteral(" "))) { + complete = true; + loop.quit(); + break; + } + pos = lineEnd + 2; + } + }; + QMetaObject::Connection readyConn = connect(m_socket, &QSslSocket::readyRead, &loop, collectResponse); + QMetaObject::Connection errorConn = connect(m_socket, &QAbstractSocket::errorOccurred, &loop, [&](QAbstractSocket::SocketError) { + loop.quit(); + }); + timer.setSingleShot(true); + QMetaObject::Connection timeoutConn = connect(&timer, &QTimer::timeout, &loop, [&]() { + timedOut = true; + loop.quit(); + }); + + m_socket->write(fullCmd.toUtf8()); + m_socket->flush(); + if (m_socket->bytesAvailable() > 0) collectResponse(); + timer.start(msecs); + loop.exec(); + QObject::disconnect(readyConn); + QObject::disconnect(errorConn); + QObject::disconnect(timeoutConn); + m_readyReadConnection = connect(m_socket, &QSslSocket::readyRead, this, &ImapConnection::onReadyRead); + + response = buffer; + if (timedOut || !complete) return false; + return true; +} diff --git a/src/services/imap/imapconnection.h b/src/services/imap/imapconnection.h new file mode 100644 index 0000000..d136068 --- /dev/null +++ b/src/services/imap/imapconnection.h @@ -0,0 +1,60 @@ +#ifndef IMAPCONNECTION_H +#define IMAPCONNECTION_H + +#include +#include +#include +#include +#include + +class ImapConnection : public QObject +{ + Q_OBJECT +public: + explicit ImapConnection(QObject *parent = nullptr); + ~ImapConnection(); + + void connectToHost(const QString &host, int port, bool useSsl); + void login(const QString &username, const QString &password, + bool useAuthPlain, + std::function callback); + void sendCommand(const QString &command, + std::function callback); + void disconnect(); + + // Synchronous version: blocks until response or timeout + bool sendCommandWait(const QString &command, QString &response, int msecs); + bool sendCommandWaitBytes(const QString &command, QByteArray &response, int msecs); + +signals: + void connected(); + void disconnected(); + void errorOccurred(const QString &message); + void untaggedResponse(const QString &line); // for * RESPONSES + void ready(); // connection ready for authentication (TCP connected for plain, encrypted for SSL) + +private slots: + void onConnected(); + void onEncrypted(); + void onReadyRead(); + void onSocketError(QAbstractSocket::SocketError error); + +private: + QSslSocket *m_socket; + QString m_host; + int m_port; + bool m_useSsl; + QString m_username; + QString m_password; + int m_tagCounter; + QString m_responseBuffer; + QMap> m_pendingCallbacks; // keyed by tag + QMetaObject::Connection m_readyReadConnection; + + void processLine(const QString &line); + QString generateTag(); +public: + void sendRaw(const QString &data); +}; + +#endif // IMAPCONNECTION_H diff --git a/src/services/imap/imapsynchronizer.cpp b/src/services/imap/imapsynchronizer.cpp index 7a66df5..94c24ab 100644 --- a/src/services/imap/imapsynchronizer.cpp +++ b/src/services/imap/imapsynchronizer.cpp @@ -8,6 +8,8 @@ #include "../../core/models/folder.h" #include "../../db/dao/folderdao.h" #include "../../db/dao/mailitemdao.h" +#include "../../services/mimestorage.h" +#include ImapSynchronizer::ImapSynchronizer(QObject* parent) : Synchronizer(parent) @@ -16,6 +18,7 @@ ImapSynchronizer::ImapSynchronizer(QObject* parent) bool ImapSynchronizer::initialize(const Account& account) { + m_account = account; const Account::ConnectionSettings& settings = account.connectionSettings(); m_host = settings.incomingHost; m_port = settings.incomingPort; @@ -37,9 +40,13 @@ bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const QEventLoop connectLoop; QTimer connectTimer; bool connectTimeout = false; - connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit); + connect(&conn, &ImapConnection::connected, &connectLoop, [&]() { + connected = true; + connectLoop.quit(); + }); connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){ qWarning() << "IMAP connection error:" << err; + connected = false; connectLoop.quit(); }); connectTimer.setSingleShot(true); @@ -54,7 +61,7 @@ bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const qWarning() << "Connection timeout to" << m_host; return false; } - connected = true; + if (!connected) return false; // Wait for server greeting (first untagged response) QEventLoop greetLoop; @@ -114,99 +121,173 @@ bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const return true; } -bool ImapSynchronizer::syncFolder(const Folder& folder) +QVector ImapSynchronizer::parseUidSearchResponse(const QString& response) const { - ImapConnection conn; - if (!connectAndLogin(conn)) return false; - - // Select folder - QString selectCmd = QString("SELECT \"%1\"").arg(folder.name()); - QString selectResp; - if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(" OK ")) { - qWarning() << "SELECT failed for" << folder.name(); - return false; - } - - // Get all UIDs - QString searchCmd = "UID SEARCH ALL"; - QString searchResp; - if (!conn.sendCommandWait(searchCmd, searchResp, 30000) || !searchResp.contains(" OK ")) { - qWarning() << "SEARCH failed"; - return false; - } - QVector uids; - QStringList lines = searchResp.split('\n'); + QStringList lines = response.split(QStringLiteral("\n")); for (const QString& line : lines) { - if (line.startsWith("* SEARCH")) { - QStringList parts = line.split(QRegularExpression("\\s+")); + if (line.startsWith(QStringLiteral("* SEARCH"))) { + QStringList parts = line.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts); for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH" bool ok; qint64 uid = parts[i].toLongLong(&ok); - if (ok && uid > 0) uids.append(uid); + if (ok && uid > 0) + uids.append(uid); } break; } } - - // If no messages, set unread count to 0 and exit - if (uids.isEmpty()) { - Folder f = folder; - f.setUnreadCount(0); - FolderDao::update(f); - conn.disconnect(); - return true; - } - - // Get flags for all UIDs in one command - QStringList uidStrs; - for (qint64 uid : uids) uidStrs.append(QString::number(uid)); - QString fetchCmd = "UID FETCH " + uidStrs.join(',') + " (FLAGS)"; - QString fetchResp; - if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000) || !fetchResp.contains(" OK ")) { - qWarning() << "FETCH FLAGS failed"; - conn.disconnect(); + return uids; +} +bool ImapSynchronizer::syncFolder(const Folder& folder) +{ + // First, get all UIDs from server to detect deletions + QVector allUids = fetchAllUids(QString::number(folder.id())); + if (allUids.isEmpty()) { + qWarning() << "Failed to fetch UIDs for folder" << folder.id(); return false; } - // Parse flags and update local DB - QMap> flagsMap; // uid -> (seen, flagged) - QRegularExpression flagsRx(R"(UID (\d+).*FLAGS \(([^)]*)\))"); - auto it = flagsRx.globalMatch(fetchResp); - while (it.hasNext()) { - auto m = it.next(); - qint64 uid = m.captured(1).toLongLong(); - QString flags = m.captured(2); - bool seen = flags.contains("\\Seen"); - bool flagged = flags.contains("\\Flagged"); - flagsMap[uid] = qMakePair(seen, flagged); + // Get local UIDs + QVector localUids = MailItemDao::getUidsForFolder(folder.id()); + + // Find deleted UIDs (exist locally but not on server) + QSet serverUidSet(allUids.begin(), allUids.end()); + QVector deletedUids; + for (qint64 uid : localUids) { + if (!serverUidSet.contains(uid)) { + deletedUids.append(uid); + } } - - // Update local items - auto localItems = MailItemDao::findByFolderId(folder.id()); - for (MailItem& item : localItems) { - if (flagsMap.contains(item.uid())) { - bool seen = flagsMap[item.uid()].first; - bool flagged = flagsMap[item.uid()].second; - if (item.isRead() != seen || item.isFlagged() != flagged) { - item.setRead(seen); - item.setFlagged(flagged); - MailItemDao::update(item); - } + + // Delete locally if removed from server + bool success = true; + for (qint64 uid : deletedUids) { + if (!MailItemDao::removeByUid(folder.id(), uid)) { + qWarning() << "Failed to delete local mail item UID" << uid; + success = false; + } else { + qDebug() << "Removed local mail item UID" << uid << "(deleted on server)"; } } - // Count unread - int unread = 0; - for (const MailItem& item : localItems) { - if (!item.isRead()) unread++; + // Now fetch new/changed messages + const qint64 sinceUid = MailItemDao::maxUidForFolder(folder.id()).value_or(0); + QVector items = fetchMailItems(QString::number(folder.id()), sinceUid); + for (MailItem &item : items) { + if (!persistFetchedItem(folder, item)) success = false; } - Folder f = folder; - f.setUnreadCount(unread); - FolderDao::update(f); + + // Also check for flag changes on existing messages + if (!allUids.isEmpty()) { + // Fetch flags for all known UIDs (in batches) + const int batchSize = 100; + for (int i = 0; i < allUids.size(); i += batchSize) { + QVector batch = allUids.mid(i, qMin(batchSize, allUids.size() - i)); + QStringList batchList; + for (qint64 uid : batch) batchList << QString::number(uid); + QString uidList = batchList.join(","); + + ImapConnection conn; + if (!connectAndLogin(conn)) continue; + + // SELECT folder + QString selectCommand = QString("SELECT \"%1\"").arg(folder.name()); + QString selectResponse; + if (!conn.sendCommandWait(selectCommand, selectResponse, 30000) || !selectResponse.contains(" OK ")) { + continue; + } + + // Fetch only FLAGS for this batch + QString fetchCommand = QString("UID FETCH %1 (FLAGS)").arg(uidList); + QByteArray fetchResponse; + if (!conn.sendCommandWaitBytes(fetchCommand, fetchResponse, 30000)) { + continue; + } + + // Parse flags and update local + parseAndUpdateFlags(fetchResponse, folder.id()); + } + } + + return success; +} - conn.disconnect(); - return true; +QVector ImapSynchronizer::fetchAllUids(const QString& folderId) const +{ + QVector uids; + ImapConnection conn; + + if (!connectAndLogin(conn)) { + return uids; + } + + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) { + qWarning() << "Folder not found for id" << folderId; + return uids; + } + QString folderName = folderOpt->name(); + + // SELECT + QString selectCommand = QString("SELECT \"%1\"").arg(folderName); + QString selectResponse; + if (!conn.sendCommandWait(selectCommand, selectResponse, 30000) || !selectResponse.contains(" OK ")) { + qWarning() << "SELECT failed:" << selectResponse; + return uids; + } + + // SEARCH ALL + QString searchCommand = "UID SEARCH ALL"; + QString searchResponse; + if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) { + qWarning() << "SEARCH command failed:" << searchResponse; + return uids; + } + + uids = parseUidSearchResponse(searchResponse); + return uids; +} + +void ImapSynchronizer::parseAndUpdateFlags(const QByteArray& response, int folderId) const +{ + // Parse FETCH response for FLAGS + QStringList lines = QString::fromUtf8(response).split('\n'); + for (const QString& line : lines) { + if (!line.startsWith("* ") || !line.contains(" FETCH ")) continue; + + // Extract UID + QRegularExpression uidRx(QStringLiteral("\\bUID\\s+(\\d+)")); + QRegularExpressionMatch uidMatch = uidRx.match(line); + if (!uidMatch.hasMatch()) continue; + qint64 uid = uidMatch.captured(1).toLongLong(); + + // Extract FLAGS + int flagsPos = line.indexOf("FLAGS ("); + if (flagsPos < 0) continue; + int flagsEnd = line.indexOf(')', flagsPos); + if (flagsEnd < 0) continue; + QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7); + + bool read = flags.contains("\\Seen"); + bool flagged = flags.contains("\\Flagged"); + + // Update local DB if different + auto itemOpt = MailItemDao::findByUid(folderId, uid); + if (itemOpt.has_value()) { + MailItem item = itemOpt.value(); + if (item.isRead() != read || item.isFlagged() != flagged) { + item.setRead(read); + item.setFlagged(flagged); + if (!MailItemDao::update(item)) { + qWarning() << "Failed to update flags for UID" << uid; + } else { + qDebug() << "Updated flags for UID" << uid << "read=" << read << "flagged=" << flagged; + } + } + } + } } QVector ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid) @@ -242,7 +323,7 @@ QVector ImapSynchronizer::fetchMailItems(const QString& folderId, qint // SEARCH QString searchCommand; if (sinceUid > 0) { - searchCommand = QString("UID SEARCH %1:*").arg(sinceUid); + searchCommand = QString("UID SEARCH UID %1:*").arg(sinceUid + 1); } else { searchCommand = "UID SEARCH ALL"; } @@ -283,24 +364,38 @@ QVector ImapSynchronizer::fetchMailItems(const QString& folderId, qint } batchList.chop(1); // remove trailing comma - // Fetch headers and flags (efficient) - QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)") + // Fetch the complete RFC822 message. Headers alone are not enough to + // reconstruct HTML, inline resources or attachments. + QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[] FLAGS INTERNALDATE)") .arg(batchList); - QString fetchResponse; - if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) { - qWarning() << "FETCH failed for batch" << i; + QByteArray fetchResponse; + if (!conn.sendCommandWaitBytes(fetchCommand, fetchResponse, 30000)) { + qWarning() << "FETCH failed for batch" << i << "(" << batch.size() << "UIDs)" + << "- first UID:" << batch.first() << "last UID:" << batch.last() + << "- response:" << QString::fromUtf8(fetchResponse).left(200); + // Fallback: try fetching UIDs one by one + for (qint64 uid : batch) { + QString singleFetchCmd = QString("UID FETCH %1 (BODY.PEEK[] FLAGS INTERNALDATE)").arg(uid); + QByteArray singleResponse; + if (conn.sendCommandWaitBytes(singleFetchCmd, singleResponse, 30000)) { + QVector singleItems = parseFetchResponseBytes(singleResponse); + for (MailItem& item : singleItems) { + item.setFolderId(fid); + items.append(item); + } + } else { + qWarning() << " Single UID fetch failed for UID" << uid + << "- response:" << QString::fromUtf8(singleResponse).left(200); + } + } continue; } // Parse and persist - QVector batchItems = parseFetchResponse(fetchResponse.split('\n')); + QVector batchItems = parseFetchResponseBytes(fetchResponse); for (MailItem& item : batchItems) { item.setFolderId(fid); - if (MailItemDao::insert(item)) { - items.append(item); - } else { - qWarning() << "Failed to insert mail item uid" << item.uid(); - } + items.append(item); } } @@ -337,6 +432,132 @@ QVector ImapSynchronizer::getFolders() const return folders; } +bool ImapSynchronizer::persistFetchedItem(const Folder& folder, MailItem& item) const +{ + MimeStorageService storage; + QVector storedPaths; + ParsedMimeMessage parsed; + if (!item.rawMime().isEmpty()) { + if (!storage.parseMessage(item.rawMime(), parsed) + || !storage.storeMessage(QString::number(m_account.id()), QString::number(folder.id()), + item, item.rawMime(), &storedPaths)) { + qWarning() << "Failed to store MIME message for UID" << item.uid(); + return false; + } + } + + if (!MailItemDao::upsert(item)) { + storage.deleteEmlFile(item.fileId()); + return false; + } + + QVector records; + for (int i = 0; i < parsed.attachments.size(); ++i) { + const ParsedMimeAttachment &attachment = parsed.attachments.at(i); + StoredAttachmentRecord record; + record.fileName = attachment.fileName; + record.mimeType = attachment.mimeType; + record.contentId = attachment.contentId; + record.size = attachment.data.size(); + if (i < storedPaths.size()) record.storedPath = storedPaths.at(i); + records.append(record); + } + if (!MailItemDao::replaceAttachments(item.id(), records)) { + storage.deleteEmlFile(item.fileId()); + return false; + } + return true; +} + +QVector ImapSynchronizer::parseFetchResponseBytes(const QByteArray& response) const +{ + QVector items; + MimeStorageService mimeStorage; + int searchFrom = 0; + + // Debug: check if response contains error + if (response.contains(" NO ") || response.contains(" BAD ")) { + qWarning() << "IMAP FETCH response contains error:" << QString::fromUtf8(response).left(500); + } + + while (true) { + const int bodyPos = response.indexOf("BODY[]", searchFrom); + if (bodyPos < 0) { + // Check if we have untagged FETCH responses without BODY[] + int fetchPos = response.indexOf(" FETCH ", searchFrom); + if (fetchPos >= 0) { + int lineEnd = response.indexOf("\r\n", fetchPos); + if (lineEnd > 0) { + QByteArray line = response.mid(fetchPos, lineEnd - fetchPos); + qWarning() << "FETCH response without BODY[]:" << QString::fromLatin1(line); + } + } + break; + } + const int bracePos = response.indexOf('{', bodyPos); + const int braceEnd = response.indexOf('}', bracePos); + if (bracePos < 0 || braceEnd < 0) break; + QByteArray literalSize = response.mid(bracePos + 1, braceEnd - bracePos - 1); + if (literalSize.endsWith('+')) literalSize.chop(1); + bool sizeOk = false; + const qint64 size = literalSize.toLongLong(&sizeOk); + const int lineEnd = response.indexOf("\r\n", braceEnd); + if (!sizeOk || lineEnd < 0 || response.size() < lineEnd + 2 + size) { + qWarning() << "FETCH parse error: sizeOk=" << sizeOk << "lineEnd=" << lineEnd + << "response.size=" << response.size() << "needed=" << (lineEnd + 2 + size); + break; + } + + const QByteArray rawMime = response.mid(lineEnd + 2, int(size)); + const int fetchLineStart = response.lastIndexOf("\r\n", bodyPos) + 2; + const QByteArray fetchLine = response.mid(fetchLineStart, bodyPos - fetchLineStart); + const QRegularExpression uidRx(QStringLiteral("\\bUID\\s+(\\d+)")); + const QRegularExpressionMatch uidMatch = uidRx.match(QString::fromLatin1(fetchLine)); + if (!uidMatch.hasMatch()) { + qWarning() << "No UID found in FETCH line:" << QString::fromLatin1(fetchLine).left(200); + searchFrom = lineEnd + 2 + int(size); + continue; + } + + ParsedMimeMessage parsed; + if (mimeStorage.parseMessage(rawMime, parsed)) { + MailItem item; + item.setUid(uidMatch.captured(1).toLongLong()); + item.setMessageId(parsed.messageId); + item.setSubject(parsed.subject.isEmpty() ? QStringLiteral("(No Subject)") : parsed.subject); + item.setSender(parsed.from); + item.setRecipient(parsed.to); + item.setTo(parsed.to); + item.setCc(parsed.cc); + item.setBcc(parsed.bcc); + item.setDate(parsed.date); + item.setBodyHtml(parsed.bodyHtml); + item.setSize(rawMime.size()); + item.setRawMime(rawMime); + QVector names; + for (const ParsedMimeAttachment &attachment : parsed.attachments) + names.append(attachment.fileName); + item.setAttachments(names); + + // Flags are in the FETCH line, not in the MIME payload. + const int flagsPos = fetchLine.indexOf("FLAGS ("); + if (flagsPos >= 0) { + const int flagsEnd = fetchLine.indexOf(')', flagsPos); + if (flagsEnd > flagsPos) { + const QString flags = QString::fromLatin1(fetchLine.mid(flagsPos + 7, flagsEnd - flagsPos - 7)); + item.setRead(flags.contains(QStringLiteral("\\Seen"))); + item.setFlagged(flags.contains(QStringLiteral("\\Flagged"))); + } + } + items.append(item); + } else { + qWarning() << "Failed to parse MIME for UID" << uidMatch.captured(1); + } + searchFrom = lineEnd + 2 + int(size); + } + return items; +} + bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item) { // Get folder name @@ -562,19 +783,20 @@ QVector ImapSynchronizer::parseFetchResponse(const QStringList& lines) int flagsEnd = line.indexOf(')', flagsPos + 7); if (flagsEnd != -1) { QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7); - item.setRead(flags.contains(QStringLiteral("\\\\Seen"))); - item.setFlagged(flags.contains(QStringLiteral("\\\\Flagged"))); + item.setRead(flags.contains(QStringLiteral("\\Seen"))); + item.setFlagged(flags.contains(QStringLiteral("\\Flagged"))); } } // Extract INTERNALDATE - int datePos = line.indexOf(QStringLiteral("INTERNALDATE \"\"")); + int datePos = line.indexOf(QStringLiteral("INTERNALDATE \"")); if (datePos != -1) { - int dateStart = datePos + 16; // length of "INTERNALDATE \"" + int dateStart = datePos + 15; // length of "INTERNALDATE \"" int dateEnd = line.indexOf('\"', dateStart); if (dateEnd != -1) { QString dateStr = line.mid(dateStart, dateEnd - dateStart); - QDateTime dt = QDateTime::fromString(dateStr, QStringLiteral("dd-MMM-yyyy hh:mm:ss zzz")); + dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000" + QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date); if (dt.isValid()) item.setDate(dt); } @@ -582,7 +804,7 @@ QVector ImapSynchronizer::parseFetchResponse(const QStringList& lines) // Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines QString bodyData; - int bodyPos = line.indexOf(QStringLiteral("BODY[")); + int bodyPos = line.indexOf(QStringLiteral("BODY[]")); if (bodyPos != -1) { int bracePos = line.indexOf('{', bodyPos); if (bracePos != -1) { diff --git a/src/services/imap/imapsynchronizer.cpp.backup b/src/services/imap/imapsynchronizer.cpp.backup new file mode 100644 index 0000000..f3266a0 --- /dev/null +++ b/src/services/imap/imapsynchronizer.cpp.backup @@ -0,0 +1,649 @@ +#include "imapsynchronizer.h" +#include +#include +#include +#include +#include "../../core/mailitem.h" +#include "../../core/models/account.h" +#include "../../core/models/folder.h" +#include "../../db/dao/folderdao.h" +#include "../../db/dao/mailitemdao.h" + +ImapSynchronizer::ImapSynchronizer(QObject* parent) + : Synchronizer(parent) +{ +} + +bool ImapSynchronizer::initialize(const Account& account) +{ + const Account::ConnectionSettings& settings = account.connectionSettings(); + m_host = settings.incomingHost; + m_port = settings.incomingPort; + m_useSsl = settings.incomingSsl; + m_username = settings.username; + m_password = settings.password; + m_authMethod = settings.authMethod; + return true; +} + +// Helper: connects and logs in to the IMAP server +bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const +{ + bool connected = false; + bool loggedIn = false; + QString response; + + // Connect to host + QEventLoop connectLoop; + QTimer connectTimer; + bool connectTimeout = false; + connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit); + connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){ + qWarning() << "IMAP connection error:" << err; + connectLoop.quit(); + }); + connectTimer.setSingleShot(true); + connect(&connectTimer, &QTimer::timeout, &connectLoop, [&](){ + connectTimeout = true; + connectLoop.quit(); + }); + connectTimer.start(30000); // 30 seconds + conn.connectToHost(m_host, m_port, m_useSsl); + connectLoop.exec(); + if (connectTimeout) { + qWarning() << "Connection timeout to" << m_host; + return false; + } + connected = true; + + // Wait for server greeting (first untagged response) + QEventLoop greetLoop; + bool gotGreeting = false; + QMetaObject::Connection greetConn = connect(&conn, &ImapConnection::untaggedResponse, + [&](const QString& line) { + Q_UNUSED(line); + gotGreeting = true; + greetLoop.quit(); + }); + QTimer greetTimer; + greetTimer.setSingleShot(true); + connect(&greetTimer, &QTimer::timeout, &greetLoop, [&]() { + greetLoop.quit(); + }); + greetTimer.start(30000); // 30 seconds + greetLoop.exec(); + QObject::disconnect(greetConn); + if (!gotGreeting) { + qWarning() << "Timeout waiting for server greeting"; + return false; + } + + // Login + QEventLoop loginLoop; + QTimer loginTimer; + bool loginTimeout = false; + bool loginSuccess = false; + // Already connected, now login + conn.login(m_username, m_password, true, [&](bool ok, const QString& resp){ + if (ok) { + loginSuccess = true; + } else { + qWarning() << "Login failed:" << resp; + } + loginLoop.quit(); + }); + connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){ + qWarning() << "IMAP error during login:" << err; + loginLoop.quit(); + }); + loginTimer.setSingleShot(true); + connect(&loginTimer, &QTimer::timeout, &loginLoop, [&](){ + loginTimeout = true; + loginLoop.quit(); + }); + loginTimer.start(30000); + loginLoop.exec(); + if (loginTimeout) { + qWarning() << "Login timeout"; + return false; + } + if (!loginSuccess) { + return false; + } + loggedIn = true; + return true; +} + +bool ImapSynchronizer::syncFolder(const Folder& folder) +{ + ImapConnection conn; + if (!connectAndLogin(conn)) return false; + + // Select folder + QString selectCmd = QString("SELECT \"%1\"").arg(folder.name()); + QString selectResp; + if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(" OK ")) { + qWarning() << "SELECT failed for" << folder.name(); + return false; + } + + // Get all UIDs + QString searchCmd = "UID SEARCH ALL"; + QString searchResp; + if (!conn.sendCommandWait(searchCmd, searchResp, 30000) || !searchResp.contains(" OK ")) { + qWarning() << "SEARCH failed"; + return false; + } + + QVector uids; + QStringList lines = searchResp.split('\n'); + for (const QString& line : lines) { + if (line.startsWith("* SEARCH")) { + QStringList parts = line.split(QRegularExpression("\\s+")); + for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH" + bool ok; + qint64 uid = parts[i].toLongLong(&ok); + if (ok && uid > 0) uids.append(uid); + } + break; + } + } + + // If no messages, set unread count to 0 and exit + if (uids.isEmpty()) { + Folder f = folder; + f.setUnreadCount(0); + FolderDao::update(f); + conn.disconnect(); + return true; + } + + // Get flags for all UIDs in one command + QStringList uidStrs; + for (qint64 uid : uids) uidStrs.append(QString::number(uid)); + QString fetchCmd = "UID FETCH " + uidStrs.join(',') + " (FLAGS)"; + QString fetchResp; + if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000) || !fetchResp.contains(" OK ")) { + qWarning() << "FETCH FLAGS failed"; + conn.disconnect(); + return false; + } + + // Parse flags and update local DB + QMap> flagsMap; // uid -> (seen, flagged) + QRegularExpression flagsRx(R"(UID (\d+).*FLAGS \(([^)]*)\))"); + auto it = flagsRx.globalMatch(fetchResp); + while (it.hasNext()) { + auto m = it.next(); + qint64 uid = m.captured(1).toLongLong(); + QString flags = m.captured(2); + bool seen = flags.contains("\\Seen"); + bool flagged = flags.contains("\\Flagged"); + flagsMap[uid] = qMakePair(seen, flagged); + } + + // Update local items + auto localItems = MailItemDao::findByFolderId(folder.id()); + for (MailItem& item : localItems) { + if (flagsMap.contains(item.uid())) { + bool seen = flagsMap[item.uid()].first; + bool flagged = flagsMap[item.uid()].second; + if (item.isRead() != seen || item.isFlagged() != flagged) { + item.setRead(seen); + item.setFlagged(flagged); + MailItemDao::update(item); + } + } + } + + // Count unread + int unread = 0; + for (const MailItem& item : localItems) { + if (!item.isRead()) unread++; + } + Folder f = folder; + f.setUnreadCount(unread); + FolderDao::update(f); + + conn.disconnect(); + return true; +} + +QVector ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid) +{ + QVector items; + ImapConnection conn; + + if (!connectAndLogin(conn)) { + return items; + } + + // Get folder name from DB + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) { + qWarning() << "Folder not found for id" << folderId; + return items; + } + QString folderName = folderOpt->name(); + + // SELECT with real folder name + QString selectCommand = QString("SELECT \"%1\"").arg(folderName); + QString selectResponse; + if (!conn.sendCommandWait(selectCommand, selectResponse, 30000)) { + qWarning() << "SELECT command failed:" << selectResponse; + return items; + } + if (!selectResponse.contains(" OK ")) { + qWarning() << "SELECT failed:" << selectResponse; + return items; + } + + // SEARCH + QString searchCommand; + if (sinceUid > 0) { + searchCommand = QString("UID SEARCH %1:*").arg(sinceUid); + } else { + searchCommand = "UID SEARCH ALL"; + } + QString searchResponse; + if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) { + qWarning() << "SEARCH command failed:" << searchResponse; + return items; + } + + // Parse UIDs (robust: look for line starting with "* SEARCH") + QVector uids; + QStringList lines = searchResponse.split('\n'); + for (const QString& line : lines) { + if (line.startsWith("* SEARCH")) { + QStringList parts = line.split(QRegularExpression("\\s+")); + for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH" + bool ok; + qint64 uid = parts[i].toLongLong(&ok); + if (ok && uid > 0) uids.append(uid); + } + break; + } + } + + if (uids.isEmpty()) { + conn.sendCommandWait("CLOSE", searchResponse, 30000); + conn.disconnect(); + return items; + } + + // Fetch in batches of 50 UIDs + const int batchSize = 50; + for (int i = 0; i < uids.size(); i += batchSize) { + QVector batch = uids.mid(i, qMin(batchSize, uids.size() - i)); + QString batchList; + for (qint64 uid : batch) { + batchList.append(QString::number(uid)).append(","); + } + batchList.chop(1); // remove trailing comma + + // Fetch headers and flags (efficient) + QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)") + .arg(batchList); + QString fetchResponse; + if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) { + qWarning() << "FETCH failed for batch" << i; + continue; + } + + // Parse and persist + QVector batchItems = parseFetchResponse(fetchResponse.split('\n')); + for (MailItem& item : batchItems) { + item.setFolderId(fid); + if (MailItemDao::insert(item)) { + items.append(item); + } else { + qWarning() << "Failed to insert mail item uid" << item.uid(); + } + } + } + + // Close and disconnect + QString dummy; + conn.sendCommandWait("CLOSE", dummy, 30000); + conn.disconnect(); + return items; +} + +QVector ImapSynchronizer::getFolders() const +{ + QVector folders; + ImapConnection conn; + + if (!connectAndLogin(conn)) { + return folders; + } + + // List folders: LIST "" "*" + QString listCommand = QStringLiteral("LIST \"\" \"*\""); + QString response; + if (!conn.sendCommandWait(listCommand, response, 30000)) { + qWarning() << "LIST command failed:" << response; + return folders; + } + + // Parse the response + QStringList lines = response.split('\n'); + folders = parseListResponse(lines); + + // Logout + conn.disconnect(); + return folders; +} + +bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item) +{ + // Get folder name + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) { + qWarning() << "Folder not found for id" << folderId; + return false; + } + QString folderName = folderOpt->name(); + + // Build RFC822 message (headers + body) + QStringList headers; + if (!item.sender().isEmpty()) headers << "From: " + item.sender(); + if (!item.recipient().isEmpty()) headers << "To: " + item.recipient(); + if (!item.cc().isEmpty()) headers << "Cc: " + item.cc(); + if (!item.bcc().isEmpty()) headers << "Bcc: " + item.bcc(); + if (!item.subject().isEmpty()) headers << "Subject: " + item.subject(); + headers << "Date: " + item.date().toString(Qt::RFC2822Date); + headers << "MIME-Version: 1.0"; + headers << "Content-Type: text/html; charset=UTF-8"; + headers << "Content-Transfer-Encoding: 7bit"; + headers << ""; // blank line separates headers from body + headers << item.bodyHtml(); // using bodyHtml as the body + + QString message = headers.join("\r\n"); + QByteArray msgData = message.toUtf8(); + + ImapConnection conn; + if (!connectAndLogin(conn)) return false; + + // SELECT folder (some servers require it) + QString selectCmd = QString("SELECT \"%1\"").arg(folderName); + QString resp; + conn.sendCommandWait(selectCmd, resp, 30000); // ignore response + + // APPEND with literal: APPEND "folder" (\Seen) {size} + QString appendCmd = QString("APPEND \"%1\" (\\Seen) {%2}").arg(folderName).arg(msgData.size()); + QString response; + if (!conn.sendCommandWait(appendCmd, response, 30000)) { + qWarning() << "APPEND command initial failed:" << response; + conn.disconnect(); + return false; + } + + // Send the literal (message data) + conn.sendRaw(QString::fromUtf8(msgData)); + + // Wait for the tagged response (tag + OK/NO) + if (!conn.sendCommandWait("", response, 30000)) { // dummy command to get response + qWarning() << "APPEND literal failed:" << response; + conn.disconnect(); + return false; + } + + conn.disconnect(); + return true; +} + +bool ImapSynchronizer::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged) +{ + qint64 uid = itemUid.toLongLong(); + if (uid <= 0) return false; + + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) return false; + QString folderName = folderOpt->name(); + + ImapConnection conn; + if (!connectAndLogin(conn)) return false; + + // SELECT + QString selectCmd = QString("SELECT \"%1\"").arg(folderName); + QString resp; + if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "SELECT failed"; + return false; + } + + // STORE for Seen flag + QString seenFlag = read ? "+" : "-"; + QString storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Seen").arg(uid).arg(seenFlag); + if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "STORE Seen failed:" << resp; + conn.disconnect(); + return false; + } + + // STORE for Flagged flag + QString flaggedFlag = flagged ? "+" : "-"; + storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Flagged").arg(uid).arg(flaggedFlag); + if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "STORE Flagged failed:" << resp; + conn.disconnect(); + return false; + } + + conn.disconnect(); + + // Update local DB + auto items = MailItemDao::findByFolderId(fid); + for (MailItem& item : items) { + if (item.uid() == uid) { + item.setRead(read); + item.setFlagged(flagged); + MailItemDao::update(item); + break; + } + } + return true; +} + +bool ImapSynchronizer::deleteMailItem(const QString& folderId, const QString& itemUid) +{ + qint64 uid = itemUid.toLongLong(); + if (uid <= 0) return false; + + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) return false; + QString folderName = folderOpt->name(); + + ImapConnection conn; + if (!connectAndLogin(conn)) return false; + + // SELECT + QString selectCmd = QString("SELECT \"%1\"").arg(folderName); + QString resp; + if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "SELECT failed"; + return false; + } + + // Mark as \\Deleted + QString storeCmd = QString("UID STORE %1 +FLAGS.SILENT \\Deleted").arg(uid); + if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "STORE Deleted failed:" << resp; + conn.disconnect(); + return false; + } + + // EXPUNGE (permanently delete) + QString expungeCmd = "EXPUNGE"; + if (!conn.sendCommandWait(expungeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "EXPUNGE failed:" << resp; + conn.disconnect(); + return false; + } + + conn.disconnect(); + + // Remove from local DB + MailItemDao::remove(uid); + return true; +} + +QString ImapSynchronizer::generateEventId() const +{ + return QString::number(QDateTime::currentMSecsSinceEpoch()); +} + +QVector ImapSynchronizer::parseListResponse(const QStringList& lines) const +{ + QVector folders; + QRegularExpression rx("\\* LIST \\\\([^)]*\\\\) \"([^\"]*)\" \"([^\"]*)\""); + for (const QString& line : lines) { + QRegularExpressionMatch match = rx.match(line); + if (match.hasMatch()) { + QString delimiter = match.captured(1); // unused for now + QString mailbox = match.captured(2); + Folder folder; + folder.setName(mailbox); + QString lower = mailbox.toLower(); + if (lower == "inbox") folder.setInbox(true); + else if (lower == "sent") folder.setSent(true); + else if (lower == "drafts") folder.setDrafts(true); + else if (lower == "trash" || lower == "deleted items") folder.setTrash(true); + folders.append(folder); + } + } + return folders; +} + +QVector ImapSynchronizer::parseFetchResponse(const QStringList& lines) const +{ + QVector items; + int i = 0; + while (i < lines.size()) { + const QString& line = lines[i]; + if (!line.startsWith(QStringLiteral("* "))) { + ++i; + continue; + } + int fetchPos = line.indexOf(QStringLiteral(" FETCH (")); + if (fetchPos == -1) { + ++i; + continue; + } + + // Extract UID + int uidPos = line.indexOf(QStringLiteral("UID ")); + if (uidPos == -1) { + ++i; + continue; + } + int uidEnd = line.indexOf(QRegularExpression(QStringLiteral("[\\s)]")), uidPos + 4); + if (uidEnd == -1) uidEnd = line.length(); + QString uidStr = line.mid(uidPos + 4, uidEnd - uidPos - 4); + bool ok; + qint64 uid = uidStr.toLongLong(&ok); + if (!ok) { + ++i; + continue; + } + + MailItem item; + item.setUid(uid); + + // Extract FLAGS + int flagsPos = line.indexOf(QStringLiteral("FLAGS (")); + if (flagsPos != -1) { + int flagsEnd = line.indexOf(')', flagsPos + 7); + if (flagsEnd != -1) { + QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7); + item.setRead(flags.contains(QStringLiteral("\\Seen"))); + item.setFlagged(flags.contains(QStringLiteral("\\Flagged"))); + } + } + + // Extract INTERNALDATE + int datePos = line.indexOf(QStringLiteral("INTERNALDATE \"")); + if (datePos != -1) { + int dateStart = datePos + 15; // length of "INTERNALDATE \"" + int dateEnd = line.indexOf('\"', dateStart); + if (dateEnd != -1) { + QString dateStr = line.mid(dateStart, dateEnd - dateStart); + dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000" + QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date); + if (dt.isValid()) + item.setDate(dt); + } + } + + // Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines + QString bodyData; + int bodyPos = line.indexOf(QStringLiteral("BODY[")); + if (bodyPos != -1) { + int bracePos = line.indexOf('{', bodyPos); + if (bracePos != -1) { + int sizeEnd = line.indexOf('}', bracePos); + if (sizeEnd != -1) { + bool sizeOk; + int size = line.mid(bracePos + 1, sizeEnd - bracePos - 1).toInt(&sizeOk); + if (sizeOk) { + int dataStart = line.indexOf(QStringLiteral("\r\n"), sizeEnd); + if (dataStart != -1) { + dataStart += 2; // skip \r\n + int available = line.length() - dataStart; + if (available > size) available = size; + bodyData = line.mid(dataStart, available); + int received = available; + int remaining = size - received; + int j = i + 1; + while (j < lines.size() && remaining > 0) { + const QString& nextLine = lines[j]; + int take = qMin(nextLine.length(), remaining); + bodyData += nextLine.left(take); + remaining -= take; + ++j; + } + // we have consumed lines up to j-1 + i = j - 1; // will be incremented at end of loop + } + } + } + } + } + + // Parse headers from bodyData + if (!bodyData.isEmpty()) { + QRegularExpression headerRx(QStringLiteral(R"((^([^:]+):\s*(.*?)\r\n))"), QRegularExpression::MultilineOption); + auto it = headerRx.globalMatch(bodyData); + while (it.hasNext()) { + auto match = it.next(); + QString key = match.captured(2).toLower().trimmed(); + QString value = match.captured(3).trimmed(); + if (key == QStringLiteral("subject")) + item.setSubject(value); + else if (key == QStringLiteral("from")) + item.setSender(value); + else if (key == QStringLiteral("to")) + item.setRecipient(value); + else if (key == QStringLiteral("date")) { + QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date); + if (dt.isValid()) + item.setDate(dt); + } + } + } + + // If subject not set, use placeholder + if (item.subject().isEmpty()) + item.setSubject(QStringLiteral("(No Subject)")); + + items.append(item); + ++i; + } + return items; +} diff --git a/src/services/imap/imapsynchronizer.cpp.backup2 b/src/services/imap/imapsynchronizer.cpp.backup2 new file mode 100644 index 0000000..f3266a0 --- /dev/null +++ b/src/services/imap/imapsynchronizer.cpp.backup2 @@ -0,0 +1,649 @@ +#include "imapsynchronizer.h" +#include +#include +#include +#include +#include "../../core/mailitem.h" +#include "../../core/models/account.h" +#include "../../core/models/folder.h" +#include "../../db/dao/folderdao.h" +#include "../../db/dao/mailitemdao.h" + +ImapSynchronizer::ImapSynchronizer(QObject* parent) + : Synchronizer(parent) +{ +} + +bool ImapSynchronizer::initialize(const Account& account) +{ + const Account::ConnectionSettings& settings = account.connectionSettings(); + m_host = settings.incomingHost; + m_port = settings.incomingPort; + m_useSsl = settings.incomingSsl; + m_username = settings.username; + m_password = settings.password; + m_authMethod = settings.authMethod; + return true; +} + +// Helper: connects and logs in to the IMAP server +bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const +{ + bool connected = false; + bool loggedIn = false; + QString response; + + // Connect to host + QEventLoop connectLoop; + QTimer connectTimer; + bool connectTimeout = false; + connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit); + connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){ + qWarning() << "IMAP connection error:" << err; + connectLoop.quit(); + }); + connectTimer.setSingleShot(true); + connect(&connectTimer, &QTimer::timeout, &connectLoop, [&](){ + connectTimeout = true; + connectLoop.quit(); + }); + connectTimer.start(30000); // 30 seconds + conn.connectToHost(m_host, m_port, m_useSsl); + connectLoop.exec(); + if (connectTimeout) { + qWarning() << "Connection timeout to" << m_host; + return false; + } + connected = true; + + // Wait for server greeting (first untagged response) + QEventLoop greetLoop; + bool gotGreeting = false; + QMetaObject::Connection greetConn = connect(&conn, &ImapConnection::untaggedResponse, + [&](const QString& line) { + Q_UNUSED(line); + gotGreeting = true; + greetLoop.quit(); + }); + QTimer greetTimer; + greetTimer.setSingleShot(true); + connect(&greetTimer, &QTimer::timeout, &greetLoop, [&]() { + greetLoop.quit(); + }); + greetTimer.start(30000); // 30 seconds + greetLoop.exec(); + QObject::disconnect(greetConn); + if (!gotGreeting) { + qWarning() << "Timeout waiting for server greeting"; + return false; + } + + // Login + QEventLoop loginLoop; + QTimer loginTimer; + bool loginTimeout = false; + bool loginSuccess = false; + // Already connected, now login + conn.login(m_username, m_password, true, [&](bool ok, const QString& resp){ + if (ok) { + loginSuccess = true; + } else { + qWarning() << "Login failed:" << resp; + } + loginLoop.quit(); + }); + connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){ + qWarning() << "IMAP error during login:" << err; + loginLoop.quit(); + }); + loginTimer.setSingleShot(true); + connect(&loginTimer, &QTimer::timeout, &loginLoop, [&](){ + loginTimeout = true; + loginLoop.quit(); + }); + loginTimer.start(30000); + loginLoop.exec(); + if (loginTimeout) { + qWarning() << "Login timeout"; + return false; + } + if (!loginSuccess) { + return false; + } + loggedIn = true; + return true; +} + +bool ImapSynchronizer::syncFolder(const Folder& folder) +{ + ImapConnection conn; + if (!connectAndLogin(conn)) return false; + + // Select folder + QString selectCmd = QString("SELECT \"%1\"").arg(folder.name()); + QString selectResp; + if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(" OK ")) { + qWarning() << "SELECT failed for" << folder.name(); + return false; + } + + // Get all UIDs + QString searchCmd = "UID SEARCH ALL"; + QString searchResp; + if (!conn.sendCommandWait(searchCmd, searchResp, 30000) || !searchResp.contains(" OK ")) { + qWarning() << "SEARCH failed"; + return false; + } + + QVector uids; + QStringList lines = searchResp.split('\n'); + for (const QString& line : lines) { + if (line.startsWith("* SEARCH")) { + QStringList parts = line.split(QRegularExpression("\\s+")); + for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH" + bool ok; + qint64 uid = parts[i].toLongLong(&ok); + if (ok && uid > 0) uids.append(uid); + } + break; + } + } + + // If no messages, set unread count to 0 and exit + if (uids.isEmpty()) { + Folder f = folder; + f.setUnreadCount(0); + FolderDao::update(f); + conn.disconnect(); + return true; + } + + // Get flags for all UIDs in one command + QStringList uidStrs; + for (qint64 uid : uids) uidStrs.append(QString::number(uid)); + QString fetchCmd = "UID FETCH " + uidStrs.join(',') + " (FLAGS)"; + QString fetchResp; + if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000) || !fetchResp.contains(" OK ")) { + qWarning() << "FETCH FLAGS failed"; + conn.disconnect(); + return false; + } + + // Parse flags and update local DB + QMap> flagsMap; // uid -> (seen, flagged) + QRegularExpression flagsRx(R"(UID (\d+).*FLAGS \(([^)]*)\))"); + auto it = flagsRx.globalMatch(fetchResp); + while (it.hasNext()) { + auto m = it.next(); + qint64 uid = m.captured(1).toLongLong(); + QString flags = m.captured(2); + bool seen = flags.contains("\\Seen"); + bool flagged = flags.contains("\\Flagged"); + flagsMap[uid] = qMakePair(seen, flagged); + } + + // Update local items + auto localItems = MailItemDao::findByFolderId(folder.id()); + for (MailItem& item : localItems) { + if (flagsMap.contains(item.uid())) { + bool seen = flagsMap[item.uid()].first; + bool flagged = flagsMap[item.uid()].second; + if (item.isRead() != seen || item.isFlagged() != flagged) { + item.setRead(seen); + item.setFlagged(flagged); + MailItemDao::update(item); + } + } + } + + // Count unread + int unread = 0; + for (const MailItem& item : localItems) { + if (!item.isRead()) unread++; + } + Folder f = folder; + f.setUnreadCount(unread); + FolderDao::update(f); + + conn.disconnect(); + return true; +} + +QVector ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid) +{ + QVector items; + ImapConnection conn; + + if (!connectAndLogin(conn)) { + return items; + } + + // Get folder name from DB + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) { + qWarning() << "Folder not found for id" << folderId; + return items; + } + QString folderName = folderOpt->name(); + + // SELECT with real folder name + QString selectCommand = QString("SELECT \"%1\"").arg(folderName); + QString selectResponse; + if (!conn.sendCommandWait(selectCommand, selectResponse, 30000)) { + qWarning() << "SELECT command failed:" << selectResponse; + return items; + } + if (!selectResponse.contains(" OK ")) { + qWarning() << "SELECT failed:" << selectResponse; + return items; + } + + // SEARCH + QString searchCommand; + if (sinceUid > 0) { + searchCommand = QString("UID SEARCH %1:*").arg(sinceUid); + } else { + searchCommand = "UID SEARCH ALL"; + } + QString searchResponse; + if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) { + qWarning() << "SEARCH command failed:" << searchResponse; + return items; + } + + // Parse UIDs (robust: look for line starting with "* SEARCH") + QVector uids; + QStringList lines = searchResponse.split('\n'); + for (const QString& line : lines) { + if (line.startsWith("* SEARCH")) { + QStringList parts = line.split(QRegularExpression("\\s+")); + for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH" + bool ok; + qint64 uid = parts[i].toLongLong(&ok); + if (ok && uid > 0) uids.append(uid); + } + break; + } + } + + if (uids.isEmpty()) { + conn.sendCommandWait("CLOSE", searchResponse, 30000); + conn.disconnect(); + return items; + } + + // Fetch in batches of 50 UIDs + const int batchSize = 50; + for (int i = 0; i < uids.size(); i += batchSize) { + QVector batch = uids.mid(i, qMin(batchSize, uids.size() - i)); + QString batchList; + for (qint64 uid : batch) { + batchList.append(QString::number(uid)).append(","); + } + batchList.chop(1); // remove trailing comma + + // Fetch headers and flags (efficient) + QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)") + .arg(batchList); + QString fetchResponse; + if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) { + qWarning() << "FETCH failed for batch" << i; + continue; + } + + // Parse and persist + QVector batchItems = parseFetchResponse(fetchResponse.split('\n')); + for (MailItem& item : batchItems) { + item.setFolderId(fid); + if (MailItemDao::insert(item)) { + items.append(item); + } else { + qWarning() << "Failed to insert mail item uid" << item.uid(); + } + } + } + + // Close and disconnect + QString dummy; + conn.sendCommandWait("CLOSE", dummy, 30000); + conn.disconnect(); + return items; +} + +QVector ImapSynchronizer::getFolders() const +{ + QVector folders; + ImapConnection conn; + + if (!connectAndLogin(conn)) { + return folders; + } + + // List folders: LIST "" "*" + QString listCommand = QStringLiteral("LIST \"\" \"*\""); + QString response; + if (!conn.sendCommandWait(listCommand, response, 30000)) { + qWarning() << "LIST command failed:" << response; + return folders; + } + + // Parse the response + QStringList lines = response.split('\n'); + folders = parseListResponse(lines); + + // Logout + conn.disconnect(); + return folders; +} + +bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item) +{ + // Get folder name + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) { + qWarning() << "Folder not found for id" << folderId; + return false; + } + QString folderName = folderOpt->name(); + + // Build RFC822 message (headers + body) + QStringList headers; + if (!item.sender().isEmpty()) headers << "From: " + item.sender(); + if (!item.recipient().isEmpty()) headers << "To: " + item.recipient(); + if (!item.cc().isEmpty()) headers << "Cc: " + item.cc(); + if (!item.bcc().isEmpty()) headers << "Bcc: " + item.bcc(); + if (!item.subject().isEmpty()) headers << "Subject: " + item.subject(); + headers << "Date: " + item.date().toString(Qt::RFC2822Date); + headers << "MIME-Version: 1.0"; + headers << "Content-Type: text/html; charset=UTF-8"; + headers << "Content-Transfer-Encoding: 7bit"; + headers << ""; // blank line separates headers from body + headers << item.bodyHtml(); // using bodyHtml as the body + + QString message = headers.join("\r\n"); + QByteArray msgData = message.toUtf8(); + + ImapConnection conn; + if (!connectAndLogin(conn)) return false; + + // SELECT folder (some servers require it) + QString selectCmd = QString("SELECT \"%1\"").arg(folderName); + QString resp; + conn.sendCommandWait(selectCmd, resp, 30000); // ignore response + + // APPEND with literal: APPEND "folder" (\Seen) {size} + QString appendCmd = QString("APPEND \"%1\" (\\Seen) {%2}").arg(folderName).arg(msgData.size()); + QString response; + if (!conn.sendCommandWait(appendCmd, response, 30000)) { + qWarning() << "APPEND command initial failed:" << response; + conn.disconnect(); + return false; + } + + // Send the literal (message data) + conn.sendRaw(QString::fromUtf8(msgData)); + + // Wait for the tagged response (tag + OK/NO) + if (!conn.sendCommandWait("", response, 30000)) { // dummy command to get response + qWarning() << "APPEND literal failed:" << response; + conn.disconnect(); + return false; + } + + conn.disconnect(); + return true; +} + +bool ImapSynchronizer::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged) +{ + qint64 uid = itemUid.toLongLong(); + if (uid <= 0) return false; + + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) return false; + QString folderName = folderOpt->name(); + + ImapConnection conn; + if (!connectAndLogin(conn)) return false; + + // SELECT + QString selectCmd = QString("SELECT \"%1\"").arg(folderName); + QString resp; + if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "SELECT failed"; + return false; + } + + // STORE for Seen flag + QString seenFlag = read ? "+" : "-"; + QString storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Seen").arg(uid).arg(seenFlag); + if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "STORE Seen failed:" << resp; + conn.disconnect(); + return false; + } + + // STORE for Flagged flag + QString flaggedFlag = flagged ? "+" : "-"; + storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Flagged").arg(uid).arg(flaggedFlag); + if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "STORE Flagged failed:" << resp; + conn.disconnect(); + return false; + } + + conn.disconnect(); + + // Update local DB + auto items = MailItemDao::findByFolderId(fid); + for (MailItem& item : items) { + if (item.uid() == uid) { + item.setRead(read); + item.setFlagged(flagged); + MailItemDao::update(item); + break; + } + } + return true; +} + +bool ImapSynchronizer::deleteMailItem(const QString& folderId, const QString& itemUid) +{ + qint64 uid = itemUid.toLongLong(); + if (uid <= 0) return false; + + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) return false; + QString folderName = folderOpt->name(); + + ImapConnection conn; + if (!connectAndLogin(conn)) return false; + + // SELECT + QString selectCmd = QString("SELECT \"%1\"").arg(folderName); + QString resp; + if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "SELECT failed"; + return false; + } + + // Mark as \\Deleted + QString storeCmd = QString("UID STORE %1 +FLAGS.SILENT \\Deleted").arg(uid); + if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "STORE Deleted failed:" << resp; + conn.disconnect(); + return false; + } + + // EXPUNGE (permanently delete) + QString expungeCmd = "EXPUNGE"; + if (!conn.sendCommandWait(expungeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "EXPUNGE failed:" << resp; + conn.disconnect(); + return false; + } + + conn.disconnect(); + + // Remove from local DB + MailItemDao::remove(uid); + return true; +} + +QString ImapSynchronizer::generateEventId() const +{ + return QString::number(QDateTime::currentMSecsSinceEpoch()); +} + +QVector ImapSynchronizer::parseListResponse(const QStringList& lines) const +{ + QVector folders; + QRegularExpression rx("\\* LIST \\\\([^)]*\\\\) \"([^\"]*)\" \"([^\"]*)\""); + for (const QString& line : lines) { + QRegularExpressionMatch match = rx.match(line); + if (match.hasMatch()) { + QString delimiter = match.captured(1); // unused for now + QString mailbox = match.captured(2); + Folder folder; + folder.setName(mailbox); + QString lower = mailbox.toLower(); + if (lower == "inbox") folder.setInbox(true); + else if (lower == "sent") folder.setSent(true); + else if (lower == "drafts") folder.setDrafts(true); + else if (lower == "trash" || lower == "deleted items") folder.setTrash(true); + folders.append(folder); + } + } + return folders; +} + +QVector ImapSynchronizer::parseFetchResponse(const QStringList& lines) const +{ + QVector items; + int i = 0; + while (i < lines.size()) { + const QString& line = lines[i]; + if (!line.startsWith(QStringLiteral("* "))) { + ++i; + continue; + } + int fetchPos = line.indexOf(QStringLiteral(" FETCH (")); + if (fetchPos == -1) { + ++i; + continue; + } + + // Extract UID + int uidPos = line.indexOf(QStringLiteral("UID ")); + if (uidPos == -1) { + ++i; + continue; + } + int uidEnd = line.indexOf(QRegularExpression(QStringLiteral("[\\s)]")), uidPos + 4); + if (uidEnd == -1) uidEnd = line.length(); + QString uidStr = line.mid(uidPos + 4, uidEnd - uidPos - 4); + bool ok; + qint64 uid = uidStr.toLongLong(&ok); + if (!ok) { + ++i; + continue; + } + + MailItem item; + item.setUid(uid); + + // Extract FLAGS + int flagsPos = line.indexOf(QStringLiteral("FLAGS (")); + if (flagsPos != -1) { + int flagsEnd = line.indexOf(')', flagsPos + 7); + if (flagsEnd != -1) { + QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7); + item.setRead(flags.contains(QStringLiteral("\\Seen"))); + item.setFlagged(flags.contains(QStringLiteral("\\Flagged"))); + } + } + + // Extract INTERNALDATE + int datePos = line.indexOf(QStringLiteral("INTERNALDATE \"")); + if (datePos != -1) { + int dateStart = datePos + 15; // length of "INTERNALDATE \"" + int dateEnd = line.indexOf('\"', dateStart); + if (dateEnd != -1) { + QString dateStr = line.mid(dateStart, dateEnd - dateStart); + dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000" + QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date); + if (dt.isValid()) + item.setDate(dt); + } + } + + // Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines + QString bodyData; + int bodyPos = line.indexOf(QStringLiteral("BODY[")); + if (bodyPos != -1) { + int bracePos = line.indexOf('{', bodyPos); + if (bracePos != -1) { + int sizeEnd = line.indexOf('}', bracePos); + if (sizeEnd != -1) { + bool sizeOk; + int size = line.mid(bracePos + 1, sizeEnd - bracePos - 1).toInt(&sizeOk); + if (sizeOk) { + int dataStart = line.indexOf(QStringLiteral("\r\n"), sizeEnd); + if (dataStart != -1) { + dataStart += 2; // skip \r\n + int available = line.length() - dataStart; + if (available > size) available = size; + bodyData = line.mid(dataStart, available); + int received = available; + int remaining = size - received; + int j = i + 1; + while (j < lines.size() && remaining > 0) { + const QString& nextLine = lines[j]; + int take = qMin(nextLine.length(), remaining); + bodyData += nextLine.left(take); + remaining -= take; + ++j; + } + // we have consumed lines up to j-1 + i = j - 1; // will be incremented at end of loop + } + } + } + } + } + + // Parse headers from bodyData + if (!bodyData.isEmpty()) { + QRegularExpression headerRx(QStringLiteral(R"((^([^:]+):\s*(.*?)\r\n))"), QRegularExpression::MultilineOption); + auto it = headerRx.globalMatch(bodyData); + while (it.hasNext()) { + auto match = it.next(); + QString key = match.captured(2).toLower().trimmed(); + QString value = match.captured(3).trimmed(); + if (key == QStringLiteral("subject")) + item.setSubject(value); + else if (key == QStringLiteral("from")) + item.setSender(value); + else if (key == QStringLiteral("to")) + item.setRecipient(value); + else if (key == QStringLiteral("date")) { + QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date); + if (dt.isValid()) + item.setDate(dt); + } + } + } + + // If subject not set, use placeholder + if (item.subject().isEmpty()) + item.setSubject(QStringLiteral("(No Subject)")); + + items.append(item); + ++i; + } + return items; +} diff --git a/src/services/imap/imapsynchronizer.cpp.bak b/src/services/imap/imapsynchronizer.cpp.bak new file mode 100644 index 0000000..3a395e9 --- /dev/null +++ b/src/services/imap/imapsynchronizer.cpp.bak @@ -0,0 +1,723 @@ +#include "imapsynchronizer.h" +#include +#include +#include +#include +#include "../../core/mailitem.h" +#include "../../core/models/account.h" +#include "../../core/models/folder.h" +#include "../../db/dao/folderdao.h" +#include "../../db/dao/mailitemdao.h" +#include + +ImapSynchronizer::ImapSynchronizer(QObject* parent) + : Synchronizer(parent) +{ +} + +bool ImapSynchronizer::initialize(const Account& account) +{ + const Account::ConnectionSettings& settings = account.connectionSettings(); + m_host = settings.incomingHost; + m_port = settings.incomingPort; + m_useSsl = settings.incomingSsl; + m_username = settings.username; + m_password = settings.password; + m_authMethod = settings.authMethod; + return true; +} + +// Helper: connects and logs in to the IMAP server +bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const +{ + bool connected = false; + bool loggedIn = false; + QString response; + + // Connect to host + QEventLoop connectLoop; + QTimer connectTimer; + bool connectTimeout = false; + connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit); + connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){ + qWarning() << "IMAP connection error:" << err; + connectLoop.quit(); + }); + connectTimer.setSingleShot(true); + connect(&connectTimer, &QTimer::timeout, &connectLoop, [&](){ + connectTimeout = true; + connectLoop.quit(); + }); + connectTimer.start(30000); // 30 seconds + conn.connectToHost(m_host, m_port, m_useSsl); + connectLoop.exec(); + if (connectTimeout) { + qWarning() << "Connection timeout to" << m_host; + return false; + } + connected = true; + + // Wait for server greeting (first untagged response) + QEventLoop greetLoop; + bool gotGreeting = false; + QMetaObject::Connection greetConn = connect(&conn, &ImapConnection::untaggedResponse, + [&](const QString& line) { + Q_UNUSED(line); + gotGreeting = true; + greetLoop.quit(); + }); + QTimer greetTimer; + greetTimer.setSingleShot(true); + connect(&greetTimer, &QTimer::timeout, &greetLoop, [&]() { + greetLoop.quit(); + }); + greetTimer.start(30000); // 30 seconds + greetLoop.exec(); + QObject::disconnect(greetConn); + if (!gotGreeting) { + qWarning() << "Timeout waiting for server greeting"; + return false; + } + + // Login + QEventLoop loginLoop; + QTimer loginTimer; + bool loginTimeout = false; + bool loginSuccess = false; + // Already connected, now login + conn.login(m_username, m_password, true, [&](bool ok, const QString& resp){ + if (ok) { + loginSuccess = true; + } else { + qWarning() << "Login failed:" << resp; + } + loginLoop.quit(); + }); + connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){ + qWarning() << "IMAP error during login:" << err; + loginLoop.quit(); + }); + loginTimer.setSingleShot(true); + connect(&loginTimer, &QTimer::timeout, &loginLoop, [&](){ + loginTimeout = true; + loginLoop.quit(); + }); + loginTimer.start(30000); + loginLoop.exec(); + if (loginTimeout) { + qWarning() << "Login timeout"; + return false; + } + if (!loginSuccess) { + return false; + } + loggedIn = true; + return true; +} + +QVector ImapSynchronizer::parseUidSearchResponse(const QString& response) const +{ + QVector uids; + QStringList lines = response.split(QStringLiteral("\n")); + for (const QString& line : lines) { + if (line.startsWith(QStringLiteral("* SEARCH"))) { + QStringList parts = line.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts); + for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH" + bool ok; + qint64 uid = parts[i].toLongLong(&ok); + if (ok && uid > 0) + uids.append(uid); + } + break; + } + } + return uids; +} +bool ImapSynchronizer::syncFolder(const Folder& folder) +{ + ImapConnection conn; + if (!connectAndLogin(conn)) + return false; + + // Select folder + QString selectCmd = QStringLiteral("SELECT \"%1\"").arg(folder.name()); + QString selectResp; + if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(QStringLiteral(" OK "))) { + qWarning() << "SELECT failed for" << folder.name(); + conn.disconnect(); + return false; + } + + // Get all UIDs from server + QString searchAllCmd = QStringLiteral("UID SEARCH ALL"); + QString searchAllResp; + if (!conn.sendCommandWait(searchAllCmd, searchAllResp, 30000) || !searchAllResp.contains(QStringLiteral(" OK "))) { + qWarning() << "UID SEARCH ALL failed"; + conn.disconnect(); + return false; + } + QVector serverUids = parseUidSearchResponse(searchAllResp); + std::sort(serverUids.begin(), serverUids.end()); + + // Get local UIDs for this folder + int fid = folder.id(); + QVector localUids = MailItemDao::getUidsForFolder(fid); + std::sort(localUids.begin(), localUids.end()); + + // Determine last known UID (max local uid) + qint64 lastUid = 0; + if (!localUids.isEmpty()) + lastUid = localUids.last(); + + // Fetch new UIDs (those > lastUid) + QVector newUids; + if (lastUid == 0) { + // No local mails, treat all as new + newUids = serverUids; + } else { + for (qint64 uid : serverUids) { + if (uid > lastUid) + newUids.append(uid); + } + } + + // Fetch new messages in batches + if (!newUids.isEmpty()) { + const int batchSize = 50; + for (int i = 0; i < newUids.size(); i += batchSize) { + QVector batch = newUids.mid(i, qMin(batchSize, newUids.size() - i)); + QStringList uidStrs; + for (qint64 uid : batch) + uidStrs.append(QString::number(uid)); + QString batchList = uidStrs.join(QStringLiteral(",")); + QString fetchCmd = QStringLiteral("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)").arg(batchList); + QString fetchResp; + if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000)) { + qWarning() << "UID FETCH failed for batch" << i; + continue; + } + if (!fetchResp.contains(QStringLiteral(" OK "))) { + qWarning() << "UID FETCH did not return OK"; + continue; + } + QVector fetched = parseFetchResponse(fetchResp.split(QStringLiteral("\n"))); + for (MailItem& item : fetched) { + item.setFolderId(fid); + if (!MailItemDao::insert(item)) { + qWarning() << "Failed to insert mail item uid" << item.uid(); + } + } + } + } + + // Update flags for all local uids (to capture flag changes) + if (!localUids.isEmpty()) { + const int batchSize = 100; + for (int i = 0; i < localUids.size(); i += batchSize) { + QVector batch = localUids.mid(i, qMin(batchSize, localUids.size() - i)); + QStringList uidStrs; + for (qint64 uid : batch) + uidStrs.append(QString::number(uid)); + QString uidList = uidStrs.join(QStringLiteral(",")); + QString fetchCmd = QStringLiteral("UID FETCH %1 (FLAGS)").arg(uidList); + QString fetchResp; + if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000)) { + qWarning() << "UID FETCH FLAGS failed"; + continue; + } + if (!fetchResp.contains(QStringLiteral(" OK "))) { + qWarning() << "UID FETCH FLAGS not OK"; + continue; + } + // Parse flags response + QRegularExpression flagsRx(QStringLiteral(R"(UID (\d+).*FLAGS \(([^)]*)\))")); + auto it = flagsRx.globalMatch(fetchResp); + while (it.hasNext()) { + auto m = it.next(); + qint64 uid = m.captured(1).toLongLong(); + QString flags = m.captured(2); + bool seen = flags.contains(QStringLiteral("\\Seen")); + bool flagged = flags.contains(QStringLiteral("\\Flagged")); + std::optional opt = MailItemDao::findById(uid); + if (opt) { + MailItem& item = *opt; + bool changed = false; + if (item.isRead() != seen) { + item.setRead(seen); + changed = true; + } + if (item.isFlagged() != flagged) { + item.setFlagged(flagged); + changed = true; + } + if (changed) + MailItemDao::update(item); + } + } + } + } + + // Handle expunged messages: remove local uids not present on server + QVector toRemove; + std::set_difference(localUids.begin(), localUids.end(), + serverUids.begin(), serverUids.end(), + std::back_inserter(toRemove)); + for (qint64 uid : toRemove) { + MailItemDao::remove(uid); + } + + // Update unread count + int unread = 0; + for (qint64 uid : localUids) { + std::optional opt = MailItemDao::findById(uid); + if (opt && !opt->isRead()) + ++unread; + } + Folder f = folder; + f.setUnreadCount(unread); + if (!FolderDao::update(f)) { + qWarning() << "Failed to update folder unread count"; + } + + conn.disconnect(); + return true; +} + +QVector ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid) +{ + QVector items; + ImapConnection conn; + + if (!connectAndLogin(conn)) { + return items; + } + + // Get folder name from DB + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) { + qWarning() << "Folder not found for id" << folderId; + return items; + } + QString folderName = folderOpt->name(); + + // SELECT with real folder name + QString selectCommand = QString("SELECT \"%1\"").arg(folderName); + QString selectResponse; + if (!conn.sendCommandWait(selectCommand, selectResponse, 30000)) { + qWarning() << "SELECT command failed:" << selectResponse; + return items; + } + if (!selectResponse.contains(" OK ")) { + qWarning() << "SELECT failed:" << selectResponse; + return items; + } + + // SEARCH + QString searchCommand; + if (sinceUid > 0) { + searchCommand = QString("UID SEARCH %1:*").arg(sinceUid); + } else { + searchCommand = "UID SEARCH ALL"; + } + QString searchResponse; + if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) { + qWarning() << "SEARCH command failed:" << searchResponse; + return items; + } + + // Parse UIDs (robust: look for line starting with "* SEARCH") + QVector uids; + QStringList lines = searchResponse.split('\n'); + for (const QString& line : lines) { + if (line.startsWith("* SEARCH")) { + QStringList parts = line.split(QRegularExpression("\\s+")); + for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH" + bool ok; + qint64 uid = parts[i].toLongLong(&ok); + if (ok && uid > 0) uids.append(uid); + } + break; + } + } + + if (uids.isEmpty()) { + conn.sendCommandWait("CLOSE", searchResponse, 30000); + conn.disconnect(); + return items; + } + + // Fetch in batches of 50 UIDs + const int batchSize = 50; + for (int i = 0; i < uids.size(); i += batchSize) { + QVector batch = uids.mid(i, qMin(batchSize, uids.size() - i)); + QString batchList; + for (qint64 uid : batch) { + batchList.append(QString::number(uid)).append(","); + } + batchList.chop(1); // remove trailing comma + + // Fetch headers and flags (efficient) + QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)") + .arg(batchList); + QString fetchResponse; + if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) { + qWarning() << "FETCH failed for batch" << i; + continue; + } + + // Parse and persist + QVector batchItems = parseFetchResponse(fetchResponse.split('\n')); + for (MailItem& item : batchItems) { + item.setFolderId(fid); + if (MailItemDao::insert(item)) { + items.append(item); + } else { + qWarning() << "Failed to insert mail item uid" << item.uid(); + } + } + } + + // Close and disconnect + QString dummy; + conn.sendCommandWait("CLOSE", dummy, 30000); + conn.disconnect(); + return items; +} + +QVector ImapSynchronizer::getFolders() const +{ + QVector folders; + ImapConnection conn; + + if (!connectAndLogin(conn)) { + return folders; + } + + // List folders: LIST "" "*" + QString listCommand = QStringLiteral("LIST \"\" \"*\""); + QString response; + if (!conn.sendCommandWait(listCommand, response, 30000)) { + qWarning() << "LIST command failed:" << response; + return folders; + } + + // Parse the response + QStringList lines = response.split('\n'); + folders = parseListResponse(lines); + + // Logout + conn.disconnect(); + return folders; +} + +bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item) +{ + // Get folder name + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) { + qWarning() << "Folder not found for id" << folderId; + return false; + } + QString folderName = folderOpt->name(); + + // Build RFC822 message (headers + body) + QStringList headers; + if (!item.sender().isEmpty()) headers << "From: " + item.sender(); + if (!item.recipient().isEmpty()) headers << "To: " + item.recipient(); + if (!item.cc().isEmpty()) headers << "Cc: " + item.cc(); + if (!item.bcc().isEmpty()) headers << "Bcc: " + item.bcc(); + if (!item.subject().isEmpty()) headers << "Subject: " + item.subject(); + headers << "Date: " + item.date().toString(Qt::RFC2822Date); + headers << "MIME-Version: 1.0"; + headers << "Content-Type: text/html; charset=UTF-8"; + headers << "Content-Transfer-Encoding: 7bit"; + headers << ""; // blank line separates headers from body + headers << item.bodyHtml(); // using bodyHtml as the body + + QString message = headers.join("\r\n"); + QByteArray msgData = message.toUtf8(); + + ImapConnection conn; + if (!connectAndLogin(conn)) return false; + + // SELECT folder (some servers require it) + QString selectCmd = QString("SELECT \"%1\"").arg(folderName); + QString resp; + conn.sendCommandWait(selectCmd, resp, 30000); // ignore response + + // APPEND with literal: APPEND "folder" (\Seen) {size} + QString appendCmd = QString("APPEND \"%1\" (\\Seen) {%2}").arg(folderName).arg(msgData.size()); + QString response; + if (!conn.sendCommandWait(appendCmd, response, 30000)) { + qWarning() << "APPEND command initial failed:" << response; + conn.disconnect(); + return false; + } + + // Send the literal (message data) + conn.sendRaw(QString::fromUtf8(msgData)); + + // Wait for the tagged response (tag + OK/NO) + if (!conn.sendCommandWait("", response, 30000)) { // dummy command to get response + qWarning() << "APPEND literal failed:" << response; + conn.disconnect(); + return false; + } + + conn.disconnect(); + return true; +} + +bool ImapSynchronizer::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged) +{ + qint64 uid = itemUid.toLongLong(); + if (uid <= 0) return false; + + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) return false; + QString folderName = folderOpt->name(); + + ImapConnection conn; + if (!connectAndLogin(conn)) return false; + + // SELECT + QString selectCmd = QString("SELECT \"%1\"").arg(folderName); + QString resp; + if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "SELECT failed"; + return false; + } + + // STORE for Seen flag + QString seenFlag = read ? "+" : "-"; + QString storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Seen").arg(uid).arg(seenFlag); + if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "STORE Seen failed:" << resp; + conn.disconnect(); + return false; + } + + // STORE for Flagged flag + QString flaggedFlag = flagged ? "+" : "-"; + storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Flagged").arg(uid).arg(flaggedFlag); + if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "STORE Flagged failed:" << resp; + conn.disconnect(); + return false; + } + + conn.disconnect(); + + // Update local DB + auto items = MailItemDao::findByFolderId(fid); + for (MailItem& item : items) { + if (item.uid() == uid) { + item.setRead(read); + item.setFlagged(flagged); + MailItemDao::update(item); + break; + } + } + return true; +} + +bool ImapSynchronizer::deleteMailItem(const QString& folderId, const QString& itemUid) +{ + qint64 uid = itemUid.toLongLong(); + if (uid <= 0) return false; + + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) return false; + QString folderName = folderOpt->name(); + + ImapConnection conn; + if (!connectAndLogin(conn)) return false; + + // SELECT + QString selectCmd = QString("SELECT \"%1\"").arg(folderName); + QString resp; + if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "SELECT failed"; + return false; + } + + // Mark as \\Deleted + QString storeCmd = QString("UID STORE %1 +FLAGS.SILENT \\Deleted").arg(uid); + if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "STORE Deleted failed:" << resp; + conn.disconnect(); + return false; + } + + // EXPUNGE (permanently delete) + QString expungeCmd = "EXPUNGE"; + if (!conn.sendCommandWait(expungeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "EXPUNGE failed:" << resp; + conn.disconnect(); + return false; + } + + conn.disconnect(); + + // Remove from local DB + MailItemDao::remove(uid); + return true; +} + +QString ImapSynchronizer::generateEventId() const +{ + return QString::number(QDateTime::currentMSecsSinceEpoch()); +} + +QVector ImapSynchronizer::parseListResponse(const QStringList& lines) const +{ + QVector folders; + QRegularExpression rx("\\* LIST \\\\([^)]*\\\\) \"([^\"]*)\" \"([^\"]*)\""); + for (const QString& line : lines) { + QRegularExpressionMatch match = rx.match(line); + if (match.hasMatch()) { + QString delimiter = match.captured(1); // unused for now + QString mailbox = match.captured(2); + Folder folder; + folder.setName(mailbox); + QString lower = mailbox.toLower(); + if (lower == "inbox") folder.setInbox(true); + else if (lower == "sent") folder.setSent(true); + else if (lower == "drafts") folder.setDrafts(true); + else if (lower == "trash" || lower == "deleted items") folder.setTrash(true); + folders.append(folder); + } + } + return folders; +} + +QVector ImapSynchronizer::parseFetchResponse(const QStringList& lines) const +{ + QVector items; + int i = 0; + while (i < lines.size()) { + const QString& line = lines[i]; + if (!line.startsWith(QStringLiteral("* "))) { + ++i; + continue; + } + int fetchPos = line.indexOf(QStringLiteral(" FETCH (")); + if (fetchPos == -1) { + ++i; + continue; + } + + // Extract UID + int uidPos = line.indexOf(QStringLiteral("UID ")); + if (uidPos == -1) { + ++i; + continue; + } + int uidEnd = line.indexOf(QRegularExpression(QStringLiteral("[\\s)]")), uidPos + 4); + if (uidEnd == -1) uidEnd = line.length(); + QString uidStr = line.mid(uidPos + 4, uidEnd - uidPos - 4); + bool ok; + qint64 uid = uidStr.toLongLong(&ok); + if (!ok) { + ++i; + continue; + } + + MailItem item; + item.setUid(uid); + + // Extract FLAGS + int flagsPos = line.indexOf(QStringLiteral("FLAGS (")); + if (flagsPos != -1) { + int flagsEnd = line.indexOf(')', flagsPos + 7); + if (flagsEnd != -1) { + QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7); + item.setRead(flags.contains(QStringLiteral("\\Seen"))); + item.setFlagged(flags.contains(QStringLiteral("\\Flagged"))); + } + } + + // Extract INTERNALDATE + int datePos = line.indexOf(QStringLiteral("INTERNALDATE \"")); + if (datePos != -1) { + int dateStart = datePos + 15; // length of "INTERNALDATE \"" + int dateEnd = line.indexOf('\"', dateStart); + if (dateEnd != -1) { + QString dateStr = line.mid(dateStart, dateEnd - dateStart); + dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000" + QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date); + if (dt.isValid()) + item.setDate(dt); + } + } + + // Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines + QString bodyData; + int bodyPos = line.indexOf(QStringLiteral("BODY[")); + if (bodyPos != -1) { + int bracePos = line.indexOf('{', bodyPos); + if (bracePos != -1) { + int sizeEnd = line.indexOf('}', bracePos); + if (sizeEnd != -1) { + bool sizeOk; + int size = line.mid(bracePos + 1, sizeEnd - bracePos - 1).toInt(&sizeOk); + if (sizeOk) { + int dataStart = line.indexOf(QStringLiteral("\r\n"), sizeEnd); + if (dataStart != -1) { + dataStart += 2; // skip \r\n + int available = line.length() - dataStart; + if (available > size) available = size; + bodyData = line.mid(dataStart, available); + int received = available; + int remaining = size - received; + int j = i + 1; + while (j < lines.size() && remaining > 0) { + const QString& nextLine = lines[j]; + int take = qMin(nextLine.length(), remaining); + bodyData += nextLine.left(take); + remaining -= take; + ++j; + } + // we have consumed lines up to j-1 + i = j - 1; // will be incremented at end of loop + } + } + } + } + } + + // Parse headers from bodyData + if (!bodyData.isEmpty()) { + QRegularExpression headerRx(QStringLiteral(R"((^([^:]+):\s*(.*?)\r\n))"), QRegularExpression::MultilineOption); + auto it = headerRx.globalMatch(bodyData); + while (it.hasNext()) { + auto match = it.next(); + QString key = match.captured(2).toLower().trimmed(); + QString value = match.captured(3).trimmed(); + if (key == QStringLiteral("subject")) + item.setSubject(value); + else if (key == QStringLiteral("from")) + item.setSender(value); + else if (key == QStringLiteral("to")) + item.setRecipient(value); + else if (key == QStringLiteral("date")) { + QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date); + if (dt.isValid()) + item.setDate(dt); + } + } + } + + // If subject not set, use placeholder + if (item.subject().isEmpty()) + item.setSubject(QStringLiteral("(No Subject)")); + + items.append(item); + ++i; + } + return items; +} \ No newline at end of file diff --git a/src/services/imap/imapsynchronizer.cpp.bak3 b/src/services/imap/imapsynchronizer.cpp.bak3 new file mode 100644 index 0000000..4cf9654 --- /dev/null +++ b/src/services/imap/imapsynchronizer.cpp.bak3 @@ -0,0 +1,723 @@ +#include "imapsynchronizer.h" +#include +#include +#include +#include +#include "../../core/mailitem.h" +#include "../../core/models/account.h" +#include "../../core/models/folder.h" +#include "../../db/dao/folderdao.h" +#include "../../db/dao/mailitemdao.h" +#include + +ImapSynchronizer::ImapSynchronizer(QObject* parent) + : Synchronizer(parent) +{ +} + +bool ImapSynchronizer::initialize(const Account& account) +{ + const Account::ConnectionSettings& settings = account.connectionSettings(); + m_host = settings.incomingHost; + m_port = settings.incomingPort; + m_useSsl = settings.incomingSsl; + m_username = settings.username; + m_password = settings.password; + m_authMethod = settings.authMethod; + return true; +} + +// Helper: connects and logs in to the IMAP server +bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const +{ + bool connected = false; + bool loggedIn = false; + QString response; + + // Connect to host + QEventLoop connectLoop; + QTimer connectTimer; + bool connectTimeout = false; + connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit); + connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){ + qWarning() << "IMAP connection error:" << err; + connectLoop.quit(); + }); + connectTimer.setSingleShot(true); + connect(&connectTimer, &QTimer::timeout, &connectLoop, [&](){ + connectTimeout = true; + connectLoop.quit(); + }); + connectTimer.start(30000); // 30 seconds + conn.connectToHost(m_host, m_port, m_useSsl); + connectLoop.exec(); + if (connectTimeout) { + qWarning() << "Connection timeout to" << m_host; + return false; + } + connected = true; + + // Wait for server greeting (first untagged response) + QEventLoop greetLoop; + bool gotGreeting = false; + QMetaObject::Connection greetConn = connect(&conn, &ImapConnection::untaggedResponse, + [&](const QString& line) { + Q_UNUSED(line); + gotGreeting = true; + greetLoop.quit(); + }); + QTimer greetTimer; + greetTimer.setSingleShot(true); + connect(&greetTimer, &QTimer::timeout, &greetLoop, [&]() { + greetLoop.quit(); + }); + greetTimer.start(30000); // 30 seconds + greetLoop.exec(); + QObject::disconnect(greetConn); + if (!gotGreeting) { + qWarning() << "Timeout waiting for server greeting"; + return false; + } + + // Login + QEventLoop loginLoop; + QTimer loginTimer; + bool loginTimeout = false; + bool loginSuccess = false; + // Already connected, now login + conn.login(m_username, m_password, true, [&](bool ok, const QString& resp){ + if (ok) { + loginSuccess = true; + } else { + qWarning() << "Login failed:" << resp; + } + loginLoop.quit(); + }); + connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){ + qWarning() << "IMAP error during login:" << err; + loginLoop.quit(); + }); + loginTimer.setSingleShot(true); + connect(&loginTimer, &QTimer::timeout, &loginLoop, [&](){ + loginTimeout = true; + loginLoop.quit(); + }); + loginTimer.start(30000); + loginLoop.exec(); + if (loginTimeout) { + qWarning() << "Login timeout"; + return false; + } + if (!loginSuccess) { + return false; + } + loggedIn = true; + return true; +} + +QVector ImapSynchronizer::parseUidSearchResponse(const QString& response) const +{ + QVector uids; + QStringList lines = response.split(QStringLiteral("\n")); + for (const QString& line : lines) { + if (line.startsWith(QStringLiteral("* SEARCH"))) { + QStringList parts = line.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts); + for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH" + bool ok; + qint64 uid = parts[i].toLongLong(&ok); + if (ok && uid > 0) + uids.append(uid); + } + break; + } + } + return uids; +} +bool ImapSynchronizer::syncFolder(const Folder& folder) +{ + ImapConnection conn; + if (!connectAndLogin(conn)) + return false; + + // Select folder + QString selectCmd = QStringLiteral("SELECT \"%1\"").arg(folder.name()); + QString selectResp; + if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(QStringLiteral(" OK "))) { + qWarning() << "SELECT failed for" << folder.name(); + conn.disconnect(); + return false; + } + + // Get all UIDs from server + QString searchAllCmd = QStringLiteral("UID SEARCH ALL"); + QString searchAllResp; + if (!conn.sendCommandWait(searchAllCmd, searchAllResp, 30000) || !searchAllResp.contains(QStringLiteral(" OK "))) { + qWarning() << "UID SEARCH ALL failed"; + conn.disconnect(); + return false; + } + QVector serverUids = parseUidSearchResponse(searchAllResp); + std::sort(serverUids.begin(), serverUids.end()); + + // Get local UIDs for this folder + int fid = folder.id(); + QVector localUids = MailItemDao::getUidsForFolder(fid); + std::sort(localUids.begin(), localUids.end()); + + // Determine last known UID (max local uid) + qint64 lastUid = 0; + if (!localUids.isEmpty()) + lastUid = localUids.last(); + + // Fetch new UIDs (those > lastUid) + QVector newUids; + if (lastUid == 0) { + // No local mails, treat all as new + newUids = serverUids; + } else { + for (qint64 uid : serverUids) { + if (uid > lastUid) + newUids.append(uid); + } + } + + // Fetch new messages in batches + if (!newUids.isEmpty()) { + const int batchSize = 50; + for (int i = 0; i < newUids.size(); i += batchSize) { + QVector batch = newUids.mid(i, qMin(batchSize, newUids.size() - i)); + QStringList uidStrs; + for (qint64 uid : batch) + uidStrs.append(QString::number(uid)); + QString batchList = uidStrs.join(QStringLiteral(",")); + QString fetchCmd = QStringLiteral("UID FETCH %1 (BODY[] FLAGS INTERNALDATE)").arg(batchList); + QString fetchResp; + if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000)) { + qWarning() << "UID FETCH failed for batch" << i; + continue; + } + if (!fetchResp.contains(QStringLiteral(" OK "))) { + qWarning() << "UID FETCH did not return OK"; + continue; + } + QVector fetched = parseFetchResponse(fetchResp.split(QStringLiteral("\n"))); + for (MailItem& item : fetched) { + item.setFolderId(fid); + if (!MailItemDao::insert(item)) { + qWarning() << "Failed to insert mail item uid" << item.uid(); + } + } + } + } + + // Update flags for all local uids (to capture flag changes) + if (!localUids.isEmpty()) { + const int batchSize = 100; + for (int i = 0; i < localUids.size(); i += batchSize) { + QVector batch = localUids.mid(i, qMin(batchSize, localUids.size() - i)); + QStringList uidStrs; + for (qint64 uid : batch) + uidStrs.append(QString::number(uid)); + QString uidList = uidStrs.join(QStringLiteral(",")); + QString fetchCmd = QStringLiteral("UID FETCH %1 (FLAGS)").arg(uidList); + QString fetchResp; + if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000)) { + qWarning() << "UID FETCH FLAGS failed"; + continue; + } + if (!fetchResp.contains(QStringLiteral(" OK "))) { + qWarning() << "UID FETCH FLAGS not OK"; + continue; + } + // Parse flags response + QRegularExpression flagsRx(QStringLiteral(R"(UID (\d+).*FLAGS \(([^)]*)\))")); + auto it = flagsRx.globalMatch(fetchResp); + while (it.hasNext()) { + auto m = it.next(); + qint64 uid = m.captured(1).toLongLong(); + QString flags = m.captured(2); + bool seen = flags.contains(QStringLiteral("\\Seen")); + bool flagged = flags.contains(QStringLiteral("\\Flagged")); + std::optional opt = MailItemDao::findById(uid); + if (opt) { + MailItem& item = *opt; + bool changed = false; + if (item.isRead() != seen) { + item.setRead(seen); + changed = true; + } + if (item.isFlagged() != flagged) { + item.setFlagged(flagged); + changed = true; + } + if (changed) + MailItemDao::update(item); + } + } + } + } + + // Handle expunged messages: remove local uids not present on server + QVector toRemove; + std::set_difference(localUids.begin(), localUids.end(), + serverUids.begin(), serverUids.end(), + std::back_inserter(toRemove)); + for (qint64 uid : toRemove) { + MailItemDao::remove(uid); + } + + // Update unread count + int unread = 0; + for (qint64 uid : localUids) { + std::optional opt = MailItemDao::findById(uid); + if (opt && !opt->isRead()) + ++unread; + } + Folder f = folder; + f.setUnreadCount(unread); + if (!FolderDao::update(f)) { + qWarning() << "Failed to update folder unread count"; + } + + conn.disconnect(); + return true; +} + +QVector ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid) +{ + QVector items; + ImapConnection conn; + + if (!connectAndLogin(conn)) { + return items; + } + + // Get folder name from DB + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) { + qWarning() << "Folder not found for id" << folderId; + return items; + } + QString folderName = folderOpt->name(); + + // SELECT with real folder name + QString selectCommand = QString("SELECT \"%1\"").arg(folderName); + QString selectResponse; + if (!conn.sendCommandWait(selectCommand, selectResponse, 30000)) { + qWarning() << "SELECT command failed:" << selectResponse; + return items; + } + if (!selectResponse.contains(" OK ")) { + qWarning() << "SELECT failed:" << selectResponse; + return items; + } + + // SEARCH + QString searchCommand; + if (sinceUid > 0) { + searchCommand = QString("UID SEARCH %1:*").arg(sinceUid); + } else { + searchCommand = "UID SEARCH ALL"; + } + QString searchResponse; + if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) { + qWarning() << "SEARCH command failed:" << searchResponse; + return items; + } + + // Parse UIDs (robust: look for line starting with "* SEARCH") + QVector uids; + QStringList lines = searchResponse.split('\n'); + for (const QString& line : lines) { + if (line.startsWith("* SEARCH")) { + QStringList parts = line.split(QRegularExpression("\\s+")); + for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH" + bool ok; + qint64 uid = parts[i].toLongLong(&ok); + if (ok && uid > 0) uids.append(uid); + } + break; + } + } + + if (uids.isEmpty()) { + conn.sendCommandWait("CLOSE", searchResponse, 30000); + conn.disconnect(); + return items; + } + + // Fetch in batches of 50 UIDs + const int batchSize = 50; + for (int i = 0; i < uids.size(); i += batchSize) { + QVector batch = uids.mid(i, qMin(batchSize, uids.size() - i)); + QString batchList; + for (qint64 uid : batch) { + batchList.append(QString::number(uid)).append(","); + } + batchList.chop(1); // remove trailing comma + + // Fetch headers and flags (efficient) + QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)") + .arg(batchList); + QString fetchResponse; + if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) { + qWarning() << "FETCH failed for batch" << i; + continue; + } + + // Parse and persist + QVector batchItems = parseFetchResponse(fetchResponse.split('\n')); + for (MailItem& item : batchItems) { + item.setFolderId(fid); + if (MailItemDao::insert(item)) { + items.append(item); + } else { + qWarning() << "Failed to insert mail item uid" << item.uid(); + } + } + } + + // Close and disconnect + QString dummy; + conn.sendCommandWait("CLOSE", dummy, 30000); + conn.disconnect(); + return items; +} + +QVector ImapSynchronizer::getFolders() const +{ + QVector folders; + ImapConnection conn; + + if (!connectAndLogin(conn)) { + return folders; + } + + // List folders: LIST "" "*" + QString listCommand = QStringLiteral("LIST \"\" \"*\""); + QString response; + if (!conn.sendCommandWait(listCommand, response, 30000)) { + qWarning() << "LIST command failed:" << response; + return folders; + } + + // Parse the response + QStringList lines = response.split('\n'); + folders = parseListResponse(lines); + + // Logout + conn.disconnect(); + return folders; +} + +bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item) +{ + // Get folder name + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) { + qWarning() << "Folder not found for id" << folderId; + return false; + } + QString folderName = folderOpt->name(); + + // Build RFC822 message (headers + body) + QStringList headers; + if (!item.sender().isEmpty()) headers << "From: " + item.sender(); + if (!item.recipient().isEmpty()) headers << "To: " + item.recipient(); + if (!item.cc().isEmpty()) headers << "Cc: " + item.cc(); + if (!item.bcc().isEmpty()) headers << "Bcc: " + item.bcc(); + if (!item.subject().isEmpty()) headers << "Subject: " + item.subject(); + headers << "Date: " + item.date().toString(Qt::RFC2822Date); + headers << "MIME-Version: 1.0"; + headers << "Content-Type: text/html; charset=UTF-8"; + headers << "Content-Transfer-Encoding: 7bit"; + headers << ""; // blank line separates headers from body + headers << item.bodyHtml(); // using bodyHtml as the body + + QString message = headers.join("\r\n"); + QByteArray msgData = message.toUtf8(); + + ImapConnection conn; + if (!connectAndLogin(conn)) return false; + + // SELECT folder (some servers require it) + QString selectCmd = QString("SELECT \"%1\"").arg(folderName); + QString resp; + conn.sendCommandWait(selectCmd, resp, 30000); // ignore response + + // APPEND with literal: APPEND "folder" (\Seen) {size} + QString appendCmd = QString("APPEND \"%1\" (\\Seen) {%2}").arg(folderName).arg(msgData.size()); + QString response; + if (!conn.sendCommandWait(appendCmd, response, 30000)) { + qWarning() << "APPEND command initial failed:" << response; + conn.disconnect(); + return false; + } + + // Send the literal (message data) + conn.sendRaw(QString::fromUtf8(msgData)); + + // Wait for the tagged response (tag + OK/NO) + if (!conn.sendCommandWait("", response, 30000)) { // dummy command to get response + qWarning() << "APPEND literal failed:" << response; + conn.disconnect(); + return false; + } + + conn.disconnect(); + return true; +} + +bool ImapSynchronizer::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged) +{ + qint64 uid = itemUid.toLongLong(); + if (uid <= 0) return false; + + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) return false; + QString folderName = folderOpt->name(); + + ImapConnection conn; + if (!connectAndLogin(conn)) return false; + + // SELECT + QString selectCmd = QString("SELECT \"%1\"").arg(folderName); + QString resp; + if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "SELECT failed"; + return false; + } + + // STORE for Seen flag + QString seenFlag = read ? "+" : "-"; + QString storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Seen").arg(uid).arg(seenFlag); + if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "STORE Seen failed:" << resp; + conn.disconnect(); + return false; + } + + // STORE for Flagged flag + QString flaggedFlag = flagged ? "+" : "-"; + storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Flagged").arg(uid).arg(flaggedFlag); + if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "STORE Flagged failed:" << resp; + conn.disconnect(); + return false; + } + + conn.disconnect(); + + // Update local DB + auto items = MailItemDao::findByFolderId(fid); + for (MailItem& item : items) { + if (item.uid() == uid) { + item.setRead(read); + item.setFlagged(flagged); + MailItemDao::update(item); + break; + } + } + return true; +} + +bool ImapSynchronizer::deleteMailItem(const QString& folderId, const QString& itemUid) +{ + qint64 uid = itemUid.toLongLong(); + if (uid <= 0) return false; + + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) return false; + QString folderName = folderOpt->name(); + + ImapConnection conn; + if (!connectAndLogin(conn)) return false; + + // SELECT + QString selectCmd = QString("SELECT \"%1\"").arg(folderName); + QString resp; + if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "SELECT failed"; + return false; + } + + // Mark as \\Deleted + QString storeCmd = QString("UID STORE %1 +FLAGS.SILENT \\Deleted").arg(uid); + if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "STORE Deleted failed:" << resp; + conn.disconnect(); + return false; + } + + // EXPUNGE (permanently delete) + QString expungeCmd = "EXPUNGE"; + if (!conn.sendCommandWait(expungeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "EXPUNGE failed:" << resp; + conn.disconnect(); + return false; + } + + conn.disconnect(); + + // Remove from local DB + MailItemDao::remove(uid); + return true; +} + +QString ImapSynchronizer::generateEventId() const +{ + return QString::number(QDateTime::currentMSecsSinceEpoch()); +} + +QVector ImapSynchronizer::parseListResponse(const QStringList& lines) const +{ + QVector folders; + QRegularExpression rx("\\* LIST \\\\([^)]*\\\\) \"([^\"]*)\" \"([^\"]*)\""); + for (const QString& line : lines) { + QRegularExpressionMatch match = rx.match(line); + if (match.hasMatch()) { + QString delimiter = match.captured(1); // unused for now + QString mailbox = match.captured(2); + Folder folder; + folder.setName(mailbox); + QString lower = mailbox.toLower(); + if (lower == "inbox") folder.setInbox(true); + else if (lower == "sent") folder.setSent(true); + else if (lower == "drafts") folder.setDrafts(true); + else if (lower == "trash" || lower == "deleted items") folder.setTrash(true); + folders.append(folder); + } + } + return folders; +} + +QVector ImapSynchronizer::parseFetchResponse(const QStringList& lines) const +{ + QVector items; + int i = 0; + while (i < lines.size()) { + const QString& line = lines[i]; + if (!line.startsWith(QStringLiteral("* "))) { + ++i; + continue; + } + int fetchPos = line.indexOf(QStringLiteral(" FETCH (")); + if (fetchPos == -1) { + ++i; + continue; + } + + // Extract UID + int uidPos = line.indexOf(QStringLiteral("UID ")); + if (uidPos == -1) { + ++i; + continue; + } + int uidEnd = line.indexOf(QRegularExpression(QStringLiteral("[\\s)]")), uidPos + 4); + if (uidEnd == -1) uidEnd = line.length(); + QString uidStr = line.mid(uidPos + 4, uidEnd - uidPos - 4); + bool ok; + qint64 uid = uidStr.toLongLong(&ok); + if (!ok) { + ++i; + continue; + } + + MailItem item; + item.setUid(uid); + + // Extract FLAGS + int flagsPos = line.indexOf(QStringLiteral("FLAGS (")); + if (flagsPos != -1) { + int flagsEnd = line.indexOf(')', flagsPos + 7); + if (flagsEnd != -1) { + QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7); + item.setRead(flags.contains(QStringLiteral("\\Seen"))); + item.setFlagged(flags.contains(QStringLiteral("\\Flagged"))); + } + } + + // Extract INTERNALDATE + int datePos = line.indexOf(QStringLiteral("INTERNALDATE \"")); + if (datePos != -1) { + int dateStart = datePos + 15; // length of "INTERNALDATE \"" + int dateEnd = line.indexOf('\"', dateStart); + if (dateEnd != -1) { + QString dateStr = line.mid(dateStart, dateEnd - dateStart); + dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000" + QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date); + if (dt.isValid()) + item.setDate(dt); + } + } + + // Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines + QString bodyData; + int bodyPos = line.indexOf(QStringLiteral("BODY[]")); + if (bodyPos != -1) { + int bracePos = line.indexOf('{', bodyPos); + if (bracePos != -1) { + int sizeEnd = line.indexOf('}', bracePos); + if (sizeEnd != -1) { + bool sizeOk; + int size = line.mid(bracePos + 1, sizeEnd - bracePos - 1).toInt(&sizeOk); + if (sizeOk) { + int dataStart = line.indexOf(QStringLiteral("\r\n"), sizeEnd); + if (dataStart != -1) { + dataStart += 2; // skip \r\n + int available = line.length() - dataStart; + if (available > size) available = size; + bodyData = line.mid(dataStart, available); + int received = available; + int remaining = size - received; + int j = i + 1; + while (j < lines.size() && remaining > 0) { + const QString& nextLine = lines[j]; + int take = qMin(nextLine.length(), remaining); + bodyData += nextLine.left(take); + remaining -= take; + ++j; + } + // we have consumed lines up to j-1 + i = j - 1; // will be incremented at end of loop + } + } + } + } + } + + // Parse headers from bodyData + if (!bodyData.isEmpty()) { + QRegularExpression headerRx(QStringLiteral(R"((^([^:]+):\s*(.*?)\r\n))"), QRegularExpression::MultilineOption); + auto it = headerRx.globalMatch(bodyData); + while (it.hasNext()) { + auto match = it.next(); + QString key = match.captured(2).toLower().trimmed(); + QString value = match.captured(3).trimmed(); + if (key == QStringLiteral("subject")) + item.setSubject(value); + else if (key == QStringLiteral("from")) + item.setSender(value); + else if (key == QStringLiteral("to")) + item.setRecipient(value); + else if (key == QStringLiteral("date")) { + QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date); + if (dt.isValid()) + item.setDate(dt); + } + } + } + + // If subject not set, use placeholder + if (item.subject().isEmpty()) + item.setSubject(QStringLiteral("(No Subject)")); + + items.append(item); + ++i; + } + return items; +} \ No newline at end of file diff --git a/src/services/imap/imapsynchronizer.cpp.syncfolder.bak b/src/services/imap/imapsynchronizer.cpp.syncfolder.bak new file mode 100644 index 0000000..f3266a0 --- /dev/null +++ b/src/services/imap/imapsynchronizer.cpp.syncfolder.bak @@ -0,0 +1,649 @@ +#include "imapsynchronizer.h" +#include +#include +#include +#include +#include "../../core/mailitem.h" +#include "../../core/models/account.h" +#include "../../core/models/folder.h" +#include "../../db/dao/folderdao.h" +#include "../../db/dao/mailitemdao.h" + +ImapSynchronizer::ImapSynchronizer(QObject* parent) + : Synchronizer(parent) +{ +} + +bool ImapSynchronizer::initialize(const Account& account) +{ + const Account::ConnectionSettings& settings = account.connectionSettings(); + m_host = settings.incomingHost; + m_port = settings.incomingPort; + m_useSsl = settings.incomingSsl; + m_username = settings.username; + m_password = settings.password; + m_authMethod = settings.authMethod; + return true; +} + +// Helper: connects and logs in to the IMAP server +bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const +{ + bool connected = false; + bool loggedIn = false; + QString response; + + // Connect to host + QEventLoop connectLoop; + QTimer connectTimer; + bool connectTimeout = false; + connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit); + connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){ + qWarning() << "IMAP connection error:" << err; + connectLoop.quit(); + }); + connectTimer.setSingleShot(true); + connect(&connectTimer, &QTimer::timeout, &connectLoop, [&](){ + connectTimeout = true; + connectLoop.quit(); + }); + connectTimer.start(30000); // 30 seconds + conn.connectToHost(m_host, m_port, m_useSsl); + connectLoop.exec(); + if (connectTimeout) { + qWarning() << "Connection timeout to" << m_host; + return false; + } + connected = true; + + // Wait for server greeting (first untagged response) + QEventLoop greetLoop; + bool gotGreeting = false; + QMetaObject::Connection greetConn = connect(&conn, &ImapConnection::untaggedResponse, + [&](const QString& line) { + Q_UNUSED(line); + gotGreeting = true; + greetLoop.quit(); + }); + QTimer greetTimer; + greetTimer.setSingleShot(true); + connect(&greetTimer, &QTimer::timeout, &greetLoop, [&]() { + greetLoop.quit(); + }); + greetTimer.start(30000); // 30 seconds + greetLoop.exec(); + QObject::disconnect(greetConn); + if (!gotGreeting) { + qWarning() << "Timeout waiting for server greeting"; + return false; + } + + // Login + QEventLoop loginLoop; + QTimer loginTimer; + bool loginTimeout = false; + bool loginSuccess = false; + // Already connected, now login + conn.login(m_username, m_password, true, [&](bool ok, const QString& resp){ + if (ok) { + loginSuccess = true; + } else { + qWarning() << "Login failed:" << resp; + } + loginLoop.quit(); + }); + connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){ + qWarning() << "IMAP error during login:" << err; + loginLoop.quit(); + }); + loginTimer.setSingleShot(true); + connect(&loginTimer, &QTimer::timeout, &loginLoop, [&](){ + loginTimeout = true; + loginLoop.quit(); + }); + loginTimer.start(30000); + loginLoop.exec(); + if (loginTimeout) { + qWarning() << "Login timeout"; + return false; + } + if (!loginSuccess) { + return false; + } + loggedIn = true; + return true; +} + +bool ImapSynchronizer::syncFolder(const Folder& folder) +{ + ImapConnection conn; + if (!connectAndLogin(conn)) return false; + + // Select folder + QString selectCmd = QString("SELECT \"%1\"").arg(folder.name()); + QString selectResp; + if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(" OK ")) { + qWarning() << "SELECT failed for" << folder.name(); + return false; + } + + // Get all UIDs + QString searchCmd = "UID SEARCH ALL"; + QString searchResp; + if (!conn.sendCommandWait(searchCmd, searchResp, 30000) || !searchResp.contains(" OK ")) { + qWarning() << "SEARCH failed"; + return false; + } + + QVector uids; + QStringList lines = searchResp.split('\n'); + for (const QString& line : lines) { + if (line.startsWith("* SEARCH")) { + QStringList parts = line.split(QRegularExpression("\\s+")); + for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH" + bool ok; + qint64 uid = parts[i].toLongLong(&ok); + if (ok && uid > 0) uids.append(uid); + } + break; + } + } + + // If no messages, set unread count to 0 and exit + if (uids.isEmpty()) { + Folder f = folder; + f.setUnreadCount(0); + FolderDao::update(f); + conn.disconnect(); + return true; + } + + // Get flags for all UIDs in one command + QStringList uidStrs; + for (qint64 uid : uids) uidStrs.append(QString::number(uid)); + QString fetchCmd = "UID FETCH " + uidStrs.join(',') + " (FLAGS)"; + QString fetchResp; + if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000) || !fetchResp.contains(" OK ")) { + qWarning() << "FETCH FLAGS failed"; + conn.disconnect(); + return false; + } + + // Parse flags and update local DB + QMap> flagsMap; // uid -> (seen, flagged) + QRegularExpression flagsRx(R"(UID (\d+).*FLAGS \(([^)]*)\))"); + auto it = flagsRx.globalMatch(fetchResp); + while (it.hasNext()) { + auto m = it.next(); + qint64 uid = m.captured(1).toLongLong(); + QString flags = m.captured(2); + bool seen = flags.contains("\\Seen"); + bool flagged = flags.contains("\\Flagged"); + flagsMap[uid] = qMakePair(seen, flagged); + } + + // Update local items + auto localItems = MailItemDao::findByFolderId(folder.id()); + for (MailItem& item : localItems) { + if (flagsMap.contains(item.uid())) { + bool seen = flagsMap[item.uid()].first; + bool flagged = flagsMap[item.uid()].second; + if (item.isRead() != seen || item.isFlagged() != flagged) { + item.setRead(seen); + item.setFlagged(flagged); + MailItemDao::update(item); + } + } + } + + // Count unread + int unread = 0; + for (const MailItem& item : localItems) { + if (!item.isRead()) unread++; + } + Folder f = folder; + f.setUnreadCount(unread); + FolderDao::update(f); + + conn.disconnect(); + return true; +} + +QVector ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid) +{ + QVector items; + ImapConnection conn; + + if (!connectAndLogin(conn)) { + return items; + } + + // Get folder name from DB + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) { + qWarning() << "Folder not found for id" << folderId; + return items; + } + QString folderName = folderOpt->name(); + + // SELECT with real folder name + QString selectCommand = QString("SELECT \"%1\"").arg(folderName); + QString selectResponse; + if (!conn.sendCommandWait(selectCommand, selectResponse, 30000)) { + qWarning() << "SELECT command failed:" << selectResponse; + return items; + } + if (!selectResponse.contains(" OK ")) { + qWarning() << "SELECT failed:" << selectResponse; + return items; + } + + // SEARCH + QString searchCommand; + if (sinceUid > 0) { + searchCommand = QString("UID SEARCH %1:*").arg(sinceUid); + } else { + searchCommand = "UID SEARCH ALL"; + } + QString searchResponse; + if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) { + qWarning() << "SEARCH command failed:" << searchResponse; + return items; + } + + // Parse UIDs (robust: look for line starting with "* SEARCH") + QVector uids; + QStringList lines = searchResponse.split('\n'); + for (const QString& line : lines) { + if (line.startsWith("* SEARCH")) { + QStringList parts = line.split(QRegularExpression("\\s+")); + for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH" + bool ok; + qint64 uid = parts[i].toLongLong(&ok); + if (ok && uid > 0) uids.append(uid); + } + break; + } + } + + if (uids.isEmpty()) { + conn.sendCommandWait("CLOSE", searchResponse, 30000); + conn.disconnect(); + return items; + } + + // Fetch in batches of 50 UIDs + const int batchSize = 50; + for (int i = 0; i < uids.size(); i += batchSize) { + QVector batch = uids.mid(i, qMin(batchSize, uids.size() - i)); + QString batchList; + for (qint64 uid : batch) { + batchList.append(QString::number(uid)).append(","); + } + batchList.chop(1); // remove trailing comma + + // Fetch headers and flags (efficient) + QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)") + .arg(batchList); + QString fetchResponse; + if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) { + qWarning() << "FETCH failed for batch" << i; + continue; + } + + // Parse and persist + QVector batchItems = parseFetchResponse(fetchResponse.split('\n')); + for (MailItem& item : batchItems) { + item.setFolderId(fid); + if (MailItemDao::insert(item)) { + items.append(item); + } else { + qWarning() << "Failed to insert mail item uid" << item.uid(); + } + } + } + + // Close and disconnect + QString dummy; + conn.sendCommandWait("CLOSE", dummy, 30000); + conn.disconnect(); + return items; +} + +QVector ImapSynchronizer::getFolders() const +{ + QVector folders; + ImapConnection conn; + + if (!connectAndLogin(conn)) { + return folders; + } + + // List folders: LIST "" "*" + QString listCommand = QStringLiteral("LIST \"\" \"*\""); + QString response; + if (!conn.sendCommandWait(listCommand, response, 30000)) { + qWarning() << "LIST command failed:" << response; + return folders; + } + + // Parse the response + QStringList lines = response.split('\n'); + folders = parseListResponse(lines); + + // Logout + conn.disconnect(); + return folders; +} + +bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item) +{ + // Get folder name + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) { + qWarning() << "Folder not found for id" << folderId; + return false; + } + QString folderName = folderOpt->name(); + + // Build RFC822 message (headers + body) + QStringList headers; + if (!item.sender().isEmpty()) headers << "From: " + item.sender(); + if (!item.recipient().isEmpty()) headers << "To: " + item.recipient(); + if (!item.cc().isEmpty()) headers << "Cc: " + item.cc(); + if (!item.bcc().isEmpty()) headers << "Bcc: " + item.bcc(); + if (!item.subject().isEmpty()) headers << "Subject: " + item.subject(); + headers << "Date: " + item.date().toString(Qt::RFC2822Date); + headers << "MIME-Version: 1.0"; + headers << "Content-Type: text/html; charset=UTF-8"; + headers << "Content-Transfer-Encoding: 7bit"; + headers << ""; // blank line separates headers from body + headers << item.bodyHtml(); // using bodyHtml as the body + + QString message = headers.join("\r\n"); + QByteArray msgData = message.toUtf8(); + + ImapConnection conn; + if (!connectAndLogin(conn)) return false; + + // SELECT folder (some servers require it) + QString selectCmd = QString("SELECT \"%1\"").arg(folderName); + QString resp; + conn.sendCommandWait(selectCmd, resp, 30000); // ignore response + + // APPEND with literal: APPEND "folder" (\Seen) {size} + QString appendCmd = QString("APPEND \"%1\" (\\Seen) {%2}").arg(folderName).arg(msgData.size()); + QString response; + if (!conn.sendCommandWait(appendCmd, response, 30000)) { + qWarning() << "APPEND command initial failed:" << response; + conn.disconnect(); + return false; + } + + // Send the literal (message data) + conn.sendRaw(QString::fromUtf8(msgData)); + + // Wait for the tagged response (tag + OK/NO) + if (!conn.sendCommandWait("", response, 30000)) { // dummy command to get response + qWarning() << "APPEND literal failed:" << response; + conn.disconnect(); + return false; + } + + conn.disconnect(); + return true; +} + +bool ImapSynchronizer::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged) +{ + qint64 uid = itemUid.toLongLong(); + if (uid <= 0) return false; + + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) return false; + QString folderName = folderOpt->name(); + + ImapConnection conn; + if (!connectAndLogin(conn)) return false; + + // SELECT + QString selectCmd = QString("SELECT \"%1\"").arg(folderName); + QString resp; + if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "SELECT failed"; + return false; + } + + // STORE for Seen flag + QString seenFlag = read ? "+" : "-"; + QString storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Seen").arg(uid).arg(seenFlag); + if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "STORE Seen failed:" << resp; + conn.disconnect(); + return false; + } + + // STORE for Flagged flag + QString flaggedFlag = flagged ? "+" : "-"; + storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Flagged").arg(uid).arg(flaggedFlag); + if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "STORE Flagged failed:" << resp; + conn.disconnect(); + return false; + } + + conn.disconnect(); + + // Update local DB + auto items = MailItemDao::findByFolderId(fid); + for (MailItem& item : items) { + if (item.uid() == uid) { + item.setRead(read); + item.setFlagged(flagged); + MailItemDao::update(item); + break; + } + } + return true; +} + +bool ImapSynchronizer::deleteMailItem(const QString& folderId, const QString& itemUid) +{ + qint64 uid = itemUid.toLongLong(); + if (uid <= 0) return false; + + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) return false; + QString folderName = folderOpt->name(); + + ImapConnection conn; + if (!connectAndLogin(conn)) return false; + + // SELECT + QString selectCmd = QString("SELECT \"%1\"").arg(folderName); + QString resp; + if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "SELECT failed"; + return false; + } + + // Mark as \\Deleted + QString storeCmd = QString("UID STORE %1 +FLAGS.SILENT \\Deleted").arg(uid); + if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "STORE Deleted failed:" << resp; + conn.disconnect(); + return false; + } + + // EXPUNGE (permanently delete) + QString expungeCmd = "EXPUNGE"; + if (!conn.sendCommandWait(expungeCmd, resp, 30000) || !resp.contains(" OK ")) { + qWarning() << "EXPUNGE failed:" << resp; + conn.disconnect(); + return false; + } + + conn.disconnect(); + + // Remove from local DB + MailItemDao::remove(uid); + return true; +} + +QString ImapSynchronizer::generateEventId() const +{ + return QString::number(QDateTime::currentMSecsSinceEpoch()); +} + +QVector ImapSynchronizer::parseListResponse(const QStringList& lines) const +{ + QVector folders; + QRegularExpression rx("\\* LIST \\\\([^)]*\\\\) \"([^\"]*)\" \"([^\"]*)\""); + for (const QString& line : lines) { + QRegularExpressionMatch match = rx.match(line); + if (match.hasMatch()) { + QString delimiter = match.captured(1); // unused for now + QString mailbox = match.captured(2); + Folder folder; + folder.setName(mailbox); + QString lower = mailbox.toLower(); + if (lower == "inbox") folder.setInbox(true); + else if (lower == "sent") folder.setSent(true); + else if (lower == "drafts") folder.setDrafts(true); + else if (lower == "trash" || lower == "deleted items") folder.setTrash(true); + folders.append(folder); + } + } + return folders; +} + +QVector ImapSynchronizer::parseFetchResponse(const QStringList& lines) const +{ + QVector items; + int i = 0; + while (i < lines.size()) { + const QString& line = lines[i]; + if (!line.startsWith(QStringLiteral("* "))) { + ++i; + continue; + } + int fetchPos = line.indexOf(QStringLiteral(" FETCH (")); + if (fetchPos == -1) { + ++i; + continue; + } + + // Extract UID + int uidPos = line.indexOf(QStringLiteral("UID ")); + if (uidPos == -1) { + ++i; + continue; + } + int uidEnd = line.indexOf(QRegularExpression(QStringLiteral("[\\s)]")), uidPos + 4); + if (uidEnd == -1) uidEnd = line.length(); + QString uidStr = line.mid(uidPos + 4, uidEnd - uidPos - 4); + bool ok; + qint64 uid = uidStr.toLongLong(&ok); + if (!ok) { + ++i; + continue; + } + + MailItem item; + item.setUid(uid); + + // Extract FLAGS + int flagsPos = line.indexOf(QStringLiteral("FLAGS (")); + if (flagsPos != -1) { + int flagsEnd = line.indexOf(')', flagsPos + 7); + if (flagsEnd != -1) { + QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7); + item.setRead(flags.contains(QStringLiteral("\\Seen"))); + item.setFlagged(flags.contains(QStringLiteral("\\Flagged"))); + } + } + + // Extract INTERNALDATE + int datePos = line.indexOf(QStringLiteral("INTERNALDATE \"")); + if (datePos != -1) { + int dateStart = datePos + 15; // length of "INTERNALDATE \"" + int dateEnd = line.indexOf('\"', dateStart); + if (dateEnd != -1) { + QString dateStr = line.mid(dateStart, dateEnd - dateStart); + dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000" + QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date); + if (dt.isValid()) + item.setDate(dt); + } + } + + // Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines + QString bodyData; + int bodyPos = line.indexOf(QStringLiteral("BODY[")); + if (bodyPos != -1) { + int bracePos = line.indexOf('{', bodyPos); + if (bracePos != -1) { + int sizeEnd = line.indexOf('}', bracePos); + if (sizeEnd != -1) { + bool sizeOk; + int size = line.mid(bracePos + 1, sizeEnd - bracePos - 1).toInt(&sizeOk); + if (sizeOk) { + int dataStart = line.indexOf(QStringLiteral("\r\n"), sizeEnd); + if (dataStart != -1) { + dataStart += 2; // skip \r\n + int available = line.length() - dataStart; + if (available > size) available = size; + bodyData = line.mid(dataStart, available); + int received = available; + int remaining = size - received; + int j = i + 1; + while (j < lines.size() && remaining > 0) { + const QString& nextLine = lines[j]; + int take = qMin(nextLine.length(), remaining); + bodyData += nextLine.left(take); + remaining -= take; + ++j; + } + // we have consumed lines up to j-1 + i = j - 1; // will be incremented at end of loop + } + } + } + } + } + + // Parse headers from bodyData + if (!bodyData.isEmpty()) { + QRegularExpression headerRx(QStringLiteral(R"((^([^:]+):\s*(.*?)\r\n))"), QRegularExpression::MultilineOption); + auto it = headerRx.globalMatch(bodyData); + while (it.hasNext()) { + auto match = it.next(); + QString key = match.captured(2).toLower().trimmed(); + QString value = match.captured(3).trimmed(); + if (key == QStringLiteral("subject")) + item.setSubject(value); + else if (key == QStringLiteral("from")) + item.setSender(value); + else if (key == QStringLiteral("to")) + item.setRecipient(value); + else if (key == QStringLiteral("date")) { + QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date); + if (dt.isValid()) + item.setDate(dt); + } + } + } + + // If subject not set, use placeholder + if (item.subject().isEmpty()) + item.setSubject(QStringLiteral("(No Subject)")); + + items.append(item); + ++i; + } + return items; +} diff --git a/src/services/imap/imapsynchronizer.h b/src/services/imap/imapsynchronizer.h index 1be125f..5ba5c8d 100644 --- a/src/services/imap/imapsynchronizer.h +++ b/src/services/imap/imapsynchronizer.h @@ -2,8 +2,8 @@ #include "../synchronizer.h" #include -#include #include +#include "imapconnection.h" class ImapSynchronizer : public Synchronizer { @@ -30,20 +30,28 @@ signals: void statusMessage(const QString& message) const; private: - // Connection details + QVector parseUidSearchResponse(const QString& response) const; + // Connection details (set by initialize) 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 parseListResponse(const QString& response) const; -}; \ No newline at end of file + // Helper: conecta y loguea al servidor (reutilizable) + bool connectAndLogin(ImapConnection &conn) const; + + // IMAP response parsers + QVector parseListResponse(const QStringList& lines) const; + QVector parseFetchResponse(const QStringList& lines) const; + QVector parseFetchResponseBytes(const QByteArray& response) const; + bool persistFetchedItem(const Folder& folder, MailItem& item) const; + + // Sync folder helpers + QVector fetchAllUids(const QString& folderId) const; + void parseAndUpdateFlags(const QByteArray& response, int folderId) const; +}; diff --git a/src/services/imap/imapsynchronizer.h.backup b/src/services/imap/imapsynchronizer.h.backup new file mode 100644 index 0000000..27157c0 --- /dev/null +++ b/src/services/imap/imapsynchronizer.h.backup @@ -0,0 +1,50 @@ +#pragma once + +#include "../synchronizer.h" +#include +#include +#include "imapconnection.h" + +class ImapSynchronizer : public Synchronizer +{ + Q_OBJECT +public: + explicit ImapSynchronizer(QObject* parent = nullptr); + ~ImapSynchronizer() override = default; + + // Synchronizer interface + bool initialize(const Account& account) override; + bool syncFolder(const Folder& folder) override; + QVector getFolders() const override; + QVector 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; + +signals: + void progressChanged(int percent) const; + void statusMessage(const QString& message) const; + +private: + // Connection details (set by initialize) + 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" + + // Helper para generar IDs รบnicos de eventos + QString generateEventId() const; + + // Helper: conecta y loguea al servidor (reutilizable) + bool connectAndLogin(ImapConnection &conn) const; + + // IMAP response parsers + QVector parseListResponse(const QStringList& lines) const; + QVector parseFetchResponse(const QStringList& lines) const; +}; diff --git a/src/services/mailservice.cpp b/src/services/mailservice.cpp index 7a87f06..fbf3da4 100644 --- a/src/services/mailservice.cpp +++ b/src/services/mailservice.cpp @@ -3,6 +3,302 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "services/mimestorage.h" + +namespace { + +QByteArray smtpHeader(const QString &name, const QString &value) +{ + const QByteArray utf8 = value.toUtf8(); + bool ascii = true; + for (char c : utf8) { + if (static_cast(c) < 32 || static_cast(c) > 126) { + ascii = false; + break; + } + } + const QByteArray encoded = ascii + ? utf8 + : QByteArray("=?UTF-8?B?") + utf8.toBase64() + QByteArray("?="); + return name.toUtf8() + QByteArray(": ") + encoded + QByteArray("\r\n"); +} + +QByteArray base64Lines(const QByteArray &data) +{ + const QByteArray encoded = data.toBase64(); + QByteArray wrapped; + for (int i = 0; i < encoded.size(); i += 76) + wrapped.append(encoded.mid(i, 76)).append("\r\n"); + return wrapped; +} + +QStringList smtpRecipients(const QString &value) +{ + QStringList result; + for (QString part : value.split(QRegularExpression(QStringLiteral("[,;]")), Qt::SkipEmptyParts)) { + const QRegularExpressionMatch match = QRegularExpression(QStringLiteral("<([^>]+)>")).match(part); + if (match.hasMatch()) part = match.captured(1); + part = part.trimmed(); + if (!part.isEmpty()) result.append(part); + } + return result; +} + +bool smtpResponse(QSslSocket &socket, int expectedCode) +{ + QByteArray line; + while (true) { + if (!socket.canReadLine() && !socket.waitForReadyRead(15000)) return false; + line = socket.readLine(); + if (line.size() < 3) continue; + bool ok = false; + const int code = line.left(3).toInt(&ok); + if (!ok) continue; + // A space marks the final line of a multiline SMTP response. + if (line.size() > 3 && line.at(3) == '-') continue; + return code == expectedCode; + } +} + +QByteArray composeMimeMessage(const MailItem &mail, const Account &account, + const QStringList &attachmentPaths, QString &error) +{ + QStringList recipients = smtpRecipients(mail.to()); + if (recipients.isEmpty()) recipients = smtpRecipients(mail.recipient()); + recipients.append(smtpRecipients(mail.cc())); + recipients.append(smtpRecipients(mail.bcc())); + if (recipients.isEmpty()) { + error = QObject::tr("No recipients specified"); + return {}; + } + recipients.removeDuplicates(); + + QByteArray message; + const QString from = mail.sender().isEmpty() ? account.email() : mail.sender(); + message.append(smtpHeader(QStringLiteral("From"), from)); + message.append(smtpHeader(QStringLiteral("To"), mail.to().isEmpty() ? mail.recipient() : mail.to())); + if (!mail.cc().isEmpty()) message.append(smtpHeader(QStringLiteral("Cc"), mail.cc())); + message.append(smtpHeader(QStringLiteral("Subject"), mail.subject())); + message.append("Date: ").append((mail.date().isValid() ? mail.date() : QDateTime::currentDateTimeUtc()).toString(Qt::RFC2822Date).toUtf8()).append("\r\n"); + message.append("MIME-Version: 1.0\r\n"); + + QByteArray body = mail.bodyHtml().toUtf8(); + body.replace("\r\n", "\n"); + body.replace('\r', '\n'); + body.replace('\n', "\r\n"); + if (attachmentPaths.isEmpty()) { + message.append("Content-Type: text/html; charset=UTF-8\r\n"); + message.append("Content-Transfer-Encoding: 8bit\r\n\r\n"); + message.append(body).append("\r\n"); + return message; + } + + const QByteArray boundary = QByteArray("----WinoMail-") + + QByteArray::number(QDateTime::currentMSecsSinceEpoch()); + message.append("Content-Type: multipart/mixed; boundary=\"").append(boundary).append("\"\r\n\r\n"); + message.append("--").append(boundary).append("\r\n"); + message.append("Content-Type: text/html; charset=UTF-8\r\n"); + message.append("Content-Transfer-Encoding: 8bit\r\n\r\n").append(body).append("\r\n"); + + QMimeDatabase mimeDatabase; + for (const QString &path : attachmentPaths) { + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + error = QObject::tr("Could not read attachment: %1").arg(path); + return {}; + } + const QFileInfo info(path); + const QString fileName = info.fileName().replace('"', '_'); + const QString mimeType = mimeDatabase.mimeTypeForFile(info).name(); + message.append("--").append(boundary).append("\r\n"); + message.append("Content-Type: ").append(mimeType.toUtf8()).append("; name=\"").append(fileName.toUtf8()).append("\"\r\n"); + message.append("Content-Transfer-Encoding: base64\r\n"); + message.append("Content-Disposition: attachment; filename=\"").append(fileName.toUtf8()).append("\"\r\n\r\n"); + message.append(base64Lines(file.readAll())); + } + message.append("--").append(boundary).append("--\r\n"); + return message; +} + +bool sendSmtp(const MailItem &mail, const Account &account, + const QStringList &attachmentPaths, QString &error) +{ + const Account::ConnectionSettings settings = account.connectionSettings(); + if (settings.outgoingHost.isEmpty() || settings.outgoingPort == 0) { + error = QObject::tr("SMTP server is not configured"); + return false; + } + const QByteArray mime = composeMimeMessage(mail, account, attachmentPaths, error); + if (mime.isEmpty()) return false; + QSslSocket socket; + if (settings.outgoingSsl && settings.outgoingPort == 465) + socket.connectToHostEncrypted(settings.outgoingHost, settings.outgoingPort); + else + socket.connectToHost(settings.outgoingHost, settings.outgoingPort); + if (!socket.waitForConnected(15000)) { error = socket.errorString(); return false; } + if (settings.outgoingSsl && settings.outgoingPort == 465) { + if (!socket.waitForEncrypted(15000)) { error = socket.errorString(); return false; } + } + if (!smtpResponse(socket, 220)) { error = QObject::tr("SMTP greeting failed"); return false; } + + auto command = [&](const QByteArray &value, int code) { + socket.write(value + QByteArray("\r\n")); + socket.flush(); + return smtpResponse(socket, code); + }; + if (!command("EHLO localhost", 250)) { error = QObject::tr("SMTP EHLO failed"); return false; } + if (settings.outgoingSsl && settings.outgoingPort != 465) { + if (!command("STARTTLS", 220)) { error = QObject::tr("SMTP STARTTLS failed"); return false; } + socket.startClientEncryption(); + if (!socket.waitForEncrypted(15000)) { error = socket.errorString(); return false; } + if (!command("EHLO localhost", 250)) { error = QObject::tr("SMTP EHLO after TLS failed"); return false; } + } + const QString username = settings.username.isEmpty() ? account.email() : settings.username; + if (!username.isEmpty()) { + if (!command("AUTH LOGIN", 334) + || !command(username.toUtf8().toBase64(), 334) + || !command(settings.password.toUtf8().toBase64(), 235)) { + error = QObject::tr("SMTP authentication failed"); + return false; + } + } + const QString from = mail.sender().isEmpty() ? account.email() : mail.sender(); + if (!command("MAIL FROM:<" + smtpRecipients(from).value(0).toUtf8() + ">", 250)) { error = QObject::tr("SMTP MAIL FROM failed"); return false; } + const QStringList recipients = smtpRecipients(mail.to()) + smtpRecipients(mail.recipient()) + + smtpRecipients(mail.cc()) + smtpRecipients(mail.bcc()); + QStringList uniqueRecipients = recipients; + uniqueRecipients.removeDuplicates(); + for (const QString &recipient : uniqueRecipients) { + if (!command("RCPT TO:<" + recipient.toUtf8() + ">", 250)) { error = QObject::tr("SMTP recipient rejected: %1").arg(recipient); return false; } + } + if (!command("DATA", 354)) { error = QObject::tr("SMTP DATA failed"); return false; } + QByteArray stuffed; + const QList lines = mime.split('\n'); + for (QByteArray line : lines) { + if (line.startsWith('.')) stuffed.append('.'); + stuffed.append(line); + if (!line.endsWith('\r')) stuffed.append('\r'); + stuffed.append('\n'); + } + socket.write(stuffed); + socket.write(".\r\n"); + socket.flush(); + if (!smtpResponse(socket, 250)) { error = QObject::tr("SMTP message rejected"); return false; } + command("QUIT", 221); + return true; +} + +QByteArray base64Url(const QByteArray &data) +{ + QByteArray result = data.toBase64(); + result.replace('+', '-'); + result.replace('/', '_'); + while (result.endsWith('=')) result.chop(1); + return result; +} + +bool postJson(const QUrl &url, const QByteArray &token, const QJsonObject &payload, + int expectedStatus, QString &error) +{ + QNetworkAccessManager network; + QNetworkRequest request(url); + request.setRawHeader("Authorization", QByteArrayLiteral("Bearer ") + token); + request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); + QNetworkReply *reply = network.post(request, QJsonDocument(payload).toJson(QJsonDocument::Compact)); + QEventLoop loop; + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + const bool ok = reply->error() == QNetworkReply::NoError && status == expectedStatus; + if (!ok) error = reply->error() == QNetworkReply::NoError + ? QObject::tr("Server returned HTTP %1").arg(status) : reply->errorString(); + reply->deleteLater(); + return ok; +} + +bool sendGmailApi(const MailItem &mail, const Account &account, + const QStringList &attachmentPaths, QString &error) +{ + if (!account.isTokenValid()) { + error = QObject::tr("Gmail access token is missing or expired"); + return false; + } + const QByteArray mime = composeMimeMessage(mail, account, attachmentPaths, error); + if (mime.isEmpty()) return false; + QJsonObject payload; + payload.insert(QStringLiteral("raw"), QString::fromLatin1(base64Url(mime))); + const QString user = QString::fromUtf8(QUrl::toPercentEncoding(account.email())); + const QUrl url(QStringLiteral("https://gmail.googleapis.com/gmail/v1/users/%1/messages/send").arg(user)); + return postJson(url, account.accessToken().toUtf8(), payload, 200, error); +} + +QJsonArray graphRecipients(const QString &value) +{ + QJsonArray result; + for (const QString &address : smtpRecipients(value)) { + QJsonObject recipient; + QJsonObject email; + email.insert(QStringLiteral("address"), address); + recipient.insert(QStringLiteral("emailAddress"), email); + result.append(recipient); + } + return result; +} + +bool sendOutlookApi(const MailItem &mail, const Account &account, + const QStringList &attachmentPaths, QString &error) +{ + if (!account.isTokenValid()) { + error = QObject::tr("Outlook access token is missing or expired"); + return false; + } + QJsonObject message; + message.insert(QStringLiteral("subject"), mail.subject()); + QJsonObject body; + body.insert(QStringLiteral("contentType"), QStringLiteral("HTML")); + body.insert(QStringLiteral("content"), mail.bodyHtml()); + message.insert(QStringLiteral("body"), body); + message.insert(QStringLiteral("toRecipients"), graphRecipients(mail.to().isEmpty() ? mail.recipient() : mail.to())); + message.insert(QStringLiteral("ccRecipients"), graphRecipients(mail.cc())); + message.insert(QStringLiteral("bccRecipients"), graphRecipients(mail.bcc())); + QJsonArray attachments; + QMimeDatabase mimeDatabase; + for (const QString &path : attachmentPaths) { + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + error = QObject::tr("Could not read attachment: %1").arg(path); + return false; + } + const QFileInfo info(path); + QJsonObject attachment; + attachment.insert(QStringLiteral("@odata.type"), QStringLiteral("#microsoft.graph.fileAttachment")); + attachment.insert(QStringLiteral("name"), info.fileName()); + attachment.insert(QStringLiteral("contentType"), mimeDatabase.mimeTypeForFile(info).name()); + attachment.insert(QStringLiteral("contentBytes"), QString::fromLatin1(file.readAll().toBase64())); + attachments.append(attachment); + } + message.insert(QStringLiteral("attachments"), attachments); + QJsonObject payload; + payload.insert(QStringLiteral("message"), message); + payload.insert(QStringLiteral("saveToSentItems"), true); + const QUrl url(QStringLiteral("https://graph.microsoft.com/v1.0/me/sendMail")); + return postJson(url, account.accessToken().toUtf8(), payload, 202, error); +} + +} MailService::MailService(AccountService *accountService, QObject *parent) : QObject(parent) @@ -18,13 +314,43 @@ QVector MailService::getMails(const QString &folderId) void MailService::sendMail(const MailItem &mail, const QString &accountId) { - // 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())); + sendMail(mail, accountId, {}); +} + +void MailService::sendMail(const MailItem &mail, const QString &accountId, + const QStringList &attachmentPaths) +{ + if (!m_accountService) { + emit mailSendFailed(accountId, tr("AccountService not available")); + return; + } + Account *account = m_accountService->findAccountById(accountId.toLongLong()); + if (!account) { + emit mailSendFailed(accountId, tr("Account not found")); + return; + } + const Account accountCopy = *account; + delete account; + QFuture future = QtConcurrent::run([mail, accountCopy, attachmentPaths]() { + QString error; + bool ok = false; + if (accountCopy.type() == AccountType::Gmail) + ok = sendGmailApi(mail, accountCopy, attachmentPaths, error); + else if (accountCopy.type() == AccountType::Outlook) + ok = sendOutlookApi(mail, accountCopy, attachmentPaths, error); + else + ok = sendSmtp(mail, accountCopy, attachmentPaths, error); + if (!ok) qWarning() << "Send failed:" << error; + return ok; + }); + auto *watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, [this, watcher, accountId]() { + const bool ok = watcher->result(); + watcher->deleteLater(); + if (ok) emit mailSent(accountId); + else emit mailSendFailed(accountId, tr("Mail server rejected the message")); + }); + watcher->setFuture(future); } void MailService::fetchMails(const QString &accountId, const QString &folderId) @@ -35,6 +361,10 @@ void MailService::fetchMails(const QString &accountId, const QString &folderId) return; } Account *acc = m_accountService->findAccountById(accountId.toLongLong()); + if (!acc) { + emit mailFetchError(accountId, folderId, tr("Account not found")); + return; + } QString providerType = QStringLiteral("imap"); // fallback if (acc) { switch (acc->type()) { @@ -65,10 +395,12 @@ void MailService::fetchMails(const QString &accountId, const QString &folderId) // Initialize synchronizer with account details before using it if (!sync->initialize(*acc)) { + delete acc; emit mailFetchError(accountId, folderId, tr("Failed to initialize synchronizer")); - sync->deleteLater(); + SynchronizerProvider::instance().unregisterSynchronizer(accountId); return; } + delete acc; // Connect progress signals (if they exist) using string-based syntax for compatibility QObject::connect(sync, SIGNAL(progressChanged(int)), this, SIGNAL(progressChanged(int)), Qt::QueuedConnection); @@ -85,12 +417,52 @@ void MailService::fetchMails(const QString &accountId, const QString &folderId) QObject::disconnect(sync, SIGNAL(progressChanged(int)), this, SIGNAL(progressChanged(int))); QObject::disconnect(sync, SIGNAL(statusMessage(const QString&)), this, SIGNAL(statusMessage(const QString&))); QVector items = watcher->result(); + QVector persisted; + for (MailItem &item : items) { + if (persistFetchedItem(item, accountId, folderId)) persisted.append(item); + } watcher->deleteLater(); - emit mailFetched(accountId, folderId, items); + emit mailFetched(accountId, folderId, persisted); }); watcher->setFuture(future); } +bool MailService::persistFetchedItem(MailItem &item, const QString &accountId, + const QString &folderId) +{ + if (item.rawMime().isEmpty()) return MailItemDao::upsert(item); + + MimeStorageService storage; + ParsedMimeMessage parsed; + QVector paths; + if (!storage.parseMessage(item.rawMime(), parsed) + || !storage.storeMessage(accountId, folderId, item, item.rawMime(), &paths)) { + qWarning() << "Failed to persist MIME message" << item.messageId(); + return false; + } + if (!MailItemDao::upsert(item)) { + storage.deleteEmlFile(item.fileId()); + return false; + } + + QVector records; + for (int i = 0; i < parsed.attachments.size(); ++i) { + const ParsedMimeAttachment &source = parsed.attachments.at(i); + StoredAttachmentRecord record; + record.fileName = source.fileName; + record.mimeType = source.mimeType; + record.contentId = source.contentId; + record.size = source.data.size(); + if (i < paths.size()) record.storedPath = paths.at(i); + records.append(record); + } + if (!MailItemDao::replaceAttachments(item.id(), records)) { + storage.deleteEmlFile(item.fileId()); + return false; + } + return true; +} + void MailService::moveMail(const QString &mailItemId, const QString &targetFolderId) { bool ok; @@ -109,8 +481,14 @@ void MailService::deleteMail(const QString &mailItemId) bool ok; qint64 id = mailItemId.toLongLong(&ok); if (!ok) return; - if (MailItemDao::remove(id)) + const std::optional item = MailItemDao::findById(id); + if (MailItemDao::remove(id)) { + if (item && !item->fileId().isEmpty()) { + MimeStorageService storage; + storage.deleteEmlFile(item->fileId()); + } emit mailDeleted(mailItemId); + } } void MailService::markAsRead(const QString &mailItemId, bool read) @@ -126,4 +504,4 @@ void MailService::markAsRead(const QString &mailItemId, bool read) emit mailReadStateChanged(mailItemId, read); } -#include "mailservice.moc" \ No newline at end of file +#include "mailservice.moc" diff --git a/src/services/mailservice.cpp.bak b/src/services/mailservice.cpp.bak new file mode 100644 index 0000000..7a87f06 --- /dev/null +++ b/src/services/mailservice.cpp.bak @@ -0,0 +1,129 @@ +#include "mailservice.h" +#include +#include +#include +#include + +MailService::MailService(AccountService *accountService, QObject *parent) + : QObject(parent) + , m_composer(new EmailComposerBridge(this)) + , m_accountService(accountService) +{ +} + +QVector MailService::getMails(const QString &folderId) +{ + return MailItemDao::findByFolderId(folderId.toInt()); +} + +void MailService::sendMail(const MailItem &mail, const QString &accountId) +{ + // 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) +{ + // 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; + } + + + // Initialize synchronizer with account details before using it + if (!sync->initialize(*acc)) { + emit mailFetchError(accountId, folderId, tr("Failed to initialize synchronizer")); + sync->deleteLater(); + 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> future = QtConcurrent::run([=]() { + // SinceUid = 0 for full sync; could be stored per folder but omitted for simplicity + return sync->fetchMailItems(folderId, 0); + }); + + QFutureWatcher> *watcher = new QFutureWatcher>(this); + QObject::connect(watcher, &QFutureWatcher>::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 items = watcher->result(); + watcher->deleteLater(); + emit mailFetched(accountId, folderId, items); + }); + watcher->setFuture(future); +} + +void MailService::moveMail(const QString &mailItemId, const QString &targetFolderId) +{ + bool ok; + qint64 id = mailItemId.toLongLong(&ok); + if (!ok) return; + std::optional 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) +{ + 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) +{ + bool ok; + qint64 id = mailItemId.toLongLong(&ok); + if (!ok) return; + std::optional opt = MailItemDao::findById(id); + if (!opt) return; + MailItem item = *opt; + item.setRead(read); + if (MailItemDao::update(item)) + emit mailReadStateChanged(mailItemId, read); +} + +#include "mailservice.moc" \ No newline at end of file diff --git a/src/services/mailservice.h b/src/services/mailservice.h index 66874e8..ae4ab5f 100644 --- a/src/services/mailservice.h +++ b/src/services/mailservice.h @@ -22,6 +22,8 @@ public: QVector getMails(const QString &folderId); void sendMail(const MailItem &mail, const QString &accountId); + void sendMail(const MailItem &mail, const QString &accountId, + const QStringList &attachmentPaths); void fetchMails(const QString &accountId, const QString &folderId); void moveMail(const QString &mailItemId, const QString &targetFolderId); void deleteMail(const QString &mailItemId); @@ -40,8 +42,10 @@ signals: void statusMessage(const QString &message); private: + bool persistFetchedItem(MailItem &item, const QString &accountId, + const QString &folderId); EmailComposerBridge *m_composer; AccountService *m_accountService; }; -#endif // MAILSERVICE_H \ No newline at end of file +#endif // MAILSERVICE_H diff --git a/src/services/mimestorage.cpp b/src/services/mimestorage.cpp index 7469702..99d9d3f 100644 --- a/src/services/mimestorage.cpp +++ b/src/services/mimestorage.cpp @@ -2,6 +2,14 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include MimeStorageService::MimeStorageService(QObject *parent) : QObject(parent) @@ -23,53 +31,99 @@ QString MimeStorageService::ensureDirectory(const QString &path) const return dir.absolutePath(); } +QString MimeStorageService::safeComponent(const QString &value) const +{ + QString result = value; + result.replace(QRegularExpression(QStringLiteral("[^A-Za-z0-9._-]")), QStringLiteral("_")); + if (result.isEmpty() || result == QStringLiteral(".") || result == QStringLiteral("..")) + result = QStringLiteral("unknown"); + return result.left(180); +} + QString MimeStorageService::saveEmlFile(const QString &accountId, const QString &folderId, const MailItem &mail) { - QString dir = storagePath() + "/" + accountId + "/" + folderId; - ensureDirectory(dir); + QByteArray raw; + raw.append("From: ").append(mail.sender().toUtf8()).append("\r\n"); + raw.append("To: ").append(mail.recipient().toUtf8()).append("\r\n"); + if (!mail.cc().isEmpty()) raw.append("Cc: ").append(mail.cc().toUtf8()).append("\r\n"); + if (!mail.bcc().isEmpty()) raw.append("Bcc: ").append(mail.bcc().toUtf8()).append("\r\n"); + raw.append("Subject: ").append(mail.subject().toUtf8()).append("\r\n"); + raw.append("Date: ").append(mail.date().toString(Qt::RFC2822Date).toUtf8()).append("\r\n"); + raw.append("MIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n"); + raw.append(mail.bodyHtml().toUtf8()).append("\r\n"); + return saveRawEmlFile(accountId, folderId, raw, mail.messageId()); +} - QString fileName = mail.messageId(); - if (fileName.isEmpty()) { - fileName = QUuid::createUuid().toString(QUuid::WithoutBraces); - } - fileName.replace('/', '_').replace('\\', '_'); - QString filePath = dir + "/" + fileName + ".eml"; +QString MimeStorageService::saveRawEmlFile(const QString &accountId, const QString &folderId, + const QByteArray &rawMime, const QString &stableId) +{ + if (rawMime.isEmpty()) return QString(); - // Write a basic .eml structure - QFile file(filePath); - if (file.open(QIODevice::WriteOnly)) { - QTextStream stream(&file); - stream << "From: " << mail.sender() << "\r\n"; - stream << "To: " << mail.recipient() << "\r\n"; - stream << "Subject: " << mail.subject() << "\r\n"; - stream << "Date: " << mail.date().toString(Qt::RFC2822Date) << "\r\n"; - stream << "MIME-Version: 1.0\r\n"; - stream << "Content-Type: text/html; charset=UTF-8\r\n"; - stream << "\r\n"; - stream << mail.bodyHtml() << "\r\n"; - file.close(); - qDebug() << "[MimeStorage] Saved .eml:" << filePath; - return fileName; + const QString accountPart = safeComponent(accountId); + const QString folderPart = safeComponent(folderId); + const QByteArray digest = QCryptographicHash::hash(rawMime, QCryptographicHash::Sha256).toHex(); + const QString fileName = safeComponent(stableId.isEmpty() + ? QString::fromLatin1(digest) + : stableId) + QStringLiteral("_") + QString::fromLatin1(digest.left(12)) + QStringLiteral(".eml"); + const QString relative = accountPart + "/" + folderPart + "/" + fileName; + const QString path = QDir(storagePath()).filePath(relative); + ensureDirectory(QFileInfo(path).absolutePath()); + + QSaveFile file(path); + if (!file.open(QIODevice::WriteOnly) || file.write(rawMime) != rawMime.size() || !file.commit()) { + qWarning() << "[MimeStorage] Failed to save raw MIME:" << path << file.errorString(); + return QString(); } - qWarning() << "[MimeStorage] Failed to save .eml:" << filePath; - return QString(); + return relative.left(relative.size() - 4); // fileId excludes .eml +} + +bool MimeStorageService::storeMessage(const QString &accountId, const QString &folderId, + MailItem &mail, const QByteArray &rawMime, + QVector *storedAttachmentPaths) +{ + ParsedMimeMessage parsed; + if (!parseMessage(rawMime, parsed)) return false; + + if (!parsed.messageId.isEmpty()) mail.setMessageId(parsed.messageId); + if (!parsed.subject.isEmpty()) mail.setSubject(parsed.subject); + if (!parsed.from.isEmpty()) mail.setSender(parsed.from); + if (!parsed.to.isEmpty()) { mail.setTo(parsed.to); mail.setRecipient(parsed.to); } + if (!parsed.cc.isEmpty()) mail.setCc(parsed.cc); + if (!parsed.bcc.isEmpty()) mail.setBcc(parsed.bcc); + if (parsed.date.isValid()) mail.setDate(parsed.date); + mail.setBodyHtml(parsed.bodyHtml); + mail.setSize(rawMime.size()); + mail.setRawMime(rawMime); + + const QString fileId = saveRawEmlFile(accountId, folderId, rawMime, mail.messageId()); + if (fileId.isEmpty()) return false; + mail.setFileId(fileId); + + QVector names; + QVector savedPaths; + for (const ParsedMimeAttachment &attachment : parsed.attachments) { + const QString path = saveAttachment(fileId, attachment.fileName, attachment.data); + if (path.isEmpty()) { + for (const QString &savedPath : savedPaths) QFile::remove(savedPath); + deleteEmlFile(fileId); + return false; + } + savedPaths.append(path); + names.append(attachment.fileName); + if (storedAttachmentPaths) storedAttachmentPaths->append(path); + } + mail.setAttachments(names); + return true; } QString MimeStorageService::getEmlFilePath(const QString &fileId) const { - // Search in all account/folder directories (simplified) - QDir baseDir(storagePath()); - QStringList filters; - filters << fileId + ".eml"; - - QFileInfoList files = baseDir.entryInfoList(filters, QDir::Files, QDir::Name); - for (const auto &info : files) { - QStringList parts = info.absoluteFilePath().split('/'); - if (parts.size() >= 2) { - return info.absoluteFilePath(); - } - } - return QString(); + QString relative = fileId; + if (relative.endsWith(QStringLiteral(".eml"))) relative.chop(4); + const QString base = QDir::cleanPath(storagePath()); + const QString path = QDir::cleanPath(QDir(base).filePath(relative + QStringLiteral(".eml"))); + if (!path.startsWith(base + QDir::separator()) || !QFileInfo::exists(path)) return QString(); + return path; } QByteArray MimeStorageService::readEmlFile(const QString &fileId) const @@ -88,29 +142,218 @@ bool MimeStorageService::deleteEmlFile(const QString &fileId) { QString path = getEmlFilePath(fileId); if (path.isEmpty()) return false; - return QFile::remove(path); + const bool removed = QFile::remove(path); + QDir attachments(QFileInfo(path).absolutePath() + QStringLiteral("/attachments")); + if (attachments.exists()) attachments.removeRecursively(); + return removed; } QStringList MimeStorageService::listAttachments(const QString &mailItemId) const { - Q_UNUSED(mailItemId); - // TODO: Parse .eml to get attachments when GMime is available - return {}; + const QString emlPath = getEmlFilePath(mailItemId); + if (emlPath.isEmpty()) return {}; + QDir dir(QFileInfo(emlPath).absolutePath() + QStringLiteral("/attachments")); + return dir.entryList(QDir::Files, QDir::Name); } -bool MimeStorageService::saveAttachment(const QString &mailItemId, const QString &fileName, const QByteArray &data) +QString MimeStorageService::saveAttachment(const QString &fileId, const QString &fileName, const QByteArray &data) { - Q_UNUSED(mailItemId); - QString dir = storagePath() + "/attachments"; - ensureDirectory(dir); - - QFile file(dir + "/" + fileName); - if (file.open(QIODevice::WriteOnly)) { - file.write(data); - file.close(); - return true; + const QString emlPath = getEmlFilePath(fileId); + if (emlPath.isEmpty()) return QString(); + const QString dir = ensureDirectory(QFileInfo(emlPath).absolutePath() + QStringLiteral("/attachments")); + const QString originalName = QFileInfo(fileName).fileName(); + const QString safeName = safeComponent(originalName.isEmpty() + ? QStringLiteral("attachment") : originalName); + QString path = QDir(dir).filePath(safeName); + if (QFileInfo::exists(path)) { + const QFileInfo info(path); + const QString suffix = info.completeSuffix().isEmpty() + ? QString() : QStringLiteral(".") + info.completeSuffix(); + const QString stem = info.completeBaseName(); + path = QDir(dir).filePath(stem + QStringLiteral("_") + + QString::fromLatin1(QCryptographicHash::hash(data, QCryptographicHash::Sha256).toHex().left(12)) + + suffix); } - return false; + QSaveFile file(path); + if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.commit()) return QString(); + return path; +} + +QString MimeStorageService::decodeHeaderValue(const QString &value) const +{ + QString result = value; + const QRegularExpression rx(QStringLiteral("=\\?([^?]+)\\?([bBqQ])\\?([^?]+)\\?=")); + QRegularExpressionMatchIterator it = rx.globalMatch(result); + while (it.hasNext()) { + const QRegularExpressionMatch match = it.next(); + QByteArray encoded = match.captured(3).toLatin1(); + QByteArray decoded; + if (match.captured(2).compare(QStringLiteral("b"), Qt::CaseInsensitive) == 0) + decoded = QByteArray::fromBase64(encoded); + else { + encoded.replace('_', ' '); + QByteArray out; + for (int i = 0; i < encoded.size(); ++i) { + if (encoded[i] == '=' && i + 2 < encoded.size()) { + bool ok = false; + const int value = QByteArray(encoded.mid(i + 1, 2)).toInt(&ok, 16); + if (ok) { out.append(char(value)); i += 2; continue; } + } + out.append(encoded[i]); + } + decoded = out; + } + QString decodedText = QString::fromUtf8(decoded); + if (decodedText.isEmpty() && !decoded.isEmpty()) decodedText = QString::fromLatin1(decoded); + result.replace(match.captured(0), decodedText); + } + return result.trimmed(); +} + +QByteArray MimeStorageService::decodeTransfer(const QByteArray &data, const QString &encoding) const +{ + const QString normalized = encoding.trimmed().toLower(); + if (normalized == QStringLiteral("base64")) return QByteArray::fromBase64(data); + if (normalized != QStringLiteral("quoted-printable")) return data; + + QByteArray result; + for (int i = 0; i < data.size(); ++i) { + if (data[i] == '=' && i + 1 < data.size()) { + if (data[i + 1] == '\r' && i + 2 < data.size() && data[i + 2] == '\n') { i += 2; continue; } + if (data[i + 1] == '\n') { ++i; continue; } + if (i + 2 < data.size()) { + bool ok = false; + const int value = QByteArray(data.mid(i + 1, 2)).toInt(&ok, 16); + if (ok) { result.append(char(value)); i += 2; continue; } + } + } + result.append(data[i]); + } + return result; +} + +void MimeStorageService::parseMimePart(const QByteArray &part, ParsedMimeMessage &message, + QString *plainText, QString *htmlText) const +{ + const int crlf = part.indexOf("\r\n\r\n"); + const int lf = part.indexOf("\n\n"); + int split = crlf >= 0 ? crlf : lf; + const int separatorSize = crlf >= 0 ? 4 : 2; + const QByteArray headerBytes = split >= 0 ? part.left(split) : part; + const QByteArray body = split >= 0 ? part.mid(split + separatorSize) : QByteArray(); + + QMap headers; + QString current; + for (QString line : QString::fromLatin1(headerBytes).split(QRegularExpression("\\r?\\n"))) { + if ((line.startsWith(' ') || line.startsWith('\t')) && !current.isEmpty()) { + headers[current] += QStringLiteral(" ") + line.trimmed(); + continue; + } + const int colon = line.indexOf(':'); + if (colon < 0) continue; + current = line.left(colon).trimmed().toLower(); + headers[current] = line.mid(colon + 1).trimmed(); + } + auto header = [&](const QString &name) { return headers.value(name.toLower()); }; + const QString contentType = header(QStringLiteral("content-type")); + const QString disposition = header(QStringLiteral("content-disposition")); + const QString transfer = header(QStringLiteral("content-transfer-encoding")); + + QRegularExpression boundaryRx(QStringLiteral("(?:^|;)\\s*boundary\\s*=\\s*(?:\\\"([^\\\"]+)\\\"|([^;\\s]+))"), QRegularExpression::CaseInsensitiveOption); + const QRegularExpressionMatch boundaryMatch = boundaryRx.match(contentType); + if (contentType.startsWith(QStringLiteral("multipart/"), Qt::CaseInsensitive) && boundaryMatch.hasMatch()) { + const QString boundary = boundaryMatch.captured(1).isEmpty() ? boundaryMatch.captured(2) : boundaryMatch.captured(1); + const QByteArray marker = QByteArrayLiteral("--") + boundary.toUtf8(); + int pos = 0; + while ((pos = body.indexOf(marker, pos)) >= 0) { + int start = pos + marker.size(); + if (body.mid(start, 2) == QByteArrayLiteral("--")) break; + if (body.mid(start, 2) == QByteArrayLiteral("\r\n")) start += 2; + else if (body.mid(start, 1) == QByteArrayLiteral("\n")) ++start; + int end = body.indexOf(marker, start); + if (end < 0) end = body.size(); + QByteArray child = body.mid(start, end - start); + while (child.endsWith("\r\n") || child.endsWith('\n')) child.chop(child.endsWith("\r\n") ? 2 : 1); + parseMimePart(child, message, plainText, htmlText); + pos = end; + } + return; + } + + const QByteArray decoded = decodeTransfer(body, transfer); + QRegularExpression parameterRx(QStringLiteral("(?:^|;)\\s*(?:filename|name)(?:\\*\\d+)?\\*?\\s*=\\s*(?:\\\"([^\\\"]+)\\\"|([^;\\s]+))"), QRegularExpression::CaseInsensitiveOption); + const QRegularExpressionMatch dispositionMatch = parameterRx.match(disposition); + const QRegularExpressionMatch typeMatch = parameterRx.match(contentType); + QString fileName = dispositionMatch.hasMatch() + ? (dispositionMatch.captured(1).isEmpty() ? dispositionMatch.captured(2) : dispositionMatch.captured(1)) + : (typeMatch.hasMatch() ? (typeMatch.captured(1).isEmpty() ? typeMatch.captured(2) : typeMatch.captured(1)) : QString()); + const int encodedPrefix = fileName.indexOf(QStringLiteral("''")); + if (encodedPrefix >= 0) fileName = fileName.mid(encodedPrefix + 2); + fileName = QUrl::fromPercentEncoding(fileName.toUtf8()); + fileName = decodeHeaderValue(fileName); + const QString mimeType = contentType.section(';', 0, 0).trimmed().toLower(); + const bool isAttachment = disposition.startsWith(QStringLiteral("attachment"), Qt::CaseInsensitive) + || !fileName.isEmpty() || (!mimeType.startsWith(QStringLiteral("text/")) && !mimeType.isEmpty()); + + if (isAttachment) { + ParsedMimeAttachment attachment; + attachment.fileName = fileName.isEmpty() ? QStringLiteral("attachment-%1").arg(message.attachments.size() + 1) : fileName; + attachment.mimeType = mimeType; + attachment.contentId = header(QStringLiteral("content-id")).trimmed(); + attachment.contentId.remove('<'); + attachment.contentId.remove('>'); + attachment.data = decoded; + message.attachments.append(attachment); + return; + } + + const QString text = QString::fromUtf8(decoded); + if (mimeType == QStringLiteral("text/html")) { + if (htmlText && htmlText->isEmpty()) *htmlText = text; + } else if (mimeType == QStringLiteral("text/plain") || mimeType.isEmpty()) { + if (plainText && plainText->isEmpty()) *plainText = text; + } +} + +bool MimeStorageService::parseMessage(const QByteArray &rawMime, ParsedMimeMessage &message) const +{ + if (rawMime.isEmpty()) return false; + message = ParsedMimeMessage(); + QString plainText; + QString htmlText; + parseMimePart(rawMime, message, &plainText, &htmlText); + + auto parseHeaders = [&](const QByteArray &data) { + const int split = data.indexOf("\r\n\r\n") >= 0 ? data.indexOf("\r\n\r\n") : data.indexOf("\n\n"); + const QByteArray headerData = split >= 0 ? data.left(split) : data; + QMap headers; + QString current; + for (QString line : QString::fromLatin1(headerData).split(QRegularExpression("\\r?\\n"))) { + if ((line.startsWith(' ') || line.startsWith('\t')) && !current.isEmpty()) { headers[current] += " " + line.trimmed(); continue; } + const int colon = line.indexOf(':'); + if (colon < 0) continue; + current = line.left(colon).trimmed().toLower(); + headers[current] = line.mid(colon + 1).trimmed(); + } + return headers; + }; + const QMap headers = parseHeaders(rawMime); + message.messageId = decodeHeaderValue(headers.value(QStringLiteral("message-id"))).remove('<').remove('>'); + message.subject = decodeHeaderValue(headers.value(QStringLiteral("subject"))); + message.from = decodeHeaderValue(headers.value(QStringLiteral("from"))); + message.to = decodeHeaderValue(headers.value(QStringLiteral("to"))); + message.cc = decodeHeaderValue(headers.value(QStringLiteral("cc"))); + message.bcc = decodeHeaderValue(headers.value(QStringLiteral("bcc"))); + message.date = QDateTime::fromString(headers.value(QStringLiteral("date")), Qt::RFC2822Date); + if (!message.date.isValid()) message.date = QDateTime::currentDateTimeUtc(); + if (!htmlText.isEmpty()) message.bodyHtml = htmlText; + else { + QString escaped = plainText.toHtmlEscaped(); + escaped.replace(QStringLiteral("\r\n"), QStringLiteral("
")); + escaped.replace(QStringLiteral("\n"), QStringLiteral("
")); + message.bodyHtml = escaped; + } + return true; } #include "mimestorage.moc" diff --git a/src/services/mimestorage.h b/src/services/mimestorage.h index bc1d068..48ba964 100644 --- a/src/services/mimestorage.h +++ b/src/services/mimestorage.h @@ -5,8 +5,31 @@ #include #include #include +#include +#include #include "core/mailitem.h" +struct ParsedMimeAttachment +{ + QString fileName; + QString mimeType; + QString contentId; + QByteArray data; +}; + +struct ParsedMimeMessage +{ + QString messageId; + QString subject; + QString from; + QString to; + QString cc; + QString bcc; + QDateTime date; + QString bodyHtml; + QVector attachments; +}; + class MimeStorageService : public QObject { Q_OBJECT @@ -15,16 +38,28 @@ public: ~MimeStorageService() override = default; QString saveEmlFile(const QString &accountId, const QString &folderId, const MailItem &mail); + QString saveRawEmlFile(const QString &accountId, const QString &folderId, + const QByteArray &rawMime, const QString &stableId = QString()); + bool storeMessage(const QString &accountId, const QString &folderId, + MailItem &mail, const QByteArray &rawMime, + QVector *storedAttachmentPaths = nullptr); + bool parseMessage(const QByteArray &rawMime, ParsedMimeMessage &message) const; QString getEmlFilePath(const QString &fileId) const; QByteArray readEmlFile(const QString &fileId) const; bool deleteEmlFile(const QString &fileId); QStringList listAttachments(const QString &mailItemId) const; - bool saveAttachment(const QString &mailItemId, const QString &fileName, const QByteArray &data); + QString saveAttachment(const QString &fileId, const QString &fileName, + const QByteArray &data); private: QString storagePath() const; QString ensureDirectory(const QString &path) const; + QString safeComponent(const QString &value) const; + QString decodeHeaderValue(const QString &value) const; + QByteArray decodeTransfer(const QByteArray &data, const QString &encoding) const; + void parseMimePart(const QByteArray &part, ParsedMimeMessage &message, + QString *plainText, QString *htmlText) const; }; #endif // MIMESTORAGESERVICE_H diff --git a/src/services/moc_accountservice.cpp b/src/services/moc_accountservice.cpp new file mode 100644 index 0000000..3b10607 --- /dev/null +++ b/src/services/moc_accountservice.cpp @@ -0,0 +1,165 @@ +/**************************************************************************** +** Meta object code from reading C++ file 'accountservice.h' +** +** Created by: The Qt Meta Object Compiler version 69 (Qt 6.10.2) +** +** WARNING! All changes made in this file will be lost! +*****************************************************************************/ + +#include "accountservice.h" +#include + +#include + +#include + + +#include +#if !defined(Q_MOC_OUTPUT_REVISION) +#error "The header file 'accountservice.h' doesn't include ." +#elif Q_MOC_OUTPUT_REVISION != 69 +#error "This file was generated using the moc from 6.10.2. It" +#error "cannot be used with the include files from this version of Qt." +#error "(The moc has changed too much.)" +#endif + +#ifndef Q_CONSTINIT +#define Q_CONSTINIT +#endif + +QT_WARNING_PUSH +QT_WARNING_DISABLE_DEPRECATED +QT_WARNING_DISABLE_GCC("-Wuseless-cast") +namespace { +struct qt_meta_tag_ZN14AccountServiceE_t {}; +} // unnamed namespace + +template <> constexpr inline auto AccountService::qt_create_metaobjectdata() +{ + namespace QMC = QtMocConstants; + QtMocHelpers::StringRefStorage qt_stringData { + "AccountService", + "accountListChanged", + "", + "accountAdded", + "Account", + "account", + "accountRemoved", + "accountId", + "authenticationRequired", + "email", + "authUrl" + }; + + QtMocHelpers::UintData qt_methods { + // Signal 'accountListChanged' + QtMocHelpers::SignalData(1, 2, QMC::AccessPublic, QMetaType::Void), + // Signal 'accountAdded' + QtMocHelpers::SignalData(3, 2, QMC::AccessPublic, QMetaType::Void, {{ + { 0x80000000 | 4, 5 }, + }}), + // Signal 'accountRemoved' + QtMocHelpers::SignalData(6, 2, QMC::AccessPublic, QMetaType::Void, {{ + { QMetaType::Int, 7 }, + }}), + // Signal 'authenticationRequired' + QtMocHelpers::SignalData(8, 2, QMC::AccessPublic, QMetaType::Void, {{ + { QMetaType::QString, 9 }, { QMetaType::QString, 10 }, + }}), + }; + QtMocHelpers::UintData qt_properties { + }; + QtMocHelpers::UintData qt_enums { + }; + return QtMocHelpers::metaObjectData(QMC::MetaObjectFlag{}, qt_stringData, + qt_methods, qt_properties, qt_enums); +} +Q_CONSTINIT const QMetaObject AccountService::staticMetaObject = { { + QMetaObject::SuperData::link(), + qt_staticMetaObjectStaticContent.stringdata, + qt_staticMetaObjectStaticContent.data, + qt_static_metacall, + nullptr, + qt_staticMetaObjectRelocatingContent.metaTypes, + nullptr +} }; + +void AccountService::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a) +{ + auto *_t = static_cast(_o); + if (_c == QMetaObject::InvokeMetaMethod) { + switch (_id) { + case 0: _t->accountListChanged(); break; + case 1: _t->accountAdded((*reinterpret_cast>(_a[1]))); break; + case 2: _t->accountRemoved((*reinterpret_cast>(_a[1]))); break; + case 3: _t->authenticationRequired((*reinterpret_cast>(_a[1])),(*reinterpret_cast>(_a[2]))); break; + default: ; + } + } + if (_c == QMetaObject::IndexOfMethod) { + if (QtMocHelpers::indexOfMethod(_a, &AccountService::accountListChanged, 0)) + return; + if (QtMocHelpers::indexOfMethod(_a, &AccountService::accountAdded, 1)) + return; + if (QtMocHelpers::indexOfMethod(_a, &AccountService::accountRemoved, 2)) + return; + if (QtMocHelpers::indexOfMethod(_a, &AccountService::authenticationRequired, 3)) + return; + } +} + +const QMetaObject *AccountService::metaObject() const +{ + return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject; +} + +void *AccountService::qt_metacast(const char *_clname) +{ + if (!_clname) return nullptr; + if (!strcmp(_clname, qt_staticMetaObjectStaticContent.strings)) + return static_cast(this); + return QObject::qt_metacast(_clname); +} + +int AccountService::qt_metacall(QMetaObject::Call _c, int _id, void **_a) +{ + _id = QObject::qt_metacall(_c, _id, _a); + if (_id < 0) + return _id; + if (_c == QMetaObject::InvokeMetaMethod) { + if (_id < 4) + qt_static_metacall(this, _c, _id, _a); + _id -= 4; + } + if (_c == QMetaObject::RegisterMethodArgumentMetaType) { + if (_id < 4) + *reinterpret_cast(_a[0]) = QMetaType(); + _id -= 4; + } + return _id; +} + +// SIGNAL 0 +void AccountService::accountListChanged() +{ + QMetaObject::activate(this, &staticMetaObject, 0, nullptr); +} + +// SIGNAL 1 +void AccountService::accountAdded(const Account & _t1) +{ + QMetaObject::activate(this, &staticMetaObject, 1, nullptr, _t1); +} + +// SIGNAL 2 +void AccountService::accountRemoved(int _t1) +{ + QMetaObject::activate(this, &staticMetaObject, 2, nullptr, _t1); +} + +// SIGNAL 3 +void AccountService::authenticationRequired(const QString & _t1, const QString & _t2) +{ + QMetaObject::activate(this, &staticMetaObject, 3, nullptr, _t1, _t2); +} +QT_WARNING_POP diff --git a/src/services/outlook/outlooksynchronizer.cpp b/src/services/outlook/outlooksynchronizer.cpp index 0fc44ad..97cd5d7 100644 --- a/src/services/outlook/outlooksynchronizer.cpp +++ b/src/services/outlook/outlooksynchronizer.cpp @@ -13,6 +13,8 @@ #include #include "../../core/events.h" #include "../../core/eventbus.h" +#include "db/dao/folderdao.h" +#include "services/mimestorage.h" OutlookSynchronizer::OutlookSynchronizer(QObject* parent) : Synchronizer(parent), @@ -84,7 +86,6 @@ bool OutlookSynchronizer::syncFolder(const Folder& folder) // En una implementaciรณn real, aquรญ compararรญamos con la base de datos local // y emitirรญamos las seรฑales apropiadas para elementos nuevos/actualizados/eliminados - // Por ahora, simulamos que obtenemos algunos elementos if (!items.isEmpty()) { for (const MailItem& item : items) { emit mailItemAdded(item); @@ -139,128 +140,123 @@ QVector OutlookSynchronizer::fetchMailItems(const QString& folderId, } } - qDebug() << "Fetching mail items for folder:" << folderId; - - // Construir la URL para Microsoft Graph API - QString endpoint = QString("/me/mailFolders/%1/messages").arg(folderId); - QString url = buildGraphUrl(endpoint); - - // Parรกmetros de consulta + qDebug() << "Fetching complete Outlook messages for folder:" << folderId; + + QString remoteFolderId = folderId; + bool localFolderId = false; + folderId.toInt(&localFolderId); + if (localFolderId) { + const auto folder = FolderDao::findById(folderId.toInt()); + if (folder) { + remoteFolderId = folder->parentFolderId(); + if (remoteFolderId.isEmpty()) { + const QString name = folder->name().toLower(); + if (name == QStringLiteral("inbox")) remoteFolderId = QStringLiteral("inbox"); + else if (name == QStringLiteral("sent") || name == QStringLiteral("sent items")) remoteFolderId = QStringLiteral("sentitems"); + else if (name == QStringLiteral("drafts")) remoteFolderId = QStringLiteral("drafts"); + else if (name == QStringLiteral("trash") || name == QStringLiteral("deleted items")) remoteFolderId = QStringLiteral("deleteditems"); + else remoteFolderId = folder->name(); + } + } + } + + QNetworkAccessManager network; + auto get = [&](const QUrl &url, QByteArray &data) { + QNetworkRequest request = createAuthRequest(url.toString()); + request.setRawHeader("Accept", "message/rfc822, application/json"); + QNetworkReply *reply = network.get(request); + QEventLoop loop; + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + const bool ok = reply->error() == QNetworkReply::NoError; + if (ok) data = reply->readAll(); + else qWarning() << "Outlook request failed:" << reply->errorString() << url; + reply->deleteLater(); + return ok; + }; + + QVector messageRefs; + QUrl nextUrl(buildGraphUrl(QStringLiteral("/me/mailFolders/%1/messages").arg(remoteFolderId))); QUrlQuery query; - query.addQueryItem("$top", "50"); // Limitar a 50 mensajes por peticiรณn - query.addQueryItem("$orderby", "receivedDateTime DESC"); - url += "?" + query.toString(); - - QNetworkRequest request = createAuthRequest(url); - QNetworkReply* reply = m_networkManager->get(request); - - // Nota: En una implementaciรณn real, esperarรญamos la respuesta asรญncronamente - // pero por simplicidad en este stub, simulamos una respuesta - - // Simular respuesta para desarrollo + query.addQueryItem(QStringLiteral("$top"), QStringLiteral("100")); + query.addQueryItem(QStringLiteral("$select"), QStringLiteral("id,isRead,importance")); + query.addQueryItem(QStringLiteral("$orderby"), QStringLiteral("receivedDateTime DESC")); + nextUrl.setQuery(query); + while (nextUrl.isValid() && !nextUrl.toString().isEmpty()) { + QByteArray response; + if (!get(nextUrl, response)) return {}; + QJsonParseError parseError; + const QJsonDocument document = QJsonDocument::fromJson(response, &parseError); + if (parseError.error != QJsonParseError::NoError || !document.isObject()) return {}; + const QJsonObject object = document.object(); + for (const QJsonValue &value : object.value(QStringLiteral("value")).toArray()) + if (value.isObject()) messageRefs.append(value.toObject()); + const QString continuation = object.value(QStringLiteral("@odata.nextLink")).toString(); + nextUrl = continuation.isEmpty() ? QUrl() : QUrl(continuation); + } + QVector items; - items.append(MailItem(1, folderId.toInt(), "Reuniรณn de proyecto", - "juan.perez@empresa.com", m_account.email(), - QDateTime::currentDateTime().addSecs(-3600), - false, false)); - items.append(MailItem(2, folderId.toInt(), "Entrega de documentaciรณn", - "ana.gomez@cliente.com", m_account.email(), - QDateTime::currentDateTime().addSecs(-7200), - true, false)); - + MimeStorageService mimeStorage; + for (const QJsonObject &reference : messageRefs) { + const QString messageId = reference.value(QStringLiteral("id")).toString(); + if (messageId.isEmpty()) continue; + const QString encodedId = QString::fromUtf8(QUrl::toPercentEncoding(messageId)); + const QUrl rawUrl(buildGraphUrl(QStringLiteral("/me/messages/%1/$value").arg(encodedId))); + QByteArray rawMime; + if (!get(rawUrl, rawMime) || rawMime.isEmpty()) continue; + ParsedMimeMessage parsed; + if (!mimeStorage.parseMessage(rawMime, parsed)) continue; + + MailItem item; + item.setFolderId(folderId.toInt()); + item.setMessageId(messageId); + item.setRawMime(rawMime); + item.setSubject(parsed.subject.isEmpty() ? QStringLiteral("(No Subject)") : parsed.subject); + item.setSender(parsed.from); + item.setRecipient(parsed.to); + item.setTo(parsed.to); + item.setCc(parsed.cc); + item.setBcc(parsed.bcc); + item.setDate(parsed.date); + item.setBodyHtml(parsed.bodyHtml); + item.setSize(rawMime.size()); + item.setRead(reference.value(QStringLiteral("isRead")).toBool(true)); + item.setFlagged(reference.value(QStringLiteral("importance")).toString() == QStringLiteral("high")); + QVector attachmentNames; + for (const ParsedMimeAttachment &attachment : parsed.attachments) attachmentNames.append(attachment.fileName); + item.setAttachments(attachmentNames); + items.append(item); + } return items; } bool OutlookSynchronizer::appendMailItem(const QString& folderId, const MailItem& item) { - if (!m_account.isTokenValid()) { - if (!refreshAccessToken()) { - qWarning() << "Failed to refresh token for appending mail item"; - return false; - } - } - - qDebug() << "Appending mail item to folder:" << folderId; - - // En una implementaciรณn real, llamarรญamos a Microsoft Graph API - // para crear un mensaje en la carpeta especificada - - // Por ahora, simulamos รฉxito - emit mailItemAdded(item); - - // Publish MailItemAddedEvent - WinoMail::Events::MailItemAddedEvent mailEvent; - mailEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand()); - mailEvent.timestamp = QDateTime::currentDateTimeUtc(); - mailEvent.item = item; - PUBLISH(mailEvent); - - return true; + Q_UNUSED(folderId); + Q_UNUSED(item); + qWarning() << "Outlook appendMailItem is not used for sending; MailService sends via Graph API"; + return false; } bool OutlookSynchronizer::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged) { - if (!m_account.isTokenValid()) { - if (!refreshAccessToken()) { - qWarning() << "Failed to refresh token for updating mail item flags"; - return false; - } - } - - qDebug() << "Updating mail item flags:" << itemUid - << "read:" << read << "flagged:" << flagged; - - // En una implementaciรณn real, llamarรญamos a Microsoft Graph API - // para actualizar las flags del mensaje - - // Por ahora, simulamos รฉxito - MailItem updatedItem; - updatedItem.setId(itemUid.toLongLong()); - updatedItem.setFolderId(folderId.toInt()); - updatedItem.setRead(read); - updatedItem.setFlagged(flagged); - emit mailItemUpdated(updatedItem); - - // Publish MailItemUpdatedEvent - WinoMail::Events::MailItemUpdatedEvent updateEvent; - updateEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand()); - updateEvent.timestamp = QDateTime::currentDateTimeUtc(); - updateEvent.item = updatedItem; - updateEvent.changedFields = QStringList() << "read" << "flagged"; // Simplified - PUBLISH(updateEvent); - - return true; + Q_UNUSED(folderId); + Q_UNUSED(itemUid); + Q_UNUSED(read); + Q_UNUSED(flagged); + qWarning() << "Outlook flag update is not available through this synchronizer"; + return false; } bool OutlookSynchronizer::deleteMailItem(const QString& folderId, const QString& itemUid) { - if (!m_account.isTokenValid()) { - if (!refreshAccessToken()) { - qWarning() << "Failed to refresh token for deleting mail item"; - return false; - } - } - - qDebug() << "Deleting mail item:" << itemUid << "from folder:" << folderId; - - // En una implementaciรณn real, llamarรญamos a Microsoft Graph API - // para eliminar el mensaje - - // Por ahora, simulamos รฉxito - emit mailItemRemoved(itemUid); - - // Publish MailItemRemovedEvent - WinoMail::Events::MailItemRemovedEvent removeEvent; - removeEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand()); - removeEvent.timestamp = QDateTime::currentDateTimeUtc(); - removeEvent.itemUid = itemUid; - removeEvent.folderId = folderId.toInt(); - PUBLISH(removeEvent); - - return true; + Q_UNUSED(folderId); + Q_UNUSED(itemUid); + qWarning() << "Outlook delete is not available through this synchronizer"; + return false; } void OutlookSynchronizer::onGraphReplyFinished(QNetworkReply* reply) @@ -340,9 +336,12 @@ bool OutlookSynchronizer::refreshAccessToken() return false; } - // TODO: These should come from secure configuration, not hardcoded - const QString clientId = "YOUR_CLIENT_ID_HERE"; // Replace with actual client ID - const QString clientSecret = "YOUR_CLIENT_SECRET_HERE"; // Replace with actual client secret + const QString clientId = qEnvironmentVariable("WINO_OUTLOOK_CLIENT_ID"); + const QString clientSecret = qEnvironmentVariable("WINO_OUTLOOK_CLIENT_SECRET"); + if (clientId.isEmpty() || clientSecret.isEmpty()) { + qWarning() << "Outlook token expired and WINO_OUTLOOK_CLIENT_ID/SECRET are not configured"; + return false; + } const QString tokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; QNetworkRequest networkRequest{QUrl(tokenUrl)}; @@ -355,7 +354,8 @@ bool OutlookSynchronizer::refreshAccessToken() postData.addQueryItem("grant_type", "refresh_token"); postData.addQueryItem("client_secret", clientSecret); - QNetworkReply* reply = m_networkManager->post(networkRequest, postData.toString(QUrl::FullyEncoded).toUtf8()); + QNetworkAccessManager network; + QNetworkReply* reply = network.post(networkRequest, postData.toString(QUrl::FullyEncoded).toUtf8()); // Wait for reply synchronously for simplicity in this context QEventLoop loop; diff --git a/src/services/pop3/pop3synchronizer.cpp b/src/services/pop3/pop3synchronizer.cpp new file mode 100644 index 0000000..feab90b --- /dev/null +++ b/src/services/pop3/pop3synchronizer.cpp @@ -0,0 +1,318 @@ +#include "pop3synchronizer.h" +#include +#include +#include +#include +#include +#include +#include +#include "../../core/mailitem.h" +#include "../../db/dao/mailitemdao.h" +#include "../../db/dao/folderdao.h" +#include "../../core/models/account.h" +#include "../../services/mimestorage.h" + +Pop3Synchronizer::Pop3Synchronizer(QObject *parent) + : Synchronizer(parent) + , m_socket(nullptr) +{ +} + +bool Pop3Synchronizer::initialize(const Account &account) +{ + m_account = account; + const auto &settings = account.connectionSettings(); + m_host = settings.incomingHost; + m_port = settings.incomingPort; + m_useSsl = settings.incomingSsl; + m_username = settings.username; + m_password = settings.password; + + if (m_socket) { + m_socket->deleteLater(); + m_socket = nullptr; + } + m_socket = new QSslSocket(this); + qDebug() << "POP3 synchronizer initialized for" << m_username << "at" << m_host; + return true; +} + +bool Pop3Synchronizer::syncFolder(const Folder &folder) +{ + Q_UNUSED(folder); + return true; +} + +QVector Pop3Synchronizer::getFolders() const +{ + QVector folders; + Folder inbox; + inbox.setId(0); + inbox.setAccountId(m_account.id()); + inbox.setName(QStringLiteral("Inbox")); + inbox.setParentFolderId(QString()); + folders.append(inbox); + return folders; +} + +QVector Pop3Synchronizer::fetchMailItems(const QString &folderId, qint64 sinceUid) +{ + QVector items; + QSslSocket socket; + if (m_useSsl) socket.connectToHostEncrypted(m_host, m_port); + else socket.connectToHost(m_host, m_port); + if (!socket.waitForConnected(15000)) { + qWarning() << "Failed to connect to POP3 server:" << socket.errorString(); + return items; + } + if (m_useSsl && !socket.waitForEncrypted(15000)) { + qWarning() << "TLS handshake failed:" << socket.errorString(); + return items; + } + + auto readLine = [&](QByteArray &line) { + if (!socket.canReadLine() && !socket.waitForReadyRead(15000)) return false; + line = socket.readLine(); + return !line.isEmpty(); + }; + auto command = [&](const QByteArray &commandText, QByteArray &response, bool multiline) { + socket.write(commandText + QByteArrayLiteral("\r\n")); + socket.flush(); + if (!readLine(response) || !response.trimmed().startsWith('+')) return false; + if (multiline) { + while (true) { + QByteArray line; + if (!readLine(line)) return false; + if (line == QByteArrayLiteral(".\r\n") || line == QByteArrayLiteral(".\n")) break; + if (line.startsWith("..")) line.remove(0, 1); + response.append(line); + } + } + return true; + }; + + QByteArray response; + if (!readLine(response) || !response.trimmed().startsWith('+')) return items; + if (!command(QByteArrayLiteral("USER ") + m_username.toUtf8(), response, false) + || !command(QByteArrayLiteral("PASS ") + m_password.toUtf8(), response, false)) { + qWarning() << "POP3 authentication failed:" << response; + return items; + } + if (!command(QByteArrayLiteral("UIDL"), response, true)) { + qWarning() << "UIDL failed:" << response; + return items; + } + + m_uidToMsgNum.clear(); + m_maxUid = 0; + QList uidsToFetch; + const QStringList lines = QString::fromUtf8(response).split(QRegularExpression("\\r?\\n")); + for (const QString &line : lines) { + const QStringList parts = line.trimmed().split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); + if (parts.size() < 2) continue; + bool okMsgId = false, okUid = false; + const int msgNum = parts.at(0).toInt(&okMsgId); + qint64 uid = parts.at(1).toLongLong(&okUid); + if (!okUid) { + const QByteArray digest = QCryptographicHash::hash(parts.at(1).toUtf8(), QCryptographicHash::Sha256).toHex().left(15); + uid = digest.toLongLong(&okUid, 16); + } + if (!okMsgId || !okUid || uid <= 0) continue; + m_uidToMsgNum[QString::number(uid)] = msgNum; + m_maxUid = qMax(m_maxUid, uid); + if (uid > sinceUid) uidsToFetch.append(uid); + } + + MimeStorageService mimeStorage; + int processed = 0; + for (const qint64 uid : uidsToFetch) { + const int msgNum = m_uidToMsgNum.value(QString::number(uid)); + QByteArray rawResponse; + if (!command(QByteArrayLiteral("RETR ") + QByteArray::number(msgNum), rawResponse, true)) { + qWarning() << "RETR failed for message" << msgNum; + continue; + } + const int firstLineEnd = rawResponse.indexOf("\r\n"); + const QByteArray rawMime = firstLineEnd >= 0 ? rawResponse.mid(firstLineEnd + 2) : QByteArray(); + ParsedMimeMessage parsed; + if (rawMime.isEmpty() || !mimeStorage.parseMessage(rawMime, parsed)) { + qWarning() << "Failed to parse POP3 MIME message with UID" << uid; + continue; + } + + MailItem item; + item.setFolderId(folderId.toInt()); + item.setMessageId(parsed.messageId); + item.setSubject(parsed.subject.isEmpty() ? QStringLiteral("(No Subject)") : parsed.subject); + item.setSender(parsed.from); + item.setRecipient(parsed.to); + item.setTo(parsed.to); + item.setCc(parsed.cc); + item.setBcc(parsed.bcc); + item.setDate(parsed.date); + item.setBodyHtml(parsed.bodyHtml); + item.setSize(rawMime.size()); + item.setRawMime(rawMime); + item.setUid(uid); + QVector attachmentNames; + for (const ParsedMimeAttachment &attachment : parsed.attachments) attachmentNames.append(attachment.fileName); + item.setAttachments(attachmentNames); + items.append(item); + ++processed; + emit progressChanged(uidsToFetch.isEmpty() ? 100 : static_cast((100.0 * processed) / uidsToFetch.size())); + emit statusMessage(tr("Fetched %1 of %2 messages").arg(processed).arg(uidsToFetch.size())); + } + command(QByteArrayLiteral("QUIT"), response, false); + socket.disconnectFromHost(); + return items; +} + +bool Pop3Synchronizer::appendMailItem(const QString &folderId, const MailItem &item) +{ + Q_UNUSED(folderId); + Q_UNUSED(item); + qWarning() << "POP3 appendMailItem is not a send transport; MailService owns SMTP sending"; + return false; +} + +bool Pop3Synchronizer::updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged) +{ + QVector items = MailItemDao::findByFolderId(folderId.toInt()); + for (MailItem &it : items) { + if (it.uid() == itemUid.toLongLong()) { + it.setRead(read); + it.setFlagged(flagged); + if (MailItemDao::update(it)) + return true; + } + } + return false; +} + +bool Pop3Synchronizer::deleteMailItem(const QString &folderId, const QString &itemUid) +{ + Q_UNUSED(folderId); + if (!m_socket) { + qWarning() << "POP3 socket not initialized"; + return false; + } + + if (m_socket->state() != QAbstractSocket::UnconnectedState) { + m_socket->disconnectFromHost(); + if (!m_socket->waitForDisconnected(3000)) { + qWarning() << "Failed to disconnect from previous connection"; + return false; + } + } + + if (m_useSsl) + m_socket->connectToHostEncrypted(m_host, m_port); + else + m_socket->connectToHost(m_host, m_port); + if (!m_socket->waitForConnected(5000)) { + qWarning() << "Failed to connect to POP3 server:" << m_socket->errorString(); + return false; + } + if (m_useSsl && !m_socket->waitForEncrypted(5000)) { + qWarning() << "TLS handshake failed:" << m_socket->errorString(); + m_socket->disconnectFromHost(); + return false; + } + + QString greeting; + if (!m_socket->waitForReadyRead(5000)) { + qWarning() << "No greeting from POP3 server"; + m_socket->disconnectFromHost(); + return false; + } + greeting = QString::fromUtf8(m_socket->readLine()).trimmed(); + if (!greeting.startsWith(QLatin1Char('+'))) { + qWarning() << "Invalid POP3 greeting:" << greeting; + m_socket->disconnectFromHost(); + return false; + } + + QString userCmd = QStringLiteral("USER %1\r\n").arg(m_username); + QString passCmd = QStringLiteral("PASS %1\r\n").arg(m_password); + QString resp; + if (!sendCommand(userCmd, resp) || !resp.startsWith(QLatin1Char('+'))) { + qWarning() << "USER command failed:" << resp; + m_socket->disconnectFromHost(); + return false; + } + if (!sendCommand(passCmd, resp) || !resp.startsWith(QLatin1Char('+'))) { + qWarning() << "PASS command failed:" << resp; + m_socket->disconnectFromHost(); + return false; + } + + // Get UIDL to map uid to message number + QString uidlResp; + if (!sendCommand(QStringLiteral("UIDL\r\n"), uidlResp) || !uidlResp.startsWith(QLatin1Char('+'))) { + qWarning() << "UIDL failed:" << uidlResp; + m_socket->disconnectFromHost(); + return false; + } + + int msgNum = -1; + QStringList lines = uidlResp.split(QRegularExpression("\\r?\\n")); + for (const QString &line : lines) { + if (line.startsWith(QLatin1Char('.')) || line.isEmpty()) + continue; + QString l = line; + if (l.startsWith(QLatin1Char('+'))) + l.remove(0, 1); + QStringList parts = l.split(QRegularExpression("\\s+")); + if (parts.size() < 2) + continue; + bool okUid; + qint64 uid = parts[1].toLongLong(&okUid); + if (!okUid) + continue; + if (uid == itemUid.toLongLong()) { + bool okMsgId; + msgNum = parts[0].toInt(&okMsgId); + if (!okMsgId) + msgNum = -1; + break; + } + } + + if (msgNum == -1) { + qWarning() << "Could not find message number for UID" << itemUid; + m_socket->disconnectFromHost(); + return false; + } + + QString deleCmd = QStringLiteral("DELE %1\r\n").arg(msgNum); + if (!sendCommand(deleCmd, resp) || !resp.startsWith(QLatin1Char('+'))) { + qWarning() << "DELE failed for msg" << msgNum << ":" << resp; + m_socket->disconnectFromHost(); + return false; + } + + // Deletion takes effect on QUIT + sendCommand(QStringLiteral("QUIT\r\n"), resp); + m_socket->disconnectFromHost(); + return true; +} + +QString Pop3Synchronizer::readResponse() +{ + if (!m_socket) + return QString(); + if (!m_socket->waitForReadyRead(5000)) + return QString(); + return QString::fromUtf8(m_socket->readLine()).trimmed(); +} + +bool Pop3Synchronizer::sendCommand(const QString &cmd, QString &response) +{ + if (!m_socket) + return false; + m_socket->write(cmd.toUtf8()); + if (!m_socket->waitForBytesWritten(5000)) + return false; + response = readResponse(); + return !response.isEmpty(); +} diff --git a/src/services/pop3/pop3synchronizer.cpp.backup2 b/src/services/pop3/pop3synchronizer.cpp.backup2 new file mode 100644 index 0000000..80a360b --- /dev/null +++ b/src/services/pop3/pop3synchronizer.cpp.backup2 @@ -0,0 +1,230 @@ +#include "pop3synchronizer.h" +#include +#include +#include +#include "../../core/mailitem.h" +#include "../../db/dao/mailitemdao.h" +#include "../../db/dao/folderdao.h" +#include "../../core/models/account.h" + +Pop3Synchronizer::Pop3Synchronizer(QObject *parent) + : Synchronizer(parent) +{ +} + +bool Pop3Synchronizer::initialize(const Account &account) +{ + m_account = account; + const auto &settings = account.connectionSettings(); + m_host = settings.incomingHost; + m_port = settings.incomingPort; + m_useSsl = settings.incomingSsl; + m_username = settings.username; + m_password = settings.password; + // For simplicity, we do not actually connect here; connection will be made per operation. + qDebug() << "POP3 synchronizer initialized for" << m_username << "at" << m_host; + return true; +} + +bool Pop3Synchronizer::syncFolder(const Folder &folder) +{ + Q_UNUSED(folder); + // POP3 does not have folder sync; we just return true. + return true; +} + +QVector Pop3Synchronizer::getFolders() const +{ + QVector folders; + Folder inbox; + inbox.setId(0); // Assuming folder ID 0 for Inbox (should be looked up) + inbox.setAccountId(m_account.id()); + inbox.setName(QStringLiteral("Inbox")); + inbox.setParentFolderId(QString()); + folders.append(inbox); + return folders; +} + +QVector Pop3Synchronizer::fetchMailItems(const QString &folderId, qint64 sinceUid) +{ + Q_UNUSED(sinceUid); + QVector items; + // Connect to POP3 server + QSslSocket socket; + if (m_useSsl) + socket.connectToHostEncrypted(m_host, m_port); + else + socket.connectToHost(m_host, m_port); + if (!socket.waitForConnected(5000)) { + qWarning() << "Failed to connect to POP3 server:" << socket.errorString(); + return items; + } + if (m_useSsl && !socket.waitForEncrypted(5000)) { + qWarning() << "TLS handshake failed:" << socket.errorString(); + socket.disconnectFromHost(); + return items; + } + // Read greeting + QString greeting; + if (!socket.waitForReadyRead(5000)) { + qWarning() << "No greeting from POP3 server"; + socket.disconnectFromHost(); + return items; + } + greeting = socket.readLine(); + if (!greeting.startsWith(QLatin1Char('+'))) { + qWarning() << "Invalid POP3 greeting:" << greeting; + socket.disconnectFromHost(); + return items; + } + // Login + QString userCmd = QStringLiteral("USER %1\r\n").arg(m_username); + QString passCmd = QStringLiteral("PASS %1\r\n").arg(m_password); + QString resp; + if (!sendCommand(userCmd, resp) || !resp.startsWith(QLatin1Char('+'))) { + qWarning() << "USER command failed:" << resp; + socket.disconnectFromHost(); + return items; + } + if (!sendCommand(passCmd, resp) || !resp.startsWith(QLatin1Char('+'))) { + qWarning() << "PASS command failed:" << resp; + socket.disconnectFromHost(); + return items; + } + // Get list of messages (STAT and LIST) + QString statResp; + if (!sendCommand(QStringLiteral("STAT\r\n"), statResp) || !statResp.startsWith(QLatin1Char('+'))) { + qWarning() << "STAT failed:" << statResp; + socket.disconnectFromHost(); + return items; + } + // Parse STAT: "+ " + QStringList statParts = statResp.split(QRegularExpression("\\s+")); + int count = 0; + if (statParts.size() >= 2) + count = statParts[1].toInt(); + // For simplicity, we fetch all messages (since POP3 doesn't support UID since easily without UIDL mapping) + // We'll use UIDL to get unique IDs, then RETR for each. + QString uidlResp; + if (!sendCommand(QStringLiteral("UIDL\r\n"), uidlResp) || !uidlResp.startsWith(QLatin1Char('+'))) { + qWarning() << "UIDL failed:" << uidlResp; + socket.disconnectFromHost(); + return items; + } + // UIDL response lines: each line: "+ " + QStringList lines = uidlResp.split(QRegularExpression("\\r?\\n")); + for (const QString &line : lines) { + if (line.startsWith(QLatin1Char('.')) || line.isEmpty()) + continue; + // Remove leading '+' + QString l = line; + if (l.startsWith(QLatin1Char('+'))) + l.remove(0,1); + QStringList parts = l.split(QRegularExpression("\\s+")); + if (parts.size() < 2) + continue; + QString msgIdStr = parts[0]; + QString uid = parts[1]; + bool ok; + int msgId = msgIdStr.toInt(&ok); + if (!ok) + continue; + // Retrieve message + QString retrCmd = QStringLiteral("RETR %1\r\n").arg(msgIdStr); + QString retrResp; + if (!sendCommand(retrCmd, retrResp) || !retrResp.startsWith(QLatin1Char('+'))) { + qWarning() << "RETR failed for msg" << msgIdStr; + continue; + } + // Read message lines until a line with a single dot. + QByteArray msgData; + while (true) { + if (!socket.waitForReadyRead(5000)) + break; + QByteArray line = socket.readLine(); + if (line == QByteArray(".\r\n")) + break; + msgData.append(line); + } + // Parse rudimentary headers: we can extract Subject, From, Date. + // For simplicity, we'll set placeholder values. + QString subject = QStringLiteral("(No Subject)"); + QString from = QStringLiteral("unknown@example.com"); + QDateTime date = QDateTime::currentDateTimeUtc(); + bool seen = false; // POP3 doesn't have read flag; we assume unread unless we store locally. + bool flagged = false; + MailItem item; + item.setFolderId(folderId.toInt()); + item.setSubject(subject); + item.setSender(from); + item.setDate(date); + item.setRead(seen); + item.setFlagged(flagged); + // We could store the UID in a custom field, but MailItem has uid field (qint64) - we can store the numeric UID if possible. + // For now, we leave uid 0. + // Insert into DB + if (MailItemDao::insert(item)) { + QSqlQuery qry; + qry.exec(QStringLiteral("SELECT last_insert_rowid()")); + qint64 newId = -1; + if (qry.next()) + newId = qry.value(0).toLongLong(); + item.setId(newId); + items.append(item); + qDebug() << "Fetched and stored msgId" << msgIdStr << "as mail id" << newId; + } else { + qWarning() << "Failed to insert mail item for msgId" << msgIdStr; + } + } + // Quit + sendCommand(QStringLiteral("QUIT\r\n"), resp); + socket.disconnectFromHost(); + return items; +} + +bool Pop3Synchronizer::appendMailItem(const QString &folderId, const MailItem &item) +{ + Q_UNUSED(folderId); + Q_UNUSED(item); + // POP3 does not support uploading; we can implement via SMTP elsewhere. + return false; +} + +bool Pop3Synchronizer::updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged) +{ + Q_UNUSED(folderId); + Q_UNUSED(itemUid); + Q_UNUSED(read); + Q_UNUSED(flagged); + // POP3 does not support flag updates. + return false; +} + +bool Pop3Synchronizer::deleteMailItem(const QString &folderId, const QString &itemUid) +{ + Q_UNUSED(folderId); + Q_UNUSED(itemUid); + // POP3 does not support deleting by UID easily without mapping; we could implement DELE by message number. + // For simplicity, we return false. + return false; +} + +QString Pop3Synchronizer::readResponse() +{ + if (!m_socket) + return QString(); + if (!m_socket->waitForReadyRead(5000)) + return QString(); + return QString::fromUtf8(m_socket->readLine()).trimmed(); +} + +bool Pop3Synchronizer::sendCommand(const QString &cmd, QString &response) +{ + if (!m_socket) + return false; + m_socket->write(cmd.toUtf8()); + if (!m_socket->waitForBytesWritten(5000)) + return false; + response = readResponse(); + return !response.isEmpty(); +} diff --git a/src/services/pop3/pop3synchronizer.cpp.bak b/src/services/pop3/pop3synchronizer.cpp.bak new file mode 100644 index 0000000..8fb72ea --- /dev/null +++ b/src/services/pop3/pop3synchronizer.cpp.bak @@ -0,0 +1,229 @@ +#include "pop3synchronizer.h" +#include +#include +#include "../../core/mailitem.h" +#include "../../db/dao/mailitemdao.h" +#include "../../db/dao/folderdao.h" +#include "../../core/models/account.h" + +Pop3Synchronizer::Pop3Synchronizer(QObject *parent) + : Synchronizer(parent) +{ +} + +bool Pop3Synchronizer::initialize(const Account &account) +{ + m_account = account; + const auto &settings = account.connectionSettings(); + m_host = settings.incomingHost; + m_port = settings.incomingPort; + m_useSsl = settings.incomingSsl; + m_username = settings.username; + m_password = settings.password; + // For simplicity, we do not actually connect here; connection will be made per operation. + qDebug() << "POP3 synchronizer initialized for" << m_username << "at" << m_host; + return true; +} + +bool Pop3Synchronizer::syncFolder(const Folder &folder) +{ + Q_UNUSED(folder); + // POP3 does not have folder sync; we just return true. + return true; +} + +QVector Pop3Synchronizer::getFolders() const +{ + QVector folders; + Folder inbox; + inbox.setId(0); // Assuming folder ID 0 for Inbox (should be looked up) + inbox.setAccountId(m_account.id()); + inbox.setName(QStringLiteral("Inbox")); + inbox.setParentId(-1); + folders.append(inbox); + return folders; +} + +QVector Pop3Synchronizer::fetchMailItems(const QString &folderId, qint64 sinceUid) +{ + Q_UNUSED(sinceUid); + QVector items; + // Connect to POP3 server + QSslSocket socket; + if (m_useSsl) + socket.connectToHostEncrypted(m_host, m_port); + else + socket.connectToHost(m_host, m_port); + if (!socket.waitForConnected(5000)) { + qWarning() << "Failed to connect to POP3 server:" << socket.errorString(); + return items; + } + if (m_useSsl && !socket.waitForEncrypted(5000)) { + qWarning() << "TLS handshake failed:" << socket.errorString(); + socket.disconnectFromHost(); + return items; + } + // Read greeting + QString greeting; + if (!socket.waitForReadyRead(5000)) { + qWarning() << "No greeting from POP3 server"; + socket.disconnectFromHost(); + return items; + } + greeting = socket.readLine(); + if (!greeting.startsWith(QLatin1Char('+'))) { + qWarning() << "Invalid POP3 greeting:" << greeting; + socket.disconnectFromHost(); + return items; + } + // Login + QString userCmd = QStringLiteral("USER %1\r\n").arg(m_username); + QString passCmd = QStringLiteral("PASS %1\r\n").arg(m_password); + QString resp; + if (!sendCommand(userCmd, resp) || !resp.startsWith(QLatin1Char('+'))) { + qWarning() << "USER command failed:" << resp; + socket.disconnectFromHost(); + return items; + } + if (!sendCommand(passCmd, resp) || !resp.startsWith(QLatin1Char('+'))) { + qWarning() << "PASS command failed:" << resp; + socket.disconnectFromHost(); + return items; + } + // Get list of messages (STAT and LIST) + QString statResp; + if (!sendCommand(QStringLiteral("STAT\r\n"), statResp) || !statResp.startsWith(QLatin1Char('+'))) { + qWarning() << "STAT failed:" << statResp; + socket.disconnectFromHost(); + return items; + } + // Parse STAT: "+ " + QStringList statParts = statResp.split(QRegExp("\\s+")); + int count = 0; + if (statParts.size() >= 2) + count = statParts[1].toInt(); + // For simplicity, we fetch all messages (since POP3 doesn't support UID since easily without UIDL mapping) + // We'll use UIDL to get unique IDs, then RETR for each. + QString uidlResp; + if (!sendCommand(QStringLiteral("UIDL\r\n"), uidlResp) || !uidlResp.startsWith(QLatin1Char('+'))) { + qWarning() << "UIDL failed:" << uidlResp; + socket.disconnectFromHost(); + return items; + } + // UIDL response lines: each line: "+ " + QStringList lines = uidlResp.split(QRegExp("\\r?\\n")); + for (const QString &line : lines) { + if (line.startsWith(QLatin1Char('.')) || line.isEmpty()) + continue; + // Remove leading '+' + QString l = line; + if (l.startsWith(QLatin1Char('+'))) + l.remove(0,1); + QStringList parts = l.split(QRegExp("\\s+")); + if (parts.size() < 2) + continue; + QString msgIdStr = parts[0]; + QString uid = parts[1]; + bool ok; + int msgId = msgIdStr.toInt(&ok); + if (!ok) + continue; + // Retrieve message + QString retrCmd = QStringLiteral("RETR %1\r\n").arg(msgIdStr); + QString retrResp; + if (!sendCommand(retrCmd, retrResp) || !retrResp.startsWith(QLatin1Char('+'))) { + qWarning() << "RETR failed for msg" << msgIdStr; + continue; + } + // Read message lines until a line with a single dot. + QByteArray msgData; + while (true) { + if (!socket.waitForReadyRead(5000)) + break; + QByteArray line = socket.readLine(); + if (line == QByteArray(".\r\n")) + break; + msgData.append(line); + } + // Parse rudimentary headers: we can extract Subject, From, Date. + // For simplicity, we'll set placeholder values. + QString subject = QStringLiteral("(No Subject)"); + QString from = QStringLiteral("unknown@example.com"); + QDateTime date = QDateTime::currentDateTimeUtc(); + bool seen = false; // POP3 doesn't have read flag; we assume unread unless we store locally. + bool flagged = false; + MailItem item; + item.setFolderId(folderId.toInt()); + item.setSubject(subject); + item.setFrom(from); + item.setDate(date); + item.setRead(seen); + item.setFlagged(flagged); + // We could store the UID in a custom field, but MailItem has uid field (qint64) - we can store the numeric UID if possible. + // For now, we leave uid 0. + // Insert into DB + if (MailItemDao::insert(item)) { + QSqlQuery qry; + qry.exec(QStringLiteral("SELECT last_insert_rowid()")); + qint64 newId = -1; + if (qry.next()) + newId = qry.value(0).toLongLong(); + item.setId(newId); + items.append(item); + qDebug() << "Fetched and stored msgId" << msgIdStr << "as mail id" << newId; + } else { + qWarning() << "Failed to insert mail item for msgId" << msgIdStr; + } + } + // Quit + sendCommand(QStringLiteral("QUIT\r\n"), resp); + socket.disconnectFromHost(); + return items; +} + +bool Pop3Synchronizer::appendMailItem(const QString &folderId, const MailItem &item) +{ + Q_UNUSED(folderId); + Q_UNUSED(item); + // POP3 does not support uploading; we can implement via SMTP elsewhere. + return false; +} + +bool Pop3Synchronizer::updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged) +{ + Q_UNUSED(folderId); + Q_UNUSED(itemUid); + Q_UNUSED(read); + Q_UNUSED(flagged); + // POP3 does not support flag updates. + return false; +} + +bool Pop3Synchronizer::deleteMailItem(const QString &folderId, const QString &itemUid) +{ + Q_UNUSED(folderId); + Q_UNUSED(itemUid); + // POP3 does not support deleting by UID easily without mapping; we could implement DELE by message number. + // For simplicity, we return false. + return false; +} + +QString Pop3Synchronizer::readResponse() +{ + if (!m_socket) + return QString(); + if (!m_socket->waitForReadyRead(5000)) + return QString(); + return QString::fromUtf8(m_socket->readLine()).trimmed(); +} + +bool Pop3Synchronizer::sendCommand(const QString &cmd, QString &response) +{ + if (!m_socket) + return false; + m_socket->write(cmd.toUtf8()); + if (!m_socket->waitForBytesWritten(5000)) + return false; + response = readResponse(); + return !response.isEmpty(); +} diff --git a/src/services/pop3/pop3synchronizer.h b/src/services/pop3/pop3synchronizer.h new file mode 100644 index 0000000..779eeb4 --- /dev/null +++ b/src/services/pop3/pop3synchronizer.h @@ -0,0 +1,44 @@ +#pragma once + +#include "../synchronizer.h" +#include +#include +#include +#include + +class Pop3Synchronizer : public Synchronizer +{ + Q_OBJECT +public: + explicit Pop3Synchronizer(QObject *parent = nullptr); + ~Pop3Synchronizer() override = default; + + // Synchronizer interface + bool initialize(const Account &account) override; + bool syncFolder(const Folder &folder) override; + QVector getFolders() const override; + QVector 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; + +signals: + void progressChanged(int percent); + void statusMessage(const QString &message); + +private: + QString m_host; + quint16 m_port{0}; + bool m_useSsl{false}; + QString m_username; + QString m_password; + + QSslSocket *m_socket{nullptr}; + // Mapping from UIDL (as string) to message number for DELE + QMap m_uidToMsgNum; + // Highest UID seen (used for sinceUid) + qint64 m_maxUid{0}; + + QString readResponse(); + bool sendCommand(const QString &cmd, QString &response); +}; \ No newline at end of file diff --git a/src/ui/accountsetupdialog.cpp b/src/ui/accountsetupdialog.cpp index 9dd1ba3..8ed58f4 100644 --- a/src/ui/accountsetupdialog.cpp +++ b/src/ui/accountsetupdialog.cpp @@ -7,6 +7,7 @@ #include #include #include +#include "ui/connectionwizard.h" // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Constructor โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ AccountSetupDialog::AccountSetupDialog(AccountService *accountService, QWidget *parent) @@ -427,7 +428,8 @@ void AccountSetupDialog::onNextClicked() if (page == PageProvider) { if (m_selectedProvider == 2) { - goToPage(PageImap); + // Launch ConnectionWizard for IMAP/POP3 setup + launchConnectionWizard(); } else { // Update OAuth page subtitle based on provider goToPage(PageOAuth); @@ -441,6 +443,20 @@ void AccountSetupDialog::onNextClicked() } } +void AccountSetupDialog::launchConnectionWizard() +{ + ConnectionWizard wizard(this, m_accountService); + wizard.setWindowTitle("Configurar cuenta IMAP / POP3"); + wizard.setWizardStyle(QWizard::ModernStyle); + + connect(&wizard, &ConnectionWizard::settingsReady, + this, &AccountSetupDialog::onConnectionWizardFinished); + + if (wizard.exec() == QDialog::Accepted) { + // Settings were emitted and handled in onConnectionWizardFinished + } +} + void AccountSetupDialog::onBackClicked() { int page = m_stack->currentIndex(); @@ -552,27 +568,81 @@ void AccountSetupDialog::submitImapAccount() m_sslCheckbox->isChecked() ? "Sรญ" : "No") ); - // Disable UI during connection attempt - m_btnBack->setEnabled(false); - m_btnNext->setEnabled(false); - m_btnCancel->setEnabled(false); - QString errorMsg; - bool success = m_accountService->testConnection(account.connectionSettings(), errorMsg); - if (success) { - if (m_isEditing) { - account.setId(m_editingAccountId); - m_accountService->updateAccount(account); - } else { - m_accountService->addAccount(account); - } - m_btnCancel->setEnabled(true); - } else { - QMessageBox::warning(this, tr("Connection failed"), tr("Could not connect to IMAP server: %1").arg(errorMsg)); - // Re-enable UI - m_btnBack->setEnabled(true); - m_btnNext->setEnabled(true); - m_btnCancel->setEnabled(true); - } + // Disable UI during connection attempt + m_btnBack->setEnabled(false); + m_btnNext->setEnabled(false); + m_btnCancel->setEnabled(false); + QString errorMsg; + bool success = m_accountService->testConnection(account.connectionSettings(), errorMsg); + if (success) { + if (m_isEditing) { + account.setId(m_editingAccountId); + m_accountService->updateAccount(account); + } else { + m_accountService->addAccount(account); + } + m_btnCancel->setEnabled(true); + } else { + QMessageBox::warning(this, tr("Connection failed"), tr("Could not connect to IMAP server: %1").arg(errorMsg)); + // Re-enable UI + m_btnBack->setEnabled(true); + m_btnNext->setEnabled(true); + m_btnCancel->setEnabled(true); + } +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Account Result โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +void AccountSetupDialog::onConnectionWizardFinished(const Account::ConnectionSettings &settings) +{ + QString email = settings.username; + QString name = email.split("@").first(); + + // Show progress + m_accountCreatedOk = false; + goToPage(PageProgress); + m_progressIcon->setText("โณ"); + m_progressText->setText("Conectando al servidor..."); + m_progressDetail->setText( + QString("Servidor IMAP: %1:%2\nServidor SMTP: %3:%4\nSSL: %5") + .arg(settings.incomingHost) + .arg(settings.incomingPort) + .arg(settings.outgoingHost) + .arg(settings.outgoingPort) + .arg(settings.incomingSsl ? "Sรญ" : "No") + ); + + // Disable UI during connection attempt + m_btnBack->setEnabled(false); + m_btnNext->setEnabled(false); + m_btnCancel->setEnabled(false); + + QString errorMsg; + bool success = m_accountService->testConnection(settings, errorMsg); + if (success) { + // Create account with the connection settings + Account account; + account.setEmail(email); + account.setDisplayName(name); + + // Determine account type from protocol + if (settings.type == "pop3") { + account.setType(AccountType::POP3); + } else { + account.setType(AccountType::IMAP); + } + + account.setConnectionSettings(settings); + + if (m_isEditing) { + account.setId(m_editingAccountId); + m_accountService->updateAccount(account); + } else { + m_accountService->addAccount(account); + } + m_btnCancel->setEnabled(true); + } else { + showError(tr("Could not connect to server: %1").arg(errorMsg)); + } } // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Account Result โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/src/ui/accountsetupdialog.cpp.backup2 b/src/ui/accountsetupdialog.cpp.backup2 new file mode 100644 index 0000000..7457c2a --- /dev/null +++ b/src/ui/accountsetupdialog.cpp.backup2 @@ -0,0 +1,620 @@ +#include "ui/accountsetupdialog.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 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); + connect(m_authTimeoutTimer, &QTimer::timeout, this, &AccountSetupDialog::onAuthTimeout); + + if (m_accountService) { + connect(m_accountService, &AccountService::accountAdded, + this, &AccountSetupDialog::onAccountAdded); + } + + setupUI(); +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 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"); + setMinimumSize(600, 500); + resize(660, 520); + + // โ”€โ”€ Global stylesheet โ”€โ”€ + setStyleSheet( + "AccountSetupDialog { background: #f5f5f7; }" + "QLabel { color: #1d1d1f; }" + "QLineEdit {" + " background: #fff; border: 1px solid #d1d1d6; border-radius: 6px;" + " padding: 8px 12px; font-size: 13px; color: #1d1d1f;" + "}" + "QLineEdit:focus { border: 2px solid #0071e3; }" + "QRadioButton { font-size: 14px; padding: 6px 0; }" + "QCheckBox { font-size: 13px; }" + "QPushButton {" + " font-weight: 600; font-size: 13px; padding: 8px 20px; border-radius: 6px;" + "}" + }); + + QVBoxLayout *root = new QVBoxLayout(this); + root->setContentsMargins(24, 24, 24, 18); + root->setSpacing(16); + + // โ”€โ”€ Stacked pages โ”€โ”€ + m_stack = new QStackedWidget(this); + m_stack->addWidget(createProviderPage()); // 0 + m_stack->addWidget(createOAuthPage()); // 1 + m_stack->addWidget(createImapPage()); // 2 + m_stack->addWidget(createProgressPage()); // 3 + root->addWidget(m_stack, 1); + + // โ”€โ”€ Separator โ”€โ”€ + QFrame *sep = new QFrame(); + sep->setFrameShape(QFrame::HLine); + sep->setStyleSheet("color: #d1d1d6;"); + root->addWidget(sep); + + // โ”€โ”€ Navigation buttons โ”€โ”€ + QHBoxLayout *nav = new QHBoxLayout(); + + m_btnBack = new QPushButton("โ† Atrรกs"); + m_btnBack->setStyleSheet( + "QPushButton { background: #fff; color: #0071e3; border: 1px solid #0071e3; }" + "QPushButton:hover { background: #e8f0fe; }" + ); + + m_btnCancel = new QPushButton("Cancelar"); + m_btnCancel->setStyleSheet( + "QPushButton { background: #e5e5ea; color: #1d1d1f; border: none; }" + "QPushButton:hover { background: #d1d1d6; }" + ); + + m_btnNext = new QPushButton("Siguiente โ†’"); + m_btnNext->setStyleSheet( + "QPushButton { background: #0071e3; color: white; border: none; }" + "QPushButton:hover { background: #005bb5; }" + "QPushButton:disabled { background: #a0c4ff; color: #e0e0e0; }" + ); + + nav->addWidget(m_btnBack); + nav->addStretch(); + nav->addWidget(m_btnCancel); + nav->addSpacing(8); + nav->addWidget(m_btnNext); + root->addLayout(nav); + + connect(m_btnBack, &QPushButton::clicked, this, &AccountSetupDialog::onBackClicked); + connect(m_btnNext, &QPushButton::clicked, this, &AccountSetupDialog::onNextClicked); + connect(m_btnCancel, &QPushButton::clicked, this, &AccountSetupDialog::onCancelClicked); + + goToPage(PageProvider); +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Page 0: Provider โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +QWidget* AccountSetupDialog::createProviderPage() +{ + QWidget *page = new QWidget(); + QVBoxLayout *lay = new QVBoxLayout(page); + lay->setContentsMargins(10, 10, 10, 10); + lay->setSpacing(20); + + // Title + QLabel *title = new QLabel("Aรฑadir nueva cuenta"); + title->setStyleSheet("font-size: 22px; font-weight: 700; color: #1d1d1f;"); + title->setAlignment(Qt::AlignCenter); + + QLabel *subtitle = new QLabel("Selecciona el tipo de cuenta que deseas configurar:"); + subtitle->setStyleSheet("font-size: 14px; color: #8e8e93;"); + subtitle->setWordWrap(true); + subtitle->setAlignment(Qt::AlignCenter); + + lay->addWidget(title); + lay->addWidget(subtitle); + lay->addSpacing(10); + + // Provider cards + m_providerGroup = new QButtonGroup(this); + + auto makeRadio = [&](const QString &text, const QString &desc, int id) -> QWidget* { + QWidget *card = new QWidget(); + card->setStyleSheet( + "QWidget { background: #fff; border: 1px solid #e0e0e5; border-radius: 10px; }" + "QWidget:hover { border-color: #0071e3; }" + ); + QHBoxLayout *h = new QHBoxLayout(card); + h->setContentsMargins(16, 12, 16, 12); + + QRadioButton *radio = new QRadioButton(text); + radio->setStyleSheet("font-size: 15px; font-weight: 600;"); + m_providerGroup->addButton(radio, id); + + QLabel *descLabel = new QLabel(desc); + descLabel->setStyleSheet("font-size: 12px; color: #8e8e93;"); + descLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + + h->addWidget(radio, 1); + h->addWidget(descLabel); + return card; + }; + + lay->addWidget(makeRadio("๐Ÿ”ด Google / Gmail", "OAuth2 seguro", 0)); + lay->addWidget(makeRadio("๐Ÿ”ต Microsoft / Outlook", "OAuth2 seguro", 1)); + lay->addWidget(makeRadio("โš™๏ธ IMAP / SMTP", "Servidor personalizado", 2)); + + m_providerGroup->button(0)->setChecked(true); + connect(m_providerGroup, QOverload::of(&QButtonGroup::idClicked), + this, &AccountSetupDialog::onProviderSelected); + + lay->addStretch(); + return page; +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Page 1: OAuth โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +QWidget* AccountSetupDialog::createOAuthPage() +{ + QWidget *page = new QWidget(); + QVBoxLayout *lay = new QVBoxLayout(page); + lay->setContentsMargins(10, 10, 10, 10); + lay->setSpacing(14); + + QLabel *title = new QLabel("Autenticaciรณn OAuth2"); + title->setStyleSheet("font-size: 18px; font-weight: 700;"); + 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" + " 4. Al terminar, esta ventana se actualizarรก automรกticamente." + ); + info->setWordWrap(true); + info->setStyleSheet("font-size: 13px; color: #555; line-height: 1.5;"); + lay->addWidget(info); + + lay->addSpacing(6); + + QLabel *emailLabel = new QLabel("Correo electrรณnico:"); + emailLabel->setStyleSheet("font-weight: 600; font-size: 13px;"); + lay->addWidget(emailLabel); + + m_oauthEmailEdit = new QLineEdit(); + m_oauthEmailEdit->setPlaceholderText("tu-correo@gmail.com"); + QRegularExpression rx(R"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})"); + m_oauthEmailEdit->setValidator(new QRegularExpressionValidator(rx, this)); + lay->addWidget(m_oauthEmailEdit); + + lay->addSpacing(8); + + m_btnOAuthStart = new QPushButton("๐Ÿ”‘ Autenticar en Navegador"); + m_btnOAuthStart->setMinimumHeight(40); + m_btnOAuthStart->setStyleSheet( + "QPushButton { background: #34a853; color: white; border: none; font-size: 14px; font-weight: 700; border-radius: 8px; }" + "QPushButton:hover { background: #2d9249; }" + "QPushButton:disabled { background: #a8d5b8; color: #e0e0e0; }" + ); + connect(m_btnOAuthStart, &QPushButton::clicked, this, &AccountSetupDialog::startOAuthAuthentication); + lay->addWidget(m_btnOAuthStart); + + m_oauthStatusLabel = new QLabel("Esperandoโ€ฆ"); + m_oauthStatusLabel->setStyleSheet("font-size: 12px; color: #8e8e93; font-style: italic;"); + m_oauthStatusLabel->setAlignment(Qt::AlignCenter); + lay->addWidget(m_oauthStatusLabel); + + lay->addStretch(); + return page; +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Page 2: IMAP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +QWidget* AccountSetupDialog::createImapPage() +{ + QWidget *page = new QWidget(); + QVBoxLayout *outerLay = new QVBoxLayout(page); + outerLay->setContentsMargins(10, 10, 10, 10); + + QLabel *title = new QLabel("Configuraciรณn IMAP / SMTP"); + title->setStyleSheet("font-size: 18px; font-weight: 700;"); + outerLay->addWidget(title); + + QLabel *info = new QLabel("Introduce los datos de tu servidor de correo. " + "Consulta a tu proveedor si no conoces los datos del servidor."); + info->setWordWrap(true); + info->setStyleSheet("font-size: 12px; color: #8e8e93; margin-bottom: 8px;"); + outerLay->addWidget(info); + + // Scrollable form + QScrollArea *scroll = new QScrollArea(); + scroll->setWidgetResizable(true); + scroll->setFrameShape(QFrame::NoFrame); + + QWidget *formWidget = new QWidget(); + QFormLayout *form = new QFormLayout(formWidget); + form->setSpacing(10); + form->setContentsMargins(0, 8, 12, 8); + form->setLabelAlignment(Qt::AlignRight); + + m_imapEmailEdit = new QLineEdit(); + m_imapEmailEdit->setPlaceholderText("usuario@empresa.com"); + + m_imapNameEdit = new QLineEdit(); + m_imapNameEdit->setPlaceholderText("Nombre a mostrar (ej. Javier)"); + + m_imapPasswordEdit = new QLineEdit(); + m_imapPasswordEdit->setEchoMode(QLineEdit::Password); + m_imapPasswordEdit->setPlaceholderText("Contraseรฑa"); + + m_imapHostEdit = new QLineEdit(); + m_imapHostEdit->setPlaceholderText("imap.empresa.com"); + + m_imapPortEdit = new QLineEdit(); + m_imapPortEdit->setPlaceholderText("993"); + m_imapPortEdit->setValidator(new QIntValidator(1, 65535, this)); + m_imapPortEdit->setMaximumWidth(100); + + m_smtpHostEdit = new QLineEdit(); + m_smtpHostEdit->setPlaceholderText("smtp.empresa.com"); + + m_smtpPortEdit = new QLineEdit(); + m_smtpPortEdit->setPlaceholderText("587"); + m_smtpPortEdit->setValidator(new QIntValidator(1, 65535, this)); + m_smtpPortEdit->setMaximumWidth(100); + + m_sslCheckbox = new QCheckBox("Usar conexiรณn segura (SSL/TLS)"); + m_sslCheckbox->setChecked(true); + + form->addRow("Correo:", m_imapEmailEdit); + form->addRow("Nombre:", m_imapNameEdit); + form->addRow("Contraseรฑa:", m_imapPasswordEdit); + + // Visual separator + QFrame *line = new QFrame(); + line->setFrameShape(QFrame::HLine); + line->setStyleSheet("color: #e0e0e5;"); + form->addRow(line); + + QLabel *serverHeader = new QLabel("Servidores"); + serverHeader->setStyleSheet("font-weight: 700; font-size: 13px; color: #0071e3;"); + form->addRow(serverHeader); + + form->addRow("Servidor IMAP:", m_imapHostEdit); + form->addRow("Puerto IMAP:", m_imapPortEdit); + form->addRow("Servidor SMTP:", m_smtpHostEdit); + form->addRow("Puerto SMTP:", m_smtpPortEdit); + form->addRow("", m_sslCheckbox); + + scroll->setWidget(formWidget); + outerLay->addWidget(scroll, 1); + + return page; +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Page 3: Progress โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +QWidget* AccountSetupDialog::createProgressPage() +{ + QWidget *page = new QWidget(); + QVBoxLayout *lay = new QVBoxLayout(page); + lay->setContentsMargins(20, 40, 20, 20); + lay->setAlignment(Qt::AlignCenter); + + m_progressIcon = new QLabel("โณ"); + m_progressIcon->setStyleSheet("font-size: 56px;"); + m_progressIcon->setAlignment(Qt::AlignCenter); + + m_progressText = new QLabel("Conectando al servidorโ€ฆ"); + m_progressText->setStyleSheet("font-size: 16px; font-weight: 600; color: #1d1d1f;"); + m_progressText->setAlignment(Qt::AlignCenter); + + m_progressDetail = new QLabel("Validando credenciales y configuraciรณn del servidor de correo."); + m_progressDetail->setWordWrap(true); + m_progressDetail->setStyleSheet("font-size: 13px; color: #8e8e93;"); + m_progressDetail->setAlignment(Qt::AlignCenter); + + lay->addWidget(m_progressIcon); + lay->addSpacing(16); + lay->addWidget(m_progressText); + lay->addSpacing(8); + lay->addWidget(m_progressDetail); + lay->addStretch(); + + return page; +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Navigation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +void AccountSetupDialog::goToPage(int page) +{ + m_stack->setCurrentIndex(page); + updateNavButtons(); +} + +void AccountSetupDialog::updateNavButtons() +{ + int page = m_stack->currentIndex(); + + m_btnBack->setVisible(page > 0 && page != PageProgress); + m_btnCancel->setVisible(page != PageProgress || !m_accountCreatedOk); + + switch (page) { + case PageProvider: + m_btnNext->setText("Siguiente โ†’"); + m_btnNext->setEnabled(true); + m_btnNext->setVisible(true); + break; + case PageOAuth: + m_btnNext->setVisible(false); // OAuth flow is driven by the authenticate button + break; + case PageImap: + m_btnNext->setText(m_isEditing ? "Guardar cambios" : "Conectar"); + m_btnNext->setEnabled(true); + m_btnNext->setVisible(true); + break; + case PageProgress: + if (m_accountCreatedOk) { + m_btnNext->setText("Finalizar โœ“"); + m_btnNext->setEnabled(true); + m_btnNext->setVisible(true); + m_btnBack->setVisible(false); + m_btnCancel->setVisible(false); + } else { + m_btnNext->setVisible(false); + } + break; + } +} + +void AccountSetupDialog::onProviderSelected(int id) +{ + m_selectedProvider = id; +} + +void AccountSetupDialog::onNextClicked() +{ + int page = m_stack->currentIndex(); + + if (page == PageProvider) { + if (m_selectedProvider == 2) { + goToPage(PageImap); + } else { + // Update OAuth page subtitle based on provider + goToPage(PageOAuth); + } + } + else if (page == PageImap) { + submitImapAccount(); + } + else if (page == PageProgress) { + accept(); // Finalizar + } +} + +void AccountSetupDialog::onBackClicked() +{ + int page = m_stack->currentIndex(); + if (page == PageOAuth || page == PageImap) { + m_authTimeoutTimer->stop(); + goToPage(PageProvider); + } +} + +void AccountSetupDialog::onCancelClicked() +{ + m_authTimeoutTimer->stop(); + reject(); +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ OAuth Flow โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +void AccountSetupDialog::startOAuthAuthentication() +{ + QString email = m_oauthEmailEdit->text().trimmed(); + if (email.isEmpty()) { + QMessageBox::warning(this, "Validaciรณn", + "Introduce tu direcciรณn de correo electrรณnico."); + return; + } + + m_btnOAuthStart->setEnabled(false); + m_oauthStatusLabel->setText("Abriendo navegadorโ€ฆ Completa el inicio de sesiรณn allรญ."); + m_oauthStatusLabel->setStyleSheet("font-size: 12px; color: #0071e3; font-weight: 600;"); + + QString provider = (m_selectedProvider == 0) ? "gmail" : "outlook"; + + // Start authentication via AccountService (opens browser) + m_accountService->startAuthentication(email, provider); + + // Start timeout timer (120 seconds to complete OAuth) + m_authTimeoutTimer->start(120000); + + qDebug() << "[AccountSetupDialog] OAuth started for" << email << "provider:" << provider; +} + +void AccountSetupDialog::onAuthTimeout() +{ + if (m_stack->currentIndex() == PageOAuth) { + m_btnOAuthStart->setEnabled(true); + m_oauthStatusLabel->setText("โš ๏ธ Tiempo de espera agotado. Intรฉntalo de nuevo."); + m_oauthStatusLabel->setStyleSheet("font-size: 12px; color: #ff3b30; font-weight: 600;"); + } +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ IMAP Flow โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +void AccountSetupDialog::submitImapAccount() +{ + QString email = m_imapEmailEdit->text().trimmed(); + QString name = m_imapNameEdit->text().trimmed(); + QString password = m_imapPasswordEdit->text().trimmed(); + QString imapHost = m_imapHostEdit->text().trimmed(); + QString imapPort = m_imapPortEdit->text().trimmed(); + QString smtpHost = m_smtpHostEdit->text().trimmed(); + QString smtpPort = m_smtpPortEdit->text().trimmed(); + + // Validation + if (email.isEmpty()) { + QMessageBox::warning(this, "Campo requerido", "Introduce tu direcciรณn de correo."); + return; + } + if (password.isEmpty()) { + QMessageBox::warning(this, "Campo requerido", "Introduce tu contraseรฑa."); + return; + } + if (imapHost.isEmpty()) { + QMessageBox::warning(this, "Campo requerido", "Introduce el servidor IMAP."); + return; + } + + // Use defaults if ports are empty + if (imapPort.isEmpty()) imapPort = m_sslCheckbox->isChecked() ? "993" : "143"; + if (smtpPort.isEmpty()) smtpPort = m_sslCheckbox->isChecked() ? "465" : "587"; + 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_progressDetail->setText( + QString("Servidor IMAP: %1:%2\nServidor SMTP: %3:%4\nSSL: %5") + .arg(imapHost, imapPort, smtpHost, smtpPort, + m_sslCheckbox->isChecked() ? "Sรญ" : "No") + ); + + // Test connection + QString errorMsg; + if (!m_accountService->testConnection(account.connectionSettings(), errorMsg)) { + showError(tr("Connection test failed: %1").arg(errorMsg)); + m_authTimeoutTimer->stop(); + return; + } + // Connection OK, proceed to add account + m_progressText->setText(tr("Guardando cuenta...")); + m_progressDetail->setText(""); + // Start timeout for safety + m_authTimeoutTimer->start(30000); + // Perform add/update after a short delay to let UI update + QTimer::singleShot(0, this, [this, account]() mutable { + if (m_isEditing) { + account.setId(m_editingAccountId); + m_accountService->updateAccount(account); + } else { + m_accountService->addAccount(account); + } + } + }); +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Account Result โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +void AccountSetupDialog::onAccountAdded(const Account &account) +{ + m_authTimeoutTimer->stop(); + m_accountCreatedOk = true; + + 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(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); +} + +void AccountSetupDialog::showSuccess(const QString &message) +{ + m_progressIcon->setText("โœ…"); + m_progressText->setText("ยกCuenta aรฑadida!"); + m_progressDetail->setText(message); + updateNavButtons(); +} + +void AccountSetupDialog::showError(const QString &message) +{ + m_accountCreatedOk = false; + m_progressIcon->setText("โŒ"); + m_progressText->setText("Error de conexiรณn"); + m_progressDetail->setText(message); + + // Allow going back + m_btnBack->setVisible(true); + m_btnCancel->setVisible(true); + m_btnNext->setVisible(false); +} + +#include "accountsetupdialog.moc" \ No newline at end of file diff --git a/src/ui/accountsetupdialog.h b/src/ui/accountsetupdialog.h index cf5ed70..af43ea9 100644 --- a/src/ui/accountsetupdialog.h +++ b/src/ui/accountsetupdialog.h @@ -40,6 +40,7 @@ private slots: void startOAuthAuthentication(); void onAccountAdded(const Account &account); void onAuthTimeout(); + void onConnectionWizardFinished(const Account::ConnectionSettings &settings); private: void setupUI(); @@ -53,6 +54,7 @@ private: void showError(const QString &message); void showSuccess(const QString &message); void submitImapAccount(); + void launchConnectionWizard(); // Services AccountService *m_accountService; diff --git a/src/ui/add_flag_action.py b/src/ui/add_flag_action.py new file mode 100644 index 0000000..fab6f22 --- /dev/null +++ b/src/ui/add_flag_action.py @@ -0,0 +1,78 @@ +import sys + +filename = '/mnt/c/Users/javie/wino-mail-dtkqt/src/ui/mainmainwindow.cpp' +with open(filename, 'r') as f: + lines = f.readlines() + +# 1. Find the line where deleteAction is declared +decl_line = None +for i, line in enumerate(lines): + if 'QAction *deleteAction = m_toolBar->addAction(\"๐Ÿ—‘ Delete\");' in line: + decl_line = i + break +if decl_line is None: + print('Could not find deleteAction declaration') + sys.exit(1) + +# Insert flag action declaration right after it +flag_decl = ' QAction *flagAction = m_toolBar->addAction(\"๐Ÿšฉ Flag\");\n' +lines = lines[:decl_line+1] + [flag_decl] + lines[decl_line+1:] + +# 2. Find the closing brace of the createToolBar function +brace_line = None +brace_count = 0 +in_function = False +for i, line in enumerate(lines): + if i < decl_line: + continue + if 'void MainMainWindow::createToolBar() {' in line: + in_function = True + continue + if not in_function: + continue + # Count braces + for ch in line: + if ch == '{': + brace_count += 1 + elif ch == '}': + brace_count -= 1 + if brace_count == 0: + brace_line = i + break + if brace_line is not None: + break +if brace_line is None: + print('Could not find closing brace of createToolBar') + sys.exit(1) + +# 3. Insert flag connection just before the closing brace +flag_conn = ''' connect(flagAction, &QAction::triggered, [this]() { + QModelIndexList indexes = m_mailListView->selectionModel()->selectedIndexes(); + if (indexes.isEmpty()) { + statusBar()->showMessage(tr("No email selected"), 2000); + return; + } + int row = indexes.first().row(); + QModelIndex idx = m_emailModel->index(row, 0); + qint64 id = idx.data(EmailListModel::IdRole).toLongLong(); + // Toggle flagged state + std::optional opt = MailItemDao::findById(id); + if (opt) { + MailItem item = *opt; + item.setFlagged(!item.isFlagged()); + if (MailItemDao::update(item)) { + statusBar()->showMessage(tr("Flag toggled"), 2000); + m_emailModel->refresh(); // optional, to update icon if any + } else { + statusBar()->showMessage(tr("Failed to update flag"), 2000); + } + } else { + statusBar()->showMessage(tr("Email not found"), 2000); + } + });\n''' +lines = lines[:brace_line] + [flag_conn] + lines[brace_line:] + +with open(filename, 'w') as f: + f.writelines(lines) + +print('Flag action added') diff --git a/src/ui/composeview.cpp b/src/ui/composeview.cpp index 0e96d62..4a21e06 100644 --- a/src/ui/composeview.cpp +++ b/src/ui/composeview.cpp @@ -1,277 +1,21 @@ -#include "ui/composeview.h" -#include -#include -#include -#include -#include +// ===================== ComposeView ===================== + +#include "composeview.h" #include +#include #include #include +#include +#include +#include #include -#include "tags_line_edit.hpp" -#include -#include -#include -#include -#include -#include -#include - -// ===================== RichTextEditor ===================== - -RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) { - setAcceptRichText(true); - setPlaceholderText("Write your message here..."); -} - -void RichTextEditor::setupToolbar(QVBoxLayout *layout) { - m_toolbar = new QToolBar("Formatting"); - m_toolbar->setIconSize(QSize(16, 16)); - m_toolbar->setStyleSheet( - "QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }" - "QToolButton { padding: 4px 6px; border-radius: 3px; }" - "QToolButton:hover { background: #e0e0e0; }" - "QToolButton:checked { background: #bbdefb; }" - ); - - // Font family combo - m_fontCombo = new QFontComboBox(); - m_fontCombo->setFixedWidth(150); - connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged); - m_toolbar->addWidget(m_fontCombo); - - // Font size spin - m_fontSizeSpin = new QSpinBox(); - m_fontSizeSpin->setRange(8, 72); - m_fontSizeSpin->setValue(14); - m_fontSizeSpin->setFixedWidth(50); - connect(m_fontSizeSpin, QOverload::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged); - m_toolbar->addWidget(m_fontSizeSpin); - - m_toolbar->addSeparator(); - - // Bold / Italic / Underline - QAction *boldAct = m_toolbar->addAction("B"); - boldAct->setCheckable(true); - QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont); - connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold); - - QAction *italicAct = m_toolbar->addAction("I"); - italicAct->setCheckable(true); - QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont); - connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic); - - QAction *underlineAct = m_toolbar->addAction("U"); - underlineAct->setCheckable(true); - QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont); - connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline); - - m_toolbar->addSeparator(); - - // Alignment - QAction *alignLeft = m_toolbar->addAction("L"); - connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft); - - QAction *alignCenter = m_toolbar->addAction("C"); - connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter); - - QAction *alignRight = m_toolbar->addAction("R"); - connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight); - - QAction *alignJustify = m_toolbar->addAction("J"); - connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify); - - m_toolbar->addSeparator(); - - // Lists - QAction *bulletAct = m_toolbar->addAction("Bullets"); - connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList); - - QAction *numAct = m_toolbar->addAction("1. List"); - connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList); - - m_toolbar->addSeparator(); - - // Indent / Outdent - QAction *indentAct = m_toolbar->addAction("Indent"); - connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent); - - QAction *outdentAct = m_toolbar->addAction("Outdent"); - connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent); - - m_toolbar->addSeparator(); - - // Insert image - QAction *imgAct = m_toolbar->addAction("Img"); - connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage); - - // Insert table - QAction *tableAct = m_toolbar->addAction("Tbl"); - connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable); - - // 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); - QAction *editSigAct = m_signatureMenu->addAction(tr("Editar firmas")); - connect(editSigAct, &QAction::triggered, this, &RichTextEditor::signatureEditRequested); - - m_toolbar->addSeparator(); - - layout->addWidget(m_toolbar); -} - -void RichTextEditor::onBold() { - QTextCharFormat fmt; - fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold); - mergeCurrentCharFormat(fmt); - setFocus(); -} - -void RichTextEditor::onItalic() { - QTextCharFormat fmt; - fmt.setFontItalic(!textCursor().charFormat().fontItalic()); - mergeCurrentCharFormat(fmt); - setFocus(); -} - -void RichTextEditor::onUnderline() { - QTextCharFormat fmt; - fmt.setFontUnderline(!textCursor().charFormat().fontUnderline()); - mergeCurrentCharFormat(fmt); - setFocus(); -} - -void RichTextEditor::onBulletList() { - QTextCursor cursor = textCursor(); - QTextList *list = cursor.currentList(); - if (list && list->format().style() == QTextListFormat::ListDisc) { - // Remove list - QTextBlockFormat bfmt; - bfmt.setIndent(0); - cursor.setBlockFormat(bfmt); - list->remove(cursor.block()); - } else { - QTextListFormat listFormat; - listFormat.setStyle(QTextListFormat::ListDisc); - cursor.createList(listFormat); - } -} - -void RichTextEditor::onNumberedList() { - QTextCursor cursor = textCursor(); - QTextList *list = cursor.currentList(); - if (list && list->format().style() == QTextListFormat::ListDecimal) { - QTextBlockFormat bfmt; - bfmt.setIndent(0); - cursor.setBlockFormat(bfmt); - list->remove(cursor.block()); - } else { - QTextListFormat listFormat; - listFormat.setStyle(QTextListFormat::ListDecimal); - cursor.createList(listFormat); - } -} - -void RichTextEditor::onIndent() { - QTextCursor cursor = textCursor(); - QTextBlockFormat bfmt = cursor.blockFormat(); - bfmt.setIndent(bfmt.indent() + 1); - cursor.setBlockFormat(bfmt); -} - -void RichTextEditor::onOutdent() { - QTextCursor cursor = textCursor(); - QTextBlockFormat bfmt = cursor.blockFormat(); - int indent = bfmt.indent(); - if (indent > 0) { - bfmt.setIndent(indent - 1); - cursor.setBlockFormat(bfmt); - } -} - -void RichTextEditor::onAlignLeft() { - QTextCursor cursor = textCursor(); - QTextBlockFormat bfmt = cursor.blockFormat(); - bfmt.setAlignment(Qt::AlignLeft); - cursor.setBlockFormat(bfmt); -} - -void RichTextEditor::onAlignCenter() { - QTextCursor cursor = textCursor(); - QTextBlockFormat bfmt = cursor.blockFormat(); - bfmt.setAlignment(Qt::AlignCenter); - cursor.setBlockFormat(bfmt); -} - -void RichTextEditor::onAlignRight() { - QTextCursor cursor = textCursor(); - QTextBlockFormat bfmt = cursor.blockFormat(); - bfmt.setAlignment(Qt::AlignRight); - cursor.setBlockFormat(bfmt); -} - -void RichTextEditor::onAlignJustify() { - QTextCursor cursor = textCursor(); - QTextBlockFormat bfmt = cursor.blockFormat(); - bfmt.setAlignment(Qt::AlignJustify); - cursor.setBlockFormat(bfmt); -} - -void RichTextEditor::onFontChanged(const QFont &font) { - QTextCharFormat fmt; - fmt.setFontFamilies({font.family()}); - mergeCurrentCharFormat(fmt); -} - -void RichTextEditor::onFontSizeChanged(int size) { - QTextCharFormat fmt; - fmt.setFontPointSize(size); - mergeCurrentCharFormat(fmt); -} - -void RichTextEditor::onInsertImage() { - QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)"); - if (filePath.isEmpty()) return; - QTextCursor cursor = textCursor(); - QTextImageFormat imgFmt; - imgFmt.setName(filePath); - // Scale down if too large - QPixmap pm(filePath); - if (pm.width() > 600) { - imgFmt.setWidth(600); - imgFmt.setHeight(pm.height() * 600 / pm.width()); - } - cursor.insertImage(imgFmt); -} - -void RichTextEditor::onInsertTable() { - bool ok; - int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok); - if (!ok) return; - int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok); - if (!ok) return; - - QTextCursor cursor = textCursor(); - QTextTableFormat tableFmt; - tableFmt.setBorder(1); - tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid); - tableFmt.setCellPadding(4); - tableFmt.setCellSpacing(0); - tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100)); - cursor.insertTable(rows, cols, tableFmt); -} - +#include +#include +#include +#include +#include +#include +#include ComposeView::ComposeView(QWidget *parent) : QWidget(parent) { setupUI(); // Connect the rich text editor's signature signal to our slot @@ -282,14 +26,14 @@ ComposeView::ComposeView(QWidget *parent) : QWidget(parent) { 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() -{ +void ComposeView::onSignatureEditRequested() { QDialog dialog(this); dialog.setWindowTitle(tr("Edit Signature")); dialog.setMinimumSize(600, 400); @@ -314,14 +58,13 @@ void ComposeView::onSignatureEditRequested() } } } -void ComposeView::setAccountService(AccountService *service) -{ + +void ComposeView::setAccountService(AccountService *service) { m_accountService = service; populateAccountCombo(); } -void ComposeView::populateAccountCombo() -{ +void ComposeView::populateAccountCombo() { if (!m_accountService) { qWarning() << "AccountService not set"; return; @@ -342,8 +85,7 @@ void ComposeView::populateAccountCombo() } } -void ComposeView::onAccountChanged(int index) -{ +void ComposeView::onAccountChanged(int index) { if (index <= 0) { // Placeholder item selected or invalid index m_currentAccountId = -1; @@ -354,8 +96,7 @@ void ComposeView::onAccountChanged(int index) loadSignatureForCurrentAccount(); } -void ComposeView::loadSignatureForCurrentAccount() -{ +void ComposeView::loadSignatureForCurrentAccount() { if (!m_accountService || m_currentAccountId <= 0) { // Clear signature if no account selected m_bodyEditor->clear(); @@ -367,12 +108,12 @@ void ComposeView::loadSignatureForCurrentAccount() // 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(); + delete account; } else { m_bodyEditor->clear(); } } - void ComposeView::setupUI() { QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins(20, 15, 20, 15); @@ -383,37 +124,9 @@ void ComposeView::setupUI() { "QLineEdit:focus, QTextEdit:focus { border-color: #1976D2; }" ); - // Header row: Subject + Detach button - QHBoxLayout *headerLayout = new QHBoxLayout(); - m_subjectField = new QLineEdit(); - m_subjectField->setPlaceholderText("Subject"); - m_subjectField->setFixedHeight(40); - QFont subjectFont = m_subjectField->font(); - subjectFont.setPointSize(14); - subjectFont.setBold(true); - m_subjectField->setFont(subjectFont); - headerLayout->addWidget(m_subjectField, 1); - - 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; }" - "QPushButton:hover { background: #f0f0f0; }" - ); - connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked); - headerLayout->addWidget(m_detachButton); - - mainLayout->addLayout(headerLayout); - - // Separator line - QFrame *line1 = new QFrame(); - line1->setFrameShape(QFrame::HLine); - line1->setStyleSheet("color: #e0e0e0;"); - mainLayout->addWidget(line1); - - // From: field + Account selector + // De: field + Account selector (renamed from From:) QHBoxLayout *fromLayout = new QHBoxLayout(); - QLabel *fromLabel = new QLabel("From:"); + QLabel *fromLabel = new QLabel(tr("De:")); fromLabel->setFixedWidth(40); fromLabel->setStyleSheet("font-weight: bold; color: #555;"); m_accountCombo = new QComboBox(); @@ -429,16 +142,17 @@ void ComposeView::setupUI() { connect(m_accountCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ComposeView::onAccountChanged); fromLayout->addWidget(fromLabel); - fromLayout->addWidget(m_accountCombo, 1); + fromLayout->addWidget(m_accountCombo); + fromLayout->addStretch(); // Este resorte empuja el par (label+combo) a la izquierda mainLayout->addLayout(fromLayout); - // To: field + Cc/Bcc toggle buttons + // Para: field + Cc/Bcc toggle buttons QHBoxLayout *toLayout = new QHBoxLayout(); - QLabel *toLabel = new QLabel("To:"); - toLabel->setFixedWidth(40); - toLabel->setStyleSheet("font-weight: bold; color: #555;"); - m_toField = new QLineEdit(); - m_toField->setPlaceholderText("Recipients (comma separated)"); + QLabel *toLabel = new QLabel(tr("Para:")); + toLabel->setFixedWidth(40); + toLabel->setStyleSheet("font-weight: bold; color: #555;"); + m_toField = new everload_tags::TagsLineEdit(); + toLayout->addWidget(toLabel); toLayout->addWidget(m_toField, 1); @@ -466,19 +180,20 @@ void ComposeView::setupUI() { m_ccRow = new QWidget(); QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow); ccLayout->setContentsMargins(0, 0, 0, 0); - QLabel *ccLabel = new QLabel("Cc:"); + QLabel *ccLabel = new QLabel(tr("Cc:")); ccLabel->setFixedWidth(40); ccLabel->setStyleSheet("color: #555;"); - m_ccField = new QLineEdit(); - m_ccField->setPlaceholderText("Carbon copy"); + m_ccField = new everload_tags::TagsLineEdit(); + ccLayout->addWidget(ccLabel); ccLayout->addWidget(m_ccField, 1); - m_hideCcButton = new QPushButton("x"); + m_hideCcButton = new QPushButton(""); + m_hideCcButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Linear/Essentional, UI/Close Square.svg"))); m_hideCcButton->setFixedSize(20, 20); - m_hideCcButton->setToolTip("Hide Cc"); + m_hideCcButton->setToolTip(tr("Hide Cc")); m_hideCcButton->setStyleSheet( - "QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }" + "QPushButton { background: transparent; border: none; color: blue; font-weight: bold; }" "QPushButton:hover { color: #333; }" ); connect(m_hideCcButton, &QPushButton::clicked, this, &ComposeView::onCcToggle); @@ -491,19 +206,20 @@ void ComposeView::setupUI() { m_bccRow = new QWidget(); QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow); bccLayout->setContentsMargins(0, 0, 0, 0); - QLabel *bccLabel = new QLabel("Bcc:"); + QLabel *bccLabel = new QLabel(tr("Bcc:")); bccLabel->setFixedWidth(40); bccLabel->setStyleSheet("color: #555;"); - m_bccField = new QLineEdit(); - m_bccField->setPlaceholderText("Blind carbon copy"); + m_bccField = new everload_tags::TagsLineEdit(); + bccLayout->addWidget(bccLabel); bccLayout->addWidget(m_bccField, 1); - m_hideBccButton = new QPushButton("x"); + m_hideBccButton = new QPushButton(""); m_hideBccButton->setFixedSize(20, 20); - m_hideBccButton->setToolTip("Hide Bcc"); + m_hideBccButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Linear/Essentional, UI/Close Square.svg"))); + m_hideBccButton->setToolTip(tr("Hide Bcc")); m_hideBccButton->setStyleSheet( - "QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }" + "QPushButton { background: transparent; border: none; color: blue; font-weight: bold; }" "QPushButton:hover { color: #333; }" ); connect(m_hideBccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle); @@ -512,11 +228,39 @@ void ComposeView::setupUI() { m_bccRow->setVisible(false); mainLayout->addWidget(m_bccRow); - // Separator - QFrame *line2 = new QFrame(); - line2->setFrameShape(QFrame::HLine); - line2->setStyleSheet("color: #e0e0e0;"); - mainLayout->addWidget(line2); + // Asunto: field + QHBoxLayout *subjectLayout = new QHBoxLayout(); + QLabel *subjectLabel = new QLabel(tr("Asunto:")); + subjectLabel->setFixedWidth(60); + subjectLabel->setStyleSheet("font-weight: bold; color: #555;"); + m_subjectField = new QLineEdit(); + m_subjectField->setPlaceholderText(tr("Subject")); + m_subjectField->setFixedHeight(40); + QFont subjectFont = m_subjectField->font(); + subjectFont.setPointSize(14); + subjectFont.setBold(true); + m_subjectField->setFont(subjectFont); + //subjectLayout->addWidget(subjectLabel); + subjectLayout->addWidget(m_subjectField, 1); + + // Detach button placed to the right of the subject line + m_detachButton = new QPushButton("");//("โ†ฅ Detach"); + m_detachButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Linear/Arrows Action/Square Top Down.svg"))); + m_detachButton->setToolTip(tr("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; }" + "QPushButton:hover { background: #f0f0f0; }" + ); + connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked); + subjectLayout->addWidget(m_detachButton); + + mainLayout->addLayout(subjectLayout); + + // Separator line admin@email.com + QFrame *line1 = new QFrame(); + line1->setFrameShape(QFrame::HLine); + line1->setStyleSheet("color: #e0e0e0;"); + mainLayout->addWidget(line1); // Rich text editor with toolbar m_bodyEditor = new RichTextEditor(); @@ -527,7 +271,7 @@ void ComposeView::setupUI() { m_schedulePanel = new QWidget(); QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel); scheduleLayout->setContentsMargins(0, 0, 0, 0); - QLabel *scheduleLabel = new QLabel("Send at:"); + QLabel *scheduleLabel = new QLabel(tr("Send at:")); scheduleLabel->setStyleSheet("color: #555; font-weight: bold;"); m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600)); m_schedulePicker->setCalendarPopup(true); @@ -536,79 +280,91 @@ void ComposeView::setupUI() { scheduleLayout->addWidget(m_schedulePicker); scheduleLayout->addStretch(); - m_scheduleSendButton = new QPushButton("Schedule Send"); + m_scheduleSendButton = new QPushButton(tr("Schedule Send")); + m_scheduleSendButton->setFixedWidth(150); m_scheduleSendButton->setStyleSheet( "QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 6px 16px; font-weight: bold; }" "QPushButton:hover { background-color: #1565C0; }" ); connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked); - scheduleLayout->addWidget(m_scheduleSendButton); - + scheduleLayout->addWidget(m_scheduleSendButton, 2); + m_schedulePanel->setVisible(false); m_schedulePanel->setVisible(false); mainLayout->addWidget(m_schedulePanel); + // Attachment section QHBoxLayout *attachmentLayout = new QHBoxLayout(); - QLabel *attachmentLabel = new QLabel(tr("Attachments:")); - attachmentLabel->setFixedWidth(80); - attachmentLabel->setStyleSheet("font-weight: bold; color: #555;"); - attachmentLayout->addWidget(attachmentLabel); - - m_attachButton = new QToolButton(); - m_attachButton->setIcon(QIcon(QStringLiteral(":/icons/attachment.svg"))); - m_attachButton->setToolTip(tr("Add attachment")); - m_attachButton->setIconSize(QSize(20,20)); - m_attachButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }"); - bool connected = connect(m_attachButton, &QToolButton::clicked, this, &ComposeView::onAddAttachmentClicked); - qDebug() << "Attach button connected:" << connected; - attachmentLayout->addWidget(m_attachButton); - m_attachmentList = new QListWidget(); + m_attachmentList->setViewMode(QListView::IconMode); + m_attachmentList->setResizeMode(QListView::Adjust); + m_attachmentList->setMovement(QListView::Static); + m_attachmentList->setGridSize(QSize(300, 50)); + m_attachmentList->setSpacing(10); + m_attachmentList->setLayoutDirection(Qt::LeftToRight); m_attachmentList->setSelectionMode(QAbstractItemView::SingleSelection); - m_attachmentList->setMaximumHeight(60); - m_attachmentList->setStyleSheet("QListWidget { border: 1px solid #d1d1d6; border-radius: 4px; }"); + m_attachmentList->setStyleSheet( + "QListWidget { border: 1px solid #d1d1d6; border-radius: 4px; }" + "QListWidget::item { border: none; padding: 5px; }" + "QListWidget::item:selected { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #1976D2, stop:1 #1565C0); border-radius: 4px; }" + ); + m_attachmentList->setVisible(false); attachmentLayout->addWidget(m_attachmentList, 1); - m_removeAttachmentButton = new QToolButton(); - m_removeAttachmentButton->setIcon(QIcon(QStringLiteral(":/icons/trash.svg"))); - m_removeAttachmentButton->setToolTip(tr("Remove selected attachment")); - m_removeAttachmentButton->setIconSize(QSize(20,20)); - m_removeAttachmentButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }"); - connect(m_removeAttachmentButton, &QToolButton::clicked, this, &ComposeView::onRemoveAttachmentClicked); - attachmentLayout->addWidget(m_removeAttachmentButton); - mainLayout->addLayout(attachmentLayout); - // Action buttons row QHBoxLayout *actionsLayout = new QHBoxLayout(); - actionsLayout->addStretch(); - m_discardButton = new QPushButton("Discard"); + // Attachments and Templates buttons (right-aligned) + QHBoxLayout *buttonRow = new QHBoxLayout(); + m_attachButton = new QToolButton(); + m_attachButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Outline/Messages, Conversation/Paperclip.svg"))); + m_attachButton->setToolTip(tr("Add attachment")); + m_attachButton->setIconSize(QSize(20,20)); + m_attachButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }"); + connect(m_attachButton, &QToolButton::clicked, this, &ComposeView::onAddAttachmentClicked); + buttonRow->addWidget(m_attachButton); + + m_templateButton = new QToolButton(); + m_templateButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Outline/Files/File Text.svg"))); // Assuming you have a template icon + m_templateButton->setToolTip(tr("Email templates")); + m_templateButton->setIconSize(QSize(20,20)); + m_templateButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }"); + connect(m_templateButton, &QToolButton::clicked, this, &ComposeView::onTemplateClicked); + buttonRow->addWidget(m_templateButton); + buttonRow->setSpacing(4); + actionsLayout->addLayout(buttonRow); + actionsLayout->addStretch(); // Este resorte empuja el par (label+combo) a la izquierda + + + m_discardButton = new QPushButton(tr("Discard")); m_discardButton->setStyleSheet( "QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px 20px; color: #555; }" "QPushButton:hover { background: #f5f5f5; }" ); - actionsLayout->addWidget(m_discardButton); connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested); + actionsLayout->addWidget(m_discardButton); // Split button for Send / Schedule m_sendMenu = new QMenu(this); - m_sendNowAction = m_sendMenu->addAction("Send Now"); + m_sendNowAction = m_sendMenu->addAction(tr("Send Now")); connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked); - m_scheduleAction = m_sendMenu->addAction("Schedule for later..."); + m_scheduleAction = m_sendMenu->addAction(tr("Schedule for later...")); connect(m_scheduleAction, &QAction::triggered, [this]() { m_schedulePanel->setVisible(true); }); m_sendSplit = new QToolButton(); - m_sendSplit->setText("Send"); - m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly); + m_sendSplit->setText(tr("Send")); + m_sendSplit->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Bold Duotone/Messages, Conversation/Plain.svg"))); + m_templateButton->setIconSize(QSize(20,20)); + //m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly); m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup); m_sendSplit->setMenu(m_sendMenu); m_sendSplit->setStyleSheet( "QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 8px 24px; font-weight: bold; }" "QToolButton:hover { background-color: #1565C0; }" - "QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; width: 40px;}" + "QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; width: 20px;}" "QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }" ); connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked); @@ -633,25 +389,27 @@ void ComposeView::onBccToggle() { void ComposeView::onSendClicked() { emit sendRequested( - m_toField->text(), - m_ccVisible ? m_ccField->text() : QString(), - m_bccVisible ? m_bccField->text() : QString(), + m_toField->tags2().join(", "), + m_ccVisible ? m_ccField->tags2().join(", ") : QString(), + m_bccVisible ? m_bccField->tags2().join(", ") : QString(), m_subjectField->text(), m_bodyEditor->toHtml(), QDateTime(), // null = send now - m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString() + m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString(), + m_attachmentFiles ); } void ComposeView::onScheduleClicked() { emit sendRequested( - m_toField->text(), - m_ccVisible ? m_ccField->text() : QString(), - m_bccVisible ? m_bccField->text() : QString(), + m_toField->tags2().join(", "), + m_ccVisible ? m_ccField->tags2().join(", ") : QString(), + m_bccVisible ? m_bccField->tags2().join(", ") : QString(), m_subjectField->text(), m_bodyEditor->toHtml(), m_schedulePicker->dateTime(), - m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString() + m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString(), + m_attachmentFiles ); } @@ -659,15 +417,19 @@ void ComposeView::onDetachClicked() { emit detachRequested(this); } -void ComposeView::setTo(const QString &to) { m_toField->setText(to); } +void ComposeView::setTo(const QString &to) { m_toField->tags(to.split(',', Qt::SkipEmptyParts)); } void ComposeView::setSubject(const QString &subject) { m_subjectField->setText(subject); } void ComposeView::setBody(const QString &body) { m_bodyEditor->setHtml(body); } + void ComposeView::initializeComposition() { - m_toField->clear(); - m_ccField->clear(); - m_bccField->clear(); + m_toField->tags(QStringList{}); + m_ccField->tags(QStringList{}); + m_bccField->tags(QStringList{}); m_subjectField->clear(); m_bodyEditor->clear(); + m_attachmentFiles.clear(); + m_attachmentList->clear(); + m_attachmentList->setVisible(false); m_ccRow->setVisible(false); m_bccRow->setVisible(false); m_schedulePanel->setVisible(false); @@ -675,35 +437,100 @@ void ComposeView::initializeComposition() { } void ComposeView::startNewEmail(const QString &initialRecipient) { - initializeComposition(); - if (!initialRecipient.isEmpty()) - m_toField->setText(initialRecipient); - m_toField->setFocus(); -} + initializeComposition(); + if (!initialRecipient.isEmpty()) + m_toField->tags(initialRecipient.split(',', Qt::SkipEmptyParts)); + m_toField->setFocus(); + } -void ComposeView::onAddAttachmentClicked() -{ - qDebug() << "Attach button clicked"; - QStringList files = QFileDialog::getOpenFileNames(this, tr("Select Attachments"), QString(), tr("All Files (*)")); - if (files.isEmpty()) - return; - for (const QString &file : files) { - m_attachmentFiles.append(file); - QListWidgetItem *item = new QListWidgetItem(QFileInfo(file).fileName(), m_attachmentList); - item->setToolTip(file); - } - QMessageBox::information(this, tr("Attachments"), tr("Attached %1 file(s).").arg(files.count())); -} + void ComposeView::onAddAttachmentClicked() { + qDebug() << "Attach button clicked"; + QStringList files = QFileDialog::getOpenFileNames(this, tr("Select Attachments"), QString(), tr("All Files (*)")); + if (files.isEmpty()) + return; + for (const QString &file : files) { + m_attachmentFiles.append(file); + QListWidgetItem *item = new QListWidgetItem(m_attachmentList); + item->setData(Qt::UserRole, file); + QWidget *widget = new QWidget; + widget->setFixedSize(300, 50); + widget->setStyleSheet( + "background-color: #f8f9fa; " + "border-radius: 8px; " + "border: 1px solid #e9ecef;" + ); + QHBoxLayout *layout = new QHBoxLayout(widget); + layout->setContentsMargins(8, 4, 8, 4); + layout->setSpacing(8); + // Icon from system + QFileIconProvider provider; + QIcon icon = provider.icon(QFileInfo(file)); + QLabel *iconLabel = new QLabel; + QPixmap pix = icon.pixmap(32, 32); + if (pix.isNull()) + pix = QIcon::fromTheme("unknown").pixmap(32,32); + iconLabel->setPixmap(pix); + iconLabel->setFixedSize(32,32); + layout->addWidget(iconLabel); + // Text vertical layout + QVBoxLayout *textLayout = new QVBoxLayout; + textLayout->setSpacing(2); + QString fileName = QFileInfo(file).fileName(); + QLabel *nameLabel = new QLabel(fileName); + nameLabel->setStyleSheet("font-weight: bold;"); + QLabel *sizeLabel = new QLabel(tr("%1 KB").arg(QFileInfo(file).size()/1024)); + sizeLabel->setStyleSheet("color: #666; font-size: 10px;"); + textLayout->addWidget(nameLabel); + textLayout->addWidget(sizeLabel); + layout->addLayout(textLayout); + layout->addStretch(); + // Delete button + QPushButton *delBtn = new QPushButton; + QIcon delIcon = QIcon::fromTheme("edit-delete"); + if (delIcon.isNull()) + delIcon = QIcon::fromTheme("list-remove"); + if (delIcon.isNull()) + delIcon = QIcon::fromTheme("gtk-delete"); + delBtn->setIcon(delIcon); + delBtn->setToolTip(tr("Remove attachment")); + delBtn->setFixedSize(24,24); + delBtn->setStyleSheet( + "QPushButton { border: none; background: transparent; }" + "QPushButton:hover { background: #e9ecef; border-radius: 4px; }" + ); + layout->addWidget(delBtn); + // Connect delete button + connect(delBtn, &QPushButton::clicked, [this, item]() { + int row = m_attachmentList->row(item); + if (row >= 0) { + QListWidgetItem *it = m_attachmentList->takeItem(row); + if (it) { + m_attachmentFiles.removeAt(row); + delete it; + } + m_attachmentList->setVisible(!m_attachmentFiles.isEmpty()); + } + }); + m_attachmentList->addItem(item); + m_attachmentList->setItemWidget(item, widget); + } + m_attachmentList->setVisible(!m_attachmentFiles.isEmpty()); + } -void ComposeView::onRemoveAttachmentClicked() -{ - QListWidgetItem *item = m_attachmentList->currentItem(); - if (!item) - return; - int row = m_attachmentList->row(item); - m_attachmentList->takeItem(row); - m_attachmentFiles.removeAt(row); - delete item; -} + void ComposeView::onRemoveAttachmentClicked() { + QListWidgetItem *item = m_attachmentList->currentItem(); + if (!item) + return; + int row = m_attachmentList->row(item); + m_attachmentList->takeItem(row); + m_attachmentFiles.removeAt(row); + delete item; + m_attachmentList->setVisible(!m_attachmentFiles.isEmpty()); + } -#include "composeview.moc" \ No newline at end of file + void ComposeView::onTemplateClicked() { + // Placeholder for template functionality + QMessageBox::information(this, tr("Email Templates"), tr("Email templates feature not yet implemented.")); + } + + #include "composeview.moc" diff --git a/src/ui/composeview.cpp.backup b/src/ui/composeview.cpp.backup new file mode 100644 index 0000000..57e842c --- /dev/null +++ b/src/ui/composeview.cpp.backup @@ -0,0 +1,632 @@ +#include "ui/composeview.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// ===================== RichTextEditor ===================== + +RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) { + setAcceptRichText(true); + setPlaceholderText("Write your message here..."); +} + +void RichTextEditor::setupToolbar(QVBoxLayout *layout) { + m_toolbar = new QToolBar("Formatting"); + m_toolbar->setIconSize(QSize(16, 16)); + m_toolbar->setStyleSheet( + "QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }" + "QToolButton { padding: 4px 6px; border-radius: 3px; }" + "QToolButton:hover { background: #e0e0e0; }" + "QToolButton:checked { background: #bbdefb; }" + ); + + // Font family combo + m_fontCombo = new QFontComboBox(); + m_fontCombo->setFixedWidth(150); + connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged); + m_toolbar->addWidget(m_fontCombo); + + // Font size spin + m_fontSizeSpin = new QSpinBox(); + m_fontSizeSpin->setRange(8, 72); + m_fontSizeSpin->setValue(14); + m_fontSizeSpin->setFixedWidth(50); + connect(m_fontSizeSpin, QOverload::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged); + m_toolbar->addWidget(m_fontSizeSpin); + + m_toolbar->addSeparator(); + + // Bold / Italic / Underline + QAction *boldAct = m_toolbar->addAction("B"); + boldAct->setCheckable(true); + QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont); + connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold); + + QAction *italicAct = m_toolbar->addAction("I"); + italicAct->setCheckable(true); + QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont); + connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic); + + QAction *underlineAct = m_toolbar->addAction("U"); + underlineAct->setCheckable(true); + QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont); + connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline); + + m_toolbar->addSeparator(); + + // Alignment + QAction *alignLeft = m_toolbar->addAction("L"); + connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft); + + QAction *alignCenter = m_toolbar->addAction("C"); + connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter); + + QAction *alignRight = m_toolbar->addAction("R"); + connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight); + + QAction *alignJustify = m_toolbar->addAction("J"); + connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify); + + m_toolbar->addSeparator(); + + // Lists + QAction *bulletAct = m_toolbar->addAction("Bullets"); + connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList); + + QAction *numAct = m_toolbar->addAction("1. List"); + connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList); + + m_toolbar->addSeparator(); + + // Indent / Outdent + QAction *indentAct = m_toolbar->addAction("Indent"); + connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent); + + QAction *outdentAct = m_toolbar->addAction("Outdent"); + connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent); + + m_toolbar->addSeparator(); + + // Insert image + QAction *imgAct = m_toolbar->addAction("Img"); + connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage); + + // Insert table + QAction *tableAct = m_toolbar->addAction("Tbl"); + connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable); + + // 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() { + QTextCharFormat fmt; + fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onItalic() { + QTextCharFormat fmt; + fmt.setFontItalic(!textCursor().charFormat().fontItalic()); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onUnderline() { + QTextCharFormat fmt; + fmt.setFontUnderline(!textCursor().charFormat().fontUnderline()); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onBulletList() { + QTextCursor cursor = textCursor(); + QTextList *list = cursor.currentList(); + if (list && list->format().style() == QTextListFormat::ListDisc) { + // Remove list + QTextBlockFormat bfmt; + bfmt.setIndent(0); + cursor.setBlockFormat(bfmt); + list->remove(cursor.block()); + } else { + QTextListFormat listFormat; + listFormat.setStyle(QTextListFormat::ListDisc); + cursor.createList(listFormat); + } +} + +void RichTextEditor::onNumberedList() { + QTextCursor cursor = textCursor(); + QTextList *list = cursor.currentList(); + if (list && list->format().style() == QTextListFormat::ListDecimal) { + QTextBlockFormat bfmt; + bfmt.setIndent(0); + cursor.setBlockFormat(bfmt); + list->remove(cursor.block()); + } else { + QTextListFormat listFormat; + listFormat.setStyle(QTextListFormat::ListDecimal); + cursor.createList(listFormat); + } +} + +void RichTextEditor::onIndent() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setIndent(bfmt.indent() + 1); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onOutdent() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + int indent = bfmt.indent(); + if (indent > 0) { + bfmt.setIndent(indent - 1); + cursor.setBlockFormat(bfmt); + } +} + +void RichTextEditor::onAlignLeft() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignLeft); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignCenter() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignCenter); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignRight() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignRight); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignJustify() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignJustify); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onFontChanged(const QFont &font) { + QTextCharFormat fmt; + fmt.setFontFamilies({font.family()}); + mergeCurrentCharFormat(fmt); +} + +void RichTextEditor::onFontSizeChanged(int size) { + QTextCharFormat fmt; + fmt.setFontPointSize(size); + mergeCurrentCharFormat(fmt); +} + +void RichTextEditor::onInsertImage() { + QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)"); + if (filePath.isEmpty()) return; + QTextCursor cursor = textCursor(); + QTextImageFormat imgFmt; + imgFmt.setName(filePath); + // Scale down if too large + QPixmap pm(filePath); + if (pm.width() > 600) { + imgFmt.setWidth(600); + imgFmt.setHeight(pm.height() * 600 / pm.width()); + } + cursor.insertImage(imgFmt); +} + +void RichTextEditor::onInsertTable() { + bool ok; + int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok); + if (!ok) return; + int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok); + if (!ok) return; + + QTextCursor cursor = textCursor(); + QTextTableFormat tableFmt; + tableFmt.setBorder(1); + tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid); + tableFmt.setCellPadding(4); + tableFmt.setCellSpacing(0); + tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100)); + cursor.insertTable(rows, cols, tableFmt); +} + +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 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); + mainLayout->setSpacing(8); + + this->setStyleSheet( + "QLineEdit, QTextEdit { border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px; font-family: 'Segoe UI', Helvetica; font-size: 13px; }" + "QLineEdit:focus, QTextEdit:focus { border-color: #1976D2; }" + ); + + // Header row: Subject + Detach button + QHBoxLayout *headerLayout = new QHBoxLayout(); + m_subjectField = new QLineEdit(); + m_subjectField->setPlaceholderText("Subject"); + m_subjectField->setFixedHeight(40); + QFont subjectFont = m_subjectField->font(); + subjectFont.setPointSize(14); + subjectFont.setBold(true); + m_subjectField->setFont(subjectFont); + headerLayout->addWidget(m_subjectField, 1); + + 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; }" + "QPushButton:hover { background: #f0f0f0; }" + ); + connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked); + headerLayout->addWidget(m_detachButton); + + mainLayout->addLayout(headerLayout); + + // Separator line + QFrame *line1 = new QFrame(); + line1->setFrameShape(QFrame::HLine); + 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::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:"); + toLabel->setFixedWidth(40); + toLabel->setStyleSheet("font-weight: bold; color: #555;"); + m_toField = new QLineEdit(); + m_toField->setPlaceholderText("Recipients (comma separated)"); + toLayout->addWidget(toLabel); + toLayout->addWidget(m_toField, 1); + + m_ccButton = new QPushButton("Cc"); + m_ccButton->setFixedWidth(32); + m_ccButton->setStyleSheet( + "QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }" + "QPushButton:hover { background: #d0d0d0; }" + ); + connect(m_ccButton, &QPushButton::clicked, this, &ComposeView::onCcToggle); + toLayout->addWidget(m_ccButton); + + m_bccButton = new QPushButton("Bcc"); + m_bccButton->setFixedWidth(36); + m_bccButton->setStyleSheet( + "QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }" + "QPushButton:hover { background: #d0d0d0; }" + ); + connect(m_bccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle); + toLayout->addWidget(m_bccButton); + + mainLayout->addLayout(toLayout); + + // Cc: row (hidden by default) + m_ccRow = new QWidget(); + QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow); + ccLayout->setContentsMargins(0, 0, 0, 0); + QLabel *ccLabel = new QLabel("Cc:"); + ccLabel->setFixedWidth(40); + ccLabel->setStyleSheet("color: #555;"); + m_ccField = new QLineEdit(); + m_ccField->setPlaceholderText("Carbon copy"); + ccLayout->addWidget(ccLabel); + ccLayout->addWidget(m_ccField, 1); + + m_hideCcButton = new QPushButton("x"); + m_hideCcButton->setFixedSize(20, 20); + m_hideCcButton->setToolTip("Hide Cc"); + m_hideCcButton->setStyleSheet( + "QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }" + "QPushButton:hover { color: #333; }" + ); + connect(m_hideCcButton, &QPushButton::clicked, this, &ComposeView::onCcToggle); + ccLayout->addWidget(m_hideCcButton); + + m_ccRow->setVisible(false); + mainLayout->addWidget(m_ccRow); + + // Bcc: row (hidden by default) + m_bccRow = new QWidget(); + QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow); + bccLayout->setContentsMargins(0, 0, 0, 0); + QLabel *bccLabel = new QLabel("Bcc:"); + bccLabel->setFixedWidth(40); + bccLabel->setStyleSheet("color: #555;"); + m_bccField = new QLineEdit(); + m_bccField->setPlaceholderText("Blind carbon copy"); + bccLayout->addWidget(bccLabel); + bccLayout->addWidget(m_bccField, 1); + + m_hideBccButton = new QPushButton("x"); + m_hideBccButton->setFixedSize(20, 20); + m_hideBccButton->setToolTip("Hide Bcc"); + m_hideBccButton->setStyleSheet( + "QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }" + "QPushButton:hover { color: #333; }" + ); + connect(m_hideBccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle); + bccLayout->addWidget(m_hideBccButton); + + m_bccRow->setVisible(false); + mainLayout->addWidget(m_bccRow); + + // Separator + QFrame *line2 = new QFrame(); + line2->setFrameShape(QFrame::HLine); + line2->setStyleSheet("color: #e0e0e0;"); + mainLayout->addWidget(line2); + + // Rich text editor with toolbar + m_bodyEditor = new RichTextEditor(); + m_bodyEditor->setupToolbar(mainLayout); + mainLayout->addWidget(m_bodyEditor, 1); + + // Schedule panel + m_schedulePanel = new QWidget(); + QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel); + scheduleLayout->setContentsMargins(0, 0, 0, 0); + QLabel *scheduleLabel = new QLabel("Send at:"); + scheduleLabel->setStyleSheet("color: #555; font-weight: bold;"); + m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600)); + m_schedulePicker->setCalendarPopup(true); + m_schedulePicker->setDisplayFormat("dd/MM/yyyy hh:mm AP"); + scheduleLayout->addWidget(scheduleLabel); + scheduleLayout->addWidget(m_schedulePicker); + scheduleLayout->addStretch(); + + m_scheduleSendButton = new QPushButton("Schedule Send"); + m_scheduleSendButton->setStyleSheet( + "QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 6px 16px; font-weight: bold; }" + "QPushButton:hover { background-color: #1565C0; }" + ); + connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked); + scheduleLayout->addWidget(m_scheduleSendButton); + + m_schedulePanel->setVisible(false); + mainLayout->addWidget(m_schedulePanel); + + // Action buttons row + QHBoxLayout *actionsLayout = new QHBoxLayout(); + actionsLayout->addStretch(); + + m_discardButton = new QPushButton("Discard"); + m_discardButton->setStyleSheet( + "QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px 20px; color: #555; }" + "QPushButton:hover { background: #f5f5f5; }" + ); + actionsLayout->addWidget(m_discardButton); + connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested); + + // Split button for Send / Schedule + m_sendMenu = new QMenu(this); + m_sendNowAction = m_sendMenu->addAction("Send Now"); + connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked); + m_scheduleAction = m_sendMenu->addAction("Schedule for later..."); + connect(m_scheduleAction, &QAction::triggered, [this]() { + m_schedulePanel->setVisible(true); + }); + + m_sendSplit = new QToolButton(); + m_sendSplit->setText("Send"); + m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly); + m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup); + m_sendSplit->setMenu(m_sendMenu); + m_sendSplit->setStyleSheet( + "QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 8px 24px; font-weight: bold; }" + "QToolButton:hover { background-color: #1565C0; }" + "QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; }" + "QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }" + ); + connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked); + actionsLayout->addWidget(m_sendSplit); + + mainLayout->addLayout(actionsLayout); +} + +void ComposeView::onCcToggle() { + m_ccVisible = !m_ccVisible; + m_ccRow->setVisible(m_ccVisible); + m_ccButton->setVisible(!m_ccVisible); + if (m_ccVisible) m_ccField->setFocus(); +} + +void ComposeView::onBccToggle() { + m_bccVisible = !m_bccVisible; + m_bccRow->setVisible(m_bccVisible); + m_bccButton->setVisible(!m_bccVisible); + if (m_bccVisible) m_bccField->setFocus(); +} + +void ComposeView::onSendClicked() { + emit sendRequested( + m_toField->text(), + m_ccVisible ? m_ccField->text() : QString(), + m_bccVisible ? m_bccField->text() : QString(), + m_subjectField->text(), + m_bodyEditor->toHtml(), + QDateTime(), // null = send now + m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString() + ); +} + +void ComposeView::onScheduleClicked() { + emit sendRequested( + m_toField->text(), + m_ccVisible ? m_ccField->text() : QString(), + m_bccVisible ? m_bccField->text() : QString(), + m_subjectField->text(), + m_bodyEditor->toHtml(), + m_schedulePicker->dateTime(), + m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString() + ); +} + +void ComposeView::onDetachClicked() { + emit detachRequested(this); +} + +void ComposeView::setTo(const QString &to) { m_toField->setText(to); } +void ComposeView::setSubject(const QString &subject) { m_subjectField->setText(subject); } +void ComposeView::setBody(const QString &body) { m_bodyEditor->setHtml(body); } +void ComposeView::initializeComposition() { + m_toField->clear(); + m_ccField->clear(); + m_bccField->clear(); + m_subjectField->clear(); + m_bodyEditor->clear(); + m_ccRow->setVisible(false); + m_bccRow->setVisible(false); + m_schedulePanel->setVisible(false); + m_toField->setFocus(); +} + +void ComposeView::startNewEmail(const QString &initialRecipient) { + initializeComposition(); + if (!initialRecipient.isEmpty()) + m_toField->setText(initialRecipient); + m_toField->setFocus(); +} +#include "composeview.moc" \ No newline at end of file diff --git a/src/ui/composeview.cpp.backup2 b/src/ui/composeview.cpp.backup2 new file mode 100644 index 0000000..0483277 --- /dev/null +++ b/src/ui/composeview.cpp.backup2 @@ -0,0 +1,691 @@ +#include "ui/composeview.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// ===================== RichTextEditor ===================== + +RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) { + setAcceptRichText(true); + setPlaceholderText("Write your message here..."); +} + +void RichTextEditor::setupToolbar(QVBoxLayout *layout) { + m_toolbar = new QToolBar("Formatting"); + m_toolbar->setIconSize(QSize(16, 16)); + m_toolbar->setStyleSheet( + "QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }" + "QToolButton { padding: 4px 6px; border-radius: 3px; }" + "QToolButton:hover { background: #e0e0e0; }" + "QToolButton:checked { background: #bbdefb; }" + ); + + // Font family combo + m_fontCombo = new QFontComboBox(); + m_fontCombo->setFixedWidth(150); + connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged); + m_toolbar->addWidget(m_fontCombo); + + // Font size spin + m_fontSizeSpin = new QSpinBox(); + m_fontSizeSpin->setRange(8, 72); + m_fontSizeSpin->setValue(14); + m_fontSizeSpin->setFixedWidth(50); + connect(m_fontSizeSpin, QOverload::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged); + m_toolbar->addWidget(m_fontSizeSpin); + + m_toolbar->addSeparator(); + + // Bold / Italic / Underline + QAction *boldAct = m_toolbar->addAction("B"); + boldAct->setCheckable(true); + QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont); + connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold); + + QAction *italicAct = m_toolbar->addAction("I"); + italicAct->setCheckable(true); + QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont); + connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic); + + QAction *underlineAct = m_toolbar->addAction("U"); + underlineAct->setCheckable(true); + QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont); + connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline); + + m_toolbar->addSeparator(); + + // Alignment + QAction *alignLeft = m_toolbar->addAction("L"); + connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft); + + QAction *alignCenter = m_toolbar->addAction("C"); + connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter); + + QAction *alignRight = m_toolbar->addAction("R"); + connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight); + + QAction *alignJustify = m_toolbar->addAction("J"); + connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify); + + m_toolbar->addSeparator(); + + // Lists + QAction *bulletAct = m_toolbar->addAction("Bullets"); + connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList); + + QAction *numAct = m_toolbar->addAction("1. List"); + connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList); + + m_toolbar->addSeparator(); + + // Indent / Outdent + QAction *indentAct = m_toolbar->addAction("Indent"); + connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent); + + QAction *outdentAct = m_toolbar->addAction("Outdent"); + connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent); + + m_toolbar->addSeparator(); + + // Insert image + QAction *imgAct = m_toolbar->addAction("Img"); + connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage); + + // Insert table + QAction *tableAct = m_toolbar->addAction("Tbl"); + connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable); + + // 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() { + QTextCharFormat fmt; + fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onItalic() { + QTextCharFormat fmt; + fmt.setFontItalic(!textCursor().charFormat().fontItalic()); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onUnderline() { + QTextCharFormat fmt; + fmt.setFontUnderline(!textCursor().charFormat().fontUnderline()); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onBulletList() { + QTextCursor cursor = textCursor(); + QTextList *list = cursor.currentList(); + if (list && list->format().style() == QTextListFormat::ListDisc) { + // Remove list + QTextBlockFormat bfmt; + bfmt.setIndent(0); + cursor.setBlockFormat(bfmt); + list->remove(cursor.block()); + } else { + QTextListFormat listFormat; + listFormat.setStyle(QTextListFormat::ListDisc); + cursor.createList(listFormat); + } +} + +void RichTextEditor::onNumberedList() { + QTextCursor cursor = textCursor(); + QTextList *list = cursor.currentList(); + if (list && list->format().style() == QTextListFormat::ListDecimal) { + QTextBlockFormat bfmt; + bfmt.setIndent(0); + cursor.setBlockFormat(bfmt); + list->remove(cursor.block()); + } else { + QTextListFormat listFormat; + listFormat.setStyle(QTextListFormat::ListDecimal); + cursor.createList(listFormat); + } +} + +void RichTextEditor::onIndent() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setIndent(bfmt.indent() + 1); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onOutdent() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + int indent = bfmt.indent(); + if (indent > 0) { + bfmt.setIndent(indent - 1); + cursor.setBlockFormat(bfmt); + } +} + +void RichTextEditor::onAlignLeft() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignLeft); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignCenter() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignCenter); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignRight() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignRight); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignJustify() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignJustify); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onFontChanged(const QFont &font) { + QTextCharFormat fmt; + fmt.setFontFamilies({font.family()}); + mergeCurrentCharFormat(fmt); +} + +void RichTextEditor::onFontSizeChanged(int size) { + QTextCharFormat fmt; + fmt.setFontPointSize(size); + mergeCurrentCharFormat(fmt); +} + +void RichTextEditor::onInsertImage() { + QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)"); + if (filePath.isEmpty()) return; + QTextCursor cursor = textCursor(); + QTextImageFormat imgFmt; + imgFmt.setName(filePath); + // Scale down if too large + QPixmap pm(filePath); + if (pm.width() > 600) { + imgFmt.setWidth(600); + imgFmt.setHeight(pm.height() * 600 / pm.width()); + } + cursor.insertImage(imgFmt); +} + +void RichTextEditor::onInsertTable() { + bool ok; + int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok); + if (!ok) return; + int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok); + if (!ok) return; + + QTextCursor cursor = textCursor(); + QTextTableFormat tableFmt; + tableFmt.setBorder(1); + tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid); + tableFmt.setCellPadding(4); + tableFmt.setCellSpacing(0); + tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100)); + cursor.insertTable(rows, cols, tableFmt); +} + +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(); + // Update completer models for address widgets with known emails + if (m_accountService) { + QStringList knownEmails; + const QVector accounts = m_accountService->getAllAccounts(); + for (const Account &acc : accounts) { + knownEmails << acc.email(); + } + // Set completion model for each widget + QStringListModel *model = new QStringListModel(knownEmails, this); + QCompleter *comp = new QCompleter(model, this); + comp->setCaseSensitivity(Qt::CaseInsensitive); + if (m_toField) m_toField->setCompleter(comp); + // For cc and bcc we need separate completers but can share model + QCompleter *comp2 = new QCompleter(model, this); + comp2->setCaseSensitivity(Qt::CaseInsensitive); + if (m_ccField) m_ccField->setCompleter(comp2); + QCompleter *comp3 = new QCompleter(model, this); + comp3->setCaseSensitivity(Qt::CaseInsensitive); + if (m_bccField) m_bccField->setCompleter(comp3); + } +} + +void ComposeView::populateAccountCombo() +{ + if (!m_accountService) { + qWarning() << "AccountService not set"; + return; + } + + m_accountCombo->clear(); + m_accountCombo->addItem(tr("Select Account..."), QVariant()); + + QVector 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); + mainLayout->setSpacing(8); + + // Top row: Account selector and detach button + QHBoxLayout *topLayout = new QHBoxLayout(); + m_accountLabel = new QLabel(tr("From:"), this); + m_accountLabel->setFixedWidth(40); + m_accountLabel->setStyleSheet(QStringLiteral("font-weight: bold; color: #555;")); + m_accountCombo = new QComboBox(this); + 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; }" + ); + m_detachButton = new QPushButton(this); + m_detachButton->setToolTip(tr("Detach compose window")); + m_detachButton->setText(QStringLiteral("โคข")); + m_detachButton->setFixedSize(28,28); + m_detachButton->setStyleSheet( + "QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; " + "padding: 6px; color: #555; font-size: 14px; }" + + "QPushButton:hover { background: #f0f0f0; }" + + ); + connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked); + + topLayout->addWidget(m_accountLabel); + topLayout->addWidget(m_accountCombo, 1); + topLayout->addWidget(m_detachButton); + mainLayout->addLayout(topLayout); + + // Separator line + QFrame *line1 = new QFrame(this); + line1->setFrameShape(QFrame::HLine); + line1->setStyleSheet(QStringLiteral("color: #e0e0e0;")); + mainLayout->addWidget(line1); + + // To row + QHBoxLayout *toLayout = new QHBoxLayout(); + QLabel *toLabel = new QLabel(tr("To:"), this); + toLabel->setFixedWidth(40); + toLabel->setStyleSheet(QStringLiteral("font-weight: bold; color: #555;")); + m_toField = new QLineEdit(this); + m_toField->setPlaceholderText(tr("To recipients")); + toLayout->addWidget(toLabel); + toLayout->addWidget(m_toField, 1); + mainLayout->addLayout(toLayout); + + // CC/BCC toggle buttons + QHBoxLayout *toggleLayout = new QHBoxLayout(); + m_ccButton = new QPushButton(tr("CC"), this); + m_ccButton->setCheckable(true); + m_ccButton->setChecked(false); + connect(m_ccButton, &QPushButton::toggled, this, &ComposeView::onCcToggled); + m_bccButton = new QPushButton(tr("BCC"), this); + m_bccButton->setCheckable(true); + m_bccButton->setChecked(false); + connect(m_bccButton, &QPushButton::toggled, this, &ComposeView::onBccToggled); + toggleLayout->addWidget(m_ccButton); + toggleLayout->addWidget(m_bccButton); + toggleLayout->addStretch(); + mainLayout->addLayout(toggleLayout); + + // Cc row (hidden by default) + m_ccRow = new QWidget(this); + QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow); + ccLayout->setContentsMargins(0,0,0,0); + QLabel *ccLabel = new QLabel(tr("Cc:"), this); + ccLabel->setFixedWidth(40); + ccLabel->setStyleSheet(QStringLiteral("color: #555;")); + m_ccField = new QLineEdit(this); + m_ccField->setPlaceholderText(tr("Cc recipients")); + ccLayout->addWidget(ccLabel); + ccLayout->addWidget(m_ccField, 1); + m_ccRow->setVisible(false); + mainLayout->addWidget(m_ccRow); + + // Bcc row (hidden by default) + m_bccRow = new QWidget(this); + QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow); + bccLayout->setContentsMargins(0,0,0,0); + QLabel *bccLabel = new QLabel(tr("Bcc:"), this); + bccLabel->setFixedWidth(40); + bccLabel->setStyleSheet(QStringLiteral("color: #555;")); + m_bccField = new QLineEdit(this); + m_bccField->setPlaceholderText(tr("Bcc recipients")); + bccLayout->addWidget(bccLabel); + bccLayout->addWidget(m_bccField, 1); + m_bccRow->setVisible(false); + mainLayout->addWidget(m_bccRow); + + // Separator + QFrame *line2 = new QFrame(this); + line2->setFrameShape(QFrame::HLine); + line2->setStyleSheet(QStringLiteral("color: #e0e0e0;")); + mainLayout->addWidget(line2); + + // Subject field + QLabel *subjectLabel = new QLabel(tr("Subject:"), this); + subjectLabel->setFixedWidth(50); + subjectLabel->setStyleSheet(QStringLiteral("font-weight: bold; color: #555;")); + m_subjectField = new QLineEdit(this); + m_subjectField->setPlaceholderText(tr("Subject")); + m_subjectField->setFixedHeight(36); + QFont subjectFont = m_subjectField->font(); + subjectFont.setPointSize(14); + subjectFont.setBold(true); + m_subjectField->setFont(subjectFont); + QHBoxLayout *subjectLayout = new QHBoxLayout(); + subjectLayout->addWidget(subjectLabel); + subjectLayout->addWidget(m_subjectField, 1); + mainLayout->addLayout(subjectLayout); + + // Body editor with toolbar + m_bodyEditor = new RichTextEditor(this); + m_bodyEditor->setupToolbar(mainLayout); + mainLayout->addWidget(m_bodyEditor, 1); + + // Schedule panel (hidden by default) + m_schedulePanel = new QWidget(this); + QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel); + scheduleLayout->setContentsMargins(0,0,0,0); + QLabel *scheduleLabel = new QLabel(tr("Send at:"), this); + scheduleLabel->setStyleSheet(QStringLiteral("color: #555; font-weight: bold;")); + m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600), this); + m_schedulePicker->setCalendarPopup(true); + m_schedulePicker->setDisplayFormat(QStringLiteral("dd/MM/yyyy hh:mm AP")); + scheduleLayout->addWidget(scheduleLabel); + scheduleLayout->addWidget(m_schedulePicker); + scheduleLayout->addStretch(); + m_scheduleSendButton = new QPushButton(tr("Schedule Send"), this); + m_scheduleSendButton->setStyleSheet( + "QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; " + "padding: 6px 16px; font-weight: bold; }" + + "QPushButton:hover { background-color: #1565C0; }" + + ); + connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked); + scheduleLayout->addWidget(m_scheduleSendButton); + m_schedulePanel->setVisible(false); + mainLayout->addWidget(m_schedulePanel); + + // Action buttons row + QHBoxLayout *actionsLayout = new QHBoxLayout(); + actionsLayout->addStretch(); + + m_discardButton = new QPushButton(tr("Discard"), this); + m_discardButton->setStyleSheet( + "QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; " + "padding: 8px 20px; color: #555; }" + + "QPushButton:hover { background: #f5f5f5; }" + + ); + actionsLayout->addWidget(m_discardButton); + connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested); + + // Split button for Send / Schedule + m_sendMenu = new QMenu(this); + m_sendNowAction = m_sendMenu->addAction(tr("Send Now")); + connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked); + m_scheduleAction = m_sendMenu->addAction(tr("Schedule for later...")); + connect(m_scheduleAction, &QAction::triggered, [this]() { + m_schedulePanel->setVisible(true); + }); + + m_sendSplit = new QToolButton(this); + m_sendSplit->setText(tr("Send")); + m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly); + m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup); + m_sendSplit->setMenu(m_sendMenu); + m_sendSplit->setStyleSheet( + "QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; " + "padding: 8px 24px; font-weight: bold; }" + + "QToolButton:hover { background-color: #1565C0; }" + + "QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; }" + + "QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }" + + ); + connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked); + actionsLayout->addWidget(m_sendSplit); + + mainLayout->addLayout(actionsLayout); + + // Connect rich text editor signature signals + connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked); + connect(m_bodyEditor, &RichTextEditor::signatureEditRequested, this, &ComposeView::onSignatureEditRequested); +} +void ComposeView::onCcToggled(bool checked) +{ + m_ccRow->setVisible(checked); + if (checked) { + m_ccField->setFocus(); + } +} +void ComposeView::onBccToggled(bool checked) +{ + m_bccRow->setVisible(checked); + if (checked) { + m_bccField->setFocus(); + } +} +void ComposeView::onSendClicked() +{ + QString to = m_toField->text(); + QString cc = m_ccButton->isChecked() ? m_ccField->text() : QString(); + QString bcc = m_bccButton->isChecked() ? m_bccField->text() : QString(); + QString subject = m_subjectField->text(); + QString body = m_bodyEditor->toHtml(); + QDateTime scheduleTime = m_schedulePanel->isVisible() ? m_schedulePicker->dateTime() : QDateTime(); + QString fromAddress; + if (m_currentAccountId > 0 && m_accountCombo->currentIndex() > 0) { + fromAddress = m_accountCombo->currentData().toString(); + } + emit sendRequested(to, cc, bcc, subject, body, scheduleTime, fromAddress); +} +void ComposeView::onScheduleClicked() { + emit sendRequested( + m_toField->text(), + m_ccVisible ? m_ccField->text() : QString(), + m_bccVisible ? m_bccField->text() : QString(), + m_subjectField->text(), + m_bodyEditor->toHtml(), + m_schedulePicker->dateTime(), + m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString() + ); +} + +void ComposeView::onDetachClicked() { + emit detachRequested(this); +} + +void ComposeView::setTo(const QString &to) +{ + if (to.isEmpty()) { + m_toField->clear(); + return; + } + // Split by semicolon or comma + QStringList parts = to.split(QRegularExpression("[,;]"), Qt::SkipEmptyParts); + QStringList cleaned; + for (const QString &part : parts) { + QString trimmed = part.trimmed(); + if (!trimmed.isEmpty()) + cleaned << trimmed; + } + m_toField->setText(cleaned.join("; ")); +} + +void ComposeView::setSubject(const QString &subject) +{ + m_subjectField->setText(subject); +} + +void ComposeView::setBody(const QString &body) +{ + m_bodyEditor->setHtml(body); +} + +void ComposeView::initializeComposition() +{ + m_subjectField->clear(); + m_bodyEditor->clear(); + m_toField->clear(); + m_ccField->clear(); + m_bccField->clear(); + m_ccRow->setVisible(false); + m_bccRow->setVisible(false); + m_schedulePanel->setVisible(false); + m_subjectField->setFocus(); +} +void ComposeView::startNewEmail(const QString &initialRecipient) +{ + initializeComposition(); + if (!initialRecipient.isEmpty()) { + m_toField->setText(initialRecipient); + } + m_toField->setFocus(); +} + +void ComposeView::setSubject(const QString &subject) +{ + m_subjectField->setText(subject); +} + +void ComposeView::setBody(const QString &body) +{ + m_bodyEditor->setHtml(body); +} + +void ComposeView::initializeComposition() +{ + m_subjectField->clear(); + m_bodyEditor->clear(); + m_toField->clear(); + m_ccField->clear(); + m_bccField->clear(); + m_ccRow->setVisible(false); + m_bccRow->setVisible(false); + m_schedulePanel->setVisible(false); + m_subjectField->setFocus(); +} +#include "composeview.moc" \ No newline at end of file diff --git a/src/ui/composeview.cpp.backup3 b/src/ui/composeview.cpp.backup3 new file mode 100644 index 0000000..0483277 --- /dev/null +++ b/src/ui/composeview.cpp.backup3 @@ -0,0 +1,691 @@ +#include "ui/composeview.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// ===================== RichTextEditor ===================== + +RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) { + setAcceptRichText(true); + setPlaceholderText("Write your message here..."); +} + +void RichTextEditor::setupToolbar(QVBoxLayout *layout) { + m_toolbar = new QToolBar("Formatting"); + m_toolbar->setIconSize(QSize(16, 16)); + m_toolbar->setStyleSheet( + "QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }" + "QToolButton { padding: 4px 6px; border-radius: 3px; }" + "QToolButton:hover { background: #e0e0e0; }" + "QToolButton:checked { background: #bbdefb; }" + ); + + // Font family combo + m_fontCombo = new QFontComboBox(); + m_fontCombo->setFixedWidth(150); + connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged); + m_toolbar->addWidget(m_fontCombo); + + // Font size spin + m_fontSizeSpin = new QSpinBox(); + m_fontSizeSpin->setRange(8, 72); + m_fontSizeSpin->setValue(14); + m_fontSizeSpin->setFixedWidth(50); + connect(m_fontSizeSpin, QOverload::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged); + m_toolbar->addWidget(m_fontSizeSpin); + + m_toolbar->addSeparator(); + + // Bold / Italic / Underline + QAction *boldAct = m_toolbar->addAction("B"); + boldAct->setCheckable(true); + QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont); + connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold); + + QAction *italicAct = m_toolbar->addAction("I"); + italicAct->setCheckable(true); + QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont); + connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic); + + QAction *underlineAct = m_toolbar->addAction("U"); + underlineAct->setCheckable(true); + QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont); + connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline); + + m_toolbar->addSeparator(); + + // Alignment + QAction *alignLeft = m_toolbar->addAction("L"); + connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft); + + QAction *alignCenter = m_toolbar->addAction("C"); + connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter); + + QAction *alignRight = m_toolbar->addAction("R"); + connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight); + + QAction *alignJustify = m_toolbar->addAction("J"); + connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify); + + m_toolbar->addSeparator(); + + // Lists + QAction *bulletAct = m_toolbar->addAction("Bullets"); + connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList); + + QAction *numAct = m_toolbar->addAction("1. List"); + connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList); + + m_toolbar->addSeparator(); + + // Indent / Outdent + QAction *indentAct = m_toolbar->addAction("Indent"); + connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent); + + QAction *outdentAct = m_toolbar->addAction("Outdent"); + connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent); + + m_toolbar->addSeparator(); + + // Insert image + QAction *imgAct = m_toolbar->addAction("Img"); + connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage); + + // Insert table + QAction *tableAct = m_toolbar->addAction("Tbl"); + connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable); + + // 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() { + QTextCharFormat fmt; + fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onItalic() { + QTextCharFormat fmt; + fmt.setFontItalic(!textCursor().charFormat().fontItalic()); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onUnderline() { + QTextCharFormat fmt; + fmt.setFontUnderline(!textCursor().charFormat().fontUnderline()); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onBulletList() { + QTextCursor cursor = textCursor(); + QTextList *list = cursor.currentList(); + if (list && list->format().style() == QTextListFormat::ListDisc) { + // Remove list + QTextBlockFormat bfmt; + bfmt.setIndent(0); + cursor.setBlockFormat(bfmt); + list->remove(cursor.block()); + } else { + QTextListFormat listFormat; + listFormat.setStyle(QTextListFormat::ListDisc); + cursor.createList(listFormat); + } +} + +void RichTextEditor::onNumberedList() { + QTextCursor cursor = textCursor(); + QTextList *list = cursor.currentList(); + if (list && list->format().style() == QTextListFormat::ListDecimal) { + QTextBlockFormat bfmt; + bfmt.setIndent(0); + cursor.setBlockFormat(bfmt); + list->remove(cursor.block()); + } else { + QTextListFormat listFormat; + listFormat.setStyle(QTextListFormat::ListDecimal); + cursor.createList(listFormat); + } +} + +void RichTextEditor::onIndent() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setIndent(bfmt.indent() + 1); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onOutdent() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + int indent = bfmt.indent(); + if (indent > 0) { + bfmt.setIndent(indent - 1); + cursor.setBlockFormat(bfmt); + } +} + +void RichTextEditor::onAlignLeft() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignLeft); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignCenter() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignCenter); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignRight() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignRight); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignJustify() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignJustify); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onFontChanged(const QFont &font) { + QTextCharFormat fmt; + fmt.setFontFamilies({font.family()}); + mergeCurrentCharFormat(fmt); +} + +void RichTextEditor::onFontSizeChanged(int size) { + QTextCharFormat fmt; + fmt.setFontPointSize(size); + mergeCurrentCharFormat(fmt); +} + +void RichTextEditor::onInsertImage() { + QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)"); + if (filePath.isEmpty()) return; + QTextCursor cursor = textCursor(); + QTextImageFormat imgFmt; + imgFmt.setName(filePath); + // Scale down if too large + QPixmap pm(filePath); + if (pm.width() > 600) { + imgFmt.setWidth(600); + imgFmt.setHeight(pm.height() * 600 / pm.width()); + } + cursor.insertImage(imgFmt); +} + +void RichTextEditor::onInsertTable() { + bool ok; + int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok); + if (!ok) return; + int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok); + if (!ok) return; + + QTextCursor cursor = textCursor(); + QTextTableFormat tableFmt; + tableFmt.setBorder(1); + tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid); + tableFmt.setCellPadding(4); + tableFmt.setCellSpacing(0); + tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100)); + cursor.insertTable(rows, cols, tableFmt); +} + +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(); + // Update completer models for address widgets with known emails + if (m_accountService) { + QStringList knownEmails; + const QVector accounts = m_accountService->getAllAccounts(); + for (const Account &acc : accounts) { + knownEmails << acc.email(); + } + // Set completion model for each widget + QStringListModel *model = new QStringListModel(knownEmails, this); + QCompleter *comp = new QCompleter(model, this); + comp->setCaseSensitivity(Qt::CaseInsensitive); + if (m_toField) m_toField->setCompleter(comp); + // For cc and bcc we need separate completers but can share model + QCompleter *comp2 = new QCompleter(model, this); + comp2->setCaseSensitivity(Qt::CaseInsensitive); + if (m_ccField) m_ccField->setCompleter(comp2); + QCompleter *comp3 = new QCompleter(model, this); + comp3->setCaseSensitivity(Qt::CaseInsensitive); + if (m_bccField) m_bccField->setCompleter(comp3); + } +} + +void ComposeView::populateAccountCombo() +{ + if (!m_accountService) { + qWarning() << "AccountService not set"; + return; + } + + m_accountCombo->clear(); + m_accountCombo->addItem(tr("Select Account..."), QVariant()); + + QVector 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); + mainLayout->setSpacing(8); + + // Top row: Account selector and detach button + QHBoxLayout *topLayout = new QHBoxLayout(); + m_accountLabel = new QLabel(tr("From:"), this); + m_accountLabel->setFixedWidth(40); + m_accountLabel->setStyleSheet(QStringLiteral("font-weight: bold; color: #555;")); + m_accountCombo = new QComboBox(this); + 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; }" + ); + m_detachButton = new QPushButton(this); + m_detachButton->setToolTip(tr("Detach compose window")); + m_detachButton->setText(QStringLiteral("โคข")); + m_detachButton->setFixedSize(28,28); + m_detachButton->setStyleSheet( + "QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; " + "padding: 6px; color: #555; font-size: 14px; }" + + "QPushButton:hover { background: #f0f0f0; }" + + ); + connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked); + + topLayout->addWidget(m_accountLabel); + topLayout->addWidget(m_accountCombo, 1); + topLayout->addWidget(m_detachButton); + mainLayout->addLayout(topLayout); + + // Separator line + QFrame *line1 = new QFrame(this); + line1->setFrameShape(QFrame::HLine); + line1->setStyleSheet(QStringLiteral("color: #e0e0e0;")); + mainLayout->addWidget(line1); + + // To row + QHBoxLayout *toLayout = new QHBoxLayout(); + QLabel *toLabel = new QLabel(tr("To:"), this); + toLabel->setFixedWidth(40); + toLabel->setStyleSheet(QStringLiteral("font-weight: bold; color: #555;")); + m_toField = new QLineEdit(this); + m_toField->setPlaceholderText(tr("To recipients")); + toLayout->addWidget(toLabel); + toLayout->addWidget(m_toField, 1); + mainLayout->addLayout(toLayout); + + // CC/BCC toggle buttons + QHBoxLayout *toggleLayout = new QHBoxLayout(); + m_ccButton = new QPushButton(tr("CC"), this); + m_ccButton->setCheckable(true); + m_ccButton->setChecked(false); + connect(m_ccButton, &QPushButton::toggled, this, &ComposeView::onCcToggled); + m_bccButton = new QPushButton(tr("BCC"), this); + m_bccButton->setCheckable(true); + m_bccButton->setChecked(false); + connect(m_bccButton, &QPushButton::toggled, this, &ComposeView::onBccToggled); + toggleLayout->addWidget(m_ccButton); + toggleLayout->addWidget(m_bccButton); + toggleLayout->addStretch(); + mainLayout->addLayout(toggleLayout); + + // Cc row (hidden by default) + m_ccRow = new QWidget(this); + QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow); + ccLayout->setContentsMargins(0,0,0,0); + QLabel *ccLabel = new QLabel(tr("Cc:"), this); + ccLabel->setFixedWidth(40); + ccLabel->setStyleSheet(QStringLiteral("color: #555;")); + m_ccField = new QLineEdit(this); + m_ccField->setPlaceholderText(tr("Cc recipients")); + ccLayout->addWidget(ccLabel); + ccLayout->addWidget(m_ccField, 1); + m_ccRow->setVisible(false); + mainLayout->addWidget(m_ccRow); + + // Bcc row (hidden by default) + m_bccRow = new QWidget(this); + QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow); + bccLayout->setContentsMargins(0,0,0,0); + QLabel *bccLabel = new QLabel(tr("Bcc:"), this); + bccLabel->setFixedWidth(40); + bccLabel->setStyleSheet(QStringLiteral("color: #555;")); + m_bccField = new QLineEdit(this); + m_bccField->setPlaceholderText(tr("Bcc recipients")); + bccLayout->addWidget(bccLabel); + bccLayout->addWidget(m_bccField, 1); + m_bccRow->setVisible(false); + mainLayout->addWidget(m_bccRow); + + // Separator + QFrame *line2 = new QFrame(this); + line2->setFrameShape(QFrame::HLine); + line2->setStyleSheet(QStringLiteral("color: #e0e0e0;")); + mainLayout->addWidget(line2); + + // Subject field + QLabel *subjectLabel = new QLabel(tr("Subject:"), this); + subjectLabel->setFixedWidth(50); + subjectLabel->setStyleSheet(QStringLiteral("font-weight: bold; color: #555;")); + m_subjectField = new QLineEdit(this); + m_subjectField->setPlaceholderText(tr("Subject")); + m_subjectField->setFixedHeight(36); + QFont subjectFont = m_subjectField->font(); + subjectFont.setPointSize(14); + subjectFont.setBold(true); + m_subjectField->setFont(subjectFont); + QHBoxLayout *subjectLayout = new QHBoxLayout(); + subjectLayout->addWidget(subjectLabel); + subjectLayout->addWidget(m_subjectField, 1); + mainLayout->addLayout(subjectLayout); + + // Body editor with toolbar + m_bodyEditor = new RichTextEditor(this); + m_bodyEditor->setupToolbar(mainLayout); + mainLayout->addWidget(m_bodyEditor, 1); + + // Schedule panel (hidden by default) + m_schedulePanel = new QWidget(this); + QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel); + scheduleLayout->setContentsMargins(0,0,0,0); + QLabel *scheduleLabel = new QLabel(tr("Send at:"), this); + scheduleLabel->setStyleSheet(QStringLiteral("color: #555; font-weight: bold;")); + m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600), this); + m_schedulePicker->setCalendarPopup(true); + m_schedulePicker->setDisplayFormat(QStringLiteral("dd/MM/yyyy hh:mm AP")); + scheduleLayout->addWidget(scheduleLabel); + scheduleLayout->addWidget(m_schedulePicker); + scheduleLayout->addStretch(); + m_scheduleSendButton = new QPushButton(tr("Schedule Send"), this); + m_scheduleSendButton->setStyleSheet( + "QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; " + "padding: 6px 16px; font-weight: bold; }" + + "QPushButton:hover { background-color: #1565C0; }" + + ); + connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked); + scheduleLayout->addWidget(m_scheduleSendButton); + m_schedulePanel->setVisible(false); + mainLayout->addWidget(m_schedulePanel); + + // Action buttons row + QHBoxLayout *actionsLayout = new QHBoxLayout(); + actionsLayout->addStretch(); + + m_discardButton = new QPushButton(tr("Discard"), this); + m_discardButton->setStyleSheet( + "QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; " + "padding: 8px 20px; color: #555; }" + + "QPushButton:hover { background: #f5f5f5; }" + + ); + actionsLayout->addWidget(m_discardButton); + connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested); + + // Split button for Send / Schedule + m_sendMenu = new QMenu(this); + m_sendNowAction = m_sendMenu->addAction(tr("Send Now")); + connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked); + m_scheduleAction = m_sendMenu->addAction(tr("Schedule for later...")); + connect(m_scheduleAction, &QAction::triggered, [this]() { + m_schedulePanel->setVisible(true); + }); + + m_sendSplit = new QToolButton(this); + m_sendSplit->setText(tr("Send")); + m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly); + m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup); + m_sendSplit->setMenu(m_sendMenu); + m_sendSplit->setStyleSheet( + "QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; " + "padding: 8px 24px; font-weight: bold; }" + + "QToolButton:hover { background-color: #1565C0; }" + + "QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; }" + + "QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }" + + ); + connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked); + actionsLayout->addWidget(m_sendSplit); + + mainLayout->addLayout(actionsLayout); + + // Connect rich text editor signature signals + connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked); + connect(m_bodyEditor, &RichTextEditor::signatureEditRequested, this, &ComposeView::onSignatureEditRequested); +} +void ComposeView::onCcToggled(bool checked) +{ + m_ccRow->setVisible(checked); + if (checked) { + m_ccField->setFocus(); + } +} +void ComposeView::onBccToggled(bool checked) +{ + m_bccRow->setVisible(checked); + if (checked) { + m_bccField->setFocus(); + } +} +void ComposeView::onSendClicked() +{ + QString to = m_toField->text(); + QString cc = m_ccButton->isChecked() ? m_ccField->text() : QString(); + QString bcc = m_bccButton->isChecked() ? m_bccField->text() : QString(); + QString subject = m_subjectField->text(); + QString body = m_bodyEditor->toHtml(); + QDateTime scheduleTime = m_schedulePanel->isVisible() ? m_schedulePicker->dateTime() : QDateTime(); + QString fromAddress; + if (m_currentAccountId > 0 && m_accountCombo->currentIndex() > 0) { + fromAddress = m_accountCombo->currentData().toString(); + } + emit sendRequested(to, cc, bcc, subject, body, scheduleTime, fromAddress); +} +void ComposeView::onScheduleClicked() { + emit sendRequested( + m_toField->text(), + m_ccVisible ? m_ccField->text() : QString(), + m_bccVisible ? m_bccField->text() : QString(), + m_subjectField->text(), + m_bodyEditor->toHtml(), + m_schedulePicker->dateTime(), + m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString() + ); +} + +void ComposeView::onDetachClicked() { + emit detachRequested(this); +} + +void ComposeView::setTo(const QString &to) +{ + if (to.isEmpty()) { + m_toField->clear(); + return; + } + // Split by semicolon or comma + QStringList parts = to.split(QRegularExpression("[,;]"), Qt::SkipEmptyParts); + QStringList cleaned; + for (const QString &part : parts) { + QString trimmed = part.trimmed(); + if (!trimmed.isEmpty()) + cleaned << trimmed; + } + m_toField->setText(cleaned.join("; ")); +} + +void ComposeView::setSubject(const QString &subject) +{ + m_subjectField->setText(subject); +} + +void ComposeView::setBody(const QString &body) +{ + m_bodyEditor->setHtml(body); +} + +void ComposeView::initializeComposition() +{ + m_subjectField->clear(); + m_bodyEditor->clear(); + m_toField->clear(); + m_ccField->clear(); + m_bccField->clear(); + m_ccRow->setVisible(false); + m_bccRow->setVisible(false); + m_schedulePanel->setVisible(false); + m_subjectField->setFocus(); +} +void ComposeView::startNewEmail(const QString &initialRecipient) +{ + initializeComposition(); + if (!initialRecipient.isEmpty()) { + m_toField->setText(initialRecipient); + } + m_toField->setFocus(); +} + +void ComposeView::setSubject(const QString &subject) +{ + m_subjectField->setText(subject); +} + +void ComposeView::setBody(const QString &body) +{ + m_bodyEditor->setHtml(body); +} + +void ComposeView::initializeComposition() +{ + m_subjectField->clear(); + m_bodyEditor->clear(); + m_toField->clear(); + m_ccField->clear(); + m_bccField->clear(); + m_ccRow->setVisible(false); + m_bccRow->setVisible(false); + m_schedulePanel->setVisible(false); + m_subjectField->setFocus(); +} +#include "composeview.moc" \ No newline at end of file diff --git a/src/ui/composeview.cpp.bak b/src/ui/composeview.cpp.bak new file mode 100644 index 0000000..4b9e523 --- /dev/null +++ b/src/ui/composeview.cpp.bak @@ -0,0 +1,698 @@ +#include "ui/composeview.h" +#include +#include +#include +#include +#include +#include "tags_line_edit.hpp" +#include +#include +#include +#include +#include +#include +#include + +// ===================== RichTextEditor ===================== + +RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) { + setAcceptRichText(true); + setPlaceholderText("Write your message here..."); +} + +void RichTextEditor::setupToolbar(QVBoxLayout *layout) { + m_toolbar = new QToolBar("Formatting"); + m_toolbar->setIconSize(QSize(16, 16)); + m_toolbar->setStyleSheet( + "QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }" + "QToolButton { padding: 4px 6px; border-radius: 3px; }" + "QToolButton:hover { background: #e0e0e0; }" + "QToolButton:checked { background: #bbdefb; }" + ); + + // Font family combo + m_fontCombo = new QFontComboBox(); + m_fontCombo->setFixedWidth(150); + connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged); + m_toolbar->addWidget(m_fontCombo); + + // Font size spin + m_fontSizeSpin = new QSpinBox(); + m_fontSizeSpin->setRange(8, 72); + m_fontSizeSpin->setValue(14); + m_fontSizeSpin->setFixedWidth(50); + connect(m_fontSizeSpin, QOverload::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged); + m_toolbar->addWidget(m_fontSizeSpin); + + m_toolbar->addSeparator(); + + // Bold / Italic / Underline + QAction *boldAct = m_toolbar->addAction("B"); + boldAct->setCheckable(true); + QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont); + connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold); + + QAction *italicAct = m_toolbar->addAction("I"); + italicAct->setCheckable(true); + QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont); + connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic); + + QAction *underlineAct = m_toolbar->addAction("U"); + underlineAct->setCheckable(true); + QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont); + connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline); + + m_toolbar->addSeparator(); + + // Alignment + QAction *alignLeft = m_toolbar->addAction("L"); + connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft); + + QAction *alignCenter = m_toolbar->addAction("C"); + connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter); + + QAction *alignRight = m_toolbar->addAction("R"); + connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight); + + QAction *alignJustify = m_toolbar->addAction("J"); + connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify); + + m_toolbar->addSeparator(); + + // Lists + QAction *bulletAct = m_toolbar->addAction("Bullets"); + connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList); + + QAction *numAct = m_toolbar->addAction("1. List"); + connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList); + + m_toolbar->addSeparator(); + + // Indent / Outdent + QAction *indentAct = m_toolbar->addAction("Indent"); + connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent); + + QAction *outdentAct = m_toolbar->addAction("Outdent"); + connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent); + + m_toolbar->addSeparator(); + + // Insert image + QAction *imgAct = m_toolbar->addAction("Img"); + connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage); + + // Insert table + QAction *tableAct = m_toolbar->addAction("Tbl"); + connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable); + + // 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); + QAction *editSigAct = m_signatureMenu->addAction(tr("Editar firmas")); + connect(editSigAct, &QAction::triggered, this, &RichTextEditor::signatureEditRequested); + + m_toolbar->addSeparator(); + + layout->addWidget(m_toolbar); +} + +void RichTextEditor::onBold() { + QTextCharFormat fmt; + fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onItalic() { + QTextCharFormat fmt; + fmt.setFontItalic(!textCursor().charFormat().fontItalic()); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onUnderline() { + QTextCharFormat fmt; + fmt.setFontUnderline(!textCursor().charFormat().fontUnderline()); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onBulletList() { + QTextCursor cursor = textCursor(); + QTextList *list = cursor.currentList(); + if (list && list->format().style() == QTextListFormat::ListDisc) { + // Remove list + QTextBlockFormat bfmt; + bfmt.setIndent(0); + cursor.setBlockFormat(bfmt); + list->remove(cursor.block()); + } else { + QTextListFormat listFormat; + listFormat.setStyle(QTextListFormat::ListDisc); + cursor.createList(listFormat); + } +} + +void RichTextEditor::onNumberedList() { + QTextCursor cursor = textCursor(); + QTextList *list = cursor.currentList(); + if (list && list->format().style() == QTextListFormat::ListDecimal) { + QTextBlockFormat bfmt; + bfmt.setIndent(0); + cursor.setBlockFormat(bfmt); + list->remove(cursor.block()); + } else { + QTextListFormat listFormat; + listFormat.setStyle(QTextListFormat::ListDecimal); + cursor.createList(listFormat); + } +} + +void RichTextEditor::onIndent() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setIndent(bfmt.indent() + 1); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onOutdent() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + int indent = bfmt.indent(); + if (indent > 0) { + bfmt.setIndent(indent - 1); + cursor.setBlockFormat(bfmt); + } +} + +void RichTextEditor::onAlignLeft() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignLeft); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignCenter() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignCenter); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignRight() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignRight); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignJustify() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignJustify); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onFontChanged(const QFont &font) { + QTextCharFormat fmt; + fmt.setFontFamilies({font.family()}); + mergeCurrentCharFormat(fmt); +} + +void RichTextEditor::onFontSizeChanged(int size) { + QTextCharFormat fmt; + fmt.setFontPointSize(size); + mergeCurrentCharFormat(fmt); +} + +void RichTextEditor::onInsertImage() { + QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)"); + if (filePath.isEmpty()) return; + QTextCursor cursor = textCursor(); + QTextImageFormat imgFmt; + imgFmt.setName(filePath); + // Scale down if too large + QPixmap pm(filePath); + if (pm.width() > 600) { + imgFmt.setWidth(600); + imgFmt.setHeight(pm.height() * 600 / pm.width()); + } + cursor.insertImage(imgFmt); +} + +void RichTextEditor::onInsertTable() { + bool ok; + int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok); + if (!ok) return; + int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok); + if (!ok) return; + + QTextCursor cursor = textCursor(); + QTextTableFormat tableFmt; + tableFmt.setBorder(1); + tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid); + tableFmt.setCellPadding(4); + tableFmt.setCellSpacing(0); + tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100)); + cursor.insertTable(rows, cols, tableFmt); +} + +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); + connect(m_bodyEditor, &RichTextEditor::signatureEditRequested, this, &ComposeView::onSignatureEditRequested); +} + +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 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); + mainLayout->setSpacing(8); + + this->setStyleSheet( + "QLineEdit, QTextEdit { border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px; font-family: 'Segoe UI', Helvetica; font-size: 13px; }" + "QLineEdit:focus, QTextEdit:focus { border-color: #1976D2; }" + ); + + // Header row: Subject + Detach button + QHBoxLayout *headerLayout = new QHBoxLayout(); + m_subjectField = new QLineEdit(); + m_subjectField->setPlaceholderText("Subject"); + m_subjectField->setFixedHeight(40); + QFont subjectFont = m_subjectField->font(); + subjectFont.setPointSize(14); + subjectFont.setBold(true); + m_subjectField->setFont(subjectFont); + headerLayout->addWidget(m_subjectField, 1); + + 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; }" + "QPushButton:hover { background: #f0f0f0; }" + ); + connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked); + headerLayout->addWidget(m_detachButton); + + mainLayout->addLayout(headerLayout); + + // Separator line + QFrame *line1 = new QFrame(); + line1->setFrameShape(QFrame::HLine); + 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::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:"); + toLabel->setFixedWidth(40); + toLabel->setStyleSheet("font-weight: bold; color: #555;"); + m_toField = new QLineEdit(); + m_toField->setPlaceholderText("Recipients (comma separated)"); + toLayout->addWidget(toLabel); + toLayout->addWidget(m_toField, 1); + + m_ccButton = new QPushButton("Cc"); + m_ccButton->setFixedWidth(32); + m_ccButton->setStyleSheet( + "QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }" + "QPushButton:hover { background: #d0d0d0; }" + ); + connect(m_ccButton, &QPushButton::clicked, this, &ComposeView::onCcToggle); + toLayout->addWidget(m_ccButton); + + m_bccButton = new QPushButton("Bcc"); + m_bccButton->setFixedWidth(36); + m_bccButton->setStyleSheet( + "QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }" + "QPushButton:hover { background: #d0d0d0; }" + ); + connect(m_bccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle); + toLayout->addWidget(m_bccButton); + + mainLayout->addLayout(toLayout); + + // Cc: row (hidden by default) + m_ccRow = new QWidget(); + QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow); + ccLayout->setContentsMargins(0, 0, 0, 0); + QLabel *ccLabel = new QLabel("Cc:"); + ccLabel->setFixedWidth(40); + ccLabel->setStyleSheet("color: #555;"); + m_ccField = new QLineEdit(); + m_ccField->setPlaceholderText("Carbon copy"); + ccLayout->addWidget(ccLabel); + ccLayout->addWidget(m_ccField, 1); + + m_hideCcButton = new QPushButton("x"); + m_hideCcButton->setFixedSize(20, 20); + m_hideCcButton->setToolTip("Hide Cc"); + m_hideCcButton->setStyleSheet( + "QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }" + "QPushButton:hover { color: #333; }" + ); + connect(m_hideCcButton, &QPushButton::clicked, this, &ComposeView::onCcToggle); + ccLayout->addWidget(m_hideCcButton); + + m_ccRow->setVisible(false); + mainLayout->addWidget(m_ccRow); + + // Bcc: row (hidden by default) + m_bccRow = new QWidget(); + QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow); + bccLayout->setContentsMargins(0, 0, 0, 0); + QLabel *bccLabel = new QLabel("Bcc:"); + bccLabel->setFixedWidth(40); + bccLabel->setStyleSheet("color: #555;"); + m_bccField = new QLineEdit(); + m_bccField->setPlaceholderText("Blind carbon copy"); + bccLayout->addWidget(bccLabel); + bccLayout->addWidget(m_bccField, 1); + + m_hideBccButton = new QPushButton("x"); + m_hideBccButton->setFixedSize(20, 20); + m_hideBccButton->setToolTip("Hide Bcc"); + m_hideBccButton->setStyleSheet( + "QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }" + "QPushButton:hover { color: #333; }" + ); + connect(m_hideBccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle); + bccLayout->addWidget(m_hideBccButton); + + m_bccRow->setVisible(false); + mainLayout->addWidget(m_bccRow); + + // Separator + QFrame *line2 = new QFrame(); + line2->setFrameShape(QFrame::HLine); + line2->setStyleSheet("color: #e0e0e0;"); + mainLayout->addWidget(line2); + + // Rich text editor with toolbar + m_bodyEditor = new RichTextEditor(); + m_bodyEditor->setupToolbar(mainLayout); + mainLayout->addWidget(m_bodyEditor, 1); + + // Schedule panel + m_schedulePanel = new QWidget(); + QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel); + scheduleLayout->setContentsMargins(0, 0, 0, 0); + QLabel *scheduleLabel = new QLabel("Send at:"); + scheduleLabel->setStyleSheet("color: #555; font-weight: bold;"); + m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600)); + m_schedulePicker->setCalendarPopup(true); + m_schedulePicker->setDisplayFormat("dd/MM/yyyy hh:mm AP"); + scheduleLayout->addWidget(scheduleLabel); + scheduleLayout->addWidget(m_schedulePicker); + scheduleLayout->addStretch(); + + m_scheduleSendButton = new QPushButton("Schedule Send"); + m_scheduleSendButton->setStyleSheet( + "QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 6px 16px; font-weight: bold; }" + "QPushButton:hover { background-color: #1565C0; }" + ); + connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked); + scheduleLayout->addWidget(m_scheduleSendButton); + + m_schedulePanel->setVisible(false); + mainLayout->addWidget(m_schedulePanel); + // Attachment section + QHBoxLayout *attachmentLayout = new QHBoxLayout(); + QLabel *attachmentLabel = new QLabel(tr("Attachments:")); + attachmentLabel->setFixedWidth(80); + attachmentLabel->setStyleSheet("font-weight: bold; color: #555;"); + attachmentLayout->addWidget(attachmentLabel); + + m_attachButton = new QToolButton(); + m_attachButton->setIcon(QIcon(QStringLiteral(":/icons/attachment.svg"))); + m_attachButton->setToolTip(tr("Add attachment")); + m_attachButton->setIconSize(QSize(20,20)); + m_attachButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }"); + bool connected = connect(m_attachButton, &QToolButton::clicked, this, &ComposeView::onAddAttachmentClicked); + qDebug() << "Attach button connected:" << connected; + attachmentLayout->addWidget(m_attachButton); + + m_attachmentList = new QListWidget(); + m_attachmentList->setSelectionMode(QAbstractItemView::SingleSelection); + m_attachmentList->setMaximumHeight(60); + m_attachmentList->setStyleSheet("QListWidget { border: 1px solid #d1d1d6; border-radius: 4px; }"); + attachmentLayout->addWidget(m_attachmentList, 1); + + m_removeAttachmentButton = new QToolButton(); + m_removeAttachmentButton->setIcon(QIcon(QStringLiteral(":/icons/trash.svg"))); + m_removeAttachmentButton->setToolTip(tr("Remove selected attachment")); + m_removeAttachmentButton->setIconSize(QSize(20,20)); + m_removeAttachmentButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }"); + connect(m_removeAttachmentButton, &QToolButton::clicked, this, &ComposeView::onRemoveAttachmentClicked); + attachmentLayout->addWidget(m_removeAttachmentButton); + + mainLayout->addLayout(attachmentLayout); + + + // Action buttons row + QHBoxLayout *actionsLayout = new QHBoxLayout(); + actionsLayout->addStretch(); + + m_discardButton = new QPushButton("Discard"); + m_discardButton->setStyleSheet( + "QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px 20px; color: #555; }" + "QPushButton:hover { background: #f5f5f5; }" + ); + actionsLayout->addWidget(m_discardButton); + connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested); + + // Split button for Send / Schedule + m_sendMenu = new QMenu(this); + m_sendNowAction = m_sendMenu->addAction("Send Now"); + connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked); + m_scheduleAction = m_sendMenu->addAction("Schedule for later..."); + connect(m_scheduleAction, &QAction::triggered, [this]() { + m_schedulePanel->setVisible(true); + }); + + m_sendSplit = new QToolButton(); + m_sendSplit->setText("Send"); + m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly); + m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup); + m_sendSplit->setMenu(m_sendMenu); + m_sendSplit->setStyleSheet( + "QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 8px 24px; font-weight: bold; }" + "QToolButton:hover { background-color: #1565C0; }" + "QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; width: 40px;}" + "QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }" + ); + connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked); + actionsLayout->addWidget(m_sendSplit); + + mainLayout->addLayout(actionsLayout); +} + +void ComposeView::onCcToggle() { + m_ccVisible = !m_ccVisible; + m_ccRow->setVisible(m_ccVisible); + m_ccButton->setVisible(!m_ccVisible); + if (m_ccVisible) m_ccField->setFocus(); +} + +void ComposeView::onBccToggle() { + m_bccVisible = !m_bccVisible; + m_bccRow->setVisible(m_bccVisible); + m_bccButton->setVisible(!m_bccVisible); + if (m_bccVisible) m_bccField->setFocus(); +} + +void ComposeView::onSendClicked() { + emit sendRequested( + m_toField->text(), + m_ccVisible ? m_ccField->text() : QString(), + m_bccVisible ? m_bccField->text() : QString(), + m_subjectField->text(), + m_bodyEditor->toHtml(), + QDateTime(), // null = send now + m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString() + ); +} + +void ComposeView::onScheduleClicked() { + emit sendRequested( + m_toField->text(), + m_ccVisible ? m_ccField->text() : QString(), + m_bccVisible ? m_bccField->text() : QString(), + m_subjectField->text(), + m_bodyEditor->toHtml(), + m_schedulePicker->dateTime(), + m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString() + ); +} + +void ComposeView::onDetachClicked() { + emit detachRequested(this); +} + +void ComposeView::setTo(const QString &to) { m_toField->setText(to); } +void ComposeView::setSubject(const QString &subject) { m_subjectField->setText(subject); } +void ComposeView::setBody(const QString &body) { m_bodyEditor->setHtml(body); } +void ComposeView::initializeComposition() { + m_toField->clear(); + m_ccField->clear(); + m_bccField->clear(); + m_subjectField->clear(); + m_bodyEditor->clear(); + m_ccRow->setVisible(false); + m_bccRow->setVisible(false); + m_schedulePanel->setVisible(false); + m_toField->setFocus(); +} + +void ComposeView::startNewEmail(const QString &initialRecipient) { + initializeComposition(); + if (!initialRecipient.isEmpty()) + m_toField->setText(initialRecipient); + m_toField->setFocus(); +} + +void ComposeView::onAddAttachmentClicked() +{ + qDebug() << "Attach button clicked"; + QStringList files = QFileDialog::getOpenFileNames(this, tr("Select Attachments"), QString(), tr("All Files (*)")); + if (files.isEmpty()) + return; + for (const QString &file : files) { + m_attachmentFiles.append(file); + QListWidgetItem *item = new QListWidgetItem(QFileInfo(file).fileName(), m_attachmentList); + item->setToolTip(file); + } + QMessageBox::information(this, tr("Attachments"), tr("Attached %1 file(s).").arg(files.count())); +} + +void ComposeView::onRemoveAttachmentClicked() +{ + QListWidgetItem *item = m_attachmentList->currentItem(); + if (!item) + return; + int row = m_attachmentList->row(item); + m_attachmentList->takeItem(row); + m_attachmentFiles.removeAt(row); + delete item; +} + +#include "composeview.moc" \ No newline at end of file diff --git a/src/ui/composeview.cpp.current b/src/ui/composeview.cpp.current new file mode 100644 index 0000000..57e842c --- /dev/null +++ b/src/ui/composeview.cpp.current @@ -0,0 +1,632 @@ +#include "ui/composeview.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// ===================== RichTextEditor ===================== + +RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) { + setAcceptRichText(true); + setPlaceholderText("Write your message here..."); +} + +void RichTextEditor::setupToolbar(QVBoxLayout *layout) { + m_toolbar = new QToolBar("Formatting"); + m_toolbar->setIconSize(QSize(16, 16)); + m_toolbar->setStyleSheet( + "QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }" + "QToolButton { padding: 4px 6px; border-radius: 3px; }" + "QToolButton:hover { background: #e0e0e0; }" + "QToolButton:checked { background: #bbdefb; }" + ); + + // Font family combo + m_fontCombo = new QFontComboBox(); + m_fontCombo->setFixedWidth(150); + connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged); + m_toolbar->addWidget(m_fontCombo); + + // Font size spin + m_fontSizeSpin = new QSpinBox(); + m_fontSizeSpin->setRange(8, 72); + m_fontSizeSpin->setValue(14); + m_fontSizeSpin->setFixedWidth(50); + connect(m_fontSizeSpin, QOverload::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged); + m_toolbar->addWidget(m_fontSizeSpin); + + m_toolbar->addSeparator(); + + // Bold / Italic / Underline + QAction *boldAct = m_toolbar->addAction("B"); + boldAct->setCheckable(true); + QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont); + connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold); + + QAction *italicAct = m_toolbar->addAction("I"); + italicAct->setCheckable(true); + QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont); + connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic); + + QAction *underlineAct = m_toolbar->addAction("U"); + underlineAct->setCheckable(true); + QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont); + connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline); + + m_toolbar->addSeparator(); + + // Alignment + QAction *alignLeft = m_toolbar->addAction("L"); + connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft); + + QAction *alignCenter = m_toolbar->addAction("C"); + connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter); + + QAction *alignRight = m_toolbar->addAction("R"); + connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight); + + QAction *alignJustify = m_toolbar->addAction("J"); + connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify); + + m_toolbar->addSeparator(); + + // Lists + QAction *bulletAct = m_toolbar->addAction("Bullets"); + connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList); + + QAction *numAct = m_toolbar->addAction("1. List"); + connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList); + + m_toolbar->addSeparator(); + + // Indent / Outdent + QAction *indentAct = m_toolbar->addAction("Indent"); + connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent); + + QAction *outdentAct = m_toolbar->addAction("Outdent"); + connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent); + + m_toolbar->addSeparator(); + + // Insert image + QAction *imgAct = m_toolbar->addAction("Img"); + connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage); + + // Insert table + QAction *tableAct = m_toolbar->addAction("Tbl"); + connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable); + + // 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() { + QTextCharFormat fmt; + fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onItalic() { + QTextCharFormat fmt; + fmt.setFontItalic(!textCursor().charFormat().fontItalic()); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onUnderline() { + QTextCharFormat fmt; + fmt.setFontUnderline(!textCursor().charFormat().fontUnderline()); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onBulletList() { + QTextCursor cursor = textCursor(); + QTextList *list = cursor.currentList(); + if (list && list->format().style() == QTextListFormat::ListDisc) { + // Remove list + QTextBlockFormat bfmt; + bfmt.setIndent(0); + cursor.setBlockFormat(bfmt); + list->remove(cursor.block()); + } else { + QTextListFormat listFormat; + listFormat.setStyle(QTextListFormat::ListDisc); + cursor.createList(listFormat); + } +} + +void RichTextEditor::onNumberedList() { + QTextCursor cursor = textCursor(); + QTextList *list = cursor.currentList(); + if (list && list->format().style() == QTextListFormat::ListDecimal) { + QTextBlockFormat bfmt; + bfmt.setIndent(0); + cursor.setBlockFormat(bfmt); + list->remove(cursor.block()); + } else { + QTextListFormat listFormat; + listFormat.setStyle(QTextListFormat::ListDecimal); + cursor.createList(listFormat); + } +} + +void RichTextEditor::onIndent() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setIndent(bfmt.indent() + 1); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onOutdent() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + int indent = bfmt.indent(); + if (indent > 0) { + bfmt.setIndent(indent - 1); + cursor.setBlockFormat(bfmt); + } +} + +void RichTextEditor::onAlignLeft() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignLeft); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignCenter() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignCenter); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignRight() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignRight); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignJustify() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignJustify); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onFontChanged(const QFont &font) { + QTextCharFormat fmt; + fmt.setFontFamilies({font.family()}); + mergeCurrentCharFormat(fmt); +} + +void RichTextEditor::onFontSizeChanged(int size) { + QTextCharFormat fmt; + fmt.setFontPointSize(size); + mergeCurrentCharFormat(fmt); +} + +void RichTextEditor::onInsertImage() { + QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)"); + if (filePath.isEmpty()) return; + QTextCursor cursor = textCursor(); + QTextImageFormat imgFmt; + imgFmt.setName(filePath); + // Scale down if too large + QPixmap pm(filePath); + if (pm.width() > 600) { + imgFmt.setWidth(600); + imgFmt.setHeight(pm.height() * 600 / pm.width()); + } + cursor.insertImage(imgFmt); +} + +void RichTextEditor::onInsertTable() { + bool ok; + int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok); + if (!ok) return; + int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok); + if (!ok) return; + + QTextCursor cursor = textCursor(); + QTextTableFormat tableFmt; + tableFmt.setBorder(1); + tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid); + tableFmt.setCellPadding(4); + tableFmt.setCellSpacing(0); + tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100)); + cursor.insertTable(rows, cols, tableFmt); +} + +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 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); + mainLayout->setSpacing(8); + + this->setStyleSheet( + "QLineEdit, QTextEdit { border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px; font-family: 'Segoe UI', Helvetica; font-size: 13px; }" + "QLineEdit:focus, QTextEdit:focus { border-color: #1976D2; }" + ); + + // Header row: Subject + Detach button + QHBoxLayout *headerLayout = new QHBoxLayout(); + m_subjectField = new QLineEdit(); + m_subjectField->setPlaceholderText("Subject"); + m_subjectField->setFixedHeight(40); + QFont subjectFont = m_subjectField->font(); + subjectFont.setPointSize(14); + subjectFont.setBold(true); + m_subjectField->setFont(subjectFont); + headerLayout->addWidget(m_subjectField, 1); + + 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; }" + "QPushButton:hover { background: #f0f0f0; }" + ); + connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked); + headerLayout->addWidget(m_detachButton); + + mainLayout->addLayout(headerLayout); + + // Separator line + QFrame *line1 = new QFrame(); + line1->setFrameShape(QFrame::HLine); + 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::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:"); + toLabel->setFixedWidth(40); + toLabel->setStyleSheet("font-weight: bold; color: #555;"); + m_toField = new QLineEdit(); + m_toField->setPlaceholderText("Recipients (comma separated)"); + toLayout->addWidget(toLabel); + toLayout->addWidget(m_toField, 1); + + m_ccButton = new QPushButton("Cc"); + m_ccButton->setFixedWidth(32); + m_ccButton->setStyleSheet( + "QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }" + "QPushButton:hover { background: #d0d0d0; }" + ); + connect(m_ccButton, &QPushButton::clicked, this, &ComposeView::onCcToggle); + toLayout->addWidget(m_ccButton); + + m_bccButton = new QPushButton("Bcc"); + m_bccButton->setFixedWidth(36); + m_bccButton->setStyleSheet( + "QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }" + "QPushButton:hover { background: #d0d0d0; }" + ); + connect(m_bccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle); + toLayout->addWidget(m_bccButton); + + mainLayout->addLayout(toLayout); + + // Cc: row (hidden by default) + m_ccRow = new QWidget(); + QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow); + ccLayout->setContentsMargins(0, 0, 0, 0); + QLabel *ccLabel = new QLabel("Cc:"); + ccLabel->setFixedWidth(40); + ccLabel->setStyleSheet("color: #555;"); + m_ccField = new QLineEdit(); + m_ccField->setPlaceholderText("Carbon copy"); + ccLayout->addWidget(ccLabel); + ccLayout->addWidget(m_ccField, 1); + + m_hideCcButton = new QPushButton("x"); + m_hideCcButton->setFixedSize(20, 20); + m_hideCcButton->setToolTip("Hide Cc"); + m_hideCcButton->setStyleSheet( + "QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }" + "QPushButton:hover { color: #333; }" + ); + connect(m_hideCcButton, &QPushButton::clicked, this, &ComposeView::onCcToggle); + ccLayout->addWidget(m_hideCcButton); + + m_ccRow->setVisible(false); + mainLayout->addWidget(m_ccRow); + + // Bcc: row (hidden by default) + m_bccRow = new QWidget(); + QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow); + bccLayout->setContentsMargins(0, 0, 0, 0); + QLabel *bccLabel = new QLabel("Bcc:"); + bccLabel->setFixedWidth(40); + bccLabel->setStyleSheet("color: #555;"); + m_bccField = new QLineEdit(); + m_bccField->setPlaceholderText("Blind carbon copy"); + bccLayout->addWidget(bccLabel); + bccLayout->addWidget(m_bccField, 1); + + m_hideBccButton = new QPushButton("x"); + m_hideBccButton->setFixedSize(20, 20); + m_hideBccButton->setToolTip("Hide Bcc"); + m_hideBccButton->setStyleSheet( + "QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }" + "QPushButton:hover { color: #333; }" + ); + connect(m_hideBccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle); + bccLayout->addWidget(m_hideBccButton); + + m_bccRow->setVisible(false); + mainLayout->addWidget(m_bccRow); + + // Separator + QFrame *line2 = new QFrame(); + line2->setFrameShape(QFrame::HLine); + line2->setStyleSheet("color: #e0e0e0;"); + mainLayout->addWidget(line2); + + // Rich text editor with toolbar + m_bodyEditor = new RichTextEditor(); + m_bodyEditor->setupToolbar(mainLayout); + mainLayout->addWidget(m_bodyEditor, 1); + + // Schedule panel + m_schedulePanel = new QWidget(); + QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel); + scheduleLayout->setContentsMargins(0, 0, 0, 0); + QLabel *scheduleLabel = new QLabel("Send at:"); + scheduleLabel->setStyleSheet("color: #555; font-weight: bold;"); + m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600)); + m_schedulePicker->setCalendarPopup(true); + m_schedulePicker->setDisplayFormat("dd/MM/yyyy hh:mm AP"); + scheduleLayout->addWidget(scheduleLabel); + scheduleLayout->addWidget(m_schedulePicker); + scheduleLayout->addStretch(); + + m_scheduleSendButton = new QPushButton("Schedule Send"); + m_scheduleSendButton->setStyleSheet( + "QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 6px 16px; font-weight: bold; }" + "QPushButton:hover { background-color: #1565C0; }" + ); + connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked); + scheduleLayout->addWidget(m_scheduleSendButton); + + m_schedulePanel->setVisible(false); + mainLayout->addWidget(m_schedulePanel); + + // Action buttons row + QHBoxLayout *actionsLayout = new QHBoxLayout(); + actionsLayout->addStretch(); + + m_discardButton = new QPushButton("Discard"); + m_discardButton->setStyleSheet( + "QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px 20px; color: #555; }" + "QPushButton:hover { background: #f5f5f5; }" + ); + actionsLayout->addWidget(m_discardButton); + connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested); + + // Split button for Send / Schedule + m_sendMenu = new QMenu(this); + m_sendNowAction = m_sendMenu->addAction("Send Now"); + connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked); + m_scheduleAction = m_sendMenu->addAction("Schedule for later..."); + connect(m_scheduleAction, &QAction::triggered, [this]() { + m_schedulePanel->setVisible(true); + }); + + m_sendSplit = new QToolButton(); + m_sendSplit->setText("Send"); + m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly); + m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup); + m_sendSplit->setMenu(m_sendMenu); + m_sendSplit->setStyleSheet( + "QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 8px 24px; font-weight: bold; }" + "QToolButton:hover { background-color: #1565C0; }" + "QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; }" + "QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }" + ); + connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked); + actionsLayout->addWidget(m_sendSplit); + + mainLayout->addLayout(actionsLayout); +} + +void ComposeView::onCcToggle() { + m_ccVisible = !m_ccVisible; + m_ccRow->setVisible(m_ccVisible); + m_ccButton->setVisible(!m_ccVisible); + if (m_ccVisible) m_ccField->setFocus(); +} + +void ComposeView::onBccToggle() { + m_bccVisible = !m_bccVisible; + m_bccRow->setVisible(m_bccVisible); + m_bccButton->setVisible(!m_bccVisible); + if (m_bccVisible) m_bccField->setFocus(); +} + +void ComposeView::onSendClicked() { + emit sendRequested( + m_toField->text(), + m_ccVisible ? m_ccField->text() : QString(), + m_bccVisible ? m_bccField->text() : QString(), + m_subjectField->text(), + m_bodyEditor->toHtml(), + QDateTime(), // null = send now + m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString() + ); +} + +void ComposeView::onScheduleClicked() { + emit sendRequested( + m_toField->text(), + m_ccVisible ? m_ccField->text() : QString(), + m_bccVisible ? m_bccField->text() : QString(), + m_subjectField->text(), + m_bodyEditor->toHtml(), + m_schedulePicker->dateTime(), + m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString() + ); +} + +void ComposeView::onDetachClicked() { + emit detachRequested(this); +} + +void ComposeView::setTo(const QString &to) { m_toField->setText(to); } +void ComposeView::setSubject(const QString &subject) { m_subjectField->setText(subject); } +void ComposeView::setBody(const QString &body) { m_bodyEditor->setHtml(body); } +void ComposeView::initializeComposition() { + m_toField->clear(); + m_ccField->clear(); + m_bccField->clear(); + m_subjectField->clear(); + m_bodyEditor->clear(); + m_ccRow->setVisible(false); + m_bccRow->setVisible(false); + m_schedulePanel->setVisible(false); + m_toField->setFocus(); +} + +void ComposeView::startNewEmail(const QString &initialRecipient) { + initializeComposition(); + if (!initialRecipient.isEmpty()) + m_toField->setText(initialRecipient); + m_toField->setFocus(); +} +#include "composeview.moc" \ No newline at end of file diff --git a/src/ui/composeview.h b/src/ui/composeview.h index 368abce..e07a23b 100644 --- a/src/ui/composeview.h +++ b/src/ui/composeview.h @@ -17,45 +17,12 @@ #include #include #include "models/EmailCompositionModel.h" -#include "tags_line_edit.hpp" +#include "../../thirdparty/tags/include/tags_line_edit.hpp" #include "services/accountservice.h" class AccountService; -class RichTextEditor : public QTextEdit { - Q_OBJECT -public: - explicit RichTextEditor(QWidget *parent = nullptr); - void setupToolbar(QVBoxLayout *layout); - -signals: - void signatureClicked(); - void signatureEditRequested(); - -public slots: - void onBold(); - void onItalic(); - void onUnderline(); - void onBulletList(); - void onNumberedList(); - void onIndent(); - void onOutdent(); - void onAlignLeft(); - void onAlignCenter(); - void onAlignRight(); - void onAlignJustify(); - void onFontChanged(const QFont &font); - void onFontSizeChanged(int size); - void onInsertImage(); - void onInsertTable(); - -private: - QToolBar *m_toolbar; - QFontComboBox *m_fontCombo; - QSpinBox *m_fontSizeSpin; - QToolButton *m_signatureButton; - QMenu *m_signatureMenu; -}; +#include "richtexteditor.h" class ComposeView : public QWidget { Q_OBJECT @@ -71,7 +38,8 @@ signals: void sendRequested(const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime, - const QString &fromAddress); + const QString &fromAddress, + const QStringList &attachmentPaths); void discardRequested(); public slots: @@ -92,6 +60,7 @@ private slots: void onSignatureEditRequested(); void onAddAttachmentClicked(); void onRemoveAttachmentClicked(); + void onTemplateClicked(); // new slot for templates private: void setupUI(); @@ -101,15 +70,17 @@ private: EmailCompositionModel *m_compositionModel; AccountService *m_accountService = nullptr; // UI components - QLineEdit *m_toField = nullptr; + QLabel *m_fromLabel = nullptr; + QComboBox *m_accountCombo = nullptr; + everload_tags::TagsLineEdit *m_toField = nullptr; + everload_tags::TagsLineEdit *m_ccField = nullptr; + everload_tags::TagsLineEdit *m_bccField = nullptr; QLineEdit *m_subjectField = nullptr; RichTextEditor *m_bodyEditor = nullptr; QPushButton *m_detachButton = nullptr; QWidget *m_ccRow = nullptr; QWidget *m_bccRow = nullptr; QWidget *m_schedulePanel = nullptr; - QLineEdit *m_ccField = nullptr; - QLineEdit *m_bccField = nullptr; QPushButton *m_ccButton = nullptr; QPushButton *m_bccButton = nullptr; QPushButton *m_hideCcButton = nullptr; @@ -121,15 +92,15 @@ 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; // Attachment UI QToolButton *m_attachButton = nullptr; + QToolButton *m_templateButton = nullptr; QListWidget *m_attachmentList = nullptr; QToolButton *m_removeAttachmentButton = nullptr; QStringList m_attachmentFiles; }; -#endif // COMPOSE_VIEW_H \ No newline at end of file +#endif // COMPOSE_VIEW_H diff --git a/src/ui/composeview.h.backup b/src/ui/composeview.h.backup new file mode 100644 index 0000000..b6690fe --- /dev/null +++ b/src/ui/composeview.h.backup @@ -0,0 +1,127 @@ +#ifndef COMPOSE_VIEW_H +#define COMPOSE_VIEW_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "models/EmailCompositionModel.h" +#include "services/accountservice.h" + +class AccountService; + +class RichTextEditor : public QTextEdit { + Q_OBJECT +public: + explicit RichTextEditor(QWidget *parent = nullptr); + void setupToolbar(QVBoxLayout *layout); + +signals: + void signatureClicked(); + void signatureEditRequested(); + +public slots: + void onBold(); + void onItalic(); + void onUnderline(); + void onBulletList(); + void onNumberedList(); + void onIndent(); + void onOutdent(); + void onAlignLeft(); + void onAlignCenter(); + void onAlignRight(); + void onAlignJustify(); + void onFontChanged(const QFont &font); + void onFontSizeChanged(int size); + void onInsertImage(); + void onInsertTable(); + +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 QString &fromAddress); + void discardRequested(); + +public slots: + void initializeComposition(); + void startNewEmail(const QString &initialRecipient); + void setTo(const QString &to); + void setSubject(const QString &subject); + void setBody(const QString &body); + +private slots: + void onCcToggle(); + void onBccToggle(); + 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; + RichTextEditor *m_bodyEditor = nullptr; + QPushButton *m_detachButton = nullptr; + QWidget *m_ccRow = nullptr; + QWidget *m_bccRow = nullptr; + QWidget *m_schedulePanel = nullptr; + QLineEdit *m_ccField = nullptr; + QLineEdit *m_bccField = nullptr; + QPushButton *m_ccButton = nullptr; + QPushButton *m_bccButton = nullptr; + QPushButton *m_hideCcButton = nullptr; + QPushButton *m_hideBccButton = nullptr; + QDateTimeEdit *m_schedulePicker = nullptr; + QPushButton *m_scheduleSendButton = nullptr; + QPushButton *m_discardButton = nullptr; + QMenu *m_sendMenu = nullptr; + 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 // COMPOSE_VIEW_H \ No newline at end of file diff --git a/src/ui/connectionwizard.cpp b/src/ui/connectionwizard.cpp new file mode 100644 index 0000000..002ba39 --- /dev/null +++ b/src/ui/connectionwizard.cpp @@ -0,0 +1,224 @@ +#include "ui/connectionwizard.h" +#include +#include +#include +#include + +ConnectionWizard::ConnectionWizard(QWidget *parent, AccountService *accountService) + : QWizard(parent), m_accountService(accountService) +{ + setWindowTitle("Configurar conexiรณn de correo"); + setWizardStyle(QWizard::ModernStyle); + setOption(QWizard::HaveHelpButton, false); + setOption(QWizard::HaveFinishButtonOnEarlyPages, true); + + setupPageIntro(); + setupPageIncoming(); + setupPageOutgoing(); + setupPageAuth(); + setupPageTest(); + + // Set page order + addPage(m_introPage); + addPage(m_incomingPage); + addPage(m_outgoingPage); + addPage(m_authPage); + addPage(m_testPage); + + // Start at intro + startId(); +} + +void ConnectionWizard::setupPageIntro() +{ + m_introPage = new QWizardPage(this); + m_introPage->setTitle("Configuraciรณn de conexiรณn"); + m_introPage->setSubTitle("Introduce los datos de los servidores entrantes y salientes para configurar tu cuenta de correo."); + + QVBoxLayout *lay = new QVBoxLayout(m_introPage); + QLabel *info = new QLabel( + "Este asistente te guiarรก paso a paso para configurar los servidores IMAP/POP3 y SMTP.\\n" + "Si no conoces los datos, consulta a tu proveedor de correo o busca en sus pรกginas de soporte." + ); + info->setWordWrap(true); + lay->addWidget(info); + lay->addStretch(); + m_introPage->setLayout(lay); +} + +void ConnectionWizard::setupPageIncoming() +{ + m_incomingPage = new QWizardPage(this); + m_incomingPage->setTitle("Servidor entrante (IMAP/POP3)"); + m_incomingPage->setSubTitle("Configura el servidor que recibirรก tus correos."); + + QFormLayout *form = new QFormLayout(); + m_incomingTypeCombo = new QComboBox(); + m_incomingTypeCombo->addItem("IMAP", QVariant::fromValue(QString("imap"))); + m_incomingTypeCombo->addItem("POP3", QVariant::fromValue(QString("pop3"))); + form->addRow("Tipo:", m_incomingTypeCombo); + + m_incomingHostEdit = new QLineEdit(); + m_incomingHostEdit->setPlaceholderText("ej. imap.ejemplo.com"); + form->addRow("Servidor:", m_incomingHostEdit); + + m_incomingPortEdit = new QLineEdit(); + m_incomingPortEdit->setPlaceholderText("993"); + m_incomingPortEdit->setValidator(new QIntValidator(1, 65535, this)); + form->addRow("Puerto:", m_incomingPortEdit); + + m_incomingSslCheck = new QCheckBox("Usar conexiรณn segura (SSL/TLS)"); + m_incomingSslCheck->setChecked(true); + form->addRow("", m_incomingSslCheck); + + m_incomingPage->setLayout(form); +} + +void ConnectionWizard::setupPageOutgoing() +{ + m_outgoingPage = new QWizardPage(this); + m_outgoingPage->setTitle("Servidor saliente (SMTP)"); + m_outgoingPage->setSubTitle("Configura el servidor que enviarรก tus correos."); + + QFormLayout *form = new QFormLayout(); + m_outgoingHostEdit = new QLineEdit(); + m_outgoingHostEdit->setPlaceholderText("ej. smtp.ejemplo.com"); + form->addRow("Servidor:", m_outgoingHostEdit); + + m_outgoingPortEdit = new QLineEdit(); + m_outgoingPortEdit->setPlaceholderText("587"); + m_outgoingPortEdit->setValidator(new QIntValidator(1, 65535, this)); + form->addRow("Puerto:", m_outgoingPortEdit); + + m_outgoingSslCheck = new QCheckBox("Usar conexiรณn segura (STARTTLS/SSL)"); + m_outgoingSslCheck->setChecked(true); + form->addRow("", m_outgoingSslCheck); + + m_outgoingPage->setLayout(form); +} + +void ConnectionWizard::setupPageAuth() +{ + m_authPage = new QWizardPage(this); + m_authPage->setTitle("Autenticaciรณn"); + m_authPage->setSubTitle("Introduce tu nombre de usuario y contraseรฑa para acceder a los servidores."); + + QFormLayout *form = new QFormLayout(); + m_usernameEdit = new QLineEdit(); + m_usernameEdit->setPlaceholderText("tu-usuario@dominio.com"); + form->addRow("Usuario:", m_usernameEdit); + + m_passwordEdit = new QLineEdit(); + m_passwordEdit->setEchoMode(QLineEdit::Password); + m_passwordEdit->setPlaceholderText("Contraseรฑa"); + form->addRow("Contraseรฑa:", m_passwordEdit); + + m_authMethodCombo = new QComboBox(); + m_authMethodCombo->addItem("Contraseรฑa normal", QVariant::fromValue(QString("plain"))); + m_authMethodCombo->addItem("Autenticaciรณn OAuth2", QVariant::fromValue(QString("oauth2"))); + form->addRow("Mรฉtodo:", m_authMethodCombo); + + m_authPage->setLayout(form); +} + +void ConnectionWizard::setupPageTest() +{ + m_testPage = new QWizardPage(this); + m_testPage->setTitle("Probar conexiรณn"); + m_testPage->setSubTitle("Verifica que los datos ingresados sean correctos."); + + QVBoxLayout *lay = new QVBoxLayout(m_testPage); + m_testStatusLabel = new QLabel("Listo para probar la conexiรณn."); + m_testStatusLabel->setAlignment(Qt::AlignCenter); + m_testStatusLabel->setStyleSheet("font-size: 14px; color: #555;"); + lay->addWidget(m_testStatusLabel); + + m_testDetailLabel = new QLabel(""); + m_testDetailLabel->setWordWrap(true); + m_testDetailLabel->setAlignment(Qt::AlignCenter); + m_testDetailLabel->setStyleSheet("font-size: 12px; color: #888;"); + lay->addWidget(m_testDetailLabel); + + lay->addStretch(); + + m_testButton = new QPushButton("Probar conexiรณn ahora"); + m_testButton->setStyleSheet( + "QPushButton { background-color: #0071e3; color: white; border: none; padding: 8px 16px; border-radius: 4px; }" + "QPushButton:hover { background-color: #005bb5; }" + ); + lay->addWidget(m_testButton, 0, Qt::AlignCenter); + + connect(m_testButton, &QPushButton::clicked, this, &ConnectionWizard::onTestClicked); + + m_testPage->setLayout(lay); +} + +void ConnectionWizard::onTestClicked() +{ + if (!m_accountService) { + m_testStatusLabel->setText("โš ๏ธ Servicio de cuenta no disponible."); + m_testStatusLabel->setStyleSheet("color: #e60000;"); + m_testDetailLabel->setText("El servicio de cuenta no estรก inicializado."); + return; + } + + Account::ConnectionSettings settings = connectionSettings(); + // Basic validation + if (settings.incomingHost.isEmpty() || settings.username.isEmpty() || settings.password.isEmpty()) { + m_testStatusLabel->setText("โš ๏ธ Faltan datos obligatorios."); + m_testStatusLabel->setStyleSheet("color: #e60000;"); + m_testDetailLabel->setText("Por favor, completa los campos de servidor entrante, usuario y contraseรฑa."); + return; + } + + m_testStatusLabel->setText("โณ Probando conexiรณn..."); + m_testStatusLabel->setStyleSheet("color: #0071e3;"); + m_testDetailLabel->setText("Conectando al servidor IMAP..."); + + // Use a small delay to allow UI update before blocking call + QTimer::singleShot(0, this, [this, settings]() { + QString errorMsg; + bool ok = m_accountService->testConnection(settings, errorMsg); + if (ok) { + m_testStatusLabel->setText("โœ… Conexiรณn exitosa."); + m_testStatusLabel->setStyleSheet("color: #34c759;"); + m_testDetailLabel->setText("Los datos ingresados son correctos. Puedes finalizar la configuraciรณn."); + } else { + m_testStatusLabel->setText("โŒ Error de conexiรณn."); + m_testStatusLabel->setStyleSheet("color: #ff3b30;"); + m_testDetailLabel->setText(errorMsg.isEmpty() ? "Error desconocido." : errorMsg); + } + }); +} + +Account::ConnectionSettings ConnectionWizard::connectionSettings() const +{ + Account::ConnectionSettings s; + // Incoming + s.type = m_incomingTypeCombo->currentData().toString(); // "imap" or "pop3" + s.incomingHost = m_incomingHostEdit->text().trimmed(); + s.incomingPort = m_incomingPortEdit->text().toInt(); + s.incomingSsl = m_incomingSslCheck->isChecked(); + + // Outgoing (SMTP) + s.outgoingHost = m_outgoingHostEdit->text().trimmed(); + s.outgoingPort = m_outgoingPortEdit->text().toInt(); + s.outgoingSsl = m_outgoingSslCheck->isChecked(); + + // Auth + s.username = m_usernameEdit->text().trimmed(); + s.password = m_passwordEdit->text(); + s.authMethod = m_authMethodCombo->currentData().toString(); // "plain" or "oauth2" + + return s; +} + +void ConnectionWizard::accept() +{ + // When Finish is pressed, emit the settings and close + Account::ConnectionSettings settings = connectionSettings(); + emit settingsReady(settings); + QWizard::accept(); +} + +#include "connectionwizard.moc" \ No newline at end of file diff --git a/src/ui/connectionwizard.h b/src/ui/connectionwizard.h new file mode 100644 index 0000000..e6d0e10 --- /dev/null +++ b/src/ui/connectionwizard.h @@ -0,0 +1,73 @@ +#ifndef CONNECTIONWIZARD_H +#define CONNECTIONWIZARD_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "core/models/account.h" +#include "services/accountservice.h" + +class ConnectionWizard : public QWizard { + Q_OBJECT + +public: + explicit ConnectionWizard(QWidget *parent = nullptr, AccountService *accountService = nullptr); + ~ConnectionWizard() override = default; + + // Retrieve the filled connection settings + Account::ConnectionSettings connectionSettings() const; + +signals: + void settingsReady(const Account::ConnectionSettings &settings); + +private slots: + void accept() override; + void onTestClicked(); + +private: + void setupPageIntro(); + void setupPageIncoming(); + void setupPageOutgoing(); + void setupPageAuth(); + void setupPageTest(); + + // Page pointers (optional) + QWizardPage *m_introPage; + QWizardPage *m_incomingPage; + QWizardPage *m_outgoingPage; + QWizardPage *m_authPage; + QWizardPage *m_testPage; + + // Incoming fields + QComboBox *m_incomingTypeCombo; // IMAP, POP3 + QLineEdit *m_incomingHostEdit; + QLineEdit *m_incomingPortEdit; + QCheckBox *m_incomingSslCheck; + + // Outgoing fields + QLineEdit *m_outgoingHostEdit; + QLineEdit *m_outgoingPortEdit; + QCheckBox *m_outgoingSslCheck; + + // Auth fields + QLineEdit *m_usernameEdit; + QLineEdit *m_passwordEdit; + QComboBox *m_authMethodCombo; // Plain, Login, OAuth2 (if supported) + + // Test page + QLabel *m_testStatusLabel; + QLabel *m_testDetailLabel; + QPushButton *m_testButton; + + AccountService *m_accountService; +}; + +#endif // CONNECTIONWIZARD_H \ No newline at end of file diff --git a/src/ui/fix.py b/src/ui/fix.py new file mode 100644 index 0000000..88c63b1 --- /dev/null +++ b/src/ui/fix.py @@ -0,0 +1,109 @@ +import sys + +filename = '/mnt/c/Users/javie/wino-mail-dtkqt/src/ui/mainmainwindow.cpp' +with open(filename, 'r') as f: + lines = f.readlines() + +# Find start of delete connect +start = None +for i, line in enumerate(lines): + if 'connect(deleteAction, &QAction::triggered' in line: + start = i + break +if start is None: + print('Could not find delete connect start') + sys.exit(1) + +# Find end of that connect (the matching '});' after start) +end = None +brace_count = 0 +for i in range(start, len(lines)): + line = lines[i] + for ch in line: + if ch == '{': + brace_count += 1 + elif ch == '}': + brace_count -= 1 + if brace_count == 0 and '});' in line: + end = i + break + if end is not None: + break +if end is None: + print('Could not find end of delete connect') + sys.exit(1) + +# Replacement delete lambda +new_delete = ''' connect(deleteAction, &QAction::triggered, [this]() { + QModelIndexList indexes = m_mailListView->selectionModel()->selectedIndexes(); + if (indexes.isEmpty()) { + statusBar()->showMessage(tr("No email selected"), 2000); + return; + } + int row = indexes.first().row(); + QModelIndex idx = m_emailModel->index(row, 0); + qint64 id = idx.data(EmailListModel::IdRole).toLongLong(); + if (m_mailService->deleteMail(QString::number(id))) { + statusBar()->showMessage(tr("Email deleted"), 2000); + m_emailModel->refresh(); + } else { + statusBar()->showMessage(tr("Failed to delete email"), 2000); + } + });\n''' + +# Replace lines[start:end+1] with new_delete +lines = lines[:start] + [new_delete] + lines[end+1:] + +# Now add flag action declaration after deleteAction declaration +decl_line = None +for i, line in enumerate(lines): + if 'QAction *deleteAction = m_toolBar->addAction(\"๐Ÿ—‘ Delete\");' in line: + decl_line = i + break +if decl_line is None: + print('Could not find deleteAction declaration') + sys.exit(1) + +flag_decl = ' QAction *flagAction = m_toolBar->addAction(\"๐Ÿšฉ Flag\");\n' +lines = lines[:decl_line+1] + [flag_decl] + lines[decl_line+1:] + +# Find where to insert flag connection: after the delete connect we just placed +# Search for the '});' that ends the new delete connect (starting from start) +insert_point = None +for i in range(start, len(lines)): + if '});' in lines[i]: + insert_point = i + 1 + break +if insert_point is None: + insert_point = len(lines) + +flag_conn = ''' connect(flagAction, &QAction::triggered, [this]() { + QModelIndexList indexes = m_mailListView->selectionModel()->selectedIndexes(); + if (indexes.isEmpty()) { + statusBar()->showMessage(tr("No email selected"), 2000); + return; + } + int row = indexes.first().row(); + QModelIndex idx = m_emailModel->index(row, 0); + qint64 id = idx.data(EmailListModel::IdRole).toLongLong(); + // Toggle flagged state + std::optional opt = MailItemDao::findById(id); + if (opt) { + MailItem item = *opt; + item.setFlagged(!item.isFlagged()); + if (MailItemDao::update(item)) { + statusBar()->showMessage(tr("Flag toggled"), 2000); + m_emailModel->refresh(); + } else { + statusBar()->showMessage(tr("Failed to update flag"), 2000); + } + } else { + statusBar()->showMessage(tr("Email not found"), 2000); + } + });\n''' +lines = lines[:insert_point] + [flag_conn] + lines[insert_point:] + +with open(filename, 'w') as f: + f.writelines(lines) + +print('Delete and flag actions updated') diff --git a/src/ui/insert.txt b/src/ui/insert.txt new file mode 100644 index 0000000..71c71a8 --- /dev/null +++ b/src/ui/insert.txt @@ -0,0 +1,24 @@ + // Trigger initial mail sync for existing accounts on startup + QVector accounts = AccountDao::findAll(); + for (const Account &account : accounts) { + if (account.type() == AccountType::IMAP) { + QVector folders = FolderDao::findByAccountId(account.id()); + QString inboxFolderId; + for (const Folder &folder : folders) { + if (folder.isInbox()) { + inboxFolderId = QString::number(folder.id()); + break; + } + } + if (inboxFolderId.isEmpty() && !folders.isEmpty()) { + // fallback to first folder + inboxFolderId = QString::number(folders.first().id()); + } + if (!inboxFolderId.isEmpty()) { + qDebug() << "[MainMainWindow] Triggering initial mail sync for account" << account.email() << "folder" << inboxFolderId; + m_mailService->fetchMails(QString::number(account.id()), inboxFolderId); + } else { + qWarning() << "[MainMainWindow] No folders found for account" << account.id(); + } + } + } diff --git a/src/ui/mainmainwindow.cpp b/src/ui/mainmainwindow.cpp index f9ee6c7..7220fdd 100644 --- a/src/ui/mainmainwindow.cpp +++ b/src/ui/mainmainwindow.cpp @@ -15,8 +15,11 @@ MainMainWindow::MainMainWindow(QWidget *parent) : QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1) { + qDebug() << "[MainMainWindow] Constructor start"; setupUI(); + qDebug() << "[MainMainWindow] setupUI done"; connectModels(); + qDebug() << "[MainMainWindow] connectModels done"; setWindowTitle("Wino Mail DTK"); resize(1280, 820); // Setup progress bar in status bar @@ -27,12 +30,20 @@ MainMainWindow::MainMainWindow(QWidget *parent) // Connect mail service progress/status signals connect(m_mailService, &MailService::progressChanged, this, &MainMainWindow::onProgressChanged); connect(m_mailService, &MailService::statusMessage, this, &MainMainWindow::onStatusMessage); + connect(m_mailService, &MailService::mailSent, this, [this](const QString &) { + statusBar()->showMessage(tr("Message sent"), 5000); + }); + connect(m_mailService, &MailService::mailSendFailed, this, [this](const QString &, const QString &error) { + statusBar()->showMessage(tr("Send failed: %1").arg(error), 8000); + }); // Provide account service to settings view m_settingsView->setAccountService(m_accountService); + qDebug() << "[MainMainWindow] Constructor complete"; } void MainMainWindow::setupUI() { + qDebug() << "[MainMainWindow::setupUI] Start"; // === Global stylesheet === this->setStyleSheet( "QMainWindow { background-color: #f5f5f7; }" @@ -40,8 +51,10 @@ void MainMainWindow::setupUI() "QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }" "QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }" ); + qDebug() << "[MainMainWindow::setupUI] Stylesheet set"; createToolBar(); + qDebug() << "[MainMainWindow::setupUI] Toolbar created"; // === Central widget === QWidget *central = new QWidget(); @@ -51,6 +64,7 @@ void MainMainWindow::setupUI() // === Sidebar === setupSidebar(); + qDebug() << "[MainMainWindow::setupUI] Sidebar created"; centralLayout->addWidget(m_sidebar); // === Separator line === @@ -62,23 +76,34 @@ void MainMainWindow::setupUI() // === Stacked pages === m_stack = new QStackedWidget(); m_stack->setStyleSheet("background-color: #f5f5f7;"); + qDebug() << "[MainMainWindow::setupUI] Stack created"; // Page 0: Mail (folder tree + mail list + reader) setupMailPage(); + qDebug() << "[MainMainWindow::setupUI] Mail page created"; m_stack->addWidget(m_mailPage); // 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, const QString &fromAddr) { - QString msg; + 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, const QStringList &attachmentPaths) { if (scheduleTime.isValid()) { - msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP")); - } else { - msg = "Message sent (simulated)"; + statusBar()->showMessage(tr("Scheduled sending is not available yet"), 5000); + return; } - if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc); - if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc); - statusBar()->showMessage(msg, 5000); + if (fromAddr.isEmpty()) { + statusBar()->showMessage(tr("Select an account before sending"), 5000); + return; + } + MailItem mail; + mail.setTo(to); + mail.setRecipient(to); + mail.setCc(cc); + mail.setBcc(bcc); + mail.setSubject(subject); + mail.setBodyHtml(body); + mail.setDate(QDateTime::currentDateTimeUtc()); + m_mailService->sendMail(mail, fromAddr, attachmentPaths); + statusBar()->showMessage(tr("Sending messageโ€ฆ"), 5000); switchToPage(PageMail); }); connect(m_composeView, &ComposeView::discardRequested, [this]() { @@ -120,6 +145,7 @@ void MainMainWindow::setupUI() statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000); }); m_stack->addWidget(m_settingsView); + qDebug() << "[MainMainWindow::setupUI] Settings page created"; // Page 3: Contacts m_contactsView = new ContactsView(); @@ -131,9 +157,11 @@ void MainMainWindow::setupUI() centralLayout->addWidget(m_stack, 1); setCentralWidget(central); + qDebug() << "[MainMainWindow::setupUI] Central widget set"; // Show mail page by default switchToPage(PageMail); + qDebug() << "[MainMainWindow::setupUI] Done"; } void MainMainWindow::setupSidebar() @@ -161,8 +189,7 @@ void MainMainWindow::setupSidebar() } void MainMainWindow::setupMailPage() -{ - m_mailPage = new QWidget(); +{ m_mailPage = new QWidget(); QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage); mailLayout->setContentsMargins(0, 0, 0, 0); mailLayout->setSpacing(0); @@ -189,7 +216,30 @@ void MainMainWindow::setupMailPage() }); m_folderSplitter->addWidget(m_mailListView); - // Reader + // Viewer stack: placeholder, reader, compose + m_viewerStack = new QStackedWidget(); + // Placeholder widget + m_placeholderWidget = new QWidget(); + QVBoxLayout *placeholderLayout = new QVBoxLayout(m_placeholderWidget); + placeholderLayout->setAlignment(Qt::AlignCenter); + QLabel *iconLabel = new QLabel(); + QPixmap pixmap = QIcon::fromTheme("mail-unread").pixmap(48, 48); + if (pixmap.isNull()) { + pixmap = QPixmap(48, 48); + pixmap.fill(Qt::gray); + } + iconLabel->setPixmap(pixmap); + iconLabel->setAlignment(Qt::AlignCenter); + QLabel *textLabel = new QLabel(tr("Seleccione un correo para leerlo")); + textLabel->setAlignment(Qt::AlignCenter); + textLabel->setStyleSheet("color: #666; font-size: 14px;"); + placeholderLayout->addStretch(); + placeholderLayout->addWidget(iconLabel); + placeholderLayout->addWidget(textLabel); + placeholderLayout->addStretch(); + m_placeholderWidget->setLayout(placeholderLayout); + + // Reader view m_emailViewer = new ReaderView(); m_emailViewer->setMinimumWidth(350); connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested); @@ -198,9 +248,22 @@ void MainMainWindow::setupMailPage() openMailInIndependentWindow(m_currentMailId); } }); - m_folderSplitter->addWidget(m_emailViewer); - // Default sizes: folder 240, list 380, reader flex + // Embedded compose view + m_embeddedComposeView = new ComposeView(); + connect(m_embeddedComposeView, &ComposeView::sendRequested, this, &MainMainWindow::onEmbeddedSendRequested); + connect(m_embeddedComposeView, &ComposeView::discardRequested, this, &MainMainWindow::onEmbeddedDiscardRequested); + connect(m_embeddedComposeView, &ComposeView::detachRequested, this, &MainMainWindow::onEmbeddedDetachRequested); + + // Add to stack + m_viewerStack->addWidget(m_placeholderWidget); + m_viewerStack->addWidget(m_emailViewer); + m_viewerStack->addWidget(m_embeddedComposeView); + m_viewerStack->setCurrentIndex(0); // show placeholder by default + + m_folderSplitter->addWidget(m_viewerStack); + + // Default sizes: folder 240, list 380, viewer flex m_folderSplitter->setSizes({240, 380, 600}); mailLayout->addWidget(m_folderSplitter); @@ -208,12 +271,18 @@ void MainMainWindow::setupMailPage() void MainMainWindow::connectModels() { + qDebug() << "[MainMainWindow::connectModels] Start"; m_accountService = new AccountService(this); + qDebug() << "[MainMainWindow::connectModels] AccountService created"; m_mailService = new MailService(m_accountService, this); + qDebug() << "[MainMainWindow::connectModels] MailService created"; m_composeView->setAccountService(m_accountService); + m_embeddedComposeView->setAccountService(m_accountService); m_folderModel = new FolderListModel(m_accountService, this); + qDebug() << "[MainMainWindow::connectModels] FolderListModel created"; m_emailModel = new EmailListModel(this); + qDebug() << "[MainMainWindow::connectModels] EmailListModel created"; m_folderTree->setModel(m_folderModel); m_mailListView->setModel(m_emailModel); @@ -221,6 +290,18 @@ void MainMainWindow::connectModels() m_folderTree->expandAll(); connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected); + connect(m_mailService, &MailService::mailFetched, this, + [this](const QString &, const QString &folderId, const QVector &) { + if (folderId.toInt() == m_currentFolderId) { + m_emailModel->refresh(); + statusBar()->showMessage(tr("Mail synchronized"), 3000); + } + }); + connect(m_mailService, &MailService::mailFetchError, this, + [this](const QString &, const QString &, const QString &error) { + statusBar()->showMessage(tr("Synchronization failed: %1").arg(error), 8000); + }); + qDebug() << "[MainMainWindow::connectModels] Done"; } void MainMainWindow::onNavChanged(int index) @@ -239,8 +320,7 @@ void MainMainWindow::switchToPage(int pageIndex) } void MainMainWindow::onFolderSelected(const QModelIndex &index) -{ - if (!index.isValid()) return; +{ if (!index.isValid()) return; int itemType = index.data(FolderListModel::ItemTypeRole).toInt(); if (itemType == FolderTreeItem::FolderNode) { @@ -257,43 +337,55 @@ void MainMainWindow::onFolderSelected(const QModelIndex &index) } } m_emailModel->refresh(); + // Clear selection and show placeholder + m_currentMailId = -1; + m_mailListView->tableView()->clearSelection(); + m_viewerStack->setCurrentIndex(0); // placeholder } } void MainMainWindow::onEmailSelected(int mailId) -{ - m_currentMailId = mailId; - std::optional item = MailItemDao::findById(mailId); - if (!item.has_value()) { +{ m_currentMailId = mailId; + if (mailId >= 0) { + std::optional item = MailItemDao::findById(mailId); + if (!item.has_value()) { + m_emailViewer->setMailItem(nullptr); + m_viewerStack->setCurrentIndex(0); // placeholder + return; + } + MailItem &mail = item.value(); + if (!mail.isRead()) { + mail.setRead(true); + MailItemDao::update(mail); + } + m_emailViewer->setMailItem(&mail); + m_viewerStack->setCurrentIndex(1); // reader + } else { m_emailViewer->setMailItem(nullptr); - return; + m_viewerStack->setCurrentIndex(0); // placeholder } - - MailItem &mail = item.value(); - if (!mail.isRead()) { - mail.setRead(true); - MailItemDao::update(mail); - } - m_emailViewer->setMailItem(&mail); } void MainMainWindow::onComposeRequested() -{ - switchToPage(PageCompose); +{ m_embeddedComposeView->initializeComposition(); + m_viewerStack->setCurrentIndex(2); // compose } void MainMainWindow::onReaderReplyRequested(const MailItem *item) -{ - if (item) { - m_composeView->setTo(item->sender()); - m_composeView->setSubject("Re: " + item->subject()); +{ if (item) { + m_embeddedComposeView->initializeComposition(); + m_embeddedComposeView->setTo(item->sender()); + m_embeddedComposeView->setSubject(QString("Re: %1").arg(item->subject())); + // Optionally set body with quoted text? We'll leave empty for now. + m_embeddedComposeView->setBody(QString()); // clear } - switchToPage(PageCompose); + m_viewerStack->setCurrentIndex(2); // compose } void MainMainWindow::onNewMessage() -{ - switchToPage(PageCompose); +{ // Show compose view in the viewer stack + m_embeddedComposeView->initializeComposition(); + m_viewerStack->setCurrentIndex(2); // compose } void MainMainWindow::openMailInIndependentWindow(int mailId) @@ -329,8 +421,11 @@ void MainMainWindow::createToolBar() connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage); connect(syncAction, &QAction::triggered, [this]() { if (m_currentFolderId >= 0) { - m_emailModel->refresh(); - statusBar()->showMessage("Refreshed", 2000); + const auto folder = FolderDao::findById(m_currentFolderId); + if (folder) { + m_mailService->fetchMails(QString::number(folder->accountId()), QString::number(m_currentFolderId)); + statusBar()->showMessage(tr("Synchronizingโ€ฆ"), 3000); + } } }); connect(openWinAction, &QAction::triggered, [this]() { @@ -341,7 +436,12 @@ void MainMainWindow::createToolBar() } }); connect(deleteAction, &QAction::triggered, [this]() { - statusBar()->showMessage("Delete would be implemented here", 3000); + if (m_currentMailId >= 0) { + m_mailService->deleteMail(QString::number(m_currentMailId)); + m_currentMailId = -1; + m_emailModel->refresh(); + m_viewerStack->setCurrentIndex(0); + } }); } @@ -385,6 +485,70 @@ void MainMainWindow::onAccountEditRequested(int accountId) void MainMainWindow::onAccountDeleteRequested(int accountId) { - Q_UNUSED(accountId); - statusBar()->showMessage(QString("Delete account %1 requested").arg(accountId), 3000); -} \ No newline at end of file + m_accountService->removeAccount(accountId); + m_folderModel->refresh(); + m_emailModel->setFolderId(-1); + m_emailModel->refresh(); + m_currentFolderId = -1; + m_currentMailId = -1; + m_viewerStack->setCurrentIndex(0); + statusBar()->showMessage(tr("Account removed"), 3000); +} +void MainMainWindow::onEmbeddedSendRequested(const QString &to, const QString &cc, const QString &bcc, + const QString &subject, const QString &body, + const QDateTime &scheduleTime, + const QString &fromAddress, + const QStringList &attachmentPaths) +{ + if (scheduleTime.isValid()) { + statusBar()->showMessage(tr("Scheduled sending is not available yet"), 5000); + return; + } + if (fromAddress.isEmpty()) { + statusBar()->showMessage(tr("Select an account before sending"), 5000); + return; + } + MailItem mail; + mail.setTo(to); + mail.setRecipient(to); + mail.setCc(cc); + mail.setBcc(bcc); + mail.setSubject(subject); + mail.setBodyHtml(body); + mail.setDate(QDateTime::currentDateTimeUtc()); + m_mailService->sendMail(mail, fromAddress, attachmentPaths); + statusBar()->showMessage(tr("Sending messageโ€ฆ"), 5000); + m_viewerStack->setCurrentIndex(0); // placeholder + m_embeddedComposeView->initializeComposition(); +} + +void MainMainWindow::onEmbeddedDiscardRequested() +{ + // Go back to placeholder + m_viewerStack->setCurrentIndex(0); // placeholder + m_embeddedComposeView->initializeComposition(); +} + +void MainMainWindow::onEmbeddedDetachRequested(QWidget *widget) +{ + // Detach the compose view to a standalone window (similar to main compose view's detach) + QStackedWidget *stack = qobject_cast(widget->parentWidget()); + if (stack) { + stack->removeWidget(widget); + } + + // Create standalone window + QMainWindow *detachedWin = new QMainWindow(); + detachedWin->setWindowTitle(tr("Compose - Wino Mail")); + + // Assign central widget and ensure it's visible and sized + detachedWin->setCentralWidget(widget); + widget->setMinimumSize(800, 600); + widget->update(); + + detachedWin->resize(800, 600); + detachedWin->setAttribute(Qt::WA_DeleteOnClose); + detachedWin->show(); + + statusBar()->showMessage(tr("Compose view detached to separate window"), 3000); +} diff --git a/src/ui/mainmainwindow.cpp.backslashfix b/src/ui/mainmainwindow.cpp.backslashfix new file mode 100644 index 0000000..4956375 --- /dev/null +++ b/src/ui/mainmainwindow.cpp.backslashfix @@ -0,0 +1,406 @@ +#include "mainmainwindow.h" +#include "core/models/account.h" +#include "core/mailitem.h" +#include "db/dao/mailitemdao.h" +#include "db/dao/folderdao.h" +#include +#include +#include "ui/accountsetupdialog.h" +#include +#include +#include +#include +#include +#include + +MainMainWindow::MainMainWindow(QWidget *parent) + : QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1) +{ + setupUI(); + connectModels(); + setWindowTitle("Wino Mail DTK"); + resize(1280, 820); +} + +void MainMainWindow::setupUI() { + // === Global stylesheet === + this->setStyleSheet( + "QMainWindow { background-color: #f5f5f7; }" + "QSplitter::handle { background-color: #d1d1d6; width: 1px; }" + "QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }" + "QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }" + ); + + createToolBar(); + + // === Central widget === + QWidget *central = new QWidget(); + QHBoxLayout *centralLayout = new QHBoxLayout(central); + centralLayout->setContentsMargins(0, 0, 0, 0); + centralLayout->setSpacing(0); + + // === Sidebar === + setupSidebar(); + centralLayout->addWidget(m_sidebar); + + // === Separator line === + QFrame *separator = new QFrame(); + separator->setFrameShape(QFrame::VLine); + separator->setStyleSheet("color: #d1d1d6;"); + centralLayout->addWidget(separator); + + // === Stacked pages === + m_stack = new QStackedWidget(); + m_stack->setStyleSheet("background-color: #f5f5f7;"); + + // Page 0: Mail (folder tree + mail list + reader) + setupMailPage(); + m_stack->addWidget(m_mailPage); + + // 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) { + QString msg; + if (scheduleTime.isValid()) { + msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP")); + } else { + msg = "Message sent (simulated)"; + } + if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc); + if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc); + statusBar()->showMessage(msg, 5000); + switchToPage(PageMail); + }); + connect(m_composeView, &ComposeView::discardRequested, [this]() { + switchToPage(PageMail); + }); + connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) { + // Detach compose view to a standalone window + QStackedWidget *stack = qobject_cast(composeView->parentWidget()); + if (stack) { + stack->removeWidget(composeView); + } + + // 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); + + // Page 2: Settings + m_settingsView = new SettingsView(); + connect(m_settingsView, &SettingsView::accountAddRequested, [this]() { + AccountSetupDialog dlg(m_accountService, this); + dlg.exec(); + }); + connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) { + statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000); + }); + m_stack->addWidget(m_settingsView); + + // Page 3: Contacts + m_contactsView = new ContactsView(); + m_stack->addWidget(m_contactsView); + + // Page 4: Calendar + m_calendarView = new CalendarView(); + m_stack->addWidget(m_calendarView); + + centralLayout->addWidget(m_stack, 1); + setCentralWidget(central); + + // Show mail page by default + switchToPage(PageMail); +} + +void MainMainWindow::setupSidebar() { + m_sidebar = new QListWidget(); + m_sidebar->setFixedWidth(64); + m_sidebar->setIconSize(QSize(24, 24)); + m_sidebar->setSpacing(4); + m_sidebar->setFrameShape(QFrame::NoFrame); + m_sidebar->setStyleSheet( + "QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }" + "QListWidget::item { color: #8e8e93; padding: 12px 0; text-align: center; font-size: 10px; border: none; border-radius: 8px; margin: 2px 8px; }" + "QListWidget::item:selected { background-color: #3a3a3c; color: #ffffff; }" + "QListWidget::item:hover { background-color: #3a3a3c; color: #ffffff; }" + ); + + m_sidebar->addItem("๐Ÿ“ง\nMail"); + m_sidebar->addItem("โœ๏ธ\nCompose"); + m_sidebar->addItem("โš™๏ธ\nSettings"); + m_sidebar->addItem("๐Ÿ‘ฅ\nContacts"); + m_sidebar->addItem("๐Ÿ“…\nCalendar"); + + m_sidebar->setCurrentRow(0); + connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged); +} + +void MainMainWindow::setupMailPage() { + m_mailPage = new QWidget(); + QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage); + mailLayout->setContentsMargins(0, 0, 0, 0); + mailLayout->setSpacing(0); + + m_folderSplitter = new QSplitter(Qt::Horizontal); + m_folderSplitter->setHandleWidth(1); + + // Folder tree + m_folderTree = new QTreeView(); + m_folderTree->setHeaderHidden(true); + m_folderTree->setIndentation(20); + m_folderTree->setMinimumWidth(220); + m_folderTree->setMaximumWidth(350); + m_folderTree->setFrameShape(QFrame::NoFrame); + m_folderTree->setExpandsOnDoubleClick(true); + m_folderSplitter->addWidget(m_folderTree); + + // Mail list (QTableView with sorting) + m_mailListView = new MailListView(); + connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected); + connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested); + connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) { + openMailInIndependentWindow(mailId); + }); + m_folderSplitter->addWidget(m_mailListView); + + // Reader + m_emailViewer = new ReaderView(); + m_emailViewer->setMinimumWidth(350); + connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested); + connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() { + if (m_currentMailId >= 0) { + openMailInIndependentWindow(m_currentMailId); + } + }); + m_folderSplitter->addWidget(m_emailViewer); + + // Default sizes: folder 240, list 380, reader flex + m_folderSplitter->setSizes({240, 380, 600}); + + mailLayout->addWidget(m_folderSplitter); +} + +void MainMainWindow::connectModels() { + m_accountService = new AccountService(this); + m_mailService = new MailService(this); + + m_folderModel = new FolderListModel(m_accountService, this); + m_emailModel = new EmailListModel(this); + + m_folderTree->setModel(m_folderModel); + m_mailListView->setModel(m_emailModel); + + m_folderTree->expandAll(); + + connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected); +} + +void MainMainWindow::onNavChanged(int index) { + switchToPage(static_cast(index)); +} + +void MainMainWindow::switchToPage(int pageIndex) { + m_stack->setCurrentIndex(pageIndex); + m_sidebar->blockSignals(true); + m_sidebar->setCurrentRow(pageIndex); + m_sidebar->blockSignals(false); + + // Show/hide toolbar actions per page +} + +void MainMainWindow::onFolderSelected(const QModelIndex &index) { + if (!index.isValid()) return; + + int itemType = index.data(FolderListModel::ItemTypeRole).toInt(); + if (itemType == FolderTreeItem::FolderNode) { + m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt(); + m_emailModel->setFolderId(m_currentFolderId); + // Fetch mails for this folder + std::optional optFolder = FolderDao::findById(m_currentFolderId); + if (optFolder.has_value()) { + Folder folder = optFolder.value(); + Account* account = m_accountService->findAccountById(folder.accountId()); + if (account) { + QString accountId = QString::number(account->id()); + QString folderId = QString::number(m_currentFolderId); + // Fetch mails asynchronously to avoid blocking UI + QMetaObject::invokeMethod(m_mailService, "fetchMails", Qt::QueuedConnection, + Q_ARG(QString, accountId), Q_ARG(QString, folderId)); + delete account; + } + } + m_emailModel->refresh(); + } +} + +void MainMainWindow::onEmailSelected(int mailId) { + m_currentMailId = mailId; + std::optional item = MailItemDao::findById(mailId); + if (!item.has_value()) { + m_emailViewer->setMailItem(nullptr); + return; + } + + MailItem &mail = item.value(); + if (!mail.isRead()) { + mail.setRead(true); + MailItemDao::update(mail); + } + m_emailViewer->setMailItem(&mail); +} + +void MainMainWindow::onComposeRequested() { + switchToPage(PageCompose); +} + +void MainMainWindow::onReaderReplyRequested(const MailItem *item) { + if (item) { + m_composeView->setTo(item->sender()); + m_composeView->setSubject("Re: " + item->subject()); + } + switchToPage(PageCompose); +} + +void MainMainWindow::onNewMessage() { + switchToPage(PageCompose); +} + +void MainMainWindow::openMailInIndependentWindow(int mailId) { + std::optional 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::onAddAccountRequested() { + AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this); + connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) { + statusBar()->showMessage(QString("Account added: %1").arg(account.email()), 3000); + m_folderModel->refresh(); // Refresh folder list to show new account's folders + }); + dialog->open(); +} + +void MainMainWindow::onAccountEditRequested(int accountId) { + Account *account = m_accountService->findAccountById(accountId); + if (!account) { + statusBar()->showMessage("Account not found", 3000); + return; + } + + AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this); + dialog->loadAccountForEditing(*account); + connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) { + statusBar()->showMessage(QString("Account updated: %1").arg(account.email()), 3000); + m_folderModel->refresh(); // Refresh folder list + }); + dialog->open(); +} + +void MainMainWindow::onAccountDeleteRequested(int accountId) { + Account *account = m_accountService->findAccountById(accountId); + if (!account) { + statusBar()->showMessage("Account not found", 3000); + return; + } + + if (QMessageBox::warning(this, "Delete Account", + QString("Are you sure you want to delete the account '%1'?").arg(account->email()), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + m_accountService->removeAccount(accountId); + statusBar()->showMessage(QString("Account deleted: %1").arg(account->email()), 3000); + m_folderModel->refresh(); // Refresh folder list to remove deleted account's folders + } +} + +void MainMainWindow::onAddAccountRequested() { + AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this); + connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) { + statusBar()->showMessage(QString(\"Account added: %1\").arg(account.email()), 3000); + m_folderModel->refresh(); // Refresh folder list to show new account's folders + }); + dialog->open(); +} + +void MainMainWindow::onAccountEditRequested(int accountId) { + Account *account = m_accountService->findAccountById(accountId); + if (!account) { + statusBar()->showMessage(\"Account not found\", 3000); + return; + } + + AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this); + dialog->loadAccountForEditing(*account); + connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) { + statusBar()->showMessage(QString(\"Account updated: %1\").arg(account.email()), 3000); + m_folderModel->refresh(); // Refresh folder list + }); + dialog->open(); +} + +void MainMainWindow::onAccountDeleteRequested(int accountId) { + Account *account = m_accountService->findAccountById(accountId); + if (!account) { + statusBar()->showMessage(\"Account not found\", 3000); + return; + } + + if (QMessageBox::warning(this, \"Delete Account\", + QString(\"Are you sure you want to delete the account '%1'?\").arg(account->email()), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + m_accountService->removeAccount(accountId); + statusBar()->showMessage(QString(\"Account deleted: %1\").arg(account->email()), 3000); + m_folderModel->refresh(); // Refresh folder list to remove deleted account's folders + } +} +void MainMainWindow::createToolBar() { + m_toolBar = addToolBar("Main Toolbar"); + m_toolBar->setMovable(false); + + QAction *newMsgAction = m_toolBar->addAction("โœ‰ New Message"); + m_toolBar->addSeparator(); + QAction *syncAction = m_toolBar->addAction("โŸณ Sync/Refresh"); + QAction *openWinAction = m_toolBar->addAction("โ†— Abrir en ventana"); + QAction *deleteAction = m_toolBar->addAction("๐Ÿ—‘ Delete"); + + connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage); + connect(syncAction, &QAction::triggered, [this]() { + if (m_currentFolderId >= 0) { + m_emailModel->refresh(); + statusBar()->showMessage("Refreshed", 2000); + } + }); + connect(openWinAction, &QAction::triggered, [this]() { + if (m_currentMailId >= 0) { + openMailInIndependentWindow(m_currentMailId); + } else { + statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000); + } + }); + connect(deleteAction, &QAction::triggered, [this]() { + statusBar()->showMessage("Delete would be implemented here", 3000); + }); +} \ No newline at end of file diff --git a/src/ui/mainmainwindow.cpp.backup2 b/src/ui/mainmainwindow.cpp.backup2 new file mode 100644 index 0000000..f9ee6c7 --- /dev/null +++ b/src/ui/mainmainwindow.cpp.backup2 @@ -0,0 +1,390 @@ +#include "mainmainwindow.h" +#include "core/models/account.h" +#include "core/mailitem.h" +#include "db/dao/mailitemdao.h" +#include "db/dao/folderdao.h" +#include +#include +#include "ui/accountsetupdialog.h" +#include +#include +#include +#include +#include + +MainMainWindow::MainMainWindow(QWidget *parent) + : QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1) +{ + setupUI(); + 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() +{ + // === Global stylesheet === + this->setStyleSheet( + "QMainWindow { background-color: #f5f5f7; }" + "QSplitter::handle { background-color: #d1d1d6; width: 1px; }" + "QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }" + "QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }" + ); + + createToolBar(); + + // === Central widget === + QWidget *central = new QWidget(); + QHBoxLayout *centralLayout = new QHBoxLayout(central); + centralLayout->setContentsMargins(0, 0, 0, 0); + centralLayout->setSpacing(0); + + // === Sidebar === + setupSidebar(); + centralLayout->addWidget(m_sidebar); + + // === Separator line === + QFrame *separator = new QFrame(); + separator->setFrameShape(QFrame::VLine); + separator->setStyleSheet("color: #d1d1d6;"); + centralLayout->addWidget(separator); + + // === Stacked pages === + m_stack = new QStackedWidget(); + m_stack->setStyleSheet("background-color: #f5f5f7;"); + + // Page 0: Mail (folder tree + mail list + reader) + setupMailPage(); + m_stack->addWidget(m_mailPage); + + // 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, const QString &fromAddr) { + QString msg; + if (scheduleTime.isValid()) { + msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP")); + } else { + msg = "Message sent (simulated)"; + } + if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc); + if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc); + statusBar()->showMessage(msg, 5000); + switchToPage(PageMail); + }); + connect(m_composeView, &ComposeView::discardRequested, [this]() { + switchToPage(PageMail); + }); + connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) { + // Detach compose view to a standalone window + QStackedWidget *stack = qobject_cast(composeView->parentWidget()); + if (stack) { + stack->removeWidget(composeView); + } + + // 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); + + // Page 2: Settings + m_settingsView = new SettingsView(); + connect(m_settingsView, &SettingsView::accountAddRequested, [this]() { + 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); + }); + m_stack->addWidget(m_settingsView); + + // Page 3: Contacts + m_contactsView = new ContactsView(); + m_stack->addWidget(m_contactsView); + + // Page 4: Calendar + m_calendarView = new CalendarView(); + m_stack->addWidget(m_calendarView); + + centralLayout->addWidget(m_stack, 1); + setCentralWidget(central); + + // Show mail page by default + switchToPage(PageMail); +} + +void MainMainWindow::setupSidebar() +{ + m_sidebar = new QListWidget(); + m_sidebar->setFixedWidth(100); + m_sidebar->setIconSize(QSize(24, 24)); + m_sidebar->setSpacing(4); + m_sidebar->setFrameShape(QFrame::NoFrame); + m_sidebar->setStyleSheet( + "QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }" + "QListWidget::item { color: #8e8e93; padding: 12px 0; text-align: center; font-size: 10px; border: none; border-radius: 8px; margin: 2px 8px; }" + "QListWidget::item:selected { background-color: #3a3a3c; color: #ffffff; }" + "QListWidget::item:hover { background-color: #3a3a3c; color: #ffffff; }" + ); + + m_sidebar->addItem("๐Ÿ“ง\nMail"); + m_sidebar->addItem("โœ๏ธ\nCompose"); + m_sidebar->addItem("โš™๏ธ\nSettings"); + m_sidebar->addItem("๐Ÿ‘ฅ\nContacts"); + m_sidebar->addItem("๐Ÿ“…\nCalendar"); + + m_sidebar->setCurrentRow(0); + connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged); +} + +void MainMainWindow::setupMailPage() +{ + m_mailPage = new QWidget(); + QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage); + mailLayout->setContentsMargins(0, 0, 0, 0); + mailLayout->setSpacing(0); + + m_folderSplitter = new QSplitter(Qt::Horizontal); + m_folderSplitter->setHandleWidth(1); + + // Folder tree + m_folderTree = new QTreeView(); + m_folderTree->setHeaderHidden(true); + m_folderTree->setIndentation(20); + m_folderTree->setMinimumWidth(220); + m_folderTree->setMaximumWidth(350); + m_folderTree->setFrameShape(QFrame::NoFrame); + m_folderTree->setExpandsOnDoubleClick(true); + m_folderSplitter->addWidget(m_folderTree); + + // Mail list (QTableView with sorting) + m_mailListView = new MailListView(); + connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected); + connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested); + connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) { + openMailInIndependentWindow(mailId); + }); + m_folderSplitter->addWidget(m_mailListView); + + // Reader + m_emailViewer = new ReaderView(); + m_emailViewer->setMinimumWidth(350); + connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested); + connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() { + if (m_currentMailId >= 0) { + openMailInIndependentWindow(m_currentMailId); + } + }); + m_folderSplitter->addWidget(m_emailViewer); + + // Default sizes: folder 240, list 380, reader flex + m_folderSplitter->setSizes({240, 380, 600}); + + mailLayout->addWidget(m_folderSplitter); +} + +void MainMainWindow::connectModels() +{ + m_accountService = new AccountService(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); + + m_folderTree->setModel(m_folderModel); + m_mailListView->setModel(m_emailModel); + + m_folderTree->expandAll(); + + connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected); +} + +void MainMainWindow::onNavChanged(int index) +{ + switchToPage(static_cast(index)); +} + +void MainMainWindow::switchToPage(int pageIndex) +{ + m_stack->setCurrentIndex(pageIndex); + m_sidebar->blockSignals(true); + m_sidebar->setCurrentRow(pageIndex); + m_sidebar->blockSignals(false); + + // Show/hide toolbar actions per page +} + +void MainMainWindow::onFolderSelected(const QModelIndex &index) +{ + if (!index.isValid()) return; + + int itemType = index.data(FolderListModel::ItemTypeRole).toInt(); + if (itemType == FolderTreeItem::FolderNode) { + m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt(); + m_emailModel->setFolderId(m_currentFolderId); + // Fetch mails for this folder + std::optional optFolder = FolderDao::findById(m_currentFolderId); + if (optFolder.has_value()) { + Folder folder = optFolder.value(); + Account* account = m_accountService->findAccountById(folder.accountId()); + if (account) { + m_mailService->fetchMails(QString::number(account->id()), QString::number(m_currentFolderId)); + delete account; + } + } + m_emailModel->refresh(); + } +} + +void MainMainWindow::onEmailSelected(int mailId) +{ + m_currentMailId = mailId; + std::optional item = MailItemDao::findById(mailId); + if (!item.has_value()) { + m_emailViewer->setMailItem(nullptr); + return; + } + + MailItem &mail = item.value(); + if (!mail.isRead()) { + mail.setRead(true); + MailItemDao::update(mail); + } + m_emailViewer->setMailItem(&mail); +} + +void MainMainWindow::onComposeRequested() +{ + switchToPage(PageCompose); +} + +void MainMainWindow::onReaderReplyRequested(const MailItem *item) +{ + if (item) { + m_composeView->setTo(item->sender()); + m_composeView->setSubject("Re: " + item->subject()); + } + switchToPage(PageCompose); +} + +void MainMainWindow::onNewMessage() +{ + switchToPage(PageCompose); +} + +void MainMainWindow::openMailInIndependentWindow(int mailId) +{ + std::optional 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() +{ + m_toolBar = addToolBar("Main Toolbar"); + m_toolBar->setMovable(false); + + QAction *newMsgAction = m_toolBar->addAction("โœ‰ New Message"); + m_toolBar->addSeparator(); + QAction *syncAction = m_toolBar->addAction("โŸณ Sync/Refresh"); + QAction *openWinAction = m_toolBar->addAction("โ†— Abrir en ventana"); + QAction *deleteAction = m_toolBar->addAction("๐Ÿ—‘ Delete"); + + connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage); + connect(syncAction, &QAction::triggered, [this]() { + if (m_currentFolderId >= 0) { + m_emailModel->refresh(); + statusBar()->showMessage("Refreshed", 2000); + } + }); + connect(openWinAction, &QAction::triggered, [this]() { + if (m_currentMailId >= 0) { + openMailInIndependentWindow(m_currentMailId); + } else { + statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000); + } + }); + 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); +} \ No newline at end of file diff --git a/src/ui/mainmainwindow.cpp.backup3 b/src/ui/mainmainwindow.cpp.backup3 new file mode 100644 index 0000000..f9ee6c7 --- /dev/null +++ b/src/ui/mainmainwindow.cpp.backup3 @@ -0,0 +1,390 @@ +#include "mainmainwindow.h" +#include "core/models/account.h" +#include "core/mailitem.h" +#include "db/dao/mailitemdao.h" +#include "db/dao/folderdao.h" +#include +#include +#include "ui/accountsetupdialog.h" +#include +#include +#include +#include +#include + +MainMainWindow::MainMainWindow(QWidget *parent) + : QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1) +{ + setupUI(); + 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() +{ + // === Global stylesheet === + this->setStyleSheet( + "QMainWindow { background-color: #f5f5f7; }" + "QSplitter::handle { background-color: #d1d1d6; width: 1px; }" + "QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }" + "QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }" + ); + + createToolBar(); + + // === Central widget === + QWidget *central = new QWidget(); + QHBoxLayout *centralLayout = new QHBoxLayout(central); + centralLayout->setContentsMargins(0, 0, 0, 0); + centralLayout->setSpacing(0); + + // === Sidebar === + setupSidebar(); + centralLayout->addWidget(m_sidebar); + + // === Separator line === + QFrame *separator = new QFrame(); + separator->setFrameShape(QFrame::VLine); + separator->setStyleSheet("color: #d1d1d6;"); + centralLayout->addWidget(separator); + + // === Stacked pages === + m_stack = new QStackedWidget(); + m_stack->setStyleSheet("background-color: #f5f5f7;"); + + // Page 0: Mail (folder tree + mail list + reader) + setupMailPage(); + m_stack->addWidget(m_mailPage); + + // 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, const QString &fromAddr) { + QString msg; + if (scheduleTime.isValid()) { + msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP")); + } else { + msg = "Message sent (simulated)"; + } + if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc); + if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc); + statusBar()->showMessage(msg, 5000); + switchToPage(PageMail); + }); + connect(m_composeView, &ComposeView::discardRequested, [this]() { + switchToPage(PageMail); + }); + connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) { + // Detach compose view to a standalone window + QStackedWidget *stack = qobject_cast(composeView->parentWidget()); + if (stack) { + stack->removeWidget(composeView); + } + + // 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); + + // Page 2: Settings + m_settingsView = new SettingsView(); + connect(m_settingsView, &SettingsView::accountAddRequested, [this]() { + 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); + }); + m_stack->addWidget(m_settingsView); + + // Page 3: Contacts + m_contactsView = new ContactsView(); + m_stack->addWidget(m_contactsView); + + // Page 4: Calendar + m_calendarView = new CalendarView(); + m_stack->addWidget(m_calendarView); + + centralLayout->addWidget(m_stack, 1); + setCentralWidget(central); + + // Show mail page by default + switchToPage(PageMail); +} + +void MainMainWindow::setupSidebar() +{ + m_sidebar = new QListWidget(); + m_sidebar->setFixedWidth(100); + m_sidebar->setIconSize(QSize(24, 24)); + m_sidebar->setSpacing(4); + m_sidebar->setFrameShape(QFrame::NoFrame); + m_sidebar->setStyleSheet( + "QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }" + "QListWidget::item { color: #8e8e93; padding: 12px 0; text-align: center; font-size: 10px; border: none; border-radius: 8px; margin: 2px 8px; }" + "QListWidget::item:selected { background-color: #3a3a3c; color: #ffffff; }" + "QListWidget::item:hover { background-color: #3a3a3c; color: #ffffff; }" + ); + + m_sidebar->addItem("๐Ÿ“ง\nMail"); + m_sidebar->addItem("โœ๏ธ\nCompose"); + m_sidebar->addItem("โš™๏ธ\nSettings"); + m_sidebar->addItem("๐Ÿ‘ฅ\nContacts"); + m_sidebar->addItem("๐Ÿ“…\nCalendar"); + + m_sidebar->setCurrentRow(0); + connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged); +} + +void MainMainWindow::setupMailPage() +{ + m_mailPage = new QWidget(); + QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage); + mailLayout->setContentsMargins(0, 0, 0, 0); + mailLayout->setSpacing(0); + + m_folderSplitter = new QSplitter(Qt::Horizontal); + m_folderSplitter->setHandleWidth(1); + + // Folder tree + m_folderTree = new QTreeView(); + m_folderTree->setHeaderHidden(true); + m_folderTree->setIndentation(20); + m_folderTree->setMinimumWidth(220); + m_folderTree->setMaximumWidth(350); + m_folderTree->setFrameShape(QFrame::NoFrame); + m_folderTree->setExpandsOnDoubleClick(true); + m_folderSplitter->addWidget(m_folderTree); + + // Mail list (QTableView with sorting) + m_mailListView = new MailListView(); + connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected); + connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested); + connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) { + openMailInIndependentWindow(mailId); + }); + m_folderSplitter->addWidget(m_mailListView); + + // Reader + m_emailViewer = new ReaderView(); + m_emailViewer->setMinimumWidth(350); + connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested); + connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() { + if (m_currentMailId >= 0) { + openMailInIndependentWindow(m_currentMailId); + } + }); + m_folderSplitter->addWidget(m_emailViewer); + + // Default sizes: folder 240, list 380, reader flex + m_folderSplitter->setSizes({240, 380, 600}); + + mailLayout->addWidget(m_folderSplitter); +} + +void MainMainWindow::connectModels() +{ + m_accountService = new AccountService(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); + + m_folderTree->setModel(m_folderModel); + m_mailListView->setModel(m_emailModel); + + m_folderTree->expandAll(); + + connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected); +} + +void MainMainWindow::onNavChanged(int index) +{ + switchToPage(static_cast(index)); +} + +void MainMainWindow::switchToPage(int pageIndex) +{ + m_stack->setCurrentIndex(pageIndex); + m_sidebar->blockSignals(true); + m_sidebar->setCurrentRow(pageIndex); + m_sidebar->blockSignals(false); + + // Show/hide toolbar actions per page +} + +void MainMainWindow::onFolderSelected(const QModelIndex &index) +{ + if (!index.isValid()) return; + + int itemType = index.data(FolderListModel::ItemTypeRole).toInt(); + if (itemType == FolderTreeItem::FolderNode) { + m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt(); + m_emailModel->setFolderId(m_currentFolderId); + // Fetch mails for this folder + std::optional optFolder = FolderDao::findById(m_currentFolderId); + if (optFolder.has_value()) { + Folder folder = optFolder.value(); + Account* account = m_accountService->findAccountById(folder.accountId()); + if (account) { + m_mailService->fetchMails(QString::number(account->id()), QString::number(m_currentFolderId)); + delete account; + } + } + m_emailModel->refresh(); + } +} + +void MainMainWindow::onEmailSelected(int mailId) +{ + m_currentMailId = mailId; + std::optional item = MailItemDao::findById(mailId); + if (!item.has_value()) { + m_emailViewer->setMailItem(nullptr); + return; + } + + MailItem &mail = item.value(); + if (!mail.isRead()) { + mail.setRead(true); + MailItemDao::update(mail); + } + m_emailViewer->setMailItem(&mail); +} + +void MainMainWindow::onComposeRequested() +{ + switchToPage(PageCompose); +} + +void MainMainWindow::onReaderReplyRequested(const MailItem *item) +{ + if (item) { + m_composeView->setTo(item->sender()); + m_composeView->setSubject("Re: " + item->subject()); + } + switchToPage(PageCompose); +} + +void MainMainWindow::onNewMessage() +{ + switchToPage(PageCompose); +} + +void MainMainWindow::openMailInIndependentWindow(int mailId) +{ + std::optional 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() +{ + m_toolBar = addToolBar("Main Toolbar"); + m_toolBar->setMovable(false); + + QAction *newMsgAction = m_toolBar->addAction("โœ‰ New Message"); + m_toolBar->addSeparator(); + QAction *syncAction = m_toolBar->addAction("โŸณ Sync/Refresh"); + QAction *openWinAction = m_toolBar->addAction("โ†— Abrir en ventana"); + QAction *deleteAction = m_toolBar->addAction("๐Ÿ—‘ Delete"); + + connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage); + connect(syncAction, &QAction::triggered, [this]() { + if (m_currentFolderId >= 0) { + m_emailModel->refresh(); + statusBar()->showMessage("Refreshed", 2000); + } + }); + connect(openWinAction, &QAction::triggered, [this]() { + if (m_currentMailId >= 0) { + openMailInIndependentWindow(m_currentMailId); + } else { + statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000); + } + }); + 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); +} \ No newline at end of file diff --git a/src/ui/mainmainwindow.cpp.backup6 b/src/ui/mainmainwindow.cpp.backup6 new file mode 100644 index 0000000..f9ee6c7 --- /dev/null +++ b/src/ui/mainmainwindow.cpp.backup6 @@ -0,0 +1,390 @@ +#include "mainmainwindow.h" +#include "core/models/account.h" +#include "core/mailitem.h" +#include "db/dao/mailitemdao.h" +#include "db/dao/folderdao.h" +#include +#include +#include "ui/accountsetupdialog.h" +#include +#include +#include +#include +#include + +MainMainWindow::MainMainWindow(QWidget *parent) + : QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1) +{ + setupUI(); + 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() +{ + // === Global stylesheet === + this->setStyleSheet( + "QMainWindow { background-color: #f5f5f7; }" + "QSplitter::handle { background-color: #d1d1d6; width: 1px; }" + "QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }" + "QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }" + ); + + createToolBar(); + + // === Central widget === + QWidget *central = new QWidget(); + QHBoxLayout *centralLayout = new QHBoxLayout(central); + centralLayout->setContentsMargins(0, 0, 0, 0); + centralLayout->setSpacing(0); + + // === Sidebar === + setupSidebar(); + centralLayout->addWidget(m_sidebar); + + // === Separator line === + QFrame *separator = new QFrame(); + separator->setFrameShape(QFrame::VLine); + separator->setStyleSheet("color: #d1d1d6;"); + centralLayout->addWidget(separator); + + // === Stacked pages === + m_stack = new QStackedWidget(); + m_stack->setStyleSheet("background-color: #f5f5f7;"); + + // Page 0: Mail (folder tree + mail list + reader) + setupMailPage(); + m_stack->addWidget(m_mailPage); + + // 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, const QString &fromAddr) { + QString msg; + if (scheduleTime.isValid()) { + msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP")); + } else { + msg = "Message sent (simulated)"; + } + if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc); + if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc); + statusBar()->showMessage(msg, 5000); + switchToPage(PageMail); + }); + connect(m_composeView, &ComposeView::discardRequested, [this]() { + switchToPage(PageMail); + }); + connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) { + // Detach compose view to a standalone window + QStackedWidget *stack = qobject_cast(composeView->parentWidget()); + if (stack) { + stack->removeWidget(composeView); + } + + // 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); + + // Page 2: Settings + m_settingsView = new SettingsView(); + connect(m_settingsView, &SettingsView::accountAddRequested, [this]() { + 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); + }); + m_stack->addWidget(m_settingsView); + + // Page 3: Contacts + m_contactsView = new ContactsView(); + m_stack->addWidget(m_contactsView); + + // Page 4: Calendar + m_calendarView = new CalendarView(); + m_stack->addWidget(m_calendarView); + + centralLayout->addWidget(m_stack, 1); + setCentralWidget(central); + + // Show mail page by default + switchToPage(PageMail); +} + +void MainMainWindow::setupSidebar() +{ + m_sidebar = new QListWidget(); + m_sidebar->setFixedWidth(100); + m_sidebar->setIconSize(QSize(24, 24)); + m_sidebar->setSpacing(4); + m_sidebar->setFrameShape(QFrame::NoFrame); + m_sidebar->setStyleSheet( + "QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }" + "QListWidget::item { color: #8e8e93; padding: 12px 0; text-align: center; font-size: 10px; border: none; border-radius: 8px; margin: 2px 8px; }" + "QListWidget::item:selected { background-color: #3a3a3c; color: #ffffff; }" + "QListWidget::item:hover { background-color: #3a3a3c; color: #ffffff; }" + ); + + m_sidebar->addItem("๐Ÿ“ง\nMail"); + m_sidebar->addItem("โœ๏ธ\nCompose"); + m_sidebar->addItem("โš™๏ธ\nSettings"); + m_sidebar->addItem("๐Ÿ‘ฅ\nContacts"); + m_sidebar->addItem("๐Ÿ“…\nCalendar"); + + m_sidebar->setCurrentRow(0); + connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged); +} + +void MainMainWindow::setupMailPage() +{ + m_mailPage = new QWidget(); + QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage); + mailLayout->setContentsMargins(0, 0, 0, 0); + mailLayout->setSpacing(0); + + m_folderSplitter = new QSplitter(Qt::Horizontal); + m_folderSplitter->setHandleWidth(1); + + // Folder tree + m_folderTree = new QTreeView(); + m_folderTree->setHeaderHidden(true); + m_folderTree->setIndentation(20); + m_folderTree->setMinimumWidth(220); + m_folderTree->setMaximumWidth(350); + m_folderTree->setFrameShape(QFrame::NoFrame); + m_folderTree->setExpandsOnDoubleClick(true); + m_folderSplitter->addWidget(m_folderTree); + + // Mail list (QTableView with sorting) + m_mailListView = new MailListView(); + connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected); + connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested); + connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) { + openMailInIndependentWindow(mailId); + }); + m_folderSplitter->addWidget(m_mailListView); + + // Reader + m_emailViewer = new ReaderView(); + m_emailViewer->setMinimumWidth(350); + connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested); + connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() { + if (m_currentMailId >= 0) { + openMailInIndependentWindow(m_currentMailId); + } + }); + m_folderSplitter->addWidget(m_emailViewer); + + // Default sizes: folder 240, list 380, reader flex + m_folderSplitter->setSizes({240, 380, 600}); + + mailLayout->addWidget(m_folderSplitter); +} + +void MainMainWindow::connectModels() +{ + m_accountService = new AccountService(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); + + m_folderTree->setModel(m_folderModel); + m_mailListView->setModel(m_emailModel); + + m_folderTree->expandAll(); + + connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected); +} + +void MainMainWindow::onNavChanged(int index) +{ + switchToPage(static_cast(index)); +} + +void MainMainWindow::switchToPage(int pageIndex) +{ + m_stack->setCurrentIndex(pageIndex); + m_sidebar->blockSignals(true); + m_sidebar->setCurrentRow(pageIndex); + m_sidebar->blockSignals(false); + + // Show/hide toolbar actions per page +} + +void MainMainWindow::onFolderSelected(const QModelIndex &index) +{ + if (!index.isValid()) return; + + int itemType = index.data(FolderListModel::ItemTypeRole).toInt(); + if (itemType == FolderTreeItem::FolderNode) { + m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt(); + m_emailModel->setFolderId(m_currentFolderId); + // Fetch mails for this folder + std::optional optFolder = FolderDao::findById(m_currentFolderId); + if (optFolder.has_value()) { + Folder folder = optFolder.value(); + Account* account = m_accountService->findAccountById(folder.accountId()); + if (account) { + m_mailService->fetchMails(QString::number(account->id()), QString::number(m_currentFolderId)); + delete account; + } + } + m_emailModel->refresh(); + } +} + +void MainMainWindow::onEmailSelected(int mailId) +{ + m_currentMailId = mailId; + std::optional item = MailItemDao::findById(mailId); + if (!item.has_value()) { + m_emailViewer->setMailItem(nullptr); + return; + } + + MailItem &mail = item.value(); + if (!mail.isRead()) { + mail.setRead(true); + MailItemDao::update(mail); + } + m_emailViewer->setMailItem(&mail); +} + +void MainMainWindow::onComposeRequested() +{ + switchToPage(PageCompose); +} + +void MainMainWindow::onReaderReplyRequested(const MailItem *item) +{ + if (item) { + m_composeView->setTo(item->sender()); + m_composeView->setSubject("Re: " + item->subject()); + } + switchToPage(PageCompose); +} + +void MainMainWindow::onNewMessage() +{ + switchToPage(PageCompose); +} + +void MainMainWindow::openMailInIndependentWindow(int mailId) +{ + std::optional 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() +{ + m_toolBar = addToolBar("Main Toolbar"); + m_toolBar->setMovable(false); + + QAction *newMsgAction = m_toolBar->addAction("โœ‰ New Message"); + m_toolBar->addSeparator(); + QAction *syncAction = m_toolBar->addAction("โŸณ Sync/Refresh"); + QAction *openWinAction = m_toolBar->addAction("โ†— Abrir en ventana"); + QAction *deleteAction = m_toolBar->addAction("๐Ÿ—‘ Delete"); + + connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage); + connect(syncAction, &QAction::triggered, [this]() { + if (m_currentFolderId >= 0) { + m_emailModel->refresh(); + statusBar()->showMessage("Refreshed", 2000); + } + }); + connect(openWinAction, &QAction::triggered, [this]() { + if (m_currentMailId >= 0) { + openMailInIndependentWindow(m_currentMailId); + } else { + statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000); + } + }); + 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); +} \ No newline at end of file diff --git a/src/ui/mainmainwindow.cpp.bak b/src/ui/mainmainwindow.cpp.bak new file mode 100644 index 0000000..12941b4 --- /dev/null +++ b/src/ui/mainmainwindow.cpp.bak @@ -0,0 +1,366 @@ +#include "mainmainwindow.h" +#include "core/models/account.h" +#include "core/mailitem.h" +#include "db/dao/mailitemdao.h" +#include "db/dao/folderdao.h" +#include +#include +#include "ui/accountsetupdialog.h" +#include +#include +#include +#include +#include +#include + +MainMainWindow::MainMainWindow(QWidget *parent) + : QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1) +{ + setupUI(); + connectModels(); + setWindowTitle("Wino Mail DTK"); + resize(1280, 820); +} + +void MainMainWindow::setupUI() { + // === Global stylesheet === + this->setStyleSheet( + "QMainWindow { background-color: #f5f5f7; }" + "QSplitter::handle { background-color: #d1d1d6; width: 1px; }" + "QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }" + "QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }" + ); + + createToolBar(); + + // === Central widget === + QWidget *central = new QWidget(); + QHBoxLayout *centralLayout = new QHBoxLayout(central); + centralLayout->setContentsMargins(0, 0, 0, 0); + centralLayout->setSpacing(0); + + // === Sidebar === + setupSidebar(); + centralLayout->addWidget(m_sidebar); + + // === Separator line === + QFrame *separator = new QFrame(); + separator->setFrameShape(QFrame::VLine); + separator->setStyleSheet("color: #d1d1d6;"); + centralLayout->addWidget(separator); + + // === Stacked pages === + m_stack = new QStackedWidget(); + m_stack->setStyleSheet("background-color: #f5f5f7;"); + + // Page 0: Mail (folder tree + mail list + reader) + setupMailPage(); + m_stack->addWidget(m_mailPage); + + // 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) { + QString msg; + if (scheduleTime.isValid()) { + msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP")); + } else { + msg = "Message sent (simulated)"; + } + if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc); + if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc); + statusBar()->showMessage(msg, 5000); + switchToPage(PageMail); + }); + connect(m_composeView, &ComposeView::discardRequested, [this]() { + switchToPage(PageMail); + }); + connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) { + // Detach compose view to a standalone window + QStackedWidget *stack = qobject_cast(composeView->parentWidget()); + if (stack) { + stack->removeWidget(composeView); + } + + // 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); + + // Page 2: Settings + m_settingsView = new SettingsView(); + connect(m_settingsView, &SettingsView::accountAddRequested, [this]() { + AccountSetupDialog dlg(m_accountService, this); + dlg.exec(); + }); + connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) { + statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000); + }); + m_stack->addWidget(m_settingsView); + + // Page 3: Contacts + m_contactsView = new ContactsView(); + m_stack->addWidget(m_contactsView); + + // Page 4: Calendar + m_calendarView = new CalendarView(); + m_stack->addWidget(m_calendarView); + + centralLayout->addWidget(m_stack, 1); + setCentralWidget(central); + + // Show mail page by default + switchToPage(PageMail); +} + +void MainMainWindow::setupSidebar() { + m_sidebar = new QListWidget(); + m_sidebar->setFixedWidth(64); + m_sidebar->setIconSize(QSize(24, 24)); + m_sidebar->setSpacing(4); + m_sidebar->setFrameShape(QFrame::NoFrame); + m_sidebar->setStyleSheet( + "QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }" + "QListWidget::item { color: #8e8e93; padding: 12px 0; text-align: center; font-size: 10px; border: none; border-radius: 8px; margin: 2px 8px; }" + "QListWidget::item:selected { background-color: #3a3a3c; color: #ffffff; }" + "QListWidget::item:hover { background-color: #3a3a3c; color: #ffffff; }" + ); + + m_sidebar->addItem("๐Ÿ“ง\nMail"); + m_sidebar->addItem("โœ๏ธ\nCompose"); + m_sidebar->addItem("โš™๏ธ\nSettings"); + m_sidebar->addItem("๐Ÿ‘ฅ\nContacts"); + m_sidebar->addItem("๐Ÿ“…\nCalendar"); + + m_sidebar->setCurrentRow(0); + connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged); +} + +void MainMainWindow::setupMailPage() { + m_mailPage = new QWidget(); + QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage); + mailLayout->setContentsMargins(0, 0, 0, 0); + mailLayout->setSpacing(0); + + m_folderSplitter = new QSplitter(Qt::Horizontal); + m_folderSplitter->setHandleWidth(1); + + // Folder tree + m_folderTree = new QTreeView(); + m_folderTree->setHeaderHidden(true); + m_folderTree->setIndentation(20); + m_folderTree->setMinimumWidth(220); + m_folderTree->setMaximumWidth(350); + m_folderTree->setFrameShape(QFrame::NoFrame); + m_folderTree->setExpandsOnDoubleClick(true); + m_folderSplitter->addWidget(m_folderTree); + + // Mail list (QTableView with sorting) + m_mailListView = new MailListView(); + connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected); + connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested); + connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) { + openMailInIndependentWindow(mailId); + }); + m_folderSplitter->addWidget(m_mailListView); + + // Reader + m_emailViewer = new ReaderView(); + m_emailViewer->setMinimumWidth(350); + connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested); + connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() { + if (m_currentMailId >= 0) { + openMailInIndependentWindow(m_currentMailId); + } + }); + m_folderSplitter->addWidget(m_emailViewer); + + // Default sizes: folder 240, list 380, reader flex + m_folderSplitter->setSizes({240, 380, 600}); + + mailLayout->addWidget(m_folderSplitter); +} + +void MainMainWindow::connectModels() { + m_accountService = new AccountService(this); + m_mailService = new MailService(this); + + m_folderModel = new FolderListModel(m_accountService, this); + m_emailModel = new EmailListModel(this); + + m_folderTree->setModel(m_folderModel); + m_mailListView->setModel(m_emailModel); + + m_folderTree->expandAll(); + + connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected); +} + +void MainMainWindow::onNavChanged(int index) { + switchToPage(static_cast(index)); +} + +void MainMainWindow::switchToPage(int pageIndex) { + m_stack->setCurrentIndex(pageIndex); + m_sidebar->blockSignals(true); + m_sidebar->setCurrentRow(pageIndex); + m_sidebar->blockSignals(false); + + // Show/hide toolbar actions per page +} + +void MainMainWindow::onFolderSelected(const QModelIndex &index) { + if (!index.isValid()) return; + + int itemType = index.data(FolderListModel::ItemTypeRole).toInt(); + if (itemType == FolderTreeItem::FolderNode) { + m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt(); + m_emailModel->setFolderId(m_currentFolderId); + // Fetch mails for this folder + std::optional optFolder = FolderDao::findById(m_currentFolderId); + if (optFolder.has_value()) { + Folder folder = optFolder.value(); + Account* account = m_accountService->findAccountById(folder.accountId()); + if (account) { + QString accountId = QString::number(account->id()); + QString folderId = QString::number(m_currentFolderId); + // Fetch mails asynchronously to avoid blocking UI + QMetaObject::invokeMethod(m_mailService, "fetchMails", Qt::QueuedConnection, + Q_ARG(QString, accountId), Q_ARG(QString, folderId)); + delete account; + } + } + m_emailModel->refresh(); + } +} + +void MainMainWindow::onEmailSelected(int mailId) { + m_currentMailId = mailId; + std::optional item = MailItemDao::findById(mailId); + if (!item.has_value()) { + m_emailViewer->setMailItem(nullptr); + return; + } + + MailItem &mail = item.value(); + if (!mail.isRead()) { + mail.setRead(true); + MailItemDao::update(mail); + } + m_emailViewer->setMailItem(&mail); +} + +void MainMainWindow::onComposeRequested() { + switchToPage(PageCompose); +} + +void MainMainWindow::onReaderReplyRequested(const MailItem *item) { + if (item) { + m_composeView->setTo(item->sender()); + m_composeView->setSubject("Re: " + item->subject()); + } + switchToPage(PageCompose); +} + +void MainMainWindow::onNewMessage() { + switchToPage(PageCompose); +} + +void MainMainWindow::openMailInIndependentWindow(int mailId) { + std::optional 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::onAddAccountRequested() { + AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this); + connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) { + statusBar()->showMessage(QString("Account added: %1").arg(account.email()), 3000); + m_folderModel->refresh(); // Refresh folder list to show new account's folders + }); + dialog->open(); +} + +void MainMainWindow::onAccountEditRequested(int accountId) { + Account *account = m_accountService->findAccountById(accountId); + if (!account) { + statusBar()->showMessage("Account not found", 3000); + return; + } + + AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this); + dialog->loadAccountForEditing(*account); + connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) { + statusBar()->showMessage(QString("Account updated: %1").arg(account.email()), 3000); + m_folderModel->refresh(); // Refresh folder list + }); + dialog->open(); +} + +void MainMainWindow::onAccountDeleteRequested(int accountId) { + Account *account = m_accountService->findAccountById(accountId); + if (!account) { + statusBar()->showMessage("Account not found", 3000); + return; + } + + if (QMessageBox::warning(this, "Delete Account", + QString("Are you sure you want to delete the account '%1'?").arg(account->email()), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + m_accountService->removeAccount(accountId); + statusBar()->showMessage(QString("Account deleted: %1").arg(account->email()), 3000); + m_folderModel->refresh(); // Refresh folder list to remove deleted account's folders + } +} + +void MainMainWindow::createToolBar() { + m_toolBar = addToolBar("Main Toolbar"); + m_toolBar->setMovable(false); + + QAction *newMsgAction = m_toolBar->addAction("โœ‰ New Message"); + m_toolBar->addSeparator(); + QAction *syncAction = m_toolBar->addAction("โŸณ Sync/Refresh"); + QAction *openWinAction = m_toolBar->addAction("โ†— Abrir en ventana"); + QAction *deleteAction = m_toolBar->addAction("๐Ÿ—‘ Delete"); + + connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage); + connect(syncAction, &QAction::triggered, [this]() { + if (m_currentFolderId >= 0) { + m_emailModel->refresh(); + statusBar()->showMessage("Refreshed", 2000); + } + }); + connect(openWinAction, &QAction::triggered, [this]() { + if (m_currentMailId >= 0) { + openMailInIndependentWindow(m_currentMailId); + } else { + statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000); + } + }); + connect(deleteAction, &QAction::triggered, [this]() { + statusBar()->showMessage("Delete would be implemented here", 3000); + }); +} \ No newline at end of file diff --git a/src/ui/mainmainwindow.cpp.bak2 b/src/ui/mainmainwindow.cpp.bak2 new file mode 100644 index 0000000..f7c6ddd --- /dev/null +++ b/src/ui/mainmainwindow.cpp.bak2 @@ -0,0 +1,377 @@ +#include "mainmainwindow.h" +#include "core/models/account.h" +#include "core/mailitem.h" +#include "db/dao/mailitemdao.h" +#include "db/dao/folderdao.h" +#include +#include +#include "ui/accountsetupdialog.h" +#include +#include +#include +#include +#include +#include + +MainMainWindow::MainMainWindow(QWidget *parent) + : QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1) +{ + setupUI(); + connectModels(); + setWindowTitle("Wino Mail DTK"); + resize(1280, 820); +} + +void MainMainWindow::setupUI() { + // === Global stylesheet === + this->setStyleSheet( + "QMainWindow { background-color: #f5f5f7; }" + "QSplitter::handle { background-color: #d1d1d6; width: 1px; }" + "QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }" + "QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }" + ); + + createToolBar(); + + // === Central widget === + QWidget *central = new QWidget(); + QHBoxLayout *centralLayout = new QHBoxLayout(central); + centralLayout->setContentsMargins(0, 0, 0, 0); + centralLayout->setSpacing(0); + + // === Sidebar === + setupSidebar(); + centralLayout->addWidget(m_sidebar); + + // === Separator line === + QFrame *separator = new QFrame(); + separator->setFrameShape(QFrame::VLine); + separator->setStyleSheet("color: #d1d1d6;"); + centralLayout->addWidget(separator); + + // === Stacked pages === + m_stack = new QStackedWidget(); + m_stack->setStyleSheet("background-color: #f5f5f7;"); + + // Page 0: Mail (folder tree + mail list + reader) + setupMailPage(); + m_stack->addWidget(m_mailPage); + + // 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) { + QString msg; + if (scheduleTime.isValid()) { + msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP")); + } else { + msg = "Message sent (simulated)"; + } + if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc); + if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc); + statusBar()->showMessage(msg, 5000); + switchToPage(PageMail); + }); + connect(m_composeView, &ComposeView::discardRequested, [this]() { + switchToPage(PageMail); + }); + connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) { + // Detach compose view to a standalone window + QStackedWidget *stack = qobject_cast(composeView->parentWidget()); + if (stack) { + stack->removeWidget(composeView); + } + + // 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); + + // Page 2: Settings + m_settingsView = new SettingsView(); + connect(m_settingsView, &SettingsView::accountAddRequested, [this]() { + AccountSetupDialog dlg(m_accountService, this); + dlg.exec(); + }); + connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) { + statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000); + }); + m_stack->addWidget(m_settingsView); + + // Page 3: Contacts + m_contactsView = new ContactsView(); + m_stack->addWidget(m_contactsView); + + // Page 4: Calendar + m_calendarView = new CalendarView(); + m_stack->addWidget(m_calendarView); + + centralLayout->addWidget(m_stack, 1); + setCentralWidget(central); + + // Show mail page by default + switchToPage(PageMail); +} + +void MainMainWindow::setupSidebar() { + m_sidebar = new QListWidget(); + m_sidebar->setFixedWidth(64); + m_sidebar->setIconSize(QSize(24, 24)); + m_sidebar->setSpacing(4); + m_sidebar->setFrameShape(QFrame::NoFrame); + m_sidebar->setStyleSheet( + "QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }" + "QListWidget::item { color: #8e8e93; padding: 12px 0; text-align: center; font-size: 10px; border: none; border-radius: 8px; margin: 2px 8px; }" + "QListWidget::item:selected { background-color: #3a3a3c; color: #ffffff; }" + "QListWidget::item:hover { background-color: #3a3a3c; color: #ffffff; }" + ); + + m_sidebar->addItem("๐Ÿ“ง\nMail"); + m_sidebar->addItem("โœ๏ธ\nCompose"); + m_sidebar->addItem("โš™๏ธ\nSettings"); + m_sidebar->addItem("๐Ÿ‘ฅ\nContacts"); + m_sidebar->addItem("๐Ÿ“…\nCalendar"); + + m_sidebar->setCurrentRow(0); + connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged); +} + +void MainMainWindow::setupMailPage() { + m_mailPage = new QWidget(); + QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage); + mailLayout->setContentsMargins(0, 0, 0, 0); + mailLayout->setSpacing(0); + + m_folderSplitter = new QSplitter(Qt::Horizontal); + m_folderSplitter->setHandleWidth(1); + + // Folder tree + m_folderTree = new QTreeView(); + m_folderTree->setHeaderHidden(true); + m_folderTree->setIndentation(20); + m_folderTree->setMinimumWidth(220); + m_folderTree->setMaximumWidth(350); + m_folderTree->setFrameShape(QFrame::NoFrame); + m_folderTree->setExpandsOnDoubleClick(true); + m_folderSplitter->addWidget(m_folderTree); + + // Mail list (QTableView with sorting) + m_mailListView = new MailListView(); + connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected); + connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested); + connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) { + openMailInIndependentWindow(mailId); + }); + m_folderSplitter->addWidget(m_mailListView); + + // Reader + m_emailViewer = new ReaderView(); + m_emailViewer->setMinimumWidth(350); + connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested); + connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() { + if (m_currentMailId >= 0) { + openMailInIndependentWindow(m_currentMailId); + } + }); + m_folderSplitter->addWidget(m_emailViewer); + + // Default sizes: folder 240, list 380, reader flex + m_folderSplitter->setSizes({240, 380, 600}); + + mailLayout->addWidget(m_folderSplitter); +} + +void MainMainWindow::connectModels() { + m_accountService = new AccountService(this); + m_mailService = new MailService(this); + + m_folderModel = new FolderListModel(m_accountService, this); + m_emailModel = new EmailListModel(this); + + m_folderTree->setModel(m_folderModel); + m_mailListView->setModel(m_emailModel); + + m_folderTree->expandAll(); + // Auto-select first folder to trigger mail fetch + if (m_folderModel->rowCount() > 0) { + QModelIndex firstIdx = m_folderModel->index(0, 0); + if (firstIdx.isValid()) { + int itemType = firstIdx.data(FolderListModel::ItemTypeRole).toInt(); + if (itemType == FolderTreeItem::FolderNode) { + m_folderTree->setCurrentIndex(firstIdx); + onFolderSelected(firstIdx); + } + } + } + + connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected); +} + +void MainMainWindow::onNavChanged(int index) { + switchToPage(static_cast(index)); +} + +void MainMainWindow::switchToPage(int pageIndex) { + m_stack->setCurrentIndex(pageIndex); + m_sidebar->blockSignals(true); + m_sidebar->setCurrentRow(pageIndex); + m_sidebar->blockSignals(false); + + // Show/hide toolbar actions per page +} + +void MainMainWindow::onFolderSelected(const QModelIndex &index) { + if (!index.isValid()) return; + + int itemType = index.data(FolderListModel::ItemTypeRole).toInt(); + if (itemType == FolderTreeItem::FolderNode) { + m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt(); + m_emailModel->setFolderId(m_currentFolderId); + // Fetch mails for this folder + std::optional optFolder = FolderDao::findById(m_currentFolderId); + if (optFolder.has_value()) { + Folder folder = optFolder.value(); + Account* account = m_accountService->findAccountById(folder.accountId()); + if (account) { + QString accountId = QString::number(account->id()); + QString folderId = QString::number(m_currentFolderId); + // Fetch mails asynchronously to avoid blocking UI + QMetaObject::invokeMethod(m_mailService, "fetchMails", Qt::QueuedConnection, + Q_ARG(QString, accountId), Q_ARG(QString, folderId)); + delete account; + } + } + m_emailModel->refresh(); + } +} + +void MainMainWindow::onEmailSelected(int mailId) { + m_currentMailId = mailId; + std::optional item = MailItemDao::findById(mailId); + if (!item.has_value()) { + m_emailViewer->setMailItem(nullptr); + return; + } + + MailItem &mail = item.value(); + if (!mail.isRead()) { + mail.setRead(true); + MailItemDao::update(mail); + } + m_emailViewer->setMailItem(&mail); +} + +void MainMainWindow::onComposeRequested() { + switchToPage(PageCompose); +} + +void MainMainWindow::onReaderReplyRequested(const MailItem *item) { + if (item) { + m_composeView->setTo(item->sender()); + m_composeView->setSubject("Re: " + item->subject()); + } + switchToPage(PageCompose); +} + +void MainMainWindow::onNewMessage() { + switchToPage(PageCompose); +} + +void MainMainWindow::openMailInIndependentWindow(int mailId) { + std::optional 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::onAddAccountRequested() { + AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this); + connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) { + statusBar()->showMessage(QString("Account added: %1").arg(account.email()), 3000); + m_folderModel->refresh(); // Refresh folder list to show new account's folders + }); + dialog->open(); +} + +void MainMainWindow::onAccountEditRequested(int accountId) { + Account *account = m_accountService->findAccountById(accountId); + if (!account) { + statusBar()->showMessage("Account not found", 3000); + return; + } + + AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this); + dialog->loadAccountForEditing(*account); + connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) { + statusBar()->showMessage(QString("Account updated: %1").arg(account.email()), 3000); + m_folderModel->refresh(); // Refresh folder list + }); + dialog->open(); +} + +void MainMainWindow::onAccountDeleteRequested(int accountId) { + Account *account = m_accountService->findAccountById(accountId); + if (!account) { + statusBar()->showMessage("Account not found", 3000); + return; + } + + if (QMessageBox::warning(this, "Delete Account", + QString("Are you sure you want to delete the account '%1'?").arg(account->email()), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + m_accountService->removeAccount(accountId); + statusBar()->showMessage(QString("Account deleted: %1").arg(account->email()), 3000); + m_folderModel->refresh(); // Refresh folder list to remove deleted account's folders + } +} + +void MainMainWindow::createToolBar() { + m_toolBar = addToolBar("Main Toolbar"); + m_toolBar->setMovable(false); + + QAction *newMsgAction = m_toolBar->addAction("โœ‰ New Message"); + m_toolBar->addSeparator(); + QAction *syncAction = m_toolBar->addAction("โŸณ Sync/Refresh"); + QAction *openWinAction = m_toolBar->addAction("โ†— Abrir en ventana"); + QAction *deleteAction = m_toolBar->addAction("๐Ÿ—‘ Delete"); + + connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage); + connect(syncAction, &QAction::triggered, [this]() { + if (m_currentFolderId >= 0) { + m_emailModel->refresh(); + statusBar()->showMessage("Refreshed", 2000); + } + }); + connect(openWinAction, &QAction::triggered, [this]() { + if (m_currentMailId >= 0) { + openMailInIndependentWindow(m_currentMailId); + } else { + statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000); + } + }); + connect(deleteAction, &QAction::triggered, [this]() { + statusBar()->showMessage("Delete would be implemented here", 3000); + }); +} \ No newline at end of file diff --git a/src/ui/mainmainwindow.cpp.cpp.backup b/src/ui/mainmainwindow.cpp.cpp.backup new file mode 100644 index 0000000..f9ee6c7 --- /dev/null +++ b/src/ui/mainmainwindow.cpp.cpp.backup @@ -0,0 +1,390 @@ +#include "mainmainwindow.h" +#include "core/models/account.h" +#include "core/mailitem.h" +#include "db/dao/mailitemdao.h" +#include "db/dao/folderdao.h" +#include +#include +#include "ui/accountsetupdialog.h" +#include +#include +#include +#include +#include + +MainMainWindow::MainMainWindow(QWidget *parent) + : QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1) +{ + setupUI(); + 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() +{ + // === Global stylesheet === + this->setStyleSheet( + "QMainWindow { background-color: #f5f5f7; }" + "QSplitter::handle { background-color: #d1d1d6; width: 1px; }" + "QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }" + "QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }" + ); + + createToolBar(); + + // === Central widget === + QWidget *central = new QWidget(); + QHBoxLayout *centralLayout = new QHBoxLayout(central); + centralLayout->setContentsMargins(0, 0, 0, 0); + centralLayout->setSpacing(0); + + // === Sidebar === + setupSidebar(); + centralLayout->addWidget(m_sidebar); + + // === Separator line === + QFrame *separator = new QFrame(); + separator->setFrameShape(QFrame::VLine); + separator->setStyleSheet("color: #d1d1d6;"); + centralLayout->addWidget(separator); + + // === Stacked pages === + m_stack = new QStackedWidget(); + m_stack->setStyleSheet("background-color: #f5f5f7;"); + + // Page 0: Mail (folder tree + mail list + reader) + setupMailPage(); + m_stack->addWidget(m_mailPage); + + // 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, const QString &fromAddr) { + QString msg; + if (scheduleTime.isValid()) { + msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP")); + } else { + msg = "Message sent (simulated)"; + } + if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc); + if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc); + statusBar()->showMessage(msg, 5000); + switchToPage(PageMail); + }); + connect(m_composeView, &ComposeView::discardRequested, [this]() { + switchToPage(PageMail); + }); + connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) { + // Detach compose view to a standalone window + QStackedWidget *stack = qobject_cast(composeView->parentWidget()); + if (stack) { + stack->removeWidget(composeView); + } + + // 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); + + // Page 2: Settings + m_settingsView = new SettingsView(); + connect(m_settingsView, &SettingsView::accountAddRequested, [this]() { + 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); + }); + m_stack->addWidget(m_settingsView); + + // Page 3: Contacts + m_contactsView = new ContactsView(); + m_stack->addWidget(m_contactsView); + + // Page 4: Calendar + m_calendarView = new CalendarView(); + m_stack->addWidget(m_calendarView); + + centralLayout->addWidget(m_stack, 1); + setCentralWidget(central); + + // Show mail page by default + switchToPage(PageMail); +} + +void MainMainWindow::setupSidebar() +{ + m_sidebar = new QListWidget(); + m_sidebar->setFixedWidth(100); + m_sidebar->setIconSize(QSize(24, 24)); + m_sidebar->setSpacing(4); + m_sidebar->setFrameShape(QFrame::NoFrame); + m_sidebar->setStyleSheet( + "QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }" + "QListWidget::item { color: #8e8e93; padding: 12px 0; text-align: center; font-size: 10px; border: none; border-radius: 8px; margin: 2px 8px; }" + "QListWidget::item:selected { background-color: #3a3a3c; color: #ffffff; }" + "QListWidget::item:hover { background-color: #3a3a3c; color: #ffffff; }" + ); + + m_sidebar->addItem("๐Ÿ“ง\nMail"); + m_sidebar->addItem("โœ๏ธ\nCompose"); + m_sidebar->addItem("โš™๏ธ\nSettings"); + m_sidebar->addItem("๐Ÿ‘ฅ\nContacts"); + m_sidebar->addItem("๐Ÿ“…\nCalendar"); + + m_sidebar->setCurrentRow(0); + connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged); +} + +void MainMainWindow::setupMailPage() +{ + m_mailPage = new QWidget(); + QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage); + mailLayout->setContentsMargins(0, 0, 0, 0); + mailLayout->setSpacing(0); + + m_folderSplitter = new QSplitter(Qt::Horizontal); + m_folderSplitter->setHandleWidth(1); + + // Folder tree + m_folderTree = new QTreeView(); + m_folderTree->setHeaderHidden(true); + m_folderTree->setIndentation(20); + m_folderTree->setMinimumWidth(220); + m_folderTree->setMaximumWidth(350); + m_folderTree->setFrameShape(QFrame::NoFrame); + m_folderTree->setExpandsOnDoubleClick(true); + m_folderSplitter->addWidget(m_folderTree); + + // Mail list (QTableView with sorting) + m_mailListView = new MailListView(); + connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected); + connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested); + connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) { + openMailInIndependentWindow(mailId); + }); + m_folderSplitter->addWidget(m_mailListView); + + // Reader + m_emailViewer = new ReaderView(); + m_emailViewer->setMinimumWidth(350); + connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested); + connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() { + if (m_currentMailId >= 0) { + openMailInIndependentWindow(m_currentMailId); + } + }); + m_folderSplitter->addWidget(m_emailViewer); + + // Default sizes: folder 240, list 380, reader flex + m_folderSplitter->setSizes({240, 380, 600}); + + mailLayout->addWidget(m_folderSplitter); +} + +void MainMainWindow::connectModels() +{ + m_accountService = new AccountService(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); + + m_folderTree->setModel(m_folderModel); + m_mailListView->setModel(m_emailModel); + + m_folderTree->expandAll(); + + connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected); +} + +void MainMainWindow::onNavChanged(int index) +{ + switchToPage(static_cast(index)); +} + +void MainMainWindow::switchToPage(int pageIndex) +{ + m_stack->setCurrentIndex(pageIndex); + m_sidebar->blockSignals(true); + m_sidebar->setCurrentRow(pageIndex); + m_sidebar->blockSignals(false); + + // Show/hide toolbar actions per page +} + +void MainMainWindow::onFolderSelected(const QModelIndex &index) +{ + if (!index.isValid()) return; + + int itemType = index.data(FolderListModel::ItemTypeRole).toInt(); + if (itemType == FolderTreeItem::FolderNode) { + m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt(); + m_emailModel->setFolderId(m_currentFolderId); + // Fetch mails for this folder + std::optional optFolder = FolderDao::findById(m_currentFolderId); + if (optFolder.has_value()) { + Folder folder = optFolder.value(); + Account* account = m_accountService->findAccountById(folder.accountId()); + if (account) { + m_mailService->fetchMails(QString::number(account->id()), QString::number(m_currentFolderId)); + delete account; + } + } + m_emailModel->refresh(); + } +} + +void MainMainWindow::onEmailSelected(int mailId) +{ + m_currentMailId = mailId; + std::optional item = MailItemDao::findById(mailId); + if (!item.has_value()) { + m_emailViewer->setMailItem(nullptr); + return; + } + + MailItem &mail = item.value(); + if (!mail.isRead()) { + mail.setRead(true); + MailItemDao::update(mail); + } + m_emailViewer->setMailItem(&mail); +} + +void MainMainWindow::onComposeRequested() +{ + switchToPage(PageCompose); +} + +void MainMainWindow::onReaderReplyRequested(const MailItem *item) +{ + if (item) { + m_composeView->setTo(item->sender()); + m_composeView->setSubject("Re: " + item->subject()); + } + switchToPage(PageCompose); +} + +void MainMainWindow::onNewMessage() +{ + switchToPage(PageCompose); +} + +void MainMainWindow::openMailInIndependentWindow(int mailId) +{ + std::optional 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() +{ + m_toolBar = addToolBar("Main Toolbar"); + m_toolBar->setMovable(false); + + QAction *newMsgAction = m_toolBar->addAction("โœ‰ New Message"); + m_toolBar->addSeparator(); + QAction *syncAction = m_toolBar->addAction("โŸณ Sync/Refresh"); + QAction *openWinAction = m_toolBar->addAction("โ†— Abrir en ventana"); + QAction *deleteAction = m_toolBar->addAction("๐Ÿ—‘ Delete"); + + connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage); + connect(syncAction, &QAction::triggered, [this]() { + if (m_currentFolderId >= 0) { + m_emailModel->refresh(); + statusBar()->showMessage("Refreshed", 2000); + } + }); + connect(openWinAction, &QAction::triggered, [this]() { + if (m_currentMailId >= 0) { + openMailInIndependentWindow(m_currentMailId); + } else { + statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000); + } + }); + 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); +} \ No newline at end of file diff --git a/src/ui/mainmainwindow.cpp.new b/src/ui/mainmainwindow.cpp.new new file mode 100644 index 0000000..033fecc --- /dev/null +++ b/src/ui/mainmainwindow.cpp.new @@ -0,0 +1,373 @@ +#include "mainmainwindow.h" +#include "core/models/account.h" +#include "core/mailitem.h" +#include "db/dao/mailitemdao.h" +#include "db/dao/folderdao.h" +#include +#include +#include "ui/accountsetupdialog.h" +#include +#include +#include +#include +#include +#include + +MainMainWindow::MainMainWindow(QWidget *parent) + : QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1) +{ + setupUI(); + connectModels(); + setWindowTitle("Wino Mail DTK"); + resize(1280, 820); +} + +void MainMainWindow::setupUI() { + // === Global stylesheet === + this->setStyleSheet( + "QMainWindow { background-color: #f5f5f7; }" + "QSplitter::handle { background-color: #d1d1d6; width: 1px; }" + "QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }" + "QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }" + ); + + createToolBar(); + + // === Central widget === + QWidget *central = new QWidget(); + QHBoxLayout *centralLayout = new QHBoxLayout(central); + centralLayout->setContentsMargins(0, 0, 0, 0); + centralLayout->setSpacing(0); + + // === Sidebar === + setupSidebar(); + centralLayout->addWidget(m_sidebar); + + // === Separator line === + QFrame *separator = new QFrame(); + separator->setFrameShape(QFrame::VLine); + separator->setStyleSheet("color: #d1d1d6;"); + centralLayout->addWidget(separator); + + // === Stacked pages === + m_stack = new QStackedWidget(); + m_stack->setStyleSheet("background-color: #f5f5f7;"); + + // Page 0: Mail (folder tree + mail list + reader) + setupMailPage(); + m_stack->addWidget(m_mailPage); + + // 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) { + QString msg; + if (scheduleTime.isValid()) { + msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP")); + } else { + msg = "Message sent (simulated)"; + } + if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc); + if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc); + statusBar()->showMessage(msg, 5000); + switchToPage(PageMail); + }); + connect(m_composeView, &ComposeView::discardRequested, [this]() { + switchToPage(PageMail); + }); + connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) { + // Detach compose view to a standalone window + QStackedWidget *stack = qobject_cast(composeView->parentWidget()); + if (stack) { + stack->removeWidget(composeView); + } + + // 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); + + // Page 2: Settings + m_settingsView = new SettingsView(); + m_settingsView->setAccountService(m_accountService); + connect(m_settingsView, &SettingsView::accountAddRequested, this, &MainMainWindow::onAddAccountRequested); + connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) { + statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000); + }); + connect(m_settingsView, &SettingsView::accountEditRequested, this, &MainMainWindow::onAccountEditRequested); + connect(m_settingsView, &SettingsView::accountDeleteRequested, this, &MainMainWindow::onAccountDeleteRequested); + m_stack->addWidget(m_settingsView); + + // Page 3: Contacts + m_contactsView = new ContactsView(); + m_stack->addWidget(m_contactsView); + + // Page 4: Calendar + m_calendarView = new CalendarView(); + m_stack->addWidget(m_calendarView); + + centralLayout->addWidget(m_stack, 1); + setCentralWidget(central); + + // Show mail page by default + switchToPage(PageMail); +} + +void MainMainWindow::setupSidebar() { + m_sidebar = new QListWidget(); + m_sidebar->setFixedWidth(64); + m_sidebar->setIconSize(QSize(24, 24)); + m_sidebar->setSpacing(4); + m_sidebar->setFrameShape(QFrame::NoFrame); + m_sidebar->setStyleSheet( + "QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }" + "QListWidget::item { color: #8e8e93; padding: 12px 0; text-align: center; font-size: 10px; border: none; border-radius: 8px; margin: 2px 8px; }" + "QListWidget::item:selected { background-color: #3a3a3c; color: #ffffff; }" + "QListWidget::item:hover { background-color: #3a3a3c; color: #ffffff; }" + ); + + m_sidebar->addItem("๐Ÿ“ง\nMail"); + m_sidebar->addItem("โœ๏ธ\nCompose"); + m_sidebar->addItem("โš™๏ธ\nSettings"); + m_sidebar->addItem("๐Ÿ‘ฅ\nContacts"); + m_sidebar->addItem("๐Ÿ“…\nCalendar"); + + m_sidebar->setCurrentRow(0); + connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged); +} + +void MainMainWindow::setupMailPage() { + m_mailPage = new QWidget(); + QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage); + mailLayout->setContentsMargins(0, 0, 0, 0); + mailLayout->setSpacing(0); + + m_folderSplitter = new QSplitter(Qt::Horizontal); + m_folderSplitter->setHandleWidth(1); + + // Folder tree + m_folderTree = new QTreeView(); + m_folderTree->setHeaderHidden(true); + m_folderTree->setIndentation(20); + m_folderTree->setMinimumWidth(220); + m_folderTree->setMaximumWidth(350); + m_folderTree->setFrameShape(QFrame::NoFrame); + m_folderTree->setExpandsOnDoubleClick(true); + m_folderSplitter->addWidget(m_folderTree); + + // Mail list (QTableView with sorting) + m_mailListView = new MailListView(); + connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected); + connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested); + connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) { + openMailInIndependentWindow(mailId); + }); + m_folderSplitter->addWidget(m_mailListView); + + // Reader + m_emailViewer = new ReaderView(); + m_emailViewer->setMinimumWidth(350); + connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested); + connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() { + if (m_currentMailId >= 0) { + openMailInIndependentWindow(m_currentMailId); + } + }); + m_folderSplitter->addWidget(m_emailViewer); + + // Default sizes: folder 240, list 380, reader flex + m_folderSplitter->setSizes({240, 380, 600}); + + mailLayout->addWidget(m_folderSplitter); +} + +void MainMainWindow::connectModels() { + m_accountService = new AccountService(this); + m_mailService = new MailService(this); + + m_folderModel = new FolderListModel(m_accountService, this); + m_emailModel = new EmailListModel(this); + + m_folderTree->setModel(m_folderModel); + m_mailListView->setModel(m_emailModel); + + m_folderTree->expandAll(); + + connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected); + connect(m_mailService, &MailService::mailFetched, this, [this](const QString &accountId, const QString &folderId, const QVector &items) { + int fid = folderId.toInt(); + if (m_currentFolderId == fid || m_currentFolderId == -1) { + m_emailModel->refresh(); + } + }); +} + +void MainMainWindow::onNavChanged(int index) { + switchToPage(static_cast(index)); +} + +void MainMainWindow::switchToPage(int pageIndex) { + m_stack->setCurrentIndex(pageIndex); + m_sidebar->blockSignals(true); + m_sidebar->setCurrentRow(pageIndex); + m_sidebar->blockSignals(false); + + // Show/hide toolbar actions per page +} + +void MainMainWindow::onFolderSelected(const QModelIndex &index) { + if (!index.isValid()) return; + + int itemType = index.data(FolderListModel::ItemTypeRole).toInt(); + if (itemType == FolderTreeItem::FolderNode) { + m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt(); + m_emailModel->setFolderId(m_currentFolderId); + // Fetch mails for this folder + std::optional optFolder = FolderDao::findById(m_currentFolderId); + if (optFolder.has_value()) { + Folder folder = optFolder.value(); + Account* account = m_accountService->findAccountById(folder.accountId()); + if (account) { + QString accountId = QString::number(account->id()); + QString folderId = QString::number(m_currentFolderId); + // Fetch mails asynchronously to avoid blocking UI + QtConcurrent::run([this, accountId, folderId]() { + m_mailService->fetchMails(accountId, folderId); + }); + delete account; + } + } + // Removed immediate refresh - will be updated via mailFetched signal + } +} + +void MainMainWindow::onEmailSelected(int mailId) { + m_currentMailId = mailId; + std::optional item = MailItemDao::findById(mailId); + if (!item.has_value()) { + m_emailViewer->setMailItem(nullptr); + return; + } + + MailItem &mail = item.value(); + if (!mail.isRead()) { + mail.setRead(true); + MailItemDao::update(mail); + } + m_emailViewer->setMailItem(&mail); +} + +void MainMainWindow::onComposeRequested() { + switchToPage(PageCompose); +} + +void MainMainWindow::onReaderReplyRequested(const MailItem *item) { + if (item) { + m_composeView->setTo(item->sender()); + m_composeView->setSubject("Re: " + item->subject()); + } + switchToPage(PageCompose); +} + +void MainMainWindow::onNewMessage() { + switchToPage(PageCompose); +} + +void MainMainWindow::openMailInIndependentWindow(int mailId) { + std::optional 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::onAddAccountRequested() { + AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this); + connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) { + statusBar()->showMessage(QString("Account added: %1").arg(account.email()), 3000); + m_folderModel->refresh(); // Refresh folder list to show new account's folders + }); + dialog->open(); +} + +void MainMainWindow::onAccountEditRequested(int accountId) { + Account *account = m_accountService->findAccountById(accountId); + if (!account) { + statusBar()->showMessage("Account not found", 3000); + return; + } + + AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this); + dialog->loadAccountForEditing(*account); + connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) { + statusBar()->showMessage(QString("Account updated: %1").arg(account.email()), 3000); + m_folderModel->refresh(); // Refresh folder list + }); + dialog->open(); +} + +void MainMainWindow::onAccountDeleteRequested(int accountId) { + Account *account = m_accountService->findAccountById(accountId); + if (!account) { + statusBar()->showMessage("Account not found", 3000); + return; + } + + if (QMessageBox::warning(this, "Delete Account", + QString("Are you sure you want to delete the account '%1'?").arg(account->email()), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + m_accountService->removeAccount(accountId); + statusBar()->showMessage(QString("Account deleted: %1").arg(account->email()), 3000); + m_folderModel->refresh(); // Refresh folder list to remove deleted account's folders + } +} + +void MainMainWindow::createToolBar() { + m_toolBar = addToolBar("Main Toolbar"); + m_toolBar->setMovable(false); + + QAction *newMsgAction = m_toolBar->addAction("โœ‰ New Message"); + m_toolBar->addSeparator(); + QAction *syncAction = m_toolBar->addAction("โŸณ Sync/Refresh"); + QAction *openWinAction = m_toolBar->addAction("โ†— Abrir en ventana"); + QAction *deleteAction = m_toolBar->addAction("๐Ÿ—‘ Delete"); + + connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage); + connect(syncAction, &QAction::triggered, [this]() { + if (m_currentFolderId >= 0) { + m_emailModel->refresh(); + statusBar()->showMessage("Refreshed", 2000); + } + }); + connect(openWinAction, &QAction::triggered, [this]() { + if (m_currentMailId >= 0) { + openMailInIndependentWindow(m_currentMailId); + } else { + statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000); + } + }); + connect(deleteAction, &QAction::triggered, [this]() { + statusBar()->showMessage("Delete would be implemented here", 3000); + }); +} \ No newline at end of file diff --git a/src/ui/mainmainwindow.h b/src/ui/mainmainwindow.h index 7cb2b5e..72d09ff 100644 --- a/src/ui/mainmainwindow.h +++ b/src/ui/mainmainwindow.h @@ -42,6 +42,14 @@ private slots: void onProgressChanged(int percent); void onStatusMessage(const QString &msg); + // Slots for embedded compose view + void onEmbeddedSendRequested(const QString &to, const QString &cc, const QString &bcc, + const QString &subject, const QString &body, + const QDateTime &scheduleTime, + const QString &fromAddress, + const QStringList &attachmentPaths); + void onEmbeddedDiscardRequested(); + void onEmbeddedDetachRequested(QWidget *widget); private: void setupUI(); void setupSidebar(); @@ -68,6 +76,9 @@ private: QTreeView *m_folderTree; MailListView *m_mailListView; ReaderView *m_emailViewer; + QStackedWidget *m_viewerStack; + QWidget *m_placeholderWidget; + ComposeView *m_embeddedComposeView; // Other pages ComposeView *m_composeView; @@ -78,12 +89,12 @@ private: // Services & Models FolderListModel *m_folderModel; EmailListModel *m_emailModel; - AccountService *m_accountService; - MailService *m_mailService; + AccountService *m_accountService = nullptr; + MailService *m_mailService = nullptr; QToolBar *m_toolBar; int m_currentFolderId; int m_currentMailId; QProgressBar *m_progressBar; -}; \ No newline at end of file +}; diff --git a/src/ui/mainmainwindow.h.backup b/src/ui/mainmainwindow.h.backup new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/mainmainwindow.h.h.backup b/src/ui/mainmainwindow.h.h.backup new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/mainmainwindow.h.header_backup b/src/ui/mainmainwindow.h.header_backup new file mode 100644 index 0000000..7cb2b5e --- /dev/null +++ b/src/ui/mainmainwindow.h.header_backup @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ui/readerview.h" +#include "ui/maillistview.h" +#include "ui/composeview.h" +#include "ui/settingsview.h" +#include "ui/contactsview.h" +#include "ui/calendarview.h" +#include "ui/models/FolderListModel.h" +#include "ui/models/EmailListModel.h" +#include "services/accountservice.h" +#include "services/mailservice.h" + +class MainMainWindow : public QMainWindow { + Q_OBJECT + +public: + explicit MainMainWindow(QWidget *parent = nullptr); + ~MainMainWindow() override = default; + +private slots: + void onNavChanged(int index); + void onFolderSelected(const QModelIndex &index); + void onEmailSelected(int mailId); + void onComposeRequested(); + 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(); + void setupSidebar(); + void setupMailPage(); + void connectModels(); + void createToolBar(); + void switchToPage(int pageIndex); + + // Navigation + QListWidget *m_sidebar; + QStackedWidget *m_stack; + + enum Page { + PageMail = 0, + PageCompose, + PageSettings, + PageContacts, + PageCalendar + }; + + // Mail page widgets + QWidget *m_mailPage; + QSplitter *m_folderSplitter; + QTreeView *m_folderTree; + MailListView *m_mailListView; + ReaderView *m_emailViewer; + + // Other pages + ComposeView *m_composeView; + SettingsView *m_settingsView; + ContactsView *m_contactsView; + CalendarView *m_calendarView; + + // Services & Models + FolderListModel *m_folderModel; + EmailListModel *m_emailModel; + AccountService *m_accountService; + MailService *m_mailService; + + QToolBar *m_toolBar; + int m_currentFolderId; + int m_currentMailId; + + QProgressBar *m_progressBar; +}; \ No newline at end of file diff --git a/src/ui/modify_composeview.py b/src/ui/modify_composeview.py new file mode 100644 index 0000000..cecf8c4 --- /dev/null +++ b/src/ui/modify_composeview.py @@ -0,0 +1,386 @@ +import sys +import re + +with open('composeview.cpp', 'r') as f: + lines = f.readlines() + +# 1. Remove AddressInputWidget class definition (lines approx 27-52) +# Find start and end lines. +new_lines = [] +i = 0 +while i < len(lines): + if lines[i].strip() == '// Address input widget with tokenized entries, delete button, drag-drop reorder, and autocomplete': + # Skip until after the closing brace of the class + # Find the line with '};' after the class + j = i + while j < len(lines) and not lines[j].strip().startswith('};'): + j += 1 + # include the '};' line + j += 1 + i = j + continue + new_lines.append(lines[i]) + i += 1 + +lines = new_lines + +# 2. Replace member variable types +# We'll process line by line, replacing specific patterns. +out_lines = [] +for line in lines: + # Replace member declarations + if 'AddressInputWidget *m_toWidget = nullptr;' in line: + line = line.replace('AddressInputWidget *m_toWidget = nullptr;', 'QLineEdit *m_toField = nullptr;') + if 'AddressInputWidget *m_ccWidget = nullptr;' in line: + line = line.replace('AddressInputWidget *m_ccWidget = nullptr;', 'QLineEdit *m_ccField = nullptr;') + if 'AddressInputWidget *m_bccWidget = nullptr;' in line: + line = line.replace('AddressInputWidget *m_bccWidget = nullptr;', 'QLineEdit *m_bccField = nullptr;') + # Remove duplicate m_ccField and m_bccField lines (they are later) + if 'QLineEdit *m_ccField = nullptr; // kept for compatibility? actually not used now' in line: + # skip this line + continue + if 'QLineEdit *m_bccField = nullptr;' in line and '// kept for compatibility' not in line: + # This is the second duplicate; we already have one above; we can keep only one. + # We'll skip this line. + continue + out_lines.append(line) + +lines = out_lines + +# Now we need to modify setAccountService, setupUI, slots, etc. +# We'll do a more aggressive approach: replace entire sections using markers. +# Let's join back to string and use regex. + +text = ''.join(lines) + +# Replace setAccountService function +pattern_setAccountService = re.compile(r'void ComposeView::setAccountService\(AccountService \*service\)\s*\{[\s\S]*?\}', re.MULTILINE) +def repl_setAccountService(match): + return '''void ComposeView::setAccountService(AccountService *service) +{ + m_accountService = service; + populateAccountCombo(); + // Update completer models for address widgets with known emails + if (m_accountService) { + QStringList knownEmails; + const QVector accounts = m_accountService->getAllAccounts(); + for (const Account &acc : accounts) { + knownEmails << acc.email(); + } + // Set completion model for each widget + QStringListModel *model = new QStringListModel(knownEmails, this); + QCompleter *comp = new QCompleter(model, this); + comp->setCaseSensitivity(Qt::CaseInsensitive); + if (m_toField) m_toField->setCompleter(comp); + // For cc and bcc we need separate completers but can share model + QCompleter *comp2 = new QCompleter(model, this); + comp2->setCaseSensitivity(Qt::CaseInsensitive); + if (m_ccField) m_ccField->setCompleter(comp2); + QCompleter *comp3 = new QCompleter(model, this); + comp3->setCaseSensitivity(Qt::CaseInsensitive); + if (m_bccField) m_bccField->setCompleter(comp3); + } +}''' +text = re.sub(pattern_setAccountService, repl_setAccountService, text) + +# Replace setupCore function: we need to replace the whole function. +# We'll rewrite from scratch using a marker: we will replace the whole function body. +# Let's capture the function and replace. +pattern_setupUI = re.compile(r'void ComposeView::setupUI\(\)\s*\{[\s\S]*?\n\}\s*\n(?=void|\Z)', re.MULTILINE) +def repl_setupUI(match): + return '''void ComposeView::setupUI() +{ + QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(20,15,20,15); + mainLayout->setSpacing(8); + + // Top row: Account selector and detach button + QHBoxLayout *topLayout = new QHBoxLayout(); + m_accountLabel = new QLabel(tr(\"From:\"), this); + m_accountLabel->setFixedWidth(40); + m_accountLabel->setStyleSheet(QStringLiteral(\"font-weight: bold; color: #555;\")); + m_accountCombo = new QComboBox(this); + m_accountCombo->setFixedWidth(180); + m_accountCombo->setStyleSheet( + \"QComboBox { border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 8px; \" + \"background: white; min-height: 20px; }\"\n + \"QComboBox:hover { border-color: #bdbdbd; }\"\n + \"QComboBox:focus { border-color: #1976D2; }\" + ); + m_detachButton = new QPushButton(this); + m_detachButton->setToolTip(tr(\"Detach compose window\")); + m_detachButton->setText(QStringLiteral(\"โคข\")); + m_detachButton->setFixedSize(28,28); + m_detachButton->setStyleSheet( + \"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; \" + \"padding: 6px; color: #555; font-size: 14px; }\"\n + \"QPushButton:hover { background: #f0f0f0; }\"\n + ); + connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked); + + topLayout->addWidget(m_accountLabel); + topLayout->addWidget(m_accountCombo, 1); + topLayout->addWidget(m_detachButton); + mainLayout->addLayout(topLayout); + + // Separator line + QFrame *line1 = new QFrame(this); + line1->setFrameShape(QFrame::HLine); + line1->setStyleSheet(QStringLiteral(\"color: #e0e0e0;\")); + mainLayout->addWidget(line1); + + // To row + QHBoxLayout *toLayout = new QHBoxLayout(); + QLabel *toLabel = new QLabel(tr(\"To:\"), this); + toLabel->setFixedWidth(40); + toLabel->setStyleSheet(QStringLiteral(\"font-weight: bold; color: #555;\")); + m_toField = new QLineEdit(this); + m_toField->setPlaceholderText(tr(\"To recipients\")); + toLayout->addWidget(toLabel); + toLayout->addWidget(m_toField, 1); + mainLayout->addLayout(toLayout); + + // CC/BCC toggle buttons + QHBoxLayout *toggleLayout = new QHBoxLayout(); + m_ccButton = new QPushButton(tr(\"CC\"), this); + m_ccButton->setCheckable(true); + m_ccButton->setChecked(false); + connect(m_ccButton, &QPushButton::toggled, this, &ComposeView::onCcToggled); + m_bccButton = new QPushButton(tr(\"BCC\"), this); + m_bccButton->setCheckable(true); + m_bccButton->setChecked(false); + connect(m_bccButton, &QPushButton::toggled, this, &ComposeView::onBccToggled); + toggleLayout->addWidget(m_ccButton); + toggleLayout->addWidget(m_bccButton); + toggleLayout->addStretch(); + mainLayout->addLayout(toggleLayout); + + // Cc row (hidden by default) + m_ccRow = new QWidget(this); + QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow); + ccLayout->setContentsMargins(0,0,0,0); + QLabel *ccLabel = new QLabel(tr(\"Cc:\"), this); + ccLabel->setFixedWidth(40); + ccLabel->setStyleSheet(QStringLiteral(\"color: #555;\")); + m_ccField = new QLineEdit(this); + m_ccField->setPlaceholderText(tr(\"Cc recipients\")); + ccLayout->addWidget(ccLabel); + ccLayout->addWidget(m_ccField, 1); + m_ccRow->setVisible(false); + mainLayout->addWidget(m_ccRow); + + // Bcc row (hidden by default) + m_bccRow = new QWidget(this); + QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow); + bccLayout->setContentsMargins(0,0,0,0); + QLabel *bccLabel = new QLabel(tr(\"Bcc:\"), this); + bccLabel->setFixedWidth(40); + bccLabel->setStyleSheet(QStringLiteral(\"color: #555;\")); + m_bccField = new QLineEdit(this); + m_bccField->setPlaceholderText(tr(\"Bcc recipients\")); + bccLayout->addWidget(bccLabel); + bccLayout->addWidget(m_bccField, 1); + m_bccRow->setVisible(false); + mainLayout->addWidget(m_bccRow); + + // Separator + QFrame *line2 = new QFrame(this); + line2->setFrameShape(QFrame::HLine); + line2->setStyleSheet(QStringLiteral(\"color: #e0e0e0;\")); + mainLayout->addWidget(line2); + + // Subject field + QLabel *subjectLabel = new QLabel(tr(\"Subject:\"), this); + subjectLabel->setFixedWidth(50); + subjectLabel->setStyleSheet(QStringLiteral(\"font-weight: bold; color: #555;\")); + m_subjectField = new QLineEdit(this); + m_subjectField->setPlaceholderText(tr(\"Subject\")); + m_subjectField->setFixedHeight(36); + QFont subjectFont = m_subjectField->font(); + subjectFont.setPointSize(14); + subjectFont.setBold(true); + m_subjectField->setFont(subjectFont); + QHBoxLayout *subjectLayout = new QHBoxLayout(); + subjectLayout->addWidget(subjectLabel); + subjectLayout->addWidget(m_subjectField, 1); + mainLayout->addLayout(subjectLayout); + + // Body editor with toolbar + m_bodyEditor = new RichTextEditor(this); + m_bodyEditor->setupToolbar(mainLayout); + mainLayout->addWidget(m_bodyEditor, 1); + + // Schedule panel (hidden by default) + m_schedulePanel = new QWidget(this); + QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel); + scheduleLayout->setContentsMargins(0,0,0,0); + QLabel *scheduleLabel = new QLabel(tr(\"Send at:\"), this); + scheduleLabel->setStyleSheet(QStringLiteral(\"color: #555; font-weight: bold;\")); + m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600), this); + m_schedulePicker->setCalendarPopup(true); + m_schedulePicker->setDisplayFormat(QStringLiteral(\"dd/MM/yyyy hh:mm AP\")); + scheduleLayout->addWidget(scheduleLabel); + scheduleLayout->addWidget(m_schedulePicker); + scheduleLayout->addStretch(); + m_scheduleSendButton = new QPushButton(tr(\"Schedule Send\"), this); + m_scheduleSendButton->setStyleSheet( + \"QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; \" + \"padding: 6px 16px; font-weight: bold; }\"\n + \"QPushButton:hover { background-color: #1565C0; }\"\n + ); + connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked); + scheduleLayout->addWidget(m_scheduleSendButton); + m_schedulePanel->setVisible(false); + mainLayout->addWidget(m_schedulePanel); + + // Action buttons row + QHBoxLayout *actionsLayout = new QHBoxLayout(); + actionsLayout->addStretch(); + + m_discardButton = new QPushButton(tr(\"Discard\"), this); + m_discardButton->setStyleSheet( + \"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; \" + \"padding: 8px 20px; color: #555; }\"\n + \"QPushButton:hover { background: #f5f5f5; }\"\n + ); + actionsLayout->addWidget(m_discardButton); + connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested); + + // Split button for Send / Schedule + m_sendMenu = new QMenu(this); + m_sendNowAction = m_sendMenu->addAction(tr(\"Send Now\")); + connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked); + m_scheduleAction = m_sendMenu->addAction(tr(\"Schedule for later...\")); + connect(m_scheduleAction, &QAction::triggered, [this]() { + m_schedulePanel->setVisible(true); + }); + + m_sendSplit = new QToolButton(this); + m_sendSplit->setText(tr(\"Send\")); + m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly); + m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup); + m_sendSplit->setMenu(m_sendMenu); + m_sendSplit->setStyleSheet( + \"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; \" + \"padding: 8px 24px; font-weight: bold; }\"\n + \"QToolButton:hover { background-color: #1565C0; }\"\n + \"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; }\"\n + \"QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }\"\n + ); + connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked); + actionsLayout->addWidget(m_sendSplit); + + mainLayout->addLayout(actionsLayout); + + // Connect rich text editor signature signals + connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked); + connect(m_bodyEditor, &RichTextEditor::signatureEditRequested, this, &ComposeView::onSignatureEditRequested); +} +''' +text = re.sub(pattern_setupUI, repl_setupUI, text) + +# Replace onCcToggle slot (we renamed to onCcToggled with bool) +pattern_onCcToggle = re.compile(r'void ComposeView::onCcToggle\(\)\s*\{[\s\S]*?\n\}\s*\n', re.MULTILINE) +def repl_onCcToggle(match): + return '''void ComposeView::onCcToggled(bool checked) +{ + m_ccRow->setVisible(checked); + if (checked) { + m_ccField->setFocus(); + } +} +''' +text = re.sub(pattern_onCcToggle, repl_onCcToggle, text) + +# Replace onBccToggle similarly +pattern_onBccToggle = re.compile(r'void ComposeView::onBccToggle\(\)\s*\{[\s\S]*?\n\}\s*\n', re.MULTILINE) +def repl_onBccToggle(match): + return '''void ComposeView::onBccToggled(bool checked) +{ + m_bccRow->setVisible(checked); + if (checked) { + m_bccField->setFocus(); + } +} +''' +text = re.sub(pattern_onBccToggle, repl_onBccToggle, text) + +# Replace onSendClicked +pattern_onSendClicked = re.compile(r'void ComposeView::onSendClicked\(\)\s*\{[\s\S]*?\n\}\s*\n', re.MULTILINE) +def repl_onSendClicked(match): + return '''void ComposeView::onSendClicked() +{ + QString to = m_toField->text(); + QString cc = m_ccButton->isChecked() ? m_ccField->text() : QString(); + QString bcc = m_bccButton->isChecked() ? m_bccField->text() : QString(); + QString subject = m_subjectField->text(); + QString body = m_bodyEditor->toHtml(); + QDateTime scheduleTime = m_schedulePanel->isVisible() ? m_schedulePicker->dateTime() : QDateTime(); + QString fromAddress; + if (m_currentAccountId > 0 && m_accountCombo->currentIndex() > 0) { + fromAddress = m_accountCombo->currentData().toString(); + } + emit sendRequested(to, cc, bcc, subject, body, scheduleTime, fromAddress); +} +''' +text = re.sub(pattern_onSendClicked, repl_onSendClicked, text) + +# Replace initializeComposition +pattern_initComp = re.compile(r'void ComposeView::initializeComposition\(\)\s*\{[\s\S]*?\n\}\s*\n', re.MULTILINE) +def repl_initComp(match): + return '''void ComposeView::initializeComposition() +{ + m_subjectField->clear(); + m_bodyEditor->clear(); + m_toField->clear(); + m_ccField->clear(); + m_bccField->clear(); + m_ccRow->setVisible(false); + m_bccRow->setVisible(false); + m_schedulePanel->setVisible(false); + m_subjectField->setFocus(); +} +''' +text = re.sub(pattern_initComp, repl_initComp, text) + +# Replace startNewEmail +pattern_startNew = re.compile(r'void ComposeView::startNewEmail\(const QString \&initialRecipient\)\s*\{[\s\S]*?\n\}\s*\n', re.MULTILINE) +def repl_startNew(match): + return '''void ComposeView::startNewEmail(const QString &initialRecipient) +{ + initializeComposition(); + if (!initialRecipient.isEmpty()) { + m_toField->setText(initialRecipient); + } + m_toField->setFocus(); +} +''' +text = re.sub(pattern_startNew, repl_startNew, text) + +# Replace setTo +pattern_setTo = re.compile(r'void ComposeView::setTo\(const QString \&to\)\s*\{[\s\S]*?\n\}\s*\n', re.MULTILINE) +def repl_setTo(match): + return '''void ComposeView::setTo(const QString &to) +{ + if (to.isEmpty()) { + m_toField->clear(); + return; + } + // Split by semicolon or comma + QStringList parts = to.split(QRegularExpression(\"[,;]\"), Qt::SkipEmptyParts); + QStringList cleaned; + for (const QString &part : parts) { + QString trimmed = part.trimmed(); + if (!trimmed.isEmpty()) + cleaned << trimmed; + } + m_toField->setText(cleaned.join(\"; \")); +} +''' +text = re.sub(pattern_setTo, repl_setTo, text) + +# We also need to add a helper function splitAddresses if we used it, but we didn't. +# Ensure we have the forward declaration for the slot in header (already done). +# Now write the file. +with open('composeview.cpp', 'w') as f: + f.write(text) diff --git a/src/ui/modify_mainwindow.py b/src/ui/modify_mainwindow.py new file mode 100644 index 0000000..45e3ff3 --- /dev/null +++ b/src/ui/modify_mainwindow.py @@ -0,0 +1,38 @@ +import sys + +filename = 'mainmainwindow.cpp' +with open(filename, 'r') as f: + lines = f.readlines() + +# Find the line after the mailFetchError connect (look for the closing brace and semicolon of that connect) +# We'll look for the pattern: '});' that ends the mailFetchError lambda. +insert_idx = None +for i, line in enumerate(lines): + if 'connect(m_mailService, &MailService::mailFetchError' in line: + # Find the closing brace of this call (could be same line or later) + # We'll just look for the next line that contains '});' after this line. + for j in range(i, len(lines)): + if '});' in lines[j]: + insert_idx = j + 1 # insert after this line + break + break + +if insert_idx is None: + print("Could not find insert point") + sys.exit(1) + +# The insertion block +insert_lines = [ + ' connect(m_mailService, &MailService::mailSendFailed, this, [this](const QString &mailItemId, const QString &error) {\n', + ' qWarning() << \"[MailSendFailed]\" << error;\n', + ' statusBar()->showMessage(tr(\"Error sending mail: %1\").arg(error), 5000);\n', + ' });\n' +] + +# Insert +lines = lines[:insert_idx] + insert_lines + lines[insert_idx:] + +with open(filename, 'w') as f: + f.writelines(lines) + +print(f"Inserted mailSendFailed connection at line {insert_idx+1}") diff --git a/src/ui/readerview.cpp b/src/ui/readerview.cpp index e113cbb..2db4f94 100644 --- a/src/ui/readerview.cpp +++ b/src/ui/readerview.cpp @@ -1,5 +1,8 @@ #include "ui/readerview.h" #include +#include +#include +#include "db/dao/mailitemdao.h" ReaderView::ReaderView(QWidget *parent) : QWidget(parent) { setupUI(); @@ -55,8 +58,19 @@ void ReaderView::setupUI() { m_bodyViewer->setOpenExternalLinks(true); m_bodyViewer->setFrameStyle(QFrame::NoFrame); + m_attachmentList = new QListWidget(); + m_attachmentList->setVisible(false); + m_attachmentList->setMaximumHeight(110); + m_attachmentList->setToolTip(tr("Double-click an attachment to open it")); + connect(m_attachmentList, &QListWidget::itemDoubleClicked, this, + [](QListWidgetItem *listItem) { + const QString path = listItem->data(Qt::UserRole).toString(); + if (!path.isEmpty()) QDesktopServices::openUrl(QUrl::fromLocalFile(path)); + }); + mainLayout->addWidget(headerWidget); mainLayout->addLayout(actionsLayout); + mainLayout->addWidget(m_attachmentList); mainLayout->addWidget(m_bodyViewer); // Connections @@ -72,6 +86,8 @@ void ReaderView::setMailItem(const MailItem* item) { m_subjectLabel->setText("No mail selected"); m_fromLabel->setText("From: "); m_dateLabel->setText("Date: "); + m_attachmentList->clear(); + m_attachmentList->setVisible(false); m_bodyViewer->setHtml("Please select a message to read"); return; } @@ -79,5 +95,13 @@ void ReaderView::setMailItem(const MailItem* item) { m_subjectLabel->setText(item->subject()); m_fromLabel->setText(QString("From: %1").arg(item->sender())); m_dateLabel->setText(QString("Date: %1").arg(item->date().toString("ddd, d MMM yyyy hh:mm"))); + m_attachmentList->clear(); + const QVector attachments = MailItemDao::attachmentsForMail(item->id()); + for (const StoredAttachmentRecord &attachment : attachments) { + auto *listItem = new QListWidgetItem(attachment.fileName, m_attachmentList); + listItem->setData(Qt::UserRole, attachment.storedPath); + listItem->setToolTip(attachment.storedPath); + } + m_attachmentList->setVisible(!attachments.isEmpty()); m_bodyViewer->setHtml(item->bodyHtml()); -} \ No newline at end of file +} diff --git a/src/ui/readerview.h b/src/ui/readerview.h index 48749b9..d12fa22 100644 --- a/src/ui/readerview.h +++ b/src/ui/readerview.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include "core/mailitem.h" @@ -31,6 +32,7 @@ private: QLabel *m_fromLabel; QLabel *m_dateLabel; QTextBrowser *m_bodyViewer; + QListWidget *m_attachmentList; QPushButton *m_replyButton; QPushButton *m_forwardButton; diff --git a/src/ui/replace_delete_action.py b/src/ui/replace_delete_action.py new file mode 100644 index 0000000..83fbc50 --- /dev/null +++ b/src/ui/replace_delete_action.py @@ -0,0 +1,50 @@ +import sys + +filename = '/mnt/c/Users/javie/wino-mail-dtkqt/src/ui/mainmainwindow.cpp' +with open(filename, 'r') as f: + lines = f.readlines() + +# Find start line +start = None +for i, line in enumerate(lines): + if line.strip().startswith('connect(deleteAction, &QAction::triggered'): + start = i + break +if start is None: + print('Could not find deleteAction connect') + sys.exit(1) + +# Find end line: look for a line that contains '});' after start, assuming it's the end of the lambda +end = None +for i in range(start, len(lines)): + if '});' in lines[i]: + end = i + break +if end is None: + print('Could not find end of lambda') + sys.exit(1) + +# Replacement lines +new_lines = [ + ' connect(deleteAction, &QAction::triggered, [this]() {\n', + ' QModelIndexList indexes = m_mailListView->selectionModel()->selectedIndexes();\n', + ' if (indexes.isEmpty()) {\n', + ' statusBar()->showMessage(tr(\"No email selected\"), 2000);\n', + ' return;\n', + ' }\n', + ' int row = indexes.first().row();\n', + ' QModelIndex idx = m_emailModel->index(row, 0);\n', + ' qint64 id = idx.data(EmailListModel::IdRole).toLongLong();\n', + ' m_mailService->deleteMail(QString::number(id)); // deletes from DB\n', + ' m_emailModel->refresh();\n', + ' statusBar()->showMessage(tr(\"Email deleted\"), 2000);\n', + ' });\n' +] + +# Replace +lines = lines[:start] + new_lines + lines[end+1:] + +with open(filename, 'w') as f: + f.writelines(lines) + +print('Replace done') diff --git a/src/ui/richtexteditor.cpp b/src/ui/richtexteditor.cpp new file mode 100644 index 0000000..0f76945 --- /dev/null +++ b/src/ui/richtexteditor.cpp @@ -0,0 +1,462 @@ +#include "richtexteditor.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../../thirdparty/tags/include/tags_line_edit.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ===================== RichTextEditor ===================== + +RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) { + setAcceptRichText(true); + setPlaceholderText("Write your message here..."); + setMouseTracking(true); +} + +void RichTextEditor::setupToolbar(QVBoxLayout *layout) { + m_toolbar = new QToolBar("Formatting"); + m_toolbar->setIconSize(QSize(16, 16)); + m_toolbar->setStyleSheet( + "QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }\n" + "QToolButton { padding: 4px 6px; border-radius: 3px; }\n" + "QToolButton:hover { background: #e0e0e0; }\n" + "QToolButton:checked { background: #bbdefb; }\n" + ); + + // Font family combo + m_fontCombo = new QFontComboBox(); + m_fontCombo->setFixedWidth(150); + connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged); + m_toolbar->addWidget(m_fontCombo); + + // Font size spin + m_fontSizeSpin = new QSpinBox(); + m_fontSizeSpin->setRange(8, 72); + m_fontSizeSpin->setValue(11); + m_fontSizeSpin->setFixedWidth(70); + m_fontSizeSpin->setFixedHeight(24); + connect(m_fontSizeSpin, QOverload::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged); + m_toolbar->addWidget(m_fontSizeSpin); + + m_toolbar->addSeparator(); + + // Bold / Italic / Underline + QAction *boldAct = m_toolbar->addAction("B"); + boldAct->setCheckable(true); + boldAct->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Bold/Text Formatting/Text Bold.svg"))); + QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont); + connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold); + + QAction *italicAct = m_toolbar->addAction("I"); + italicAct->setCheckable(true); + italicAct->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Outline/Text Formatting/Text Italic.svg"))); + QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont); + connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic); + + QAction *underlineAct = m_toolbar->addAction("U"); + underlineAct->setCheckable(true); + underlineAct->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Outline/Text Formatting/Text Underline.svg"))); + QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont); + connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline); + + m_toolbar->addSeparator(); + + // Alignment + QAction *alignLeft = m_toolbar->addAction("L"); + connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft); + + QAction *alignCenter = m_toolbar->addAction("C"); + connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter); + + QAction *alignRight = m_toolbar->addAction("R"); + connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight); + + QAction *alignJustify = m_toolbar->addAction("J"); + connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify); + + m_toolbar->addSeparator(); + + // Lists + QAction *bulletAct = m_toolbar->addAction("Bullets"); + connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList); + + QAction *numAct = m_toolbar->addAction("1. List"); + connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList); + + m_toolbar->addSeparator(); + + // Indent / Outdent + QAction *indentAct = m_toolbar->addAction("Indent"); + connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent); + + QAction *outdentAct = m_toolbar->addAction("Outdent"); + connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent); + + m_toolbar->addSeparator(); + + // Insert image + QAction *imgAct = m_toolbar->addAction("Img"); + connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage); + + // Insert table + QAction *tableAct = m_toolbar->addAction("Tbl"); + connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable); + + // 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; }\n" + "QToolButton:hover { background: #e0e0e0; }\n" + "QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }\n" + ); + connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked); + m_toolbar->addWidget(m_signatureButton); + QAction *editSigAct = m_signatureMenu->addAction(tr("Editar firmas")); + connect(editSigAct, &QAction::triggered, this, &RichTextEditor::signatureEditRequested); + + m_toolbar->addSeparator(); + + layout->addWidget(m_toolbar); +} + +void RichTextEditor::onBold() { + QTextCharFormat fmt; + fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onItalic() { + QTextCharFormat fmt; + fmt.setFontItalic(!textCursor().charFormat().fontItalic()); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onUnderline() { + QTextCharFormat fmt; + fmt.setFontUnderline(!textCursor().charFormat().fontUnderline()); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onBulletList() { + QTextCursor cursor = textCursor(); + QTextList *list = cursor.currentList(); + if (list && list->format().style() == QTextListFormat::ListDisc) { + // Remove list + QTextBlockFormat bfmt; + bfmt.setIndent(0); + cursor.setBlockFormat(bfmt); + list->remove(cursor.block()); + } else { + QTextListFormat listFormat; + listFormat.setStyle(QTextListFormat::ListDisc); + cursor.createList(listFormat); + } +} + +void RichTextEditor::onNumberedList() { + QTextCursor cursor = textCursor(); + QTextList *list = cursor.currentList(); + if (list && list->format().style() == QTextListFormat::ListDecimal) { + QTextBlockFormat bfmt; + bfmt.setIndent(0); + cursor.setBlockFormat(bfmt); + list->remove(cursor.block()); + } else { + QTextListFormat listFormat; + listFormat.setStyle(QTextListFormat::ListDecimal); + cursor.createList(listFormat); + } +} + +void RichTextEditor::onIndent() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setIndent(bfmt.indent() + 1); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onOutdent() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + int indent = bfmt.indent(); + if (indent > 0) { + bfmt.setIndent(indent - 1); + cursor.setBlockFormat(bfmt); + } +} + +void RichTextEditor::onAlignLeft() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignLeft); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignCenter() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignCenter); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignRight() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignRight); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignJustify() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignJustify); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onFontChanged(const QFont &font) { + QTextCharFormat fmt; + fmt.setFontFamilies({font.family()}); + mergeCurrentCharFormat(fmt); +} + +void RichTextEditor::onFontSizeChanged(int size) { + QTextCharFormat fmt; + fmt.setFontPointSize(size); + mergeCurrentCharFormat(fmt); +} + +void RichTextEditor::onInsertImage() { + QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)"); + if (filePath.isEmpty()) return; + QTextCursor cursor = textCursor(); + QTextImageFormat imgFmt; + imgFmt.setName(filePath); + // Scale down if too large + QPixmap pm(filePath); + if (pm.width() > 600) { + imgFmt.setWidth(600); + imgFmt.setHeight(pm.height() * 600 / pm.width()); + } + cursor.insertImage(imgFmt); +} + +void RichTextEditor::onInsertTable() { + bool ok; + int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok); + if (!ok) return; + int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok); + if (!ok) return; + + QTextCursor cursor = textCursor(); + QTextTableFormat tableFmt; + tableFmt.setBorder(1); + tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid); + tableFmt.setCellPadding(4); + tableFmt.setCellSpacing(0); + tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100)); + cursor.insertTable(rows, cols, tableFmt); +} + +/* ===================== Mouse events for image/table resizing ===================== */ +void RichTextEditor::mousePressEvent(QMouseEvent *event) { + if (event->button() == Qt::LeftButton) { + QPoint pos = event->pos(); + QTextCursor cursor = cursorForPosition(pos); + // Image + if (cursor.charFormat().isImageFormat()) { + qDebug() << "Mouse press on image"; + m_resizingImage = true; + m_resizeStartPos = pos; + QTextImageFormat imgFormat = cursor.charFormat().toImageFormat(); + m_imageStartSize = QSize(imgFormat.width(), imgFormat.height()); + if (m_imageStartSize.isEmpty()) { + QPixmap pm(imgFormat.name()); + if (!pm.isNull()) + m_imageStartSize = pm.size(); + } + // Change cursor to indicate resizing + setCursor(Qt::SizeAllCursor); + event->accept(); + return; + } + // Table + QTextTable *table = cursor.currentTable(); + if (table) { + qDebug() << "Mouse press on table"; + m_resizingTable = true; + m_tableStartPos = pos; + m_currentTable = table; + QTextTableFormat fmt = table->format(); + m_tableStartSize = QSizeF(fmt.width().rawValue(), fmt.height().rawValue()); + setCursor(Qt::SizeAllCursor); + event->accept(); + return; + } + } + QTextEdit::mousePressEvent(event); +} + +void RichTextEditor::mouseMoveEvent(QMouseEvent *event) { + if (m_resizingImage) { + qDebug() << "Mouse move resizing image"; + QPoint delta = event->pos() - m_resizeStartPos; + int newWidth = qMax(20, m_imageStartSize.width() + delta.x()); + int newHeight = qMax(20, m_imageStartSize.height() + delta.y()); + QTextCursor cursor = cursorForPosition(m_resizeStartPos); + if (cursor.charFormat().isImageFormat()) { + QTextImageFormat fmt = cursor.charFormat().toImageFormat(); + fmt.setWidth(newWidth); + fmt.setHeight(newHeight); + cursor.mergeCharFormat(fmt); + setTextCursor(cursor); + } + event->accept(); + return; + } + if (m_resizingTable) { + qDebug() << "Mouse move resizing table"; + QPoint delta = event->pos() - m_tableStartPos; + qreal newWidth = qMax(10.0, m_tableStartSize.width() + delta.x()); + qreal newHeight = qMax(10.0, m_tableStartSize.height() + delta.y()); + QTextTableFormat fmt = m_currentTable->format(); + fmt.setWidth(QTextLength(QTextLength::FixedLength, newWidth)); + fmt.setHeight(QTextLength(QTextLength::FixedLength, newHeight)); + m_currentTable->setFormat(fmt); + event->accept(); + return; + } + QTextEdit::mouseMoveEvent(event); +} + +void RichTextEditor::mouseReleaseEvent(QMouseEvent *event) { + if (event->button() == Qt::LeftButton) { + if (m_resizingImage) { + qDebug() << "Mouse release image resize"; + m_resizingImage = false; + setCursor(Qt::ArrowCursor); + event->accept(); + return; + } + if (m_resizingTable) { + qDebug() << "Mouse release table resize"; + m_resizingTable = false; + setCursor(Qt::ArrowCursor); + event->accept(); + return; + } + } + QTextEdit::mouseReleaseEvent(event); +} + +void RichTextEditor::contextMenuEvent(QContextMenuEvent *event) { + QPoint pos = event->pos(); + QTextCursor cursor = cursorForPosition(pos); + QTextTable *table = cursor.currentTable(); + if (table) { + showTableContextMenu(pos); + event->accept(); + return; + } + QTextEdit::contextMenuEvent(event); +} + +/* ===================== Helper functions ===================== */ +void RichTextEditor::showTableContextMenu(const QPoint &pos) { + if (!m_tableContextMenu) { + m_tableContextMenu = new QMenu(this); + m_tableContextMenu->addAction(tr("Insert row above"), this, &RichTextEditor::addTableRow); + m_tableContextMenu->addAction(tr("Insert row below"), this, &RichTextEditor::addTableRow); + m_tableContextMenu->addAction(tr("Insert column left"), this, &RichTextEditor::addTableColumn); + m_tableContextMenu->addAction(tr("Insert column right"), this, &RichTextEditor::addTableColumn); + m_tableContextMenu->addSeparator(); + m_tableContextMenu->addAction(tr("Delete selected rows"), this, &RichTextEditor::removeTableRow); + m_tableContextMenu->addAction(tr("Delete selected columns"), this, &RichTextEditor::removeTableColumn); + m_tableContextMenu->addSeparator(); + m_tableContextMenu->addAction(tr("Border..."), this, &RichTextEditor::modifyTableBorder); + } + m_tableContextMenu->exec(mapToGlobal(pos)); +} + +void RichTextEditor::addTableRow() { + if (!m_currentTable) return; + QTextCursor cursor = textCursor(); + QTextTable *table = cursor.currentTable(); + if (!table) return; + QTextTableCell cell = table->cellAt(cursor.position()); + int row = cell.row(); + table->insertRows(row, 1); +} + +void RichTextEditor::removeTableRow() { + if (!m_currentTable) return; + QTextCursor cursor = textCursor(); + QTextTable *table = cursor.currentTable(); + if (!table) return; + QTextTableCell cell = table->cellAt(cursor.position()); + int row = cell.row(); + table->removeRows(row, 1); +} + +void RichTextEditor::addTableColumn() { + if (!m_currentTable) return; + QTextCursor cursor = textCursor(); + QTextTable *table = cursor.currentTable(); + if (!table) return; + QTextTableCell cell = table->cellAt(cursor.position()); + int col = cell.column(); + table->insertColumns(col, 1); +} + +void RichTextEditor::removeTableColumn() { + if (!m_currentTable) return; + QTextCursor cursor = textCursor(); + QTextTable *table = cursor.currentTable(); + if (!table) return; + QTextTableCell cell = table->cellAt(cursor.position()); + int col = cell.column(); + table->removeColumns(col, 1); +} + +void RichTextEditor::modifyTableBorder() { + if (!m_currentTable) return; + bool ok; + int border = QInputDialog::getInt(this, tr("Table Border"), tr("Border width (px):"), + m_currentTable->format().border(), 0, 20, 1, &ok); + if (!ok) return; + QTextTableFormat fmt = m_currentTable->format(); + fmt.setBorder(border); + m_currentTable->setFormat(fmt); +} \ No newline at end of file diff --git a/src/ui/richtexteditor.cpp.bak b/src/ui/richtexteditor.cpp.bak new file mode 100644 index 0000000..a1bce4a --- /dev/null +++ b/src/ui/richtexteditor.cpp.bak @@ -0,0 +1,281 @@ +#include "richtexteditor.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../../thirdparty/tags/include/tags_line_edit.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ===================== RichTextEditor ===================== + +RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) { + setAcceptRichText(true); + setPlaceholderText(\"Write your message here...\"); +} + +void RichTextEditor::setupToolbar(QVBoxLayout *layout) { + m_toolbar = new QToolBar(\"Formatting\"); + m_toolbar->setIconSize(QSize(16, 16)); + m_toolbar->setStyleSheet( + \"QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }\"\n + \"QToolButton { padding: 4px 6px; border-radius: 3px; }\"\n + \"QToolButton:hover { background: #e0e0e0; }\"\n + \"QToolButton:checked { background: #bbdefb; }\"\n + ); + + // Font family combo + m_fontCombo = new QFontComboBox(); + m_fontCombo->setFixedWidth(150); + connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged); + m_toolbar->addWidget(m_fontCombo); + + // Font size spin + m_fontSizeSpin = new QSpinBox(); + m_fontSizeSpin->setRange(8, 72); + m_fontSizeSpin->setValue(11); + m_fontSizeSpin->setFixedWidth(70); + m_fontSizeSpin->setFixedHeight(24); + connect(m_fontSizeSpin, QOverload::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged); + m_toolbar->addWidget(m_fontSizeSpin); + + m_toolbar->addSeparator(); + + // Bold / Italic / + QAction *boldAct = m_toolbar->addAction(\"B\"); + boldAct->setCheckable(true); + boldAct->setIcon(QIcon(QStringLiteral(\":/icons/resources/icons/SVG/Bold/Text Formatting/Text Bold.svg\"))); + QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont); + connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold); + + QAction *italicAct = m_toolbar->addAction(\"I\"); + italicAct->setCheckable(true); + italicAct->setIcon(QIcon(QStringLiteral(\":/icons/resources/icons/SVG/Outline/Text Formatting/Text Italic.svg\"))); + QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont); + connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic); + + QAction *underlineAct = m_toolbar->addAction(\"U\"); + underlineAct->setCheckable(true); + underlineAct->setIcon(QIcon(QStringLiteral(\":/icons/resources/icons/SVG/Outline/Text Formatting/Text Underline.svg\"))); + QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont); + connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline); + + m_toolbar->addSeparator(); + + // Alignment + QAction *alignLeft = m_toolbar->addAction(\"L\"); + connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft); + + QAction *alignCenter = m_toolbar->addAction(\"C\"); + connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter); + + QAction *alignRight = m_toolbar->addAction(\"R\"); + connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight); + + QAction *alignJustify = m_toolbar->addAction(\"J\"); + connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify); + + m_toolbar->addSeparator(); + + // Lists + QAction *bulletAct = m_toolbar->addAction(\"Bullets\"); + connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList); + + QAction *numAct = m_toolbar->addAction(\"1. List\"); + connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList); + + m_toolbar->addSeparator(); + + // Indent / Outdent + QAction *indentAct = m_toolbar->addAction(\"Indent\"); + connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent); + + QAction *outdentAct = m_toolbar->addAction(\"Outdent\"); + connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent); + + m_toolbar->addSeparator(); + + // Insert image + QAction *imgAct = m_toolbar->addAction(\"Img\"); + connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage); + + // Insert table + QAction *tableAct = m_toolbar->addAction(\"Tbl\"); + connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable); + + // 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; }\"\n + \"QToolButton:hover { background: #e0e0e0; }\"\n + \"QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }\"\n + ); + connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked); + m_toolbar->addWidget(m_signatureButton); + QAction *editSigAct = m_signatureMenu->addAction(tr(\"Editar firmas\")); + connect(editSigAct, &QAction::triggered, this, &RichTextEditor::signatureEditRequested); + + m_toolbar->addSeparator(); + + layout->addWidget(m_toolbar); +} + +void RichTextEditor::onBold() { + QTextCharFormat fmt; + fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onItalic() { + QTextCharFormat fmt; + fmt.setFontItalic(!textCursor().charFormat().fontItalic()); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onUnderline() { + QTextCharFormat fmt; + fmt.setFontUnderline(!textCursor().charFormat().fontUnderline()); + mergeCurrentCharFormat(fmt); + setFocus(); +} + +void RichTextEditor::onBulletList() { + QTextCursor cursor = textCursor(); + QTextList *list = cursor.currentList(); + if (list && list->format().style() == QTextListFormat::ListDisc) { + // Remove list + QTextBlockFormat bfmt; + bfmt.setIndent(0); + cursor.setBlockFormat(bfmt); + list->remove(cursor.block()); + } else { + QTextListFormat listFormat; + listFormat.setStyle(QTextListFormat::ListDisc); + cursor.createList(listFormat); + } +} + +void RichTextEditor::onNumberedList() { + QTextCursor cursor = textCursor(); + QTextList *list = cursor.currentList(); + if (list && list->format().style() == QTextListFormat::ListDecimal) { + QTextBlockFormat bfmt; + bfmt.setIndent(0); + cursor.setBlockFormat(bfmt); + list->remove(cursor.block()); + } else { + QTextListFormat listFormat; + listFormat.setStyle(QTextListFormat::ListDecimal); + cursor.createList(listFormat); + } +} + +void RichTextEditor::onIndent() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setIndent(bfmt.indent() + 1); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onOutdent() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + int indent = bfmt.indent(); + if (indent > 0) { + bfmt.setIndent(indent - 1); + cursor.setBlockFormat(bfmt); + } +} + +void RichTextEditor::onAlignLeft() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignLeft); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignCenter() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignCenter); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignRight() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignRight); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onAlignJustify() { + QTextCursor cursor = textCursor(); + QTextBlockFormat bfmt = cursor.blockFormat(); + bfmt.setAlignment(Qt::AlignJustify); + cursor.setBlockFormat(bfmt); +} + +void RichTextEditor::onFontChanged(const QFont &font) { + QTextCharFormat fmt; + fmt.setFontFamilies({font.family()}); + mergeCurrentCharFormat(fmt); +} + +void RichTextEditor::onFontSizeChanged(int size) { + QTextCharFormat fmt; + fmt.setFontPointSize(size); + mergeCurrentCharFormat(fmt); +} + +void RichTextEditor::onInsertImage() { + QString filePath = QFileDialog::getOpenFileName(this, \"Insert Image\", QString(), \"Images (*.png *.jpg *.jpeg *.gif *.bmp)\"); + if (filePath.isEmpty()) return; + QTextCursor cursor = textCursor(); + QTextImageFormat imgFmt; + imgFmt.setName(filePath); + // Scale down if too large + QPixmap pm(filePath); + if (pm.width() > 600) { + imgFmt.setWidth(600); + imgFmt.setHeight(pm.height() * 600 / pm.width()); + } + cursor.insertImage(imgFmt); +} + +void RichTextEditor::onInsertTable() { + bool ok; + int rows = QInputDialog::getInt(this, \"Table Rows\", \"Rows:\", 3, 1, 50, 1, &ok); + if (!ok) return; + int cols = QInputDialog::getInt(this, \"Table Columns\", \"Columns:\", 3, 1, 20, 1, &ok); + if (!ok) return; + + QTextCursor cursor = textCursor(); + QTextTableFormat tableFmt; + tableFmt.setBorder(1); + tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid); + tableFmt.setCellPadding(4); + tableFmt.setCellSpacing(0); + tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100)); + cursor.insertTable(rows, cols, tableFmt); +} \ No newline at end of file diff --git a/src/ui/richtexteditor.h b/src/ui/richtexteditor.h new file mode 100644 index 0000000..4bfa5f5 --- /dev/null +++ b/src/ui/richtexteditor.h @@ -0,0 +1,81 @@ +#ifndef RICHTEXTEDITOR_H +#define RICHTEXTEDITOR_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class QTextTable; +class QTextCursor; + +class RichTextEditor : public QTextEdit +{ + Q_OBJECT +public: + explicit RichTextEditor(QWidget *parent = nullptr); + void setupToolbar(QVBoxLayout *layout); + +signals: + void signatureClicked(); + void signatureEditRequested(); + +public slots: + void onBold(); + void onItalic(); + void onUnderline(); + void onBulletList(); + void onNumberedList(); + void onIndent(); + void onOutdent(); + void onAlignLeft(); + void onAlignCenter(); + void onAlignRight(); + void onAlignJustify(); + void onFontChanged(const QFont &font); + void onFontSizeChanged(int size); + void onInsertImage(); + void onInsertTable(); + +protected: + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void contextMenuEvent(QContextMenuEvent *event) override; + +private: + QToolBar *m_toolbar; + QFontComboBox *m_fontCombo; + QSpinBox *m_fontSizeSpin; + QToolButton *m_signatureButton; + QMenu *m_signatureMenu; + + // Image resize handling + bool m_resizingImage = false; + QPoint m_resizeStartPos; + QSize m_imageStartSize; + + // Table resize handling + bool m_resizingTable = false; + QPoint m_tableStartPos; + QTextTable *m_currentTable = nullptr; + QSizeF m_tableStartSize; + QMenu *m_tableContextMenu = nullptr; + + // Helper functions + void showTableContextMenu(const QPoint &pos); + void addTableRow(); + void removeTableRow(); + void addTableColumn(); + void removeTableColumn(); + void modifyTableBorder(); +}; +#endif // RICHTEXTEDITOR_H \ No newline at end of file diff --git a/src/ui/update_delete_and_add_flag.py b/src/ui/update_delete_and_add_flag.py new file mode 100644 index 0000000..f1c059c --- /dev/null +++ b/src/ui/update_delete_and_add_flag.py @@ -0,0 +1,116 @@ +import sys + +filename = '/mnt/c/Users/javie/wino-mail-dtkqt/src/ui/mainmainwindow.cpp' +with open(filename, 'r') as f: + lines = f.readlines() + +# Find start of delete connect +start = None +for i, line in enumerate(lines): + if 'connect(deleteAction, &QAction::triggered' in line: + start = i + break +if start is None: + print('Could not find delete connect start') + sys.exit(1) + +# Find end of that connect (the matching '});' after start) +end = None +brace_count = 0 +for i in range(start, len(lines)): + line = lines[i] + for ch in line: + if ch == '{': + brace_count += 1 + elif ch == '}': + brace_count -= 1 + if brace_count == 0 and '});' in line: + end = i + break + if end is not None: + break +if end is None: + print('Could not find end of delete connect') + sys.exit(1) + +# Replacement delete lambda +new_delete = ''' connect(deleteAction, &QAction::triggered, [this]() { + QModelIndexList indexes = m_mailListView->selectionModel()->selectedIndexes(); + if (indexes.isEmpty()) { + statusBar()->showMessage(tr("No email selected"), 2000); + return; + } + int row = indexes.first().row(); + QModelIndex idx = m_emailModel->index(row, 0); + qint64 id = idx.data(EmailListModel::IdRole).toLongLong(); + if (m_mailService->deleteMail(QString::number(id))) { + statusBar()->showMessage(tr("Email deleted"), 2000); + m_emailModel->refresh(); + } else { + statusBar()->showMessage(tr("Failed to delete email"), 2000); + } + });\n''' + +# Replace lines[start:end+1] with new_delete +lines = lines[:start] + [new_delete] + lines[end+1:] + +# Now add flag action declaration after deleteAction declaration +# Find deleteAction declaration line +decl_line = None +for i, line in enumerate(lines): + if 'QAction *deleteAction = m_toolBar->addAction(\"๐Ÿ—‘ Delete\");' in line: + decl_line = i + break +if decl_line is None: + print('Could not find deleteAction declaration') + sys.exit(1) + +flag_decl = ' QAction *flagAction = m_toolBar->addAction(\"๐Ÿšฉ Flag\");\n' +lines = lines[:decl_line+1] + [flag_decl] + lines[decl_line+1:] + +# Find where to insert flag connection: after the delete connect we just placed (now at start) +# Actually after the delete connect (which now starts at 'start' and ends at start (since we replaced with one line?) Actually new_delete is multiple lines; we inserted as a single string but it contains newlines. +# We'll just insert flag connection after the delete connection block (i.e., after the line where we inserted new_delete). +# We'll search for the end of the new_delete block by looking for the line that contains '});' after start. +# But easier: insert after the deleteAction declaration line? No, we want to be after_decl_line. +# Let's just insert flag connection after the delete connection by adding it after the line that contains the end of delete connect. +# We'll search for the pattern '});' that ends the delete connect (should be in new_delete). +# We'll find the line index of that '});' after start. +for i in range(start, len(lines)): + if '});' in lines[i]: + insert_point = i + 1 + break +else: + insert_point is None: + insert_point = len(lines) + +flag_conn = ''' connect(flagAction, &QAction::triggered, [this]() { + QModelIndexList indexes = m_mailListView->selectionModel()->selectedIndexes(); + if (indexes.isEmpty()) { + statusBar()->showMessage(tr("No email selected"), 2000); + return; + } + int row = indexes.first().row(); + QModelIndex idx = m_emailModel->index(row, 0); + qint64 id = idx.data(EmailListModel::IdRole).toLongLong(); + // Toggle flagged state + std::optional opt = MailItemDao::findById(id); + if (opt) { + MailItem item = *opt; + item.setFlagged(!item.isFlagged()); + if (MailItemDao::update(item)) { + statusBar()->showMessage(tr("Flag toggled"), 2000); + m_emailModel->refresh(); + } else { + statusBar()->showMessage(tr("Failed to update flag"), 2000); + } + } else { + statusBar()->showMessage(tr("Email not found"), 2000); + } + });\n''' +lines = lines[:insert_point] + [flag_conn] + lines[insert_point:] + +with open(filename, 'w') as f: + f.writelines(lines) + +print('Delete and flag actions updated') diff --git a/src/ui/update_ui.py b/src/ui/update_ui.py new file mode 100644 index 0000000..835023f --- /dev/null +++ b/src/ui/update_ui.py @@ -0,0 +1,240 @@ +import sys + +filename = 'mainmainwindow.cpp' +with open(filename, 'r') as f: + lines = f.readlines() + +# 1. Modify onFolderSelected function +# Find the start of onFolderSelected +on_folder_start = None +for i, line in enumerate(lines): + if line.strip().startswith('void MainMainWindow::onFolderSelected(const QModelIndex &index)'): + on_folder_start = i + break +if on_folder_start is None: + print('Could not find onFolderSelected') + sys.exit(1) + +# Find the end of the function (look for a line that is just '}' after the function start) +# We'll assume the function ends at the next line that starts with 'void ' or '}' at indentation 0? Safer: find the matching brace. +brace_count = 0 +in_function = False +on_folder_end = None +for i in range(on_folder_start, len(lines)): + line = lines[i] + for ch in line: + if ch == '{': + brace_count += 1 + in_function = True + elif ch == '}': + brace_count -= 1 + if brace_count == 0 and in_function: + on_folder_end = i + break + if on_folder_end is not None: + break +if on_folder_end is None: + print('Could not find end of onFolderSelected') + sys.exit(1) + +# Now we need to replace the content between on_folder_start+1 and on_folder_end with new implementation. +# Let's first extract the current function to see what we have. +# We'll replace from line after the opening brace? Actually we want to keep the signature and the opening brace. +# We'll replace lines from on_folder_start+1 to on_folder_end-1 with new body. +# But we need to keep the opening brace line (which is at on_folder_start? Actually the signature line is on_folder_start, the opening brace is on the same line or next? +# Look at the signature line: it ends with '{'? Let's check. + +# Let's just replace the whole block from on_folder_start to on_folder_end with new function. +new_on_folder = '''void MainMainWindow::onFolderSelected(const QModelIndex &index) +{ + if (!index.isValid()) return; + + int itemType = index.data(FolderListModel::ItemTypeRole).toInt(); + if (itemType == FolderTreeItem::FolderNode) { + m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt(); + m_emailModel->setFolderId(m_currentFolderId); + // Fetch mails for this folder + std::optional optFolder = FolderDao::findById(m_currentFolderId); + if (optFolder.has_value()) { + Folder folder = optFolder.value(); + Account* account = m_accountService->findAccountById(folder.accountId()); + if (account) { + QString accountId = QString::number(account->id()); + QString folderId = QString::number(m_currentFolderId); + // Fetch mails asynchronously to avoid blocking UI + QMetaObject::invokeMethod(m_mailService, "fetchMails", Qt::QueuedConnection, + Q_ARG(QString, accountId), Q_ARG(QString, folderId)); + } + } + } else if (itemType == FolderTreeItem::AccountNode) { + // When an account node is selected, select its first folder (if any) + int childCount = m_folderModel->rowCount(index); + if (childCount > 0) { + QModelIndex firstChildIdx = m_folderModel->index(0, 0, index); + m_folderTree->setCurrentIndex(firstChildIdx); + onFolderSelected(firstChildIdx); // recursive call to handle folder selection + } else { + // No folders yet; clear email list + m_currentFolderId = -1; + m_emailModel->setFolderId(m_currentFolderId); + m_emailModel->refresh(); + } + } +} +''' + +# Replace lines +lines = lines[:on_folder_start] + [new_on_folder] + lines[on_folder_end+1:] + +# 2. Modify syncAction slot +# Find the createToolBox function? Actually we need to find the connect(syncAction, ...) line. +# Let's search for 'connect(syncAction, &QAction::triggered' +sync_connect_line = None +for i, line in enumerate(lines): + if 'connect(syncAction, &QAction::triggered' in line: + sync_connect_line = i + break +if sync_connect_line is None: + print('Could not find syncAction connect') + sys.exit(1) + +# Find the end of that lambda (the matching '});' after that line) +brace_count = 0 +in_lambda = False +sync_end = None +for i in range(sync_connect_line, len(lines)): + line = lines[i] + for ch in line: + if ch == '{': + brace_count += 1 + in_lambda = True + elif ch == '}': + brace_count -= 1 + if brace_count == 0 and in_lambda: + # Check if the line contains '});' (the end of the lambda) + if '});' in line: + sync_end = i + break + if sync_end is not None: + break +if sync_end is None: + print('Could not find end of syncAction lambda') + sys.exit(1) + +# Replace the lambda body with new implementation +new_sync_lambda = ''' connect(syncAction, &QAction::triggered, [this]() { + if (m_currentFolderId >= 0) { + statusBar()->showMessage(tr(\"Syncing...\"), 0); // 0 means until cleared + std::optional optFolder = FolderDao::findById(m_currentFolderId); + if (optFolder.has_value()) { + Folder folder = optFolder.value(); + Account* account = m_accountService->findAccountById(folder.accountId()); + if (account) { + QString accountId = QString::number(account->id()); + QString folderId = QString::number(m_currentFolderId); + // Disconnect previous connections to avoid multiple slots? We'll just call directly via queued connection. + QMetaObject::invokeMethod(m_mailService, \"fetchMails\", Qt::QueuedConnection, + Q_ARG(QString, accountId), Q_ARG(QString, folderId)); + } + } + } + });''' + +# Replace lines from sync_connect_line to sync_end inclusive +lines = lines[:sync_connect_line] + [new_sync_lambda] + lines[sync_end+1:] + +# 3. Update mailFetched and mailFetchError lambdas in connectModels +# Find the connectModels function +connect_models_start = None +for i, line in enumerate(lines): + if line.strip().startswith('void MainMainWindow::connectModels()'): + connect_models_start = i + break +if connect_models_start is None: + print('Could not find connectModels') + sys.exit(1) + +# Find end of connectModels +brace_count = 0 +in_func = False +connect_models_end = None +for i in range(connect_models_start, len(lines)): + line = lines[i] + for ch in line: + if ch == '{': + brace_count += 1 + in_func = True + elif ch == '}': + brace_count -= 1 + if brace_count == 0 and in_func: + connect_models_end = i + break + if connect_models_end is not None: + break +if connect_models_end is None: + print('Could not find end of connectModels') + sys.exit(1) + +# Within this function, we need to find the two lambdas: mailFetched and mailFetchError. +# We'll replace the whole function with a new version? That's risky. +# Instead, we'll replace the specific lambda bodies. +# Let's find the line numbers for the mailFetched connect and mailFetchError connect. +mail_fetched_line = None +mail_fetch_error_line = None +for i in range(connect_models_start, connect_models_end+1): + if 'connect(m_mailService, &MailService::mailFetched' in lines[i]: + mail_fetched_line = i + if 'connect(m_mailService, &MailService::mailFetchError' in lines[i]: + mail_fetch_error_line = i + +# For each, find the end of the lambda (the '});' line) +def find_lambda_end(start_line): + brace_count = 0 + in_lambda = False + for i in range(start_line, len(lines)): + line = lines[i] + for ch in line: + if ch == '{': + brace_count += 1 + in_lambda = True + elif ch == '}': + brace_count -= 1 + if brace_count == 0 and in_lambda: + # Check if line ends with '});' + if '});' in line: + return i + return None + +mfe_end = None +mfer_end = None +if mail_fetched_line is not None: + mfe_end = find_lambda_end(mail_fetched_line) +if mail_fetch_error_line is not None: + mfer_end = find_lambda_end(mail_fetch_error_line) + +if mfe_end is None or mfer_end is None: + print('Could not find lambda ends') + sys.exit(1) + +# New lambda bodies +new_mfe_lambda = ''' connect(m_mailService, &MailService::mailFetched, this, [this](const QString &accountId, const QString &folderId, const QVector &items) { + int fid = folderId.toInt(); + if (m_currentFolderId == fid || m_currentFolderId == -1) { + m_emailModel->refresh(); + } + statusBar()->showMessage(tr(\"Synced %1 message(s)\").arg(items.size()), 3000); + });''' + +new_mfer_lambda = ''' connect(m_mailService, &MailService::mailFetchError, this, [this](const QString &accountId, const QString &folderId, const QString &error) { + qWarning() << \"[MailFetchError]\" << error; + statusBar()->showMessage(tr(\"Error fetching mail: %1\").arg(error), 5000); + });''' + +# Replace the lambdas +lines = lines[:mail_fetched_line] + [new_mfe_lambda] + lines[mfe_end+1:mail_fetch_error_line] + [new_mfer_lambda] + lines[mfer_end+1:] + +# Write back +with open(filename, 'w') as f: + f.writelines(lines) + +print('Updated mainmainwindow.cpp')