From 59692b57062b60d7ffff4bef3c2d93e2ff6685ac Mon Sep 17 00:00:00 2001 From: Javier Date: Sun, 30 Aug 2026 01:46:34 +0200 Subject: [PATCH] feat: Search Online (IMAP) + real Calendar/Contacts UI + iCloud auth wiring Search Online: - Synchronizer::searchOnline(folderId, query) virtual with default {} - ImapSynchronizer: SELECT + UID SEARCH (Subject/From/To/TEXT) + fetch results - MailService::searchOnline() + onlineSearchFinished/onlineSearchError signals - MainMainWindow::onOnlineSearchRequested shows real server results Calendar: - Real QCalendarWidget view with per-day agenda from local mails - New MailItemDao::findByDateRange(start, end) Contacts: - QTableWidget aggregating addresses from all local mails - Regex email extraction + live search filter iCloud/AccountService: - Wire IcloudAuthenticator into AccountService::createAuthenticator --- src/core/accountsetupdialoglauncher.cpp | 2 +- src/core/accountsetupdialoglauncher.h | 2 +- src/core/authenticator.cpp | 3 +- src/core/authenticator.h | 2 +- src/core/emailcomposerbridge.cpp | 1 + src/core/emailcomposerbridge.h | 2 +- src/core/emailmanager.cpp | 1 + src/core/emailmanager.h | 2 +- src/core/gmailauthenticator.cpp | 2 +- src/core/gmailauthenticator.h | 2 +- src/core/icloudauthenticator.cpp | 225 ++++++++++++++++++ src/core/icloudauthenticator.h | 28 +++ src/core/imapauthenticator.cpp | 2 +- src/core/imapauthenticator.h | 2 +- src/core/outlookauthenticator.cpp | 2 +- src/core/outlookauthenticator.h | 2 +- src/core/synchronizerprovider.cpp | 6 +- src/db/dao/mailitemdao.cpp | 46 ++++ src/db/dao/mailitemdao.h | 1 + src/services/accountservice.cpp | 2 + src/services/concreterequests.cpp | 1 - src/services/gmail/gmailsynchronizer.cpp | 1 + src/services/gmail/gmailsynchronizer.h | 2 +- src/services/imap/imapsynchronizer.cpp | 208 ++++++++++++++++ src/services/imap/imapsynchronizer.h | 25 +- src/services/imap/imapsynchronizer_mailio.cpp | 2 +- src/services/imap/imapsynchronizer_mailio.h | 2 +- src/services/mailservice.cpp | 41 ++++ src/services/mailservice.h | 4 + src/services/outlook/outlooksynchronizer.cpp | 1 + src/services/outlook/outlooksynchronizer.h | 2 +- src/services/pop3/pop3synchronizer.cpp | 1 + src/services/pop3/pop3synchronizer.h | 2 +- src/services/pop3/pop3synchronizer_mailio.cpp | 2 +- src/services/pop3/pop3synchronizer_mailio.h | 2 +- src/services/rulesengine.cpp | 60 ++++- src/services/rulesengine.h | 1 + src/services/synchronizer.cpp | 1 + src/services/synchronizer.h | 5 + src/syncscheduler.cpp | 1 + src/syncscheduler.h | 2 +- src/ui/accountsetupdialog.cpp | 10 +- src/ui/calendarview.cpp | 127 ++++++++-- src/ui/calendarview.h | 11 + src/ui/categorytreewidget.cpp | 1 - src/ui/contactsview.cpp | 148 ++++++++++-- src/ui/contactsview.h | 11 + src/ui/delegates/CompactMailDelegate.cpp | 4 +- src/ui/delegates/CompactMailDelegate.h | 2 +- src/ui/maillistview.cpp | 23 +- src/ui/maillistview.h | 1 + src/ui/mainmainwindow.cpp | 46 ++-- src/ui/models/EmailListModel.cpp | 21 +- src/ui/models/EmailListModel.h | 5 +- src/ui/richtexteditor.cpp | 1 - src/ui/ruleditordialog.cpp | 1 - src/ui/rulesmanagerdialog.cpp | 1 - src/ui/settingsview.cpp | 16 ++ src/ui/signaturemanagerdialog.cpp | 1 - src/ui/templateeditordialog.cpp | 1 - src/ui/templatesmanagerdialog.cpp | 1 - 61 files changed, 1018 insertions(+), 115 deletions(-) create mode 100644 src/core/icloudauthenticator.cpp create mode 100644 src/core/icloudauthenticator.h diff --git a/src/core/accountsetupdialoglauncher.cpp b/src/core/accountsetupdialoglauncher.cpp index f692f2c..d0e40da 100644 --- a/src/core/accountsetupdialoglauncher.cpp +++ b/src/core/accountsetupdialoglauncher.cpp @@ -71,4 +71,4 @@ void AccountSetupDialogLauncher::initializeSynchronizer(const Account &account) qWarning() << "[AccountSetupDialogLauncher] Could not create synchronizer for:" << providerType; } } - +AccountSetupDialogLauncher::~AccountSetupDialogLauncher() = default; diff --git a/src/core/accountsetupdialoglauncher.h b/src/core/accountsetupdialoglauncher.h index 2a9ea9f..bfca3e6 100644 --- a/src/core/accountsetupdialoglauncher.h +++ b/src/core/accountsetupdialoglauncher.h @@ -13,7 +13,7 @@ class AccountSetupDialogLauncher : public QObject Q_OBJECT public: explicit AccountSetupDialogLauncher(QObject *parent = nullptr); - ~AccountSetupDialogLauncher() override = default; + ~AccountSetupDialogLauncher() override; void launchSetupDialog(); void setupAccountManually(const Account &account); diff --git a/src/core/authenticator.cpp b/src/core/authenticator.cpp index a81fe6c..ff7dbd2 100644 --- a/src/core/authenticator.cpp +++ b/src/core/authenticator.cpp @@ -63,4 +63,5 @@ QNetworkRequest Authenticator::createTokenRequest(const QUrl &url) const QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); return request; -} \ No newline at end of file +} +Authenticator::~Authenticator() = default; diff --git a/src/core/authenticator.h b/src/core/authenticator.h index 7e69a44..12766dd 100644 --- a/src/core/authenticator.h +++ b/src/core/authenticator.h @@ -15,7 +15,7 @@ class Authenticator : public QObject Q_OBJECT public: explicit Authenticator(QObject *parent = nullptr); - ~Authenticator() override = default; + ~Authenticator() override; virtual void authenticate(const QString &email) = 0; virtual void refreshToken(const Account &account) = 0; diff --git a/src/core/emailcomposerbridge.cpp b/src/core/emailcomposerbridge.cpp index ace028f..2bae190 100644 --- a/src/core/emailcomposerbridge.cpp +++ b/src/core/emailcomposerbridge.cpp @@ -46,3 +46,4 @@ QString EmailComposerBridge::renderBodyToHtml(const QString &plainText) const html.replace("\r\n", "
"); return "" + html + ""; } +EmailComposerBridge::~EmailComposerBridge() = default; diff --git a/src/core/emailcomposerbridge.h b/src/core/emailcomposerbridge.h index b8a594b..8ebc491 100644 --- a/src/core/emailcomposerbridge.h +++ b/src/core/emailcomposerbridge.h @@ -11,7 +11,7 @@ class EmailComposerBridge : public QObject Q_OBJECT public: explicit EmailComposerBridge(QObject *parent = nullptr); - ~EmailComposerBridge() override = default; + ~EmailComposerBridge() override; struct Attachment { QString fileName; diff --git a/src/core/emailmanager.cpp b/src/core/emailmanager.cpp index e0f93af..4a434c1 100644 --- a/src/core/emailmanager.cpp +++ b/src/core/emailmanager.cpp @@ -48,3 +48,4 @@ bool EmailManager::sendEmail(const QString& to, const QString& subject, const QS Q_UNUSED(body); return false; } +EmailManager::~EmailManager() = default; diff --git a/src/core/emailmanager.h b/src/core/emailmanager.h index 8aaf8f6..b1d373a 100644 --- a/src/core/emailmanager.h +++ b/src/core/emailmanager.h @@ -11,7 +11,7 @@ class EmailManager : public QObject Q_OBJECT public: explicit EmailManager(QObject *parent = nullptr); - ~EmailManager() override = default; + ~EmailManager() override; // Returns a MailItem by id, or null if not found Q_INVOKABLE MailItem getMailItemById(qint64 id) const; diff --git a/src/core/gmailauthenticator.cpp b/src/core/gmailauthenticator.cpp index b3925f6..5b63a1c 100644 --- a/src/core/gmailauthenticator.cpp +++ b/src/core/gmailauthenticator.cpp @@ -94,4 +94,4 @@ void GmailAuthenticator::onTokenReply(QNetworkReply *reply) { qDebug() << "[GmailAuthenticator] onTokenReply called"; reply->deleteLater(); } - +GmailAuthenticator::~GmailAuthenticator() = default; diff --git a/src/core/gmailauthenticator.h b/src/core/gmailauthenticator.h index a300a21..deb7bf0 100644 --- a/src/core/gmailauthenticator.h +++ b/src/core/gmailauthenticator.h @@ -9,7 +9,7 @@ class GmailAuthenticator : public Authenticator Q_OBJECT public: explicit GmailAuthenticator(QObject *parent = nullptr); - ~GmailAuthenticator() override = default; + ~GmailAuthenticator() override; void authenticate(const QString &email) override; void refreshToken(const Account &account) override; diff --git a/src/core/icloudauthenticator.cpp b/src/core/icloudauthenticator.cpp new file mode 100644 index 0000000..75c8bb3 --- /dev/null +++ b/src/core/icloudauthenticator.cpp @@ -0,0 +1,225 @@ +#include "icloudauthenticator.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +IcloudAuthenticator::IcloudAuthenticator(QObject *parent) + : Authenticator(parent) + , m_callbackServer(nullptr) +{ + // iCloud OAuth2 endpoints (Apple Sign In) + m_authEndpoint = "https://appleid.apple.com/auth/authorize"; + m_tokenEndpoint = "https://appleid.apple.com/auth/token"; + m_scopes = "name email"; + + // iCloud/Apple specific client configuration + // These would typically come from configuration + m_clientId = ""; // Services ID from Apple Developer + m_clientSecret = ""; // JWT token generated from private key + m_redirectUri = "http://localhost:8080/callback"; + + m_callbackServer = new OAuthCallbackServer(8080, this); + connect(m_callbackServer, &OAuthCallbackServer::codeReceived, + this, &IcloudAuthenticator::onAuthCodeReceived); + connect(m_callbackServer, &OAuthCallbackServer::errorOccurred, + this, &IcloudAuthenticator::authenticationFailed); +} + +IcloudAuthenticator::~IcloudAuthenticator() +{ +} + +void IcloudAuthenticator::authenticate(const QString &email) +{ + m_email = email; + + // Generate state parameter for CSRF protection + QString state = QString::number(QRandomGenerator::global()->generate64(), 16); + + // Build authorization URL + QUrl authUrl(m_authEndpoint); + QUrlQuery query; + query.addQueryItem("client_id", m_clientId); + query.addQueryItem("redirect_uri", m_redirectUri); + query.addQueryItem("response_type", "code"); + query.addQueryItem("response_mode", "form_post"); + query.addQueryItem("scope", m_scopes); + query.addQueryItem("state", state); + query.addQueryItem("response_type", "code"); + authUrl.setQuery(query); + + // Start local callback server + if (!m_callbackServer) { + m_callbackServer = new OAuthCallbackServer(8080, this); + connect(m_callbackServer, &OAuthCallbackServer::codeReceived, + this, &IcloudAuthenticator::onAuthCodeReceived); + connect(m_callbackServer, &OAuthCallbackServer::errorOccurred, + this, &IcloudAuthenticator::authenticationFailed); + } + + if (!m_callbackServer->start()) { + emit authenticationFailed("Could not start local callback server"); + return; + } + + // Open browser for user authentication + QUrl authUrlFinal(m_authEndpoint); + QUrlQuery queryFinal; + queryFinal.addQueryItem("client_id", m_clientId); + queryFinal.addQueryItem("redirect_uri", m_redirectUri); + queryFinal.addQueryItem("response_type", "code"); + queryFinal.addQueryItem("response_mode", "form_post"); + queryFinal.addQueryItem("scope", m_scopes); + queryFinal.addQueryItem("state", state); + authUrlFinal.setQuery(queryFinal); + + if (!QDesktopServices::openUrl(authUrlFinal)) { + emit authenticationFailed("Could not open browser for authentication"); + return; + } + + // Timeout after 5 minutes + QTimer::singleShot(5 * 60 * 1000, this, [this]() { + if (m_callbackServer && m_callbackServer->port() > 0) { + m_callbackServer->stop(); + } + emit authenticationFailed("Authentication timed out"); + }); +} + +void IcloudAuthenticator::refreshToken(const Account &account) +{ + // iCloud uses app-specific passwords for IMAP/SMTP + // OAuth tokens are typically long-lived or handled differently + // For IMAP access, iCloud uses app-specific passwords + // OAuth refresh would be for the Apple ID itself, not email access + + QString refreshToken = account.connectionSettings().password; // Store refresh token in password field + if (refreshToken.isEmpty()) { + emit tokenRefreshed(QString::number(account.id()), QString()); + return; + } + + // Exchange refresh token for new access token + QUrl url(m_tokenEndpoint); + QUrlQuery query; + query.addQueryItem("client_id", m_clientId); + query.addQueryItem("client_secret", m_clientSecret); + query.addQueryItem("grant_type", "refresh_token"); + query.addQueryItem("refresh_token", refreshToken); + + QNetworkRequest request(m_tokenEndpoint); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + QNetworkReply *reply = m_networkManager->post(request, query.toString(QUrl::FullyEncoded).toUtf8()); + connect(reply, &QNetworkReply::finished, this, [this, accountId = QString::number(account.id()), reply]() { + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "Token refresh failed:" << reply->errorString(); + emit tokenRefreshed(accountId, QString()); + } else { + QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + QJsonObject obj = doc.object(); + QString newToken = obj.value("access_token").toString(); + QString newRefreshToken = obj.value("refresh_token").toString(); + if (!newRefreshToken.isEmpty()) { + emit tokenRefreshed(accountId, newRefreshToken); + } else if (!newToken.isEmpty()) { + emit tokenRefreshed(accountId, newToken); + } else { + emit tokenRefreshed(accountId, QString()); + } + } + reply->deleteLater(); + }); +} + +void IcloudAuthenticator::onAuthCodeReceived(const QString &code, const QString &state) +{ + // Exchange authorization code for tokens + QUrl url(m_tokenEndpoint); + QUrlQuery query; + query.addQueryItem("client_id", m_clientId); + query.addQueryItem("client_secret", m_clientSecret); + query.addQueryItem("grant_type", "authorization_code"); + query.addQueryItem("code", code); + query.addQueryItem("redirect_uri", m_redirectUri); + + QNetworkRequest request(m_tokenEndpoint); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + QNetworkReply *reply = m_networkManager->post(request, query.toString(QUrl::FullyEncoded).toUtf8()); + connect(reply, &QNetworkReply::finished, this, [this, code, reply]() { + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "Token exchange failed:" << reply->errorString(); + emit authenticationFailed("Failed to exchange authorization code: " + reply->errorString()); + } else { + QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); + QJsonObject obj = doc.object(); + + QString accessToken = obj.value("access_token").toString(); + QString refreshToken = obj.value("refresh_token").toString(); + QString idToken = obj.value("id_token").toString(); // Apple ID token with user info + + if (accessToken.isEmpty() && refreshToken.isEmpty()) { + emit authenticationFailed("No tokens received from Apple"); + return; + } + + // Parse id_token to get user email + QString email = m_email; // fallback + if (!idToken.isEmpty()) { + QStringList parts = idToken.split('.'); + if (parts.size() >= 2) { + QByteArray payload = QByteArray::fromBase64(parts[1].toUtf8(), QByteArray::Base64UrlEncoding); + QJsonDocument doc = QJsonDocument::fromJson(payload); + if (!doc.isNull()) { + QJsonObject obj = doc.object(); + email = obj.value("email").toString(); + } + } + } + + // Create account with tokens + Account account; + account.setEmail(email); + account.setDisplayName(email); + account.setType(AccountType::IMAP); // iCloud uses IMAP + + Account::ConnectionSettings settings; + settings.type = "imap"; + settings.incomingHost = "imap.mail.me.com"; + settings.incomingPort = 993; + settings.incomingSsl = true; + settings.outgoingHost = "smtp.mail.me.com"; + settings.outgoingPort = 587; + settings.outgoingSsl = true; + settings.authMethod = "oauth2"; + settings.username = email; + settings.password = refreshToken; // Store refresh token for IMAP + // Note: OAuth tokens stored in account's accessToken/refreshToken fields + account.setConnectionSettings(settings); + + // Store OAuth tokens in account fields + account.setAccessToken(accessToken); + account.setRefreshToken(refreshToken); + + emit authenticationCompleted(account); + } + reply->deleteLater(); + }); +} + +void IcloudAuthenticator::onTokenReply(QNetworkReply *reply) +{ + // Not used directly, handled in lambdas above +} \ No newline at end of file diff --git a/src/core/icloudauthenticator.h b/src/core/icloudauthenticator.h new file mode 100644 index 0000000..536e2ba --- /dev/null +++ b/src/core/icloudauthenticator.h @@ -0,0 +1,28 @@ +#ifndef ICLOUDAUTHENTICATOR_H +#define ICLOUDAUTHENTICATOR_H + +#include "authenticator.h" +#include "oauthcallbackserver.h" + +class IcloudAuthenticator : public Authenticator +{ + Q_OBJECT +public: + explicit IcloudAuthenticator(QObject *parent = nullptr); + ~IcloudAuthenticator() override; + + void authenticate(const QString &email) override; + void refreshToken(const Account &account) override; + QString providerName() const override { return "icloud"; } + +private slots: + void onTokenReply(QNetworkReply *reply); + void onAuthCodeReceived(const QString &code, const QString &state); + +private: + QString m_email; + QString m_authCode; + OAuthCallbackServer *m_callbackServer; +}; + +#endif // ICLOUDAUTHENTICATOR_H \ No newline at end of file diff --git a/src/core/imapauthenticator.cpp b/src/core/imapauthenticator.cpp index 341ac85..050c529 100644 --- a/src/core/imapauthenticator.cpp +++ b/src/core/imapauthenticator.cpp @@ -37,4 +37,4 @@ void ImapAuthenticator::configure(const QString &imapServer, int imapPort, m_username = username; m_password = password; } - +ImapAuthenticator::~ImapAuthenticator() = default; diff --git a/src/core/imapauthenticator.h b/src/core/imapauthenticator.h index e724ac8..2d50921 100644 --- a/src/core/imapauthenticator.h +++ b/src/core/imapauthenticator.h @@ -8,7 +8,7 @@ class ImapAuthenticator : public Authenticator Q_OBJECT public: explicit ImapAuthenticator(QObject *parent = nullptr); - ~ImapAuthenticator() override = default; + ~ImapAuthenticator() override; void authenticate(const QString &email) override; void refreshToken(const Account &account) override; diff --git a/src/core/outlookauthenticator.cpp b/src/core/outlookauthenticator.cpp index 803838c..ff4f3b1 100644 --- a/src/core/outlookauthenticator.cpp +++ b/src/core/outlookauthenticator.cpp @@ -86,4 +86,4 @@ void OutlookAuthenticator::onTokenReply(QNetworkReply *reply) { qDebug() << "[OutlookAuthenticator] onTokenReply called"; reply->deleteLater(); } - +OutlookAuthenticator::~OutlookAuthenticator() = default; diff --git a/src/core/outlookauthenticator.h b/src/core/outlookauthenticator.h index 274733e..394d6ad 100644 --- a/src/core/outlookauthenticator.h +++ b/src/core/outlookauthenticator.h @@ -9,7 +9,7 @@ class OutlookAuthenticator : public Authenticator Q_OBJECT public: explicit OutlookAuthenticator(QObject *parent = nullptr); - ~OutlookAuthenticator() override = default; + ~OutlookAuthenticator() override; void authenticate(const QString &email) override; void refreshToken(const Account &account) override; diff --git a/src/core/synchronizerprovider.cpp b/src/core/synchronizerprovider.cpp index 12e4b1f..0948a2f 100644 --- a/src/core/synchronizerprovider.cpp +++ b/src/core/synchronizerprovider.cpp @@ -48,11 +48,13 @@ Synchronizer* SynchronizerProvider::createSynchronizer(const QString &accountId, } Synchronizer *sync = nullptr; - + if (providerType == "gmail" || providerType == "google") { sync = new GmailSynchronizer(this); } else if (providerType == "outlook" || providerType == "microsoft") { sync = new OutlookSynchronizer(this); + } else if (providerType == "icloud" || providerType == "apple") { + sync = new ImapSynchronizer(this); } else if (providerType == "imap") { #if defined(USE_MAILIO_IMAP) && USE_MAILIO_IMAP // Use mailio-based IMAP synchronizer (more robust) @@ -73,7 +75,7 @@ Synchronizer* SynchronizerProvider::createSynchronizer(const QString &accountId, } registerSynchronizer(accountId, sync); - qDebug() << "[SynchronizerProvider] Created synchronizer for account:" << accountId + qDebug() << "[SynchronizerProvider] Created synchronizer for account:" << accountId << "type:" << providerType; return sync; } diff --git a/src/db/dao/mailitemdao.cpp b/src/db/dao/mailitemdao.cpp index a501271..2b3e280 100644 --- a/src/db/dao/mailitemdao.cpp +++ b/src/db/dao/mailitemdao.cpp @@ -220,6 +220,52 @@ QVector MailItemDao::findAll() return items; } +QVector MailItemDao::findByDateRange(const QDateTime &start, const QDateTime &end) +{ + 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, threadId, inReplyTo, \"references\", isPinned " + "FROM MailCopy WHERE date >= :start AND date < :end ORDER BY date ASC"); + query.bindValue(":start", start); + query.bindValue(":end", end); + + if (!query.exec()) { + qWarning() << "Failed to fetch mail items by date range:" << 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()); + item.setThreadId(query.value(17).toString()); + item.setInReplyTo(query.value(18).toString()); + QString refs = query.value(19).toString(); + if (!refs.isEmpty()) item.setReferences(refs.split(" ", Qt::SkipEmptyParts)); + item.setPinned(query.value(20).toBool()); + QVector names; + for (const auto &attachment : attachmentsForMail(item.id())) names.append(attachment.fileName); + item.setAttachments(names); + items.append(item); + } + return items; +} + QVector MailItemDao::findByFolderId(int folderId) { QVector items; diff --git a/src/db/dao/mailitemdao.h b/src/db/dao/mailitemdao.h index 7a25206..641f73b 100644 --- a/src/db/dao/mailitemdao.h +++ b/src/db/dao/mailitemdao.h @@ -25,6 +25,7 @@ public: static QVector findAll(); static QVector findByFolderId(int folderId); static QVector findByFolderIdSinceUid(int folderId, qint64 sinceUid); + static QVector findByDateRange(const QDateTime &start, const QDateTime &end); static std::optional maxUidForFolder(int folderId); static QVector getUidsForFolder(int folderId); static QVector uidsForFolder(int folderId); diff --git a/src/services/accountservice.cpp b/src/services/accountservice.cpp index 44cf4c5..ee50906 100644 --- a/src/services/accountservice.cpp +++ b/src/services/accountservice.cpp @@ -9,6 +9,7 @@ #include "core/gmailauthenticator.h" #include "core/outlookauthenticator.h" #include "core/imapauthenticator.h" +#include "core/icloudauthenticator.h" #include "core/synchronizerprovider.h" #include "core/eventbus.h" #include "core/events.h" @@ -146,6 +147,7 @@ Authenticator* AccountService::createAuthenticator(const QString &providerType) { if (providerType == "gmail") return new GmailAuthenticator(this); if (providerType == "outlook") return new OutlookAuthenticator(this); + if (providerType == "icloud" || providerType == "apple") return new IcloudAuthenticator(this); if (providerType == "imap") return new ImapAuthenticator(this); qWarning() << "[AccountService] Unknown provider:" << providerType; return nullptr; diff --git a/src/services/concreterequests.cpp b/src/services/concreterequests.cpp index 9ef63f8..7564fd9 100644 --- a/src/services/concreterequests.cpp +++ b/src/services/concreterequests.cpp @@ -60,4 +60,3 @@ GraphGetMessageRequest::GraphGetMessageRequest(const QString &accountId, const Q QUrl url(QString("https://graph.microsoft.com/v1.0/me/messages/%1").arg(messageId)); setUrl(url); } -Request::~Request() = default; diff --git a/src/services/gmail/gmailsynchronizer.cpp b/src/services/gmail/gmailsynchronizer.cpp index 3dc3d02..46ebab0 100644 --- a/src/services/gmail/gmailsynchronizer.cpp +++ b/src/services/gmail/gmailsynchronizer.cpp @@ -459,3 +459,4 @@ QStringList GmailSynchronizer::extractHeaderValue(const QJsonArray& headers, con return values; } GmailSynchronizer::~GmailSynchronizer() = default; + diff --git a/src/services/gmail/gmailsynchronizer.h b/src/services/gmail/gmailsynchronizer.h index 8cb0bf8..0c9dc19 100644 --- a/src/services/gmail/gmailsynchronizer.h +++ b/src/services/gmail/gmailsynchronizer.h @@ -16,7 +16,7 @@ class GmailSynchronizer : public Synchronizer Q_OBJECT public: explicit GmailSynchronizer(QObject* parent = nullptr); - ~GmailSynchronizer() override = default; + ~GmailSynchronizer() override; // Synchronizer interface bool initialize(const Account& account) override; diff --git a/src/services/imap/imapsynchronizer.cpp b/src/services/imap/imapsynchronizer.cpp index 6358f3b..c58aa1a 100644 --- a/src/services/imap/imapsynchronizer.cpp +++ b/src/services/imap/imapsynchronizer.cpp @@ -722,6 +722,94 @@ QString ImapSynchronizer::generateEventId() const return QString::number(QDateTime::currentMSecsSinceEpoch()); } +QVector ImapSynchronizer::searchOnline(const QString &folderId, const QString &query) +{ + QVector items; + + QString trimmed = query.trimmed(); + if (trimmed.isEmpty()) return items; + + ImapConnection conn; + if (!connectAndLogin(conn)) { + qWarning() << "[ImapSynchronizer] searchOnline: connect/login failed"; + return items; + } + + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) { + qWarning() << "[ImapSynchronizer] searchOnline: folder not found" << folderId; + return items; + } + QString folderName = folderOpt->name(); + + // SELECT the folder + QString selectCommand = QString("SELECT \"%1\"").arg(folderName); + QString selectResponse; + if (!conn.sendCommandWait(selectCommand, selectResponse, 30000) || !selectResponse.contains(" OK ")) { + qWarning() << "[ImapSynchronizer] searchOnline: SELECT failed:" << selectResponse; + return items; + } + + // Escape the query for safe quoting inside the IMAP SEARCH criteria. + QString safeQuery = trimmed; + safeQuery.replace('\\', "\\\\").replace('"', "\\\""); + + // UID SEARCH matching subject/from/to/body text. + // Wrap in OR(HEADER Subject, HEADER From, HEADER To, TEXT) — IMAP OR is binary, + // so we issue a single TEXT search plus SUBJECT/HEADER alternatives is complex. + // Practical approach: search subject, from, to and text by issuing one command: + // UID SEARCH OR OR OR HEADER Subject "" HEADER From "" HEADER To "" TEXT "" + // IMAP OR only takes two terms, so we chain nested ORs if supported. To stay portable + // across servers we prefer: UID SEARCH TEXT "" and, if empty, fall back to headers. + QString searchCommand = QStringLiteral("UID SEARCH OR OR OR HEADER Subject \"%1\" HEADER From \"%2\" HEADER To \"%3\" TEXT \"%4\"") + .arg(safeQuery, safeQuery, safeQuery, safeQuery); + QString searchResponse; + if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) { + // Fallback: simpler TEXT search + QString simple = QStringLiteral("UID SEARCH TEXT \"%1\"").arg(safeQuery); + if (!conn.sendCommandWait(simple, searchResponse, 30000)) { + qWarning() << "[ImapSynchronizer] searchOnline: SEARCH failed"; + conn.sendCommandWait("CLOSE", selectResponse, 30000); + conn.disconnect(); + return items; + } + } + + QVector uids = parseUidSearchResponse(searchResponse); + if (uids.isEmpty()) { + conn.sendCommandWait("CLOSE", selectResponse, 30000); + conn.disconnect(); + return items; + } + + // Fetch full messages for the matched UIDs in small batches. + const int batchSize = 5; + 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); + + QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[] FLAGS INTERNALDATE)").arg(batchList); + QByteArray fetchResponse; + if (!conn.sendCommandWaitBytes(fetchCommand, fetchResponse, 300000)) { + qWarning() << "[ImapSynchronizer] searchOnline: FETCH failed for batch"; + continue; + } + QVector batchItems = parseFetchResponseBytes(fetchResponse); + for (MailItem &item : batchItems) { + item.setFolderId(fid); + items.append(item); + } + } + + conn.sendCommandWait("CLOSE", selectResponse, 30000); + conn.disconnect(); + return items; +} + QVector ImapSynchronizer::parseListResponse(const QStringList& lines) const { QVector folders; @@ -871,3 +959,123 @@ QVector ImapSynchronizer::parseFetchResponse(const QStringList& lines) return items; } ImapSynchronizer::~ImapSynchronizer() = default; + +// IDLE support implementation +bool ImapSynchronizer::startIdle(const QString& folderId) +{ + if (m_idleActive) { + qWarning() << "IDLE already active on folder" << m_idleFolderId; + return false; + } + + // Create connection on heap for IDLE + m_idleConnectionObj = new ImapConnection(this); + ImapConnection* conn = m_idleConnectionObj; + + if (!connectAndLogin(*conn)) { + delete m_idleConnectionObj; + m_idleConnectionObj = nullptr; + return false; + } + + int fid = folderId.toInt(); + auto folderOpt = FolderDao::findById(fid); + if (!folderOpt) { + qWarning() << "Folder not found for id" << folderId; + delete m_idleConnectionObj; + m_idleConnectionObj = nullptr; + return false; + } + QString folderName = folderOpt->name(); + + // SELECT folder + QString selectCommand = QString("SELECT \"%1\"").arg(folderName); + QString selectResponse; + if (!conn->sendCommandWait(selectCommand, selectResponse, 30000) || !selectResponse.contains(" OK ")) { + qWarning() << "SELECT failed for IDLE:" << selectResponse; + delete m_idleConnectionObj; + m_idleConnectionObj = nullptr; + return false; + } + + // Send IDLE command + QString idleCommand = "IDLE"; + QString idleResponse; + if (!conn->sendCommandWait(idleCommand, idleResponse, 5000) || !idleResponse.contains("+")) { + qWarning() << "IDLE command failed:" << idleResponse; + delete m_idleConnectionObj; + m_idleConnectionObj = nullptr; + return false; + } + + m_idleFolderId = folderId; + m_idleActive = true; + + // Set up connection to handle untagged responses during IDLE + m_idleConnection = connect(conn, &ImapConnection::untaggedResponse, + this, [this, folderId](const QString& line) { + processIdleResponse(line); + }); + + qDebug() << "IDLE started for folder" << folderId; + return true; +} + +void ImapSynchronizer::stopIdle() +{ + if (!m_idleActive) return; + + if (m_idleConnectionObj) { + // Send DONE command to end IDLE + m_idleConnectionObj->sendRaw("DONE"); + m_idleConnectionObj->disconnect(); + delete m_idleConnectionObj; + m_idleConnectionObj = nullptr; + } + + if (m_idleConnection) { + disconnect(m_idleConnection); + m_idleConnection = QMetaObject::Connection(); + } + + m_idleActive = false; + m_idleFolderId.clear(); + + qDebug() << "IDLE stopped"; +} + +void ImapSynchronizer::processIdleResponse(const QString& line) +{ + if (!m_idleActive) return; + + // Handle EXPUNGE + if (line.contains("EXPUNGE")) { + qDebug() << "IDLE: EXPUNGE received"; + // Could emit signal for expunged message + return; + } + + // Handle EXISTS (new message count) + if (line.contains("EXISTS")) { + // New message arrived, fetch it + qDebug() << "IDLE: New message detected"; + // Trigger a quick sync for the folder + emit statusMessage(tr("New mail received in %1").arg(m_idleFolderId)); + return; + } + + // Handle FLAGS change + if (line.contains("FETCH") && line.contains("FLAGS")) { + qDebug() << "IDLE: Flags changed"; + // Parse flags and emit signal + // This would require parsing the FETCH response + return; + } + + // Handle RECENT + if (line.contains("RECENT")) { + qDebug() << "IDLE: Recent message"; + return; + } +} + diff --git a/src/services/imap/imapsynchronizer.h b/src/services/imap/imapsynchronizer.h index 5ba5c8d..e542c58 100644 --- a/src/services/imap/imapsynchronizer.h +++ b/src/services/imap/imapsynchronizer.h @@ -10,7 +10,7 @@ class ImapSynchronizer : public Synchronizer Q_OBJECT public: explicit ImapSynchronizer(QObject* parent = nullptr); - ~ImapSynchronizer() override = default; + ~ImapSynchronizer() override; // Synchronizer interface bool initialize(const Account& account) override; @@ -22,12 +22,17 @@ public: bool updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged) override; - bool deleteMailItem(const QString& folderId, - const QString& itemUid) override; + bool deleteMailItem(const QString &folderId, + const QString &itemUid) override; -signals: + /// Server-side IMAP SEARCH over a folder. Returns matching items. + QVector searchOnline(const QString &folderId, const QString &query) override; + + signals: void progressChanged(int percent) const; void statusMessage(const QString& message) const; + void newMailReceived(const QString& folderId, const MailItem& mail); // New: IDLE notification + void mailFlagsChanged(const QString& folderId, const QString& itemUid, bool read, bool flagged); // New: flags changed private: QVector parseUidSearchResponse(const QString& response) const; @@ -50,8 +55,18 @@ private: 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; + + // IDLE support + bool startIdle(const QString& folderId); + void stopIdle(); + void processIdleResponse(const QString& line); + bool isIdling() const { return m_idleActive; } + QString m_idleFolderId; + bool m_idleActive = false; + QMetaObject::Connection m_idleConnection; + ImapConnection* m_idleConnectionObj = nullptr; // Store connection for IDLE (heap allocated) }; diff --git a/src/services/imap/imapsynchronizer_mailio.cpp b/src/services/imap/imapsynchronizer_mailio.cpp index 488e30d..dc590b3 100644 --- a/src/services/imap/imapsynchronizer_mailio.cpp +++ b/src/services/imap/imapsynchronizer_mailio.cpp @@ -446,4 +446,4 @@ message ImapSynchronizerMailio::createMailioMessage(const MailItem& item) return msg; } -#endif // USE_MAILIO_IMAP \ No newline at end of file +#endif // USE_MAILIO_IMAPimapsynchronizer_mailio::~imapsynchronizer_mailio() = default; diff --git a/src/services/imap/imapsynchronizer_mailio.h b/src/services/imap/imapsynchronizer_mailio.h index e372c57..978ae7b 100644 --- a/src/services/imap/imapsynchronizer_mailio.h +++ b/src/services/imap/imapsynchronizer_mailio.h @@ -14,7 +14,7 @@ class ImapSynchronizerMailio : public Synchronizer Q_OBJECT public: explicit ImapSynchronizerMailio(QObject* parent = nullptr); - ~ImapSynchronizerMailio() override = default; + ~ImapSynchronizerMailio() override; // Synchronizer interface bool initialize(const Account& account) override; diff --git a/src/services/mailservice.cpp b/src/services/mailservice.cpp index 2d431b4..e508cb7 100644 --- a/src/services/mailservice.cpp +++ b/src/services/mailservice.cpp @@ -427,6 +427,47 @@ void MailService::fetchMails(const QString &accountId, const QString &folderId) watcher->setFuture(future); } +QVector MailService::searchOnline(const QString &accountId, const QString &folderId, const QString &query) +{ + QVector results; + + if (!m_accountService) { + emit onlineSearchError(accountId, tr("AccountService not available")); + return results; + } + Account *acc = m_accountService->findAccountById(accountId.toLongLong()); + if (!acc) { + emit onlineSearchError(accountId, tr("Account not found")); + return results; + } + + QString providerType = QStringLiteral("imap"); + switch (acc->type()) { + case AccountType::Gmail: providerType = QStringLiteral("gmail"); break; + case AccountType::Outlook: providerType = QStringLiteral("outlook"); break; + case AccountType::POP3: providerType = QStringLiteral("pop3"); break; + default: providerType = QStringLiteral("imap"); break; + } + + Synchronizer *sync = SynchronizerProvider::instance().createSynchronizer(accountId, providerType); + if (!sync) { + delete acc; + emit onlineSearchError(accountId, tr("Failed to create synchronizer")); + return results; + } + bool ok = sync->initialize(*acc); + delete acc; + if (!ok) { + SynchronizerProvider::instance().unregisterSynchronizer(accountId); + emit onlineSearchError(accountId, tr("Failed to initialize synchronizer")); + return results; + } + + results = sync->searchOnline(folderId, query); + emit onlineSearchFinished(accountId, folderId, results); + return results; +} + bool MailService::persistFetchedItem(MailItem &item, const QString &accountId, const QString &folderId) { diff --git a/src/services/mailservice.h b/src/services/mailservice.h index 1d80fa7..c2a1b3d 100644 --- a/src/services/mailservice.h +++ b/src/services/mailservice.h @@ -28,11 +28,15 @@ public: void moveMail(const QString &mailItemId, const QString &targetFolderId); void deleteMail(const QString &mailItemId); void markAsRead(const QString &mailItemId, bool read); + /// Server-side online search over a folder. Returns matching items or empty. + QVector searchOnline(const QString &accountId, const QString &folderId, const QString &query); signals: void mailSent(const QString &mailItemId); void mailSendFailed(const QString &mailItemId, const QString &error); void mailFetched(const QString &accountId, const QString &folderId, const QVector &items); + void onlineSearchFinished(const QString &accountId, const QString &folderId, const QVector &results); + void onlineSearchError(const QString &accountId, const QString &error); void mailMoved(const QString &mailItemId); void mailDeleted(const QString &mailItemId); void mailReadStateChanged(const QString &mailItemId, bool read); diff --git a/src/services/outlook/outlooksynchronizer.cpp b/src/services/outlook/outlooksynchronizer.cpp index 5de3cd2..4a1e891 100644 --- a/src/services/outlook/outlooksynchronizer.cpp +++ b/src/services/outlook/outlooksynchronizer.cpp @@ -425,3 +425,4 @@ Folder OutlookSynchronizer::parseFolderFromGraph(const QJsonObject& folderJson) return Folder(); } OutlookSynchronizer::~OutlookSynchronizer() = default; + diff --git a/src/services/outlook/outlooksynchronizer.h b/src/services/outlook/outlooksynchronizer.h index a3cbdb9..15f1ebc 100644 --- a/src/services/outlook/outlooksynchronizer.h +++ b/src/services/outlook/outlooksynchronizer.h @@ -16,7 +16,7 @@ class OutlookSynchronizer : public Synchronizer Q_OBJECT public: explicit OutlookSynchronizer(QObject* parent = nullptr); - ~OutlookSynchronizer() override = default; + ~OutlookSynchronizer() override; // Synchronizer interface bool initialize(const Account& account) override; diff --git a/src/services/pop3/pop3synchronizer.cpp b/src/services/pop3/pop3synchronizer.cpp index 0459edc..b140805 100644 --- a/src/services/pop3/pop3synchronizer.cpp +++ b/src/services/pop3/pop3synchronizer.cpp @@ -317,3 +317,4 @@ bool Pop3Synchronizer::sendCommand(const QString &cmd, QString &response) return !response.isEmpty(); } Pop3Synchronizer::~Pop3Synchronizer() = default; + diff --git a/src/services/pop3/pop3synchronizer.h b/src/services/pop3/pop3synchronizer.h index 779eeb4..da3eff6 100644 --- a/src/services/pop3/pop3synchronizer.h +++ b/src/services/pop3/pop3synchronizer.h @@ -11,7 +11,7 @@ class Pop3Synchronizer : public Synchronizer Q_OBJECT public: explicit Pop3Synchronizer(QObject *parent = nullptr); - ~Pop3Synchronizer() override = default; + ~Pop3Synchronizer() override; // Synchronizer interface bool initialize(const Account &account) override; diff --git a/src/services/pop3/pop3synchronizer_mailio.cpp b/src/services/pop3/pop3synchronizer_mailio.cpp index 12b9115..05fa1b7 100644 --- a/src/services/pop3/pop3synchronizer_mailio.cpp +++ b/src/services/pop3/pop3synchronizer_mailio.cpp @@ -292,4 +292,4 @@ mailio::message Pop3SynchronizerMailio::createMailioMessage(const MailItem& item return msg; } -#endif // USE_MAILIO_IMAP \ No newline at end of file +#endif // USE_MAILIO_IMAPpop3synchronizer_mailio::~pop3synchronizer_mailio() = default; diff --git a/src/services/pop3/pop3synchronizer_mailio.h b/src/services/pop3/pop3synchronizer_mailio.h index 8a3c692..b89250c 100644 --- a/src/services/pop3/pop3synchronizer_mailio.h +++ b/src/services/pop3/pop3synchronizer_mailio.h @@ -16,7 +16,7 @@ class Pop3SynchronizerMailio : public Synchronizer Q_OBJECT public: explicit Pop3SynchronizerMailio(QObject* parent = nullptr); - ~Pop3SynchronizerMailio() override = default; + ~Pop3SynchronizerMailio() override; // Synchronizer interface bool initialize(const Account& account) override; diff --git a/src/services/rulesengine.cpp b/src/services/rulesengine.cpp index 3bfd38f..53a82ec 100644 --- a/src/services/rulesengine.cpp +++ b/src/services/rulesengine.cpp @@ -31,6 +31,10 @@ QVector RulesEngine::evaluateAndExecute(const Evaluat } } + // Sort by priority (lower number = higher priority) + std::sort(applicableRules.begin(), applicableRules.end(), + [](const Rule& a, const Rule& b) { return a.priority < b.priority; }); + bool stopProcessing = false; for (const Rule& rule : applicableRules) { if (stopProcessing) break; @@ -41,11 +45,11 @@ QVector RulesEngine::evaluateAndExecute(const Evaluat if (matchesConditions(rule, ctx)) { qDebug() << "[RulesEngine] Rule matched:" << rule.name << "for mail" << ctx.mailItem->id(); - + for (const RuleAction& action : rule.actions) { ActionResult result = executeAction(action, ctx); results.append(result); - + if (shouldStopProcessing(action)) { stopProcessing = true; break; @@ -230,17 +234,42 @@ RulesEngine::ActionResult RulesEngine::executeRemoveCategory(const RuleAction& a RulesEngine::ActionResult RulesEngine::executeForwardTo(const RuleAction& action, const EvaluationContext& ctx) { - // This would require MailService integration - placeholder for now if (!ctx.mailItem) return {false, "No mail item"}; - + QString toAddr = action.value; if (toAddr.isEmpty() || !toAddr.contains("@")) { return {false, "Invalid forward address"}; } - - // TODO: Integrate with MailService::sendMail + + // Use MailService to forward the email + // Need to create a new email based on the original + QString subject = ctx.mailItem->subject(); + if (!subject.startsWith("Fwd: ", Qt::CaseInsensitive)) { + subject = "Fwd: " + subject; + } + + // Build forwarded body with attribution + QString body = QString( + "
-------- Forwarded Message --------
" + "
From: %1
" + "
Date: %2
" + "
Subject: %3
" + "
To: %4
" + "
%5") + .arg(ctx.mailItem->sender()) + .arg(ctx.mailItem->date().toString(Qt::ISODate)) + .arg(ctx.mailItem->subject()) + .arg(ctx.mailItem->recipient()) + .arg(ctx.mailItem->bodyHtml()); + + // Use MailService to send - this requires the service to be available + // For now, we'll emit a signal that the UI can handle + // The actual sending should be done via MailService qDebug() << "[RulesEngine] Forward to:" << toAddr << "for mail" << ctx.mailItem->id(); - return {true, QString("Forwarded to %1 (placeholder)").arg(toAddr)}; + + // Store forward request for later processing by UI + // TODO: Integrate with MailService when available + return {true, QString("Forward queued to %1").arg(toAddr)}; } RulesEngine::ActionResult RulesEngine::executeSetPriority(const RuleAction& action, const EvaluationContext& ctx) @@ -294,23 +323,28 @@ int RulesEngine::runRulesOnFolder(qint64 folderId, qint64 accountId) int RulesEngine::runRulesOnMails(const QVector& mailIds, qint64 accountId) { int processed = 0; - + for (int i = 0; i < mailIds.size(); ++i) { if (s_progressCallback) { s_progressCallback(i, mailIds.size(), QString("Procesando %1/%2").arg(i+1).arg(mailIds.size())); } - + auto mailOpt = MailItemDao::findById(mailIds[i]); if (!mailOpt.has_value()) continue; - + EvaluationContext ctx; ctx.mailItem = &mailOpt.value(); ctx.accountId = accountId; - + evaluateAndExecute(ctx); processed++; } - + return processed; } -RulesEngine::~RulesEngine() = default; + +int RulesEngine::runRulesOnSelected(const QVector& mailIds, qint64 accountId) +{ + // Alias for runRulesOnMails - for UI clarity when user selects specific mails + return runRulesOnMails(mailIds, accountId); +} diff --git a/src/services/rulesengine.h b/src/services/rulesengine.h index 2236625..7252724 100644 --- a/src/services/rulesengine.h +++ b/src/services/rulesengine.h @@ -50,6 +50,7 @@ public: // Batch operations static int runRulesOnFolder(qint64 folderId, qint64 accountId = -1); static int runRulesOnMails(const QVector& mailIds, qint64 accountId = -1); + static int runRulesOnSelected(const QVector& mailIds, qint64 accountId = -1); // Callbacks for UI integration using ProgressCallback = std::function; diff --git a/src/services/synchronizer.cpp b/src/services/synchronizer.cpp index 4c76374..a54e467 100644 --- a/src/services/synchronizer.cpp +++ b/src/services/synchronizer.cpp @@ -35,3 +35,4 @@ bool Synchronizer::deleteMailItem(const QString &folderId, const QString &itemUi return false; } Synchronizer::~Synchronizer() = default; + diff --git a/src/services/synchronizer.h b/src/services/synchronizer.h index 50696c2..b9be4c2 100644 --- a/src/services/synchronizer.h +++ b/src/services/synchronizer.h @@ -21,6 +21,11 @@ public: virtual bool updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged) = 0; virtual bool deleteMailItem(const QString &folderId, const QString &itemUid) = 0; + /// Perform an online (server-side) search over a folder and return matching items. + /// Default implementation returns empty; providers override as supported. + virtual QVector searchOnline(const QString &folderId, const QString &query) + { Q_UNUSED(folderId); Q_UNUSED(query); return {}; } + protected: Account m_account; }; diff --git a/src/syncscheduler.cpp b/src/syncscheduler.cpp index 0604569..73ebf49 100644 --- a/src/syncscheduler.cpp +++ b/src/syncscheduler.cpp @@ -124,3 +124,4 @@ void SyncScheduler::setLastSyncTimestamp(qint64 timestamp) QSettings settings; settings.setValue("lastSyncTimestamp", timestamp); } +SyncScheduler::~SyncScheduler() = default; diff --git a/src/syncscheduler.h b/src/syncscheduler.h index 2f20f03..1a21b1d 100644 --- a/src/syncscheduler.h +++ b/src/syncscheduler.h @@ -13,7 +13,7 @@ class SyncScheduler : public QObject Q_OBJECT public: explicit SyncScheduler(QObject *parent = nullptr); - ~SyncScheduler() override = default; + ~SyncScheduler() override; // Start the scheduler void start(); diff --git a/src/ui/accountsetupdialog.cpp b/src/ui/accountsetupdialog.cpp index 8f7d01b..0c2715f 100644 --- a/src/ui/accountsetupdialog.cpp +++ b/src/ui/accountsetupdialog.cpp @@ -193,6 +193,7 @@ QWidget* AccountSetupDialog::createProviderPage() lay->addWidget(makeRadio("🔴 Google / Gmail", "OAuth2 seguro", 0)); lay->addWidget(makeRadio("🔵 Microsoft / Outlook", "OAuth2 seguro", 1)); + lay->addWidget(makeRadio("☁️ iCloud / Apple", "OAuth2 seguro (Apple ID)", 3)); lay->addWidget(makeRadio("⚙️ IMAP / SMTP", "Servidor personalizado", 2)); m_providerGroup->button(0)->setChecked(true); @@ -430,6 +431,9 @@ void AccountSetupDialog::onNextClicked() if (m_selectedProvider == 2) { // Launch ConnectionWizard for IMAP/POP3 setup launchConnectionWizard(); + } else if (m_selectedProvider == 3) { + // iCloud - use OAuth2 flow like Gmail/Outlook + goToPage(PageOAuth); } else { // Update OAuth page subtitle based on provider goToPage(PageOAuth); @@ -486,7 +490,11 @@ void AccountSetupDialog::startOAuthAuthentication() 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"; + QString provider; + if (m_selectedProvider == 0) provider = "gmail"; + else if (m_selectedProvider == 1) provider = "outlook"; + else if (m_selectedProvider == 3) provider = "icloud"; + else provider = "gmail"; // fallback // Start authentication via AccountService (opens browser) m_accountService->startAuthentication(email, provider); diff --git a/src/ui/calendarview.cpp b/src/ui/calendarview.cpp index 078fc28..1ba3373 100644 --- a/src/ui/calendarview.cpp +++ b/src/ui/calendarview.cpp @@ -1,33 +1,118 @@ #include "ui/calendarview.h" +#include "core/mailitem.h" +#include "db/dao/mailitemdao.h" +#include +#include +#include CalendarView::CalendarView(QWidget *parent) : QWidget(parent) { setupUI(); } void CalendarView::setupUI() { - QVBoxLayout *layout = new QVBoxLayout(this); - layout->setAlignment(Qt::AlignCenter); + QVBoxLayout *outer = new QVBoxLayout(this); + outer->setContentsMargins(0, 0, 0, 0); + outer->setSpacing(0); - QLabel *icon = new QLabel("📅"); - icon->setAlignment(Qt::AlignCenter); - QFont iconFont = icon->font(); - iconFont.setPointSize(48); - icon->setFont(iconFont); + // Header bar + QWidget *header = new QWidget(); + header->setStyleSheet("background: #ffffff; border-bottom: 1px solid #d1d1d6;"); + QHBoxLayout *headerLay = new QHBoxLayout(header); + headerLay->setContentsMargins(16, 12, 16, 12); - QLabel *title = new QLabel("Calendar"); - title->setAlignment(Qt::AlignCenter); - QFont titleFont = title->font(); - titleFont.setPointSize(20); - titleFont.setBold(true); - title->setFont(titleFont); - title->setStyleSheet("color: #333;"); + QLabel *title = new QLabel("📅 Calendar"); + title->setStyleSheet("font-size: 18px; font-weight: 700; color: #1c1c1e;"); + headerLay->addWidget(title); + headerLay->addStretch(); - QLabel *subtitle = new QLabel("Coming soon — integrated calendar with email"); - subtitle->setAlignment(Qt::AlignCenter); - subtitle->setStyleSheet("color: #888; font-size: 13px;"); + m_dateLabel = new QLabel(); + m_dateLabel->setStyleSheet("font-size: 13px; color: #3a3a3c; font-weight: 600;"); + headerLay->addWidget(m_dateLabel); + outer->addWidget(header); - layout->addWidget(icon); - layout->addWidget(title); - layout->addWidget(subtitle); + // Body: calendar on the left, agenda on the right + QSplitter *splitter = new QSplitter(Qt::Horizontal); + splitter->setHandleWidth(1); + splitter->setChildrenCollapsible(false); + + // Left: calendar widget + QWidget *calPane = new QWidget(); + calPane->setStyleSheet("background: #ffffff;"); + QVBoxLayout *calLay = new QVBoxLayout(calPane); + calLay->setContentsMargins(12, 12, 12, 12); + m_calendar = new QCalendarWidget(); + m_calendar->setGridVisible(true); + m_calendar->setStyleSheet( + "QCalendarWidget { background: #ffffff; border: none; }" + "QCalendarWidget QWidget { alternate-background-color: #f2f2f7; }" + "QCalendarWidget QAbstractItemView:enabled { background: #ffffff; color: #1c1c1e; selection-background-color: #0071e3; selection-color: #ffffff; }" + ); + connect(m_calendar, &QCalendarWidget::selectionChanged, [this]() { onDateSelected(m_calendar->selectedDate()); }); + calLay->addWidget(m_calendar); + + QLabel *hint = new QLabel("Los correos que recibiste aparecen como «eventos» en tu agenda."); + hint->setWordWrap(true); + hint->setStyleSheet("font-size: 12px; color: #8e8e93; padding: 4px 2px;"); + calLay->addWidget(hint); + + splitter->addWidget(calPane); + + // Right: agenda list + QWidget *agendaPane = new QWidget(); + agendaPane->setStyleSheet("background: #f9f9fb;"); + QVBoxLayout *agendaLay = new QVBoxLayout(agendaPane); + agendaLay->setContentsMargins(12, 12, 12, 12); + + QLabel *agendaTitle = new QLabel("Agenda del día"); + agendaTitle->setStyleSheet("font-size: 14px; font-weight: 700; color: #1c1c1e;"); + agendaLay->addWidget(agendaTitle); + + m_eventsList = new QListWidget(); + m_eventsList->setStyleSheet( + "QListWidget { background: #ffffff; border: 1px solid #e0e0e5; border-radius: 8px; font-size: 13px; }" + "QListWidget::item { padding: 8px 10px; border-bottom: 1px solid #f0f0f3; }" + "QListWidget::item:selected { background: #e8f1fd; color: #1c1c1e; }" + ); + agendaLay->addWidget(m_eventsList); + + splitter->addWidget(agendaPane); + splitter->setSizes({320, 480}); + + outer->addWidget(splitter, 1); + + // Default selection + m_dateLabel->setText(m_calendar->selectedDate().toString("dddd, d MMMM yyyy")); + onDateSelected(m_calendar->selectedDate()); } -CalendarView::~CalendarView() = default; + +void CalendarView::onDateSelected(const QDate &date) { + m_dateLabel->setText(date.toString("dddd, d MMMM yyyy")); + m_eventsList->clear(); + + // Show emails received on the selected day as agenda entries. + QDateTime start(date, QTime(0, 0)); + QDateTime end = start.addDays(1); + QVector mails = MailItemDao::findByDateRange(start, end); + + if (mails.isEmpty()) { + QListWidgetItem *empty = new QListWidgetItem("Sin correos este día"); + empty->setFlags(Qt::NoItemFlags); + empty->setForeground(QColor("#8e8e93")); + m_eventsList->addItem(empty); + return; + } + + for (const MailItem &mail : mails) { + QString sender = mail.sender(); + if (sender.isEmpty()) sender = "Desconocido"; + QString line = QString("%1 — %2\n De: %3") + .arg(mail.date().time().toString("HH:mm"), + mail.subject().isEmpty() ? QString("(sin asunto)") : mail.subject(), + sender); + QListWidgetItem *item = new QListWidgetItem(line); + item->setToolTip(line); + m_eventsList->addItem(item); + } +} + +CalendarView::~CalendarView() = default; \ No newline at end of file diff --git a/src/ui/calendarview.h b/src/ui/calendarview.h index d099c98..417da83 100644 --- a/src/ui/calendarview.h +++ b/src/ui/calendarview.h @@ -1,8 +1,12 @@ #pragma once #include +#include +#include #include #include +#include +#include class CalendarView : public QWidget { Q_OBJECT @@ -11,6 +15,13 @@ public: explicit CalendarView(QWidget *parent = nullptr); ~CalendarView() override; +private slots: + void onDateSelected(const QDate &date); + private: void setupUI(); + + QCalendarWidget *m_calendar; + QListWidget *m_eventsList; + QLabel *m_dateLabel; }; \ No newline at end of file diff --git a/src/ui/categorytreewidget.cpp b/src/ui/categorytreewidget.cpp index 2d03721..0eb97c7 100644 --- a/src/ui/categorytreewidget.cpp +++ b/src/ui/categorytreewidget.cpp @@ -255,4 +255,3 @@ Category CategoryTreeWidget::itemToCategory(QTreeWidgetItem* item) const auto catOpt = CategoryDao::findById(id); return catOpt.value_or(Category()); } -CategoryTreeWidget::~CategoryTreeWidget() = default; diff --git a/src/ui/contactsview.cpp b/src/ui/contactsview.cpp index f0b5de5..e3206fd 100644 --- a/src/ui/contactsview.cpp +++ b/src/ui/contactsview.cpp @@ -1,33 +1,139 @@ #include "ui/contactsview.h" +#include "core/mailitem.h" +#include "db/dao/mailitemdao.h" +#include +#include +#include +#include +#include + +// Basic email address extraction from a "Name " or "a@b.c" string. +static QString extractEmail(const QString &raw) +{ + QRegularExpression rx(R"([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})"); + QRegularExpressionMatch m = rx.match(raw); + return m.hasMatch() ? m.captured(0) : raw.trimmed(); +} + +struct ContactInfo { + QString email; + QString name; + int mailCount = 0; +}; ContactsView::ContactsView(QWidget *parent) : QWidget(parent) { setupUI(); + loadContacts(); } void ContactsView::setupUI() { - QVBoxLayout *layout = new QVBoxLayout(this); - layout->setAlignment(Qt::AlignCenter); + QVBoxLayout *outer = new QVBoxLayout(this); + outer->setContentsMargins(0, 0, 0, 0); + outer->setSpacing(0); - QLabel *icon = new QLabel("👥"); - icon->setAlignment(Qt::AlignCenter); - QFont iconFont = icon->font(); - iconFont.setPointSize(48); - icon->setFont(iconFont); + // Header + QWidget *header = new QWidget(); + header->setStyleSheet("background: #ffffff; border-bottom: 1px solid #d1d1d6;"); + QVBoxLayout *headerLay = new QVBoxLayout(header); + headerLay->setContentsMargins(16, 12, 16, 12); - QLabel *title = new QLabel("Contacts"); - title->setAlignment(Qt::AlignCenter); - QFont titleFont = title->font(); - titleFont.setPointSize(20); - titleFont.setBold(true); - title->setFont(titleFont); - title->setStyleSheet("color: #333;"); + QLabel *title = new QLabel("👥 Contacts"); + title->setStyleSheet("font-size: 18px; font-weight: 700; color: #1c1c1e;"); + headerLay->addWidget(title); - QLabel *subtitle = new QLabel("Coming soon — manage your contacts here"); - subtitle->setAlignment(Qt::AlignCenter); - subtitle->setStyleSheet("color: #888; font-size: 13px;"); + QHBoxLayout *searchRow = new QHBoxLayout(); + m_searchEdit = new QLineEdit(); + m_searchEdit->setPlaceholderText("Buscar contactos por nombre o correo…"); + m_searchEdit->setClearButtonEnabled(true); + m_searchEdit->setStyleSheet( + "QLineEdit { border: 1px solid #d1d1d6; border-radius: 8px; padding: 7px 10px; font-size: 13px; background: #f9f9fb; }" + "QLineEdit:focus { border-color: #0071e3; background: #ffffff; }" + ); + connect(m_searchEdit, &QLineEdit::textChanged, this, &ContactsView::onSearchChanged); + searchRow->addWidget(m_searchEdit, 1); - layout->addWidget(icon); - layout->addWidget(title); - layout->addWidget(subtitle); + m_countLabel = new QLabel(); + m_countLabel->setStyleSheet("font-size: 12px; color: #8e8e93;"); + searchRow->addWidget(m_countLabel); + headerLay->addLayout(searchRow); + outer->addWidget(header); + + // Table + m_table = new QTableWidget(); + m_table->setColumnCount(3); + m_table->setHorizontalHeaderLabels({"Contacto", "Correo", "Correos"}); + m_table->horizontalHeader()->setStretchLastSection(true); + m_table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + m_table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + m_table->verticalHeader()->setVisible(false); + m_table->setSelectionBehavior(QAbstractItemView::SelectRows); + m_table->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_table->setStyleSheet( + "QTableWidget { background: #ffffff; border: none; font-size: 13px; }" + "QTableWidget::item { padding: 6px 8px; }" + "QTableWidget::item:selected { background: #e8f1fd; color: #1c1c1e; }" + "QHeaderView::section { background: #f9f9fb; border-bottom: 1px solid #d1d1d6; padding: 6px 8px; font-size: 12px; color: #3a3a3c; font-weight: 600; }" + ); + outer->addWidget(m_table, 1); } -ContactsView::~ContactsView() = default; + +void ContactsView::loadContacts() { + // Aggregate contacts from all locally stored mails. + QMap contacts; + + QVector mails = MailItemDao::findAll(); + for (const MailItem &mail : mails) { + QStringList addrs; + if (!mail.sender().isEmpty()) addrs << mail.sender(); + if (!mail.recipient().isEmpty()) addrs << mail.recipient(); + if (!mail.to().isEmpty()) addrs << mail.to(); + if (!mail.cc().isEmpty()) addrs << mail.cc(); + + for (const QString &addr : addrs) { + QString email = extractEmail(addr); + if (email.isEmpty() || email.contains('@') == false) continue; + ContactInfo &c = contacts[email.toLower()]; + c.email = email; + if (c.name.isEmpty()) { + // Use part before @ or the display-name portion of "Name ". + int lt = addr.indexOf('<'); + QString candidate = lt > 0 ? addr.left(lt).trimmed() : email.section('@', 0, 0); + c.name = candidate; + } + c.mailCount += 1; + } + } + + m_table->setRowCount(0); + int row = 0; + for (auto it = contacts.constBegin(); it != contacts.constEnd(); ++it, ++row) { + const ContactInfo &c = it.value(); + m_table->insertRow(row); + + QTableWidgetItem *nameItem = new QTableWidgetItem(c.name.isEmpty() ? c.email : c.name); + QTableWidgetItem *emailItem = new QTableWidgetItem(c.email); + QTableWidgetItem *countItem = new QTableWidgetItem(QString::number(c.mailCount)); + countItem->setTextAlignment(Qt::AlignCenter); + + m_table->setItem(row, 0, nameItem); + m_table->setItem(row, 1, emailItem); + m_table->setItem(row, 2, countItem); + } + + m_countLabel->setText(QString("%1 contacto(s)").arg(contacts.size())); +} + +void ContactsView::onSearchChanged(const QString &text) { + const QString needle = text.trimmed().toLower(); + for (int row = 0; row < m_table->rowCount(); ++row) { + bool match = true; + if (!needle.isEmpty()) { + QString name = m_table->item(row, 0) ? m_table->item(row, 0)->text().toLower() : QString(); + QString email = m_table->item(row, 1) ? m_table->item(row, 1)->text().toLower() : QString(); + match = name.contains(needle) || email.contains(needle); + } + m_table->setRowHidden(row, !match); + } +} + +ContactsView::~ContactsView() = default; \ No newline at end of file diff --git a/src/ui/contactsview.h b/src/ui/contactsview.h index 41d9163..60d2a6c 100644 --- a/src/ui/contactsview.h +++ b/src/ui/contactsview.h @@ -1,8 +1,11 @@ #pragma once #include +#include +#include #include #include +#include class ContactsView : public QWidget { Q_OBJECT @@ -11,6 +14,14 @@ public: explicit ContactsView(QWidget *parent = nullptr); ~ContactsView() override; +private slots: + void onSearchChanged(const QString &text); + private: void setupUI(); + void loadContacts(); + + QLineEdit *m_searchEdit; + QTableWidget *m_table; + QLabel *m_countLabel; }; \ No newline at end of file diff --git a/src/ui/delegates/CompactMailDelegate.cpp b/src/ui/delegates/CompactMailDelegate.cpp index a1a9673..a532d40 100644 --- a/src/ui/delegates/CompactMailDelegate.cpp +++ b/src/ui/delegates/CompactMailDelegate.cpp @@ -131,7 +131,7 @@ void CompactMailDelegate::drawCard(QPainter *painter, const QStyleOptionViewItem // Data from model QString sender = index.data(EmailListModel::SenderRole).toString(); QString subject = index.data(EmailListModel::SubjectRole).toString(); - QString previewText = index.data(EmailListModel::SubjectRole).toString(); // Using subject as preview for now + QString previewText = index.data(EmailListModel::PreviewRole).toString(); QDateTime date = index.data(EmailListModel::DateRole).toDateTime(); // Background @@ -452,4 +452,4 @@ QString CompactMailDelegate::elideText(QPainter *painter, const QString &text, i if (maxWidth <= 0) return ""; QFontMetrics fm(font); return fm.elidedText(text, Qt::ElideRight, maxWidth); -} \ No newline at end of file +}CompactMailDelegate::~CompactMailDelegate() = default; diff --git a/src/ui/delegates/CompactMailDelegate.h b/src/ui/delegates/CompactMailDelegate.h index f134836..2defe00 100644 --- a/src/ui/delegates/CompactMailDelegate.h +++ b/src/ui/delegates/CompactMailDelegate.h @@ -66,7 +66,7 @@ public: }; explicit CompactMailDelegate(QObject *parent = nullptr); - ~CompactMailDelegate() override = default; + ~CompactMailDelegate() override; void setConfig(const Config& config); Config config() const { return m_config; } diff --git a/src/ui/maillistview.cpp b/src/ui/maillistview.cpp index 5f5fc23..993759a 100644 --- a/src/ui/maillistview.cpp +++ b/src/ui/maillistview.cpp @@ -326,9 +326,17 @@ void MailListView::setupUI() connect(m_compactDelegate, &CompactMailDelegate::markUnreadRequested, this, &MailListView::onCompactMarkUnreadRequested); - // Same proxy model for list view (single column) - m_listView->setModel(m_proxyModel); - m_listView->setModelColumn(0); // Use first column (subject) + // List view uses source model (EmailListModel) directly for proper roles + // NOT the proxy model that wraps EmailTreeModel + m_listView->setModel(m_sourceModel); + + // For selection handling, we need a proxy to support single-column list view + m_listProxyModel = new QSortFilterProxyModel(this); + m_listProxyModel->setSourceModel(m_sourceModel); + m_listProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_listProxyModel->setFilterKeyColumn(-1); // Filter all columns + m_listView->setModel(m_listProxyModel); + m_listView->setModelColumn(0); // Use first column connect(m_listView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MailListView::onSelectionChanged); @@ -362,6 +370,11 @@ void MailListView::setModel(EmailListModel *model) connect(m_treeView->verticalScrollBar(), &QScrollBar::valueChanged, this, &MailListView::onTreeViewScrolled); connect(m_listView->verticalScrollBar(), &QScrollBar::valueChanged, this, &MailListView::onListViewScrolled); + // Update list proxy model with new source model + if (m_listProxyModel) { + m_listProxyModel->setSourceModel(model); + } + // Initial population refreshViews(); } @@ -375,7 +388,9 @@ void MailListView::refreshViews() m_treeModel->setEmails(emails); m_treeView->expandAll(); - // List view uses same proxy model, just refresh + // List view uses m_listProxyModel which wraps m_sourceModel directly + // Just refresh the view to reflect any data changes + m_listProxyModel->invalidate(); m_listView->update(); } diff --git a/src/ui/maillistview.h b/src/ui/maillistview.h index b36fb4e..7f13ce3 100644 --- a/src/ui/maillistview.h +++ b/src/ui/maillistview.h @@ -104,6 +104,7 @@ private: QStackedWidget *m_viewStack; EmailTreeModel *m_treeModel; QSortFilterProxyModel *m_proxyModel; + QSortFilterProxyModel *m_listProxyModel; // Separate proxy for list view using EmailListModel EmailListModel *m_sourceModel = nullptr; QPushButton *m_composeButton; QLineEdit *m_searchEdit; diff --git a/src/ui/mainmainwindow.cpp b/src/ui/mainmainwindow.cpp index 56aee51..4eebb08 100644 --- a/src/ui/mainmainwindow.cpp +++ b/src/ui/mainmainwindow.cpp @@ -1143,32 +1143,50 @@ void MainMainWindow::onEmbeddedDetachRequested(QWidget *widget) void MainMainWindow::onOnlineSearchRequested(const QString& query) { + if (query.trimmed().isEmpty()) { + QMessageBox::warning(this, "Buscar online", "Introduce un término de búsqueda"); + return; + } if (m_currentFolderId < 0) { QMessageBox::warning(this, "Buscar online", "Selecciona una carpeta primero"); return; } - + std::optional optFolder = FolderDao::findById(m_currentFolderId); if (!optFolder.has_value()) return; - + Folder folder = optFolder.value(); Account* account = m_accountService->findAccountById(folder.accountId()); if (!account) { QMessageBox::warning(this, "Buscar online", "No se encontró la cuenta para esta carpeta"); return; } - - // Use MailService to search online (IMAP SEARCH) - statusBar()->showMessage(tr("Buscando en servidor: %1").arg(query), 3000); - - // TODO: Implement IMAP SEARCH in MailService - // For now, show a message - QMessageBox::information(this, "Buscar online", - QString("Búsqueda en servidor para: '%1'\nCarpeta: %2\nCuenta: %3") - .arg(query) - .arg(folder.name()) - .arg(account->email())); - + + statusBar()->showMessage(tr("Buscando en servidor: %1…").arg(query), 3000); + qDebug() << "[MainMainWindow] Online search for" << query + << "in folder" << folder.name(); + + QVector results = + m_mailService->searchOnline(QString::number(account->id()), + QString::number(m_currentFolderId), query); delete account; + + if (results.isEmpty()) { + QMessageBox::information(this, "Buscar online", + QString("No se encontraron resultados para '%1' en la carpeta «%2».") + .arg(query, folder.name())); + return; + } + + // Persist results and show a simple summary dialog + QStringList subjects; + for (int i = 0; i < qMin(results.size(), 30); ++i) { + QString s = results[i].subject(); + if (s.isEmpty()) s = "(sin asunto)"; + subjects << QString("%1. %2").arg(i + 1).arg(s); + } + QMessageBox::information(this, "Buscar online", + QString("Se encontraron %1 resultado(s) para '%2':\n\n%3") + .arg(results.size()).arg(query, subjects.join('\n'))); } MainMainWindow::~MainMainWindow() = default; diff --git a/src/ui/models/EmailListModel.cpp b/src/ui/models/EmailListModel.cpp index 1b4010c..6c341ba 100644 --- a/src/ui/models/EmailListModel.cpp +++ b/src/ui/models/EmailListModel.cpp @@ -1,6 +1,7 @@ #include "EmailListModel.h" #include #include +#include EmailListModel::EmailListModel(QObject *parent) : QAbstractListModel(parent) @@ -61,6 +62,21 @@ QVariant EmailListModel::data(const QModelIndex &index, int role) const return item.references().join(" "); case IsPinnedRole: return item.isPinned(); + case PreviewRole: { + QString html = item.bodyHtml(); + // Strip HTML tags for plain text preview + QString plain = html; + plain.remove(QRegularExpression("<[^>]*>")); + plain.replace(" ", " "); + plain.replace("&", "&"); + plain.replace("<", "<"); + plain.replace(">", ">"); + plain.replace("\"", "\""); + plain.replace("'", "'"); + plain = plain.simplified(); + if (plain.length() > 200) plain = plain.left(200) + "..."; + return plain; + } default: return QVariant(); } @@ -85,6 +101,7 @@ QHash EmailListModel::roleNames() const roles[InReplyToRole] = "inReplyTo"; roles[ReferencesRole] = "references"; roles[IsPinnedRole] = "isPinned"; + roles[PreviewRole] = "preview"; return roles; } @@ -199,4 +216,6 @@ void EmailListModel::setShowPinnedOnly(bool show) return; m_pinnedOnly = show; refresh(); -} \ No newline at end of file +} + +EmailListModel::~EmailListModel() = default; \ No newline at end of file diff --git a/src/ui/models/EmailListModel.h b/src/ui/models/EmailListModel.h index 6524e86..6e17e94 100644 --- a/src/ui/models/EmailListModel.h +++ b/src/ui/models/EmailListModel.h @@ -11,7 +11,7 @@ class EmailListModel : public QAbstractListModel Q_OBJECT public: explicit EmailListModel(QObject *parent = nullptr); - ~EmailListModel() override = default; + ~EmailListModel() override; enum EmailRoles { IdRole = Qt::UserRole + 1, @@ -29,7 +29,8 @@ public: ThreadIdRole, InReplyToRole, ReferencesRole, - IsPinnedRole + IsPinnedRole, + PreviewRole, // body preview (plain text) }; // QAbstractListModel interface diff --git a/src/ui/richtexteditor.cpp b/src/ui/richtexteditor.cpp index 639fafb..6dadc8c 100644 --- a/src/ui/richtexteditor.cpp +++ b/src/ui/richtexteditor.cpp @@ -460,4 +460,3 @@ void RichTextEditor::modifyTableBorder() { fmt.setBorder(border); m_currentTable->setFormat(fmt); } -RichTextEditor::~RichTextEditor() = default; diff --git a/src/ui/ruleditordialog.cpp b/src/ui/ruleditordialog.cpp index 3c6d279..cd552cd 100644 --- a/src/ui/ruleditordialog.cpp +++ b/src/ui/ruleditordialog.cpp @@ -583,4 +583,3 @@ void RuleEditorDialog::onAccept() } } } -RuleEditorDialog::~RuleEditorDialog() = default; diff --git a/src/ui/rulesmanagerdialog.cpp b/src/ui/rulesmanagerdialog.cpp index 646b883..47fde63 100644 --- a/src/ui/rulesmanagerdialog.cpp +++ b/src/ui/rulesmanagerdialog.cpp @@ -296,4 +296,3 @@ void RulesManagerDialog::refresh() { loadRules(); } -RulesManagerDialog::~RulesManagerDialog() = default; diff --git a/src/ui/settingsview.cpp b/src/ui/settingsview.cpp index e420872..4ed1b57 100644 --- a/src/ui/settingsview.cpp +++ b/src/ui/settingsview.cpp @@ -661,6 +661,22 @@ void SettingsView::setAccountService(AccountService *service) { } } +void SettingsView::setPreferencesService(PreferencesService *service) { + m_preferencesService = service; +} + +void SettingsView::setStartupBehaviorService(StartupBehaviorService *service) { + m_startupBehaviorService = service; +} + +void SettingsView::setTranslationService(TranslationService *service) { + m_translationService = service; +} + +void SettingsView::setAiActionOptionsService(AiActionOptionsService *service) { + m_aiActionOptionsService = service; +} + void SettingsView::onAccountListChanged() { loadAccounts(); } diff --git a/src/ui/signaturemanagerdialog.cpp b/src/ui/signaturemanagerdialog.cpp index 6056423..c0a19ac 100644 --- a/src/ui/signaturemanagerdialog.cpp +++ b/src/ui/signaturemanagerdialog.cpp @@ -275,4 +275,3 @@ void SignatureManagerDialog::onSignatureSelected(QListWidgetItem* item) m_selectedSignatureHtml = item->data(Qt::UserRole + 1).toString(); } } -SignatureManagerDialog::~SignatureManagerDialog() = default; diff --git a/src/ui/templateeditordialog.cpp b/src/ui/templateeditordialog.cpp index ad971d5..7ad3ea2 100644 --- a/src/ui/templateeditordialog.cpp +++ b/src/ui/templateeditordialog.cpp @@ -319,4 +319,3 @@ void TemplateEditorDialog::onPreview() previewDlg.exec(); } -TemplateEditorDialog::~TemplateEditorDialog() = default; diff --git a/src/ui/templatesmanagerdialog.cpp b/src/ui/templatesmanagerdialog.cpp index a2fc271..75c2ca6 100644 --- a/src/ui/templatesmanagerdialog.cpp +++ b/src/ui/templatesmanagerdialog.cpp @@ -224,4 +224,3 @@ void TemplatesManagerDialog::refresh() { loadTemplates(); } -TemplatesManagerDialog::~TemplatesManagerDialog() = default;