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
This commit is contained in:
@@ -71,4 +71,4 @@ void AccountSetupDialogLauncher::initializeSynchronizer(const Account &account)
|
||||
qWarning() << "[AccountSetupDialogLauncher] Could not create synchronizer for:" << providerType;
|
||||
}
|
||||
}
|
||||
|
||||
AccountSetupDialogLauncher::~AccountSetupDialogLauncher() = default;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -64,3 +64,4 @@ QNetworkRequest Authenticator::createTokenRequest(const QUrl &url) const
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
return request;
|
||||
}
|
||||
Authenticator::~Authenticator() = default;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -46,3 +46,4 @@ QString EmailComposerBridge::renderBodyToHtml(const QString &plainText) const
|
||||
html.replace("\r\n", "<br>");
|
||||
return "<html><body>" + html + "</body></html>";
|
||||
}
|
||||
EmailComposerBridge::~EmailComposerBridge() = default;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -48,3 +48,4 @@ bool EmailManager::sendEmail(const QString& to, const QString& subject, const QS
|
||||
Q_UNUSED(body);
|
||||
return false;
|
||||
}
|
||||
EmailManager::~EmailManager() = default;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -94,4 +94,4 @@ void GmailAuthenticator::onTokenReply(QNetworkReply *reply) {
|
||||
qDebug() << "[GmailAuthenticator] onTokenReply called";
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
GmailAuthenticator::~GmailAuthenticator() = default;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
#include "icloudauthenticator.h"
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QDesktopServices>
|
||||
#include <QCryptographicHash>
|
||||
#include <QRandomGenerator>
|
||||
#include <QMessageBox>
|
||||
#include <QDebug>
|
||||
#include <QTimer>
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
@@ -37,4 +37,4 @@ void ImapAuthenticator::configure(const QString &imapServer, int imapPort,
|
||||
m_username = username;
|
||||
m_password = password;
|
||||
}
|
||||
|
||||
ImapAuthenticator::~ImapAuthenticator() = default;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -86,4 +86,4 @@ void OutlookAuthenticator::onTokenReply(QNetworkReply *reply) {
|
||||
qDebug() << "[OutlookAuthenticator] onTokenReply called";
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
OutlookAuthenticator::~OutlookAuthenticator() = default;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -53,6 +53,8 @@ Synchronizer* SynchronizerProvider::createSynchronizer(const QString &accountId,
|
||||
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)
|
||||
|
||||
@@ -220,6 +220,52 @@ QVector<MailItem> MailItemDao::findAll()
|
||||
return items;
|
||||
}
|
||||
|
||||
QVector<MailItem> MailItemDao::findByDateRange(const QDateTime &start, const QDateTime &end)
|
||||
{
|
||||
QVector<MailItem> items;
|
||||
QSqlDatabase db = DatabaseManager::instance().database();
|
||||
QSqlQuery query(db);
|
||||
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, 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<QString> names;
|
||||
for (const auto &attachment : attachmentsForMail(item.id())) names.append(attachment.fileName);
|
||||
item.setAttachments(names);
|
||||
items.append(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
QVector<MailItem> MailItemDao::findByFolderId(int folderId)
|
||||
{
|
||||
QVector<MailItem> items;
|
||||
|
||||
@@ -25,6 +25,7 @@ public:
|
||||
static QVector<MailItem> findAll();
|
||||
static QVector<MailItem> findByFolderId(int folderId);
|
||||
static QVector<MailItem> findByFolderIdSinceUid(int folderId, qint64 sinceUid);
|
||||
static QVector<MailItem> findByDateRange(const QDateTime &start, const QDateTime &end);
|
||||
static std::optional<qint64> maxUidForFolder(int folderId);
|
||||
static QVector<qint64> getUidsForFolder(int folderId);
|
||||
static QVector<qint64> uidsForFolder(int folderId);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -459,3 +459,4 @@ QStringList GmailSynchronizer::extractHeaderValue(const QJsonArray& headers, con
|
||||
return values;
|
||||
}
|
||||
GmailSynchronizer::~GmailSynchronizer() = default;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -722,6 +722,94 @@ QString ImapSynchronizer::generateEventId() const
|
||||
return QString::number(QDateTime::currentMSecsSinceEpoch());
|
||||
}
|
||||
|
||||
QVector<MailItem> ImapSynchronizer::searchOnline(const QString &folderId, const QString &query)
|
||||
{
|
||||
QVector<MailItem> 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 "<q>" HEADER From "<q>" HEADER To "<q>" TEXT "<q>"
|
||||
// IMAP OR only takes two terms, so we chain nested ORs if supported. To stay portable
|
||||
// across servers we prefer: UID SEARCH TEXT "<q>" 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<qint64> 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<qint64> 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<MailItem> batchItems = parseFetchResponseBytes(fetchResponse);
|
||||
for (MailItem &item : batchItems) {
|
||||
item.setFolderId(fid);
|
||||
items.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
conn.sendCommandWait("CLOSE", selectResponse, 30000);
|
||||
conn.disconnect();
|
||||
return items;
|
||||
}
|
||||
|
||||
QVector<Folder> ImapSynchronizer::parseListResponse(const QStringList& lines) const
|
||||
{
|
||||
QVector<Folder> folders;
|
||||
@@ -871,3 +959,123 @@ QVector<MailItem> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<MailItem> 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<qint64> parseUidSearchResponse(const QString& response) const;
|
||||
@@ -54,4 +59,14 @@ private:
|
||||
// Sync folder helpers
|
||||
QVector<qint64> 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)
|
||||
};
|
||||
|
||||
@@ -446,4 +446,4 @@ message ImapSynchronizerMailio::createMailioMessage(const MailItem& item)
|
||||
return msg;
|
||||
}
|
||||
|
||||
#endif // USE_MAILIO_IMAP
|
||||
#endif // USE_MAILIO_IMAPimapsynchronizer_mailio::~imapsynchronizer_mailio() = default;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -427,6 +427,47 @@ void MailService::fetchMails(const QString &accountId, const QString &folderId)
|
||||
watcher->setFuture(future);
|
||||
}
|
||||
|
||||
QVector<MailItem> MailService::searchOnline(const QString &accountId, const QString &folderId, const QString &query)
|
||||
{
|
||||
QVector<MailItem> 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)
|
||||
{
|
||||
|
||||
@@ -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<MailItem> 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<MailItem> &items);
|
||||
void onlineSearchFinished(const QString &accountId, const QString &folderId, const QVector<MailItem> &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);
|
||||
|
||||
@@ -425,3 +425,4 @@ Folder OutlookSynchronizer::parseFolderFromGraph(const QJsonObject& folderJson)
|
||||
return Folder();
|
||||
}
|
||||
OutlookSynchronizer::~OutlookSynchronizer() = default;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -317,3 +317,4 @@ bool Pop3Synchronizer::sendCommand(const QString &cmd, QString &response)
|
||||
return !response.isEmpty();
|
||||
}
|
||||
Pop3Synchronizer::~Pop3Synchronizer() = default;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -292,4 +292,4 @@ mailio::message Pop3SynchronizerMailio::createMailioMessage(const MailItem& item
|
||||
return msg;
|
||||
}
|
||||
|
||||
#endif // USE_MAILIO_IMAP
|
||||
#endif // USE_MAILIO_IMAPpop3synchronizer_mailio::~pop3synchronizer_mailio() = default;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -31,6 +31,10 @@ QVector<RulesEngine::ActionResult> 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;
|
||||
@@ -230,7 +234,6 @@ 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;
|
||||
@@ -238,9 +241,35 @@ RulesEngine::ActionResult RulesEngine::executeForwardTo(const RuleAction& action
|
||||
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(
|
||||
"<div>-------- Forwarded Message --------</div>"
|
||||
"<div><b>From:</b> %1</div>"
|
||||
"<div><b>Date:</b> %2</div>"
|
||||
"<div><b>Subject:</b> %3</div>"
|
||||
"<div><b>To:</b> %4</div>"
|
||||
"<br/>%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)
|
||||
@@ -313,4 +342,9 @@ int RulesEngine::runRulesOnMails(const QVector<qint64>& mailIds, qint64 accountI
|
||||
|
||||
return processed;
|
||||
}
|
||||
RulesEngine::~RulesEngine() = default;
|
||||
|
||||
int RulesEngine::runRulesOnSelected(const QVector<qint64>& mailIds, qint64 accountId)
|
||||
{
|
||||
// Alias for runRulesOnMails - for UI clarity when user selects specific mails
|
||||
return runRulesOnMails(mailIds, accountId);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ public:
|
||||
// Batch operations
|
||||
static int runRulesOnFolder(qint64 folderId, qint64 accountId = -1);
|
||||
static int runRulesOnMails(const QVector<qint64>& mailIds, qint64 accountId = -1);
|
||||
static int runRulesOnSelected(const QVector<qint64>& mailIds, qint64 accountId = -1);
|
||||
|
||||
// Callbacks for UI integration
|
||||
using ProgressCallback = std::function<void(int processed, int total, const QString& currentRule)>;
|
||||
|
||||
@@ -35,3 +35,4 @@ bool Synchronizer::deleteMailItem(const QString &folderId, const QString &itemUi
|
||||
return false;
|
||||
}
|
||||
Synchronizer::~Synchronizer() = default;
|
||||
|
||||
|
||||
@@ -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<MailItem> searchOnline(const QString &folderId, const QString &query)
|
||||
{ Q_UNUSED(folderId); Q_UNUSED(query); return {}; }
|
||||
|
||||
protected:
|
||||
Account m_account;
|
||||
};
|
||||
|
||||
@@ -124,3 +124,4 @@ void SyncScheduler::setLastSyncTimestamp(qint64 timestamp)
|
||||
QSettings settings;
|
||||
settings.setValue("lastSyncTimestamp", timestamp);
|
||||
}
|
||||
SyncScheduler::~SyncScheduler() = default;
|
||||
|
||||
+1
-1
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
+105
-20
@@ -1,33 +1,118 @@
|
||||
#include "ui/calendarview.h"
|
||||
#include "core/mailitem.h"
|
||||
#include "db/dao/mailitemdao.h"
|
||||
#include <QDate>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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<MailItem> 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;
|
||||
@@ -1,8 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QCalendarWidget>
|
||||
#include <QListWidget>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QSplitter>
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -255,4 +255,3 @@ Category CategoryTreeWidget::itemToCategory(QTreeWidgetItem* item) const
|
||||
auto catOpt = CategoryDao::findById(id);
|
||||
return catOpt.value_or(Category());
|
||||
}
|
||||
CategoryTreeWidget::~CategoryTreeWidget() = default;
|
||||
|
||||
+126
-20
@@ -1,33 +1,139 @@
|
||||
#include "ui/contactsview.h"
|
||||
#include "core/mailitem.h"
|
||||
#include "db/dao/mailitemdao.h"
|
||||
#include <QTableWidgetItem>
|
||||
#include <QHeaderView>
|
||||
#include <QMap>
|
||||
#include <QRegularExpression>
|
||||
#include <QDebug>
|
||||
|
||||
// Basic email address extraction from a "Name <a@b.c>" 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);
|
||||
}
|
||||
|
||||
void ContactsView::loadContacts() {
|
||||
// Aggregate contacts from all locally stored mails.
|
||||
QMap<QString, ContactInfo> contacts;
|
||||
|
||||
QVector<MailItem> 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 <email>".
|
||||
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;
|
||||
@@ -1,8 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QTableWidget>
|
||||
#include <QLineEdit>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}CompactMailDelegate::~CompactMailDelegate() = default;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
+19
-4
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+28
-10
@@ -1143,6 +1143,10 @@ 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;
|
||||
@@ -1158,17 +1162,31 @@ void MainMainWindow::onOnlineSearchRequested(const QString& query)
|
||||
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<MailItem> 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;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "EmailListModel.h"
|
||||
#include <QDebug>
|
||||
#include <QDateTime>
|
||||
#include <QRegularExpression>
|
||||
|
||||
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<int, QByteArray> EmailListModel::roleNames() const
|
||||
roles[InReplyToRole] = "inReplyTo";
|
||||
roles[ReferencesRole] = "references";
|
||||
roles[IsPinnedRole] = "isPinned";
|
||||
roles[PreviewRole] = "preview";
|
||||
return roles;
|
||||
}
|
||||
|
||||
@@ -200,3 +217,5 @@ void EmailListModel::setShowPinnedOnly(bool show)
|
||||
m_pinnedOnly = show;
|
||||
refresh();
|
||||
}
|
||||
|
||||
EmailListModel::~EmailListModel() = default;
|
||||
@@ -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
|
||||
|
||||
@@ -460,4 +460,3 @@ void RichTextEditor::modifyTableBorder() {
|
||||
fmt.setBorder(border);
|
||||
m_currentTable->setFormat(fmt);
|
||||
}
|
||||
RichTextEditor::~RichTextEditor() = default;
|
||||
|
||||
@@ -583,4 +583,3 @@ void RuleEditorDialog::onAccept()
|
||||
}
|
||||
}
|
||||
}
|
||||
RuleEditorDialog::~RuleEditorDialog() = default;
|
||||
|
||||
@@ -296,4 +296,3 @@ void RulesManagerDialog::refresh()
|
||||
{
|
||||
loadRules();
|
||||
}
|
||||
RulesManagerDialog::~RulesManagerDialog() = default;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -275,4 +275,3 @@ void SignatureManagerDialog::onSignatureSelected(QListWidgetItem* item)
|
||||
m_selectedSignatureHtml = item->data(Qt::UserRole + 1).toString();
|
||||
}
|
||||
}
|
||||
SignatureManagerDialog::~SignatureManagerDialog() = default;
|
||||
|
||||
@@ -319,4 +319,3 @@ void TemplateEditorDialog::onPreview()
|
||||
|
||||
previewDlg.exec();
|
||||
}
|
||||
TemplateEditorDialog::~TemplateEditorDialog() = default;
|
||||
|
||||
@@ -224,4 +224,3 @@ void TemplatesManagerDialog::refresh()
|
||||
{
|
||||
loadTemplates();
|
||||
}
|
||||
TemplatesManagerDialog::~TemplatesManagerDialog() = default;
|
||||
|
||||
Reference in New Issue
Block a user