feat: IMAP sync incremental + Account Setup Wizard + robust FETCH parser

- ImapSynchronizer::syncFolder(): detecta eliminados (UIDs locales no en servidor), fetch nuevos (sinceUid), actualiza flags (FLAGS batch)
- fetchAllUids(): SELECT + SEARCH ALL para lista completa UIDs servidor
- parseAndUpdateFlags(): FETCH FLAGS en lotes 100, update DB si cambió read/flagged
- AccountSetupDialog: integra ConnectionWizard para IMAP/POP3 con test de conexión real
- IMAP FETCH parser robusto: logging respuesta servidor + fallback UID-by-UID
- Fix: FETCH failed logging muestra first/last UID + respuesta truncada
This commit is contained in:
2026-08-17 15:34:49 +02:00
parent 364624aada
commit a004bdd60f
94 changed files with 19408 additions and 1047 deletions
+12 -7
View File
@@ -3,6 +3,8 @@
#include <QSslSocket>
#include "db/dao/accountdao.h"
#include "db/dao/folderdao.h"
#include "db/dao/mailitemdao.h"
#include "services/mimestorage.h"
#include "core/authenticator.h"
#include "core/gmailauthenticator.h"
#include "core/outlookauthenticator.h"
@@ -79,7 +81,7 @@ void AccountService::addAccount(const Account &account)
else if (folderName == "Sent") folder.setSent(true);
else if (folderName == "Drafts") folder.setDrafts(true);
else if (folderName == "Trash") folder.setTrash(true);
if (!FolderDao::insert(folder)) {
if (FolderDao::insert(folder) == -1) {
qWarning() << "[AccountService] Failed to insert folder" << folder.name()
<< "for account ID" << accToAdd.id();
}
@@ -100,6 +102,12 @@ void AccountService::removeAccount(int accountId)
{
Account *account = AccountDao::findById(accountId);
if (account) {
MimeStorageService storage;
for (const Folder &folder : FolderDao::findByAccountId(accountId)) {
for (const MailItem &item : MailItemDao::findByFolderId(folder.id())) {
if (!item.fileId().isEmpty()) storage.deleteEmlFile(item.fileId());
}
}
SynchronizerProvider::instance().unregisterSynchronizer(QString::number(accountId));
AccountDao::remove(accountId);
publishAccountEvent(*account, false);
@@ -124,10 +132,7 @@ void AccountService::startAuthentication(const QString &email, const QString &pr
Authenticator *auth = createAuthenticator(providerType);
if (auth) {
connect(auth, &Authenticator::authenticationCompleted, this, [this](const Account &account) {
addAccount(account.email(),
account.type() == AccountType::Gmail ? "gmail"
: account.type() == AccountType::Outlook ? "outlook" : "imap",
account.accessToken(), account.refreshToken());
addAccount(account);
});
connect(auth, &Authenticator::authenticationFailed, this, [](const QString &error) {
qWarning() << "[AccountService] Authentication failed:" << error;
@@ -239,7 +244,7 @@ void AccountService::syncFoldersForAccount(const Account &account)
folder.setAccountId(account.id());
// Ensure the folder has an invalid ID (0) so DB assigns new one
folder.setId(0);
if (!FolderDao::insert(folder)) {
if (FolderDao::insert(folder) == -1) {
qWarning() << QStringLiteral("[AccountService] Failed to insert folder") << folder.name() << QStringLiteral("for account") << accountIdStr;
}
}
@@ -252,4 +257,4 @@ void AccountService::syncFoldersForAccount(const Account &account)
emit accountAdded(account);
emit accountListChanged();
}
#include "accountservice.moc"
#include "accountservice.moc"
+255
View File
@@ -0,0 +1,255 @@
#include "accountservice.h"
#include <QDebug>
#include <QSslSocket>
#include "db/dao/accountdao.h"
#include "db/dao/folderdao.h"
#include "core/authenticator.h"
#include "core/gmailauthenticator.h"
#include "core/outlookauthenticator.h"
#include "core/imapauthenticator.h"
#include "core/synchronizerprovider.h"
#include "core/eventbus.h"
#include "core/events.h"
#include "db/databasemanager.h"
#include <QSqlQuery>
AccountService::AccountService(QObject *parent)
: QObject(parent)
{
}
QVector<Account> AccountService::getAllAccounts()
{
return AccountDao::findAll();
}
Account* AccountService::findAccountById(int id)
{
return AccountDao::findById(id);
}
Account* AccountService::findAccountByEmail(const QString &email)
{
return AccountDao::findByEmail(email);
}
void AccountService::addAccount(const QString &email, const QString &providerType,
const QString &accessToken, const QString &refreshToken)
{
Account account;
account.setEmail(email);
account.setDisplayName(email);
if (providerType == "gmail") account.setType(AccountType::Gmail);
else if (providerType == "outlook") account.setType(AccountType::Outlook);
else account.setType(AccountType::IMAP);
account.setAccessToken(accessToken);
account.setRefreshToken(refreshToken);
qint64 id = AccountDao::insert(account);
if (id != -1) {
account.setId(id);
qDebug() << "[AccountService] Account added:" << email;
publishAccountEvent(account, true);
emit accountAdded(account);
emit accountListChanged();
} else {
qWarning() << "[AccountService] Failed to add account:" << email;
}
}
void AccountService::addAccount(const Account &account)
{
qint64 id = AccountDao::insert(account);
if (id != -1) {
Account accToAdd = account; // make a copy we can modify
accToAdd.setId(id);
qDebug() << "[AccountService] Account added:" << accToAdd.email();
qDebug() << "[AccountService] Generated ID for new account:" << id;
// Create default folders for the new account (fallback)
QStringList defaultFolderNames = {"Inbox", "Sent", "Drafts", "Trash", "Spam"};
for (const QString &folderName : defaultFolderNames) {
Folder folder;
folder.setName(folderName);
folder.setAccountId(accToAdd.id());
// Set special folder flags
if (folderName == "Inbox") folder.setInbox(true);
else if (folderName == "Sent") folder.setSent(true);
else if (folderName == "Drafts") folder.setDrafts(true);
else if (folderName == "Trash") folder.setTrash(true);
if (!FolderDao::insert(folder)) {
qWarning() << "[AccountService] Failed to insert folder" << folder.name()
<< "for account ID" << accToAdd.id();
}
}
// Attempt to sync real folders from the server; if successful, replace defaults
syncFoldersForAccount(accToAdd);
publishAccountEvent(accToAdd, true);
emit accountAdded(accToAdd);
emit accountListChanged();
} else {
qWarning() << "[AccountService] Failed to add account:" << account.email();
}
}
void AccountService::removeAccount(int accountId)
{
Account *account = AccountDao::findById(accountId);
if (account) {
SynchronizerProvider::instance().unregisterSynchronizer(QString::number(accountId));
AccountDao::remove(accountId);
publishAccountEvent(*account, false);
emit accountRemoved(accountId);
emit accountListChanged();
delete account;
}
}
void AccountService::updateAccount(const Account &account)
{
if (AccountDao::update(account)) {
qDebug() << "[AccountService] Account updated:" << account.email();
// Ensure synchronizer is reinitialized with new connection settings
syncFoldersForAccount(account);
emit accountListChanged();
}
}
void AccountService::startAuthentication(const QString &email, const QString &providerType)
{
Authenticator *auth = createAuthenticator(providerType);
if (auth) {
connect(auth, &Authenticator::authenticationCompleted, this, [this](const Account &account) {
addAccount(account.email(),
account.type() == AccountType::Gmail ? "gmail"
: account.type() == AccountType::Outlook ? "outlook" : "imap",
account.accessToken(), account.refreshToken());
});
connect(auth, &Authenticator::authenticationFailed, this, [](const QString &error) {
qWarning() << "[AccountService] Authentication failed:" << error;
});
connect(auth, &QObject::destroyed, auth, [auth]() { /* cleanup handled by Qt */ });
auth->authenticate(email);
}
}
Authenticator* AccountService::createAuthenticator(const QString &providerType)
{
if (providerType == "gmail") return new GmailAuthenticator(this);
if (providerType == "outlook") return new OutlookAuthenticator(this);
if (providerType == "imap") return new ImapAuthenticator(this);
qWarning() << "[AccountService] Unknown provider:" << providerType;
return nullptr;
}
bool AccountService::testConnection(const Account::ConnectionSettings &settings, QString &errorMessage)
{
QSslSocket socket;
if (settings.incomingSsl) {
socket.connectToHostEncrypted(settings.incomingHost, settings.incomingPort);
if (!socket.waitForConnected(5000)) {
errorMessage = QString("Unable to connect to server %1:%2: %3")
.arg(settings.incomingHost)
.arg(settings.incomingPort)
.arg(socket.errorString());
return false;
}
if (!socket.waitForEncrypted(5000)) {
errorMessage = QString("TLS handshake failed: %1").arg(socket.errorString());
return false;
}
} else {
socket.connectToHost(settings.incomingHost, settings.incomingPort);
if (!socket.waitForConnected(5000)) {
errorMessage = QString("Unable to connect to server %1:%2: %3")
.arg(settings.incomingHost)
.arg(settings.incomingPort)
.arg(socket.errorString());
return false;
}
}
// Optionally read greeting (ignore)
if (!socket.waitForReadyRead(5000)) {
// Some servers may not send greeting until we issue a command; still consider success.
}
return true;
}
void AccountService::publishAccountEvent(const Account &account, bool added)
{
if (added) {
WinoMail::Events::AccountAddedEvent event;
event.account = account;
EVENT_BUS.publish(event);
} else {
WinoMail::Events::AccountRemovedEvent event;
event.accountId = account.id();
EVENT_BUS.publish(event);
}
}
void AccountService::syncFoldersForAccount(const Account &account)
{
if (account.email().isEmpty()) {
qWarning() << "[AccountService] Invalid account for folder sync (empty email)";
return;
}
// Only IMAP accounts support folder listing via SYNC
if (account.type() != AccountType::IMAP) {
qDebug() << QStringLiteral("[AccountService] Folder sync only supported for IMAP accounts; skipping");
return;
}
QString accountIdStr = QString::number(account.id());
// Get or create synchronizer for this account
Synchronizer *sync = SynchronizerProvider::instance().getSynchronizer(accountIdStr);
if (!sync) {
sync = SynchronizerProvider::instance().createSynchronizer(accountIdStr, QStringLiteral("imap"));
if (!sync) {
qWarning() << QStringLiteral("[AccountService] Failed to create IMAP synchronizer for account") << accountIdStr;
return;
}
}
// Initialize the synchronizer with the account (sets m_account)
if (!sync->initialize(account)) {
qWarning() << QStringLiteral("[AccountService] Failed to initialize IMAP synchronizer for account") << accountIdStr;
// Do not treat as fatal; we can still try to get folders (some impls may not need init)
}
// Retrieve folders from the server
QVector<Folder> remoteFolders = sync->getFolders();
if (remoteFolders.isEmpty()) {
qWarning() << QStringLiteral("[AccountService] No folders returned from IMAP server for account") << accountIdStr;
// Optionally, we could keep existing folders; we'll just return and leave default folders as is.
return;
}
// Remove existing folders for this account to avoid duplicates
if (!FolderDao::removeByAccountId(account.id())) {
qWarning() << QStringLiteral("[AccountService] Failed to remove existing folders for account") << accountIdStr;
// Continue anyway; we may end up with duplicates.
}
// Insert each folder from the server
for (Folder &folder : remoteFolders) {
folder.setAccountId(account.id());
// Ensure the folder has an invalid ID (0) so DB assigns new one
folder.setId(0);
if (!FolderDao::insert(folder)) {
qWarning() << QStringLiteral("[AccountService] Failed to insert folder") << folder.name() << QStringLiteral("for account") << accountIdStr;
}
}
qDebug() << QStringLiteral("[AccountService] Synced %1 folders for account %2").arg(remoteFolders.size()).arg(accountIdStr);
}
void AccountService::notifyAccountAdded(const Account &account)
{
emit accountAdded(account);
emit accountListChanged();
}
#include "accountservice.moc"
+165 -115
View File
@@ -11,8 +11,11 @@
#include <QDateTime>
#include <QTimer>
#include <QEventLoop>
#include <QNetworkRequest>
#include "../../core/events.h"
#include "../../core/eventbus.h"
#include "db/dao/folderdao.h"
#include "services/mimestorage.h"
GmailSynchronizer::GmailSynchronizer(QObject* parent)
: Synchronizer(parent),
@@ -84,7 +87,6 @@ bool GmailSynchronizer::syncFolder(const Folder& folder)
// En una implementación real, aquí compararíamos con la base de datos local
// y emitiríamos las señales apropiadas para elementos nuevos/actualizados/eliminados
// Por ahora, simulamos que obtenemos algunos elementos
if (!items.isEmpty()) {
for (const MailItem& item : items) {
emit mailItemAdded(item);
@@ -142,129 +144,146 @@ QVector<MailItem> GmailSynchronizer::fetchMailItems(const QString& folderId,
}
}
qDebug() << "Fetching mail items for folder (label):" << folderId;
// Construir la URL para Gmail API
// Nota: En Gmail, las carpetas se identifican por su nombre de label (ej: INBOX, DRAFTS, etc.)
QString endpoint = QString("/me/mailFolders/%1/messages").arg(folderId);
QString url = buildGmailUrl(endpoint);
// Parámetros de consulta
QUrlQuery query;
query.addQueryItem("maxResults", "50"); // Limitar a 50 mensajes por petición
query.addQueryItem("q", "in:" + folderId); // Filtrar por label
url += "?" + query.toString();
QNetworkRequest request = createAuthRequest(url);
QNetworkReply* reply = m_networkManager->get(request);
// Nota: En una implementación real, esperaríamos la respuesta asíncronamente
// pero por simplicidad en este stub, simulamos una respuesta
// Simular respuesta para desarrollo
qDebug() << "Fetching complete Gmail messages for label:" << folderId;
QString labelId = folderId;
bool localFolderId = false;
folderId.toInt(&localFolderId);
if (localFolderId) {
const auto folder = FolderDao::findById(folderId.toInt());
if (folder) {
labelId = folder->parentFolderId();
if (labelId.isEmpty()) {
const QString name = folder->name().toLower();
if (name == QStringLiteral("inbox")) labelId = QStringLiteral("INBOX");
else if (name == QStringLiteral("sent") || name == QStringLiteral("sent mail")) labelId = QStringLiteral("SENT");
else if (name == QStringLiteral("drafts")) labelId = QStringLiteral("DRAFT");
else if (name == QStringLiteral("trash")) labelId = QStringLiteral("TRASH");
else if (name == QStringLiteral("spam")) labelId = QStringLiteral("SPAM");
else labelId = folder->name();
}
}
}
QNetworkAccessManager network;
auto get = [&](const QUrl &url, QByteArray &data, QString *contentType = nullptr) {
QNetworkReply *reply = network.get(createAuthRequest(url.toString()));
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
const bool ok = reply->error() == QNetworkReply::NoError;
if (ok) {
if (contentType) *contentType = QString::fromLatin1(reply->header(QNetworkRequest::ContentTypeHeader).toByteArray());
data = reply->readAll();
} else {
qWarning() << "Gmail request failed:" << reply->errorString() << url;
}
reply->deleteLater();
return ok;
};
QVector<QJsonObject> messageRefs;
QString pageToken;
do {
QUrl url(buildGmailUrl(QStringLiteral("messages")));
QUrlQuery query;
query.addQueryItem(QStringLiteral("labelIds"), labelId);
query.addQueryItem(QStringLiteral("maxResults"), QStringLiteral("100"));
if (!pageToken.isEmpty()) query.addQueryItem(QStringLiteral("pageToken"), pageToken);
url.setQuery(query);
QByteArray response;
if (!get(url, response)) return {};
QJsonParseError parseError;
const QJsonDocument document = QJsonDocument::fromJson(response, &parseError);
if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
qWarning() << "Invalid Gmail message list:" << parseError.errorString();
return {};
}
const QJsonObject object = document.object();
for (const QJsonValue &value : object.value(QStringLiteral("messages")).toArray())
if (value.isObject()) messageRefs.append(value.toObject());
pageToken = object.value(QStringLiteral("nextPageToken")).toString();
} while (!pageToken.isEmpty());
QVector<MailItem> items;
items.append(MailItem(1, folderId.toInt(), "Actualización de proyecto",
"carlos.rodriguez@proveedor.com", m_account.email(),
QDateTime::currentDateTime().addSecs(-1800),
false, false));
items.append(MailItem(2, folderId.toInt(), "Factura adjunta",
"facturacion@servicios.com", m_account.email(),
QDateTime::currentDateTime().addSecs(-5400),
true, true)); // Marcada como importante y leída
MimeStorageService mimeStorage;
for (const QJsonObject &reference : messageRefs) {
const QString gmailId = reference.value(QStringLiteral("id")).toString();
if (gmailId.isEmpty()) continue;
QUrl rawUrl(buildGmailUrl(QStringLiteral("messages/%1").arg(gmailId)));
QUrlQuery rawQuery;
rawQuery.addQueryItem(QStringLiteral("format"), QStringLiteral("raw"));
rawUrl.setQuery(rawQuery);
QByteArray response;
if (!get(rawUrl, response)) continue;
QJsonParseError parseError;
const QJsonDocument document = QJsonDocument::fromJson(response, &parseError);
if (parseError.error != QJsonParseError::NoError || !document.isObject()) continue;
const QJsonObject object = document.object();
QByteArray encoded = object.value(QStringLiteral("raw")).toString().toLatin1();
encoded.replace('-', '+');
encoded.replace('_', '/');
while (encoded.size() % 4) encoded.append('=');
const QByteArray rawMime = QByteArray::fromBase64(encoded);
ParsedMimeMessage parsed;
if (rawMime.isEmpty() || !mimeStorage.parseMessage(rawMime, parsed)) continue;
MailItem item;
item.setFolderId(folderId.toInt());
item.setMessageId(gmailId);
item.setRawMime(rawMime);
item.setSubject(parsed.subject.isEmpty() ? QStringLiteral("(No Subject)") : parsed.subject);
item.setSender(parsed.from);
item.setRecipient(parsed.to);
item.setTo(parsed.to);
item.setCc(parsed.cc);
item.setBcc(parsed.bcc);
item.setDate(parsed.date);
item.setBodyHtml(parsed.bodyHtml);
item.setSize(rawMime.size());
item.setRead(true);
QVector<QString> attachmentNames;
for (const ParsedMimeAttachment &attachment : parsed.attachments) attachmentNames.append(attachment.fileName);
item.setAttachments(attachmentNames);
const QJsonArray labels = object.value(QStringLiteral("labelIds")).toArray();
for (const QJsonValue &label : labels) {
if (label.toString() == QStringLiteral("UNREAD")) item.setRead(false);
if (label.toString() == QStringLiteral("STARRED")) item.setFlagged(true);
}
items.append(item);
}
return items;
}
bool GmailSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
{
if (!m_account.isTokenValid()) {
if (!refreshAccessToken()) {
qWarning() << "Failed to refresh token for appending mail item";
return false;
}
}
qDebug() << "Appending mail item to folder (label):" << folderId;
// En una implementación real, llamaríamos a Gmail API
// para crear un mensaje en la etiqueta especificada
// Por ahora, simulamos éxito
emit mailItemAdded(item);
// Publish MailItemAddedEvent
WinoMail::Events::MailItemAddedEvent mailEvent;
mailEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
mailEvent.timestamp = QDateTime::currentDateTimeUtc();
mailEvent.item = item;
PUBLISH(mailEvent);
return true;
Q_UNUSED(folderId);
Q_UNUSED(item);
qWarning() << "Gmail appendMailItem is not used for sending; MailService sends via Gmail API";
return false;
}
bool GmailSynchronizer::updateMailItemFlags(const QString& folderId,
const QString& itemUid,
bool read, bool flagged)
{
if (!m_account.isTokenValid()) {
if (!refreshAccessToken()) {
qWarning() << "Failed to refresh token for updating mail item flags";
return false;
}
}
qDebug() << "Updating mail item flags:" << itemUid
<< "read:" << read << "flagged:" << flagged;
// En una implementación real, llamaríamos a Gmail API
// para actualizar las etiquetas del mensaje (leído, importante, etc.)
// Por ahora, simulamos éxito
MailItem updatedItem;
updatedItem.setId(itemUid.toLongLong());
updatedItem.setFolderId(folderId.toInt());
updatedItem.setRead(read);
updatedItem.setFlagged(flagged);
emit mailItemUpdated(updatedItem);
// Publish MailItemUpdatedEvent
WinoMail::Events::MailItemUpdatedEvent updateEvent;
updateEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
updateEvent.timestamp = QDateTime::currentDateTimeUtc();
updateEvent.item = updatedItem;
updateEvent.changedFields = QStringList() << "read" << "flagged"; // Simplified
PUBLISH(updateEvent);
return true;
Q_UNUSED(folderId);
Q_UNUSED(itemUid);
Q_UNUSED(read);
Q_UNUSED(flagged);
qWarning() << "Gmail flag update is not available through this synchronizer";
return false;
}
bool GmailSynchronizer::deleteMailItem(const QString& folderId,
const QString& itemUid)
{
if (!m_account.isTokenValid()) {
if (!refreshAccessToken()) {
qWarning() << "Failed to refresh token for deleting mail item";
return false;
}
}
qDebug() << "Deleting mail item:" << itemUid << "from folder (label):" << folderId;
// En una implementación real, llamaríamos a Gmail API
// para eliminar el mensaje (moverlo a la papelera)
// Por ahora, simulamos éxito
emit mailItemRemoved(itemUid);
// Publish MailItemRemovedEvent
WinoMail::Events::MailItemRemovedEvent removeEvent;
removeEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
removeEvent.timestamp = QDateTime::currentDateTimeUtc();
removeEvent.itemUid = itemUid;
removeEvent.folderId = folderId.toInt();
PUBLISH(removeEvent);
return true;
Q_UNUSED(folderId);
Q_UNUSED(itemUid);
qWarning() << "Gmail delete is not available through this synchronizer";
return false;
}
void GmailSynchronizer::onGmailReplyFinished(QNetworkReply* reply)
@@ -321,9 +340,11 @@ void GmailSynchronizer::onHistoryTimer()
QString GmailSynchronizer::buildGmailUrl(const QString& endpoint) const
{
QString normalized = endpoint;
while (normalized.startsWith('/')) normalized.remove(0, 1);
return QStringLiteral("https://gmail.googleapis.com/gmail/v1/users/%1/%2")
.arg(m_account.email())
.arg(endpoint);
.arg(QString::fromUtf8(QUrl::toPercentEncoding(m_account.email())))
.arg(normalized);
}
QNetworkRequest GmailSynchronizer::createAuthRequest(const QString& url) const
@@ -338,12 +359,41 @@ QNetworkRequest GmailSynchronizer::createAuthRequest(const QString& url) const
bool GmailSynchronizer::refreshAccessToken()
{
// En una implementación real, aquí usaríamos el refresh token
// para obtener un nuevo access token desde Google OAuth2
qWarning() << "Token refresh not implemented - using stub";
// Simulamos éxito por ahora
return !m_refreshToken.isEmpty();
if (m_refreshToken.isEmpty()) return false;
const QString clientId = qEnvironmentVariable("WINO_GMAIL_CLIENT_ID");
const QString clientSecret = qEnvironmentVariable("WINO_GMAIL_CLIENT_SECRET");
if (clientId.isEmpty() || clientSecret.isEmpty()) {
qWarning() << "Gmail token expired and WINO_GMAIL_CLIENT_ID/SECRET are not configured";
return false;
}
QNetworkAccessManager network;
QNetworkRequest request(QUrl(QStringLiteral("https://oauth2.googleapis.com/token")));
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded"));
QUrlQuery form;
form.addQueryItem(QStringLiteral("client_id"), clientId);
form.addQueryItem(QStringLiteral("client_secret"), clientSecret);
form.addQueryItem(QStringLiteral("refresh_token"), m_refreshToken);
form.addQueryItem(QStringLiteral("grant_type"), QStringLiteral("refresh_token"));
QNetworkReply *reply = network.post(request, form.toString(QUrl::FullyEncoded).toUtf8());
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "Gmail token refresh failed:" << reply->errorString();
reply->deleteLater();
return false;
}
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
reply->deleteLater();
const QString token = document.object().value(QStringLiteral("access_token")).toString();
if (token.isEmpty()) return false;
m_accessToken = token;
const int expiresIn = document.object().value(QStringLiteral("expires_in")).toInt(3600);
m_tokenExpires = QDateTime::currentDateTimeUtc().addSecs(expiresIn);
m_account.setAccessToken(m_accessToken);
m_account.setTokenExpires(m_tokenExpires);
return true;
}
qint64 GmailSynchronizer::getHistoryId(const QString& folderId) const
+217
View File
@@ -0,0 +1,217 @@
#include "imapconnection.h"
#include <QDebug>
#include <QEventLoop>
#include <QTimer>
ImapConnection::ImapConnection(QObject *parent) : QObject(parent),
m_socket(new QSslSocket(this)),
m_tagCounter(1)
{
connect(m_socket, &QAbstractSocket::connected, this, &ImapConnection::onConnected);
connect(m_socket, &QSslSocket::encrypted, this, &ImapConnection::onEncrypted);
m_readyReadConnection = connect(m_socket, &QSslSocket::readyRead, this, &ImapConnection::onReadyRead);
connect(m_socket, &QAbstractSocket::errorOccurred, this, &ImapConnection::onSocketError);
}
ImapConnection::~ImapConnection()
{
disconnect();
}
void ImapConnection::connectToHost(const QString &host, int port, bool useSsl)
{
m_host = host;
m_port = port;
m_useSsl = useSsl;
if (useSsl) {
m_socket->connectToHostEncrypted(host, port);
} else {
m_socket->connectToHost(host, port);
}
}
void ImapConnection::login(const QString &username, const QString &password,
bool useAuthPlain,
std::function<void(bool, const QString&)> callback)
{
if (!m_socket->isEncrypted() && m_useSsl) {
callback(false, "Not encrypted");
return;
}
QString cmd;
if (useAuthPlain) {
QByteArray auth;
auth.append('\0');
auth.append(username.toUtf8());
auth.append('\0');
auth.append(password.toUtf8());
cmd = QString("AUTHENTICATE PLAIN %1").arg(QString::fromLatin1(auth.toBase64()));
} else {
cmd = QString("LOGIN %1 %2").arg(username, password);
}
sendCommand(cmd, callback);
}
void ImapConnection::sendCommand(const QString &command,
std::function<void(bool, const QString&)> callback)
{
QString tag = generateTag();
QString fullCmd = tag + " " + command + "\r\n";
m_socket->write(fullCmd.toUtf8());
m_socket->flush();
m_pendingCallbacks[tag] = callback;
}
void ImapConnection::disconnect()
{
if (m_socket->state() != QAbstractSocket::UnconnectedState) {
m_socket->disconnectFromHost();
m_socket->waitForDisconnected(1000);
}
m_pendingCallbacks.clear();
}
void ImapConnection::onConnected()
{
// For non-SSL connections, signal connected immediately.
// For SSL connections, onEncrypted() fires instead after the SSL handshake.
if (!m_useSsl) {
emit connected();
}
}
void ImapConnection::onEncrypted()
{
emit connected();
}
void ImapConnection::onReadyRead()
{
while (m_socket->canReadLine()) {
QString line = QString::fromUtf8(m_socket->readLine()).trimmed();
processLine(line);
}
}
void ImapConnection::onSocketError(QAbstractSocket::SocketError error)
{
emit errorOccurred(m_socket->errorString());
}
void ImapConnection::processLine(const QString &line)
{
// If it's an untagged response (starts with *), emit signal
if (line.startsWith('*')) {
emit untaggedResponse(line);
return;
}
// Extract the tag prefix (first word) and look it up in pending callbacks
int spacePos = line.indexOf(' ');
if (spacePos == -1) return; // malformed line
QString tagFromLine = line.left(spacePos);
auto it = m_pendingCallbacks.find(tagFromLine);
if (it != m_pendingCallbacks.end()) {
bool ok = line.contains(" OK ");
auto cb = it.value();
m_pendingCallbacks.erase(it);
cb(ok, line);
return;
}
// If we get here, it's an unhandled response
qWarning() << "Unhandled IMAP response:" << line;
}
void ImapConnection::sendRaw(const QString &data)
{
m_socket->write(data.toUtf8() + "\r\n");
m_socket->flush();
}
QString ImapConnection::generateTag()
{
// Simple tag: A001, A002, ... (like traditional IMAP clients)
return QString("A%1").arg(m_tagCounter++, 4, 10, QChar('0'));
}
bool ImapConnection::sendCommandWait(const QString &command, QString &response, int msecs)
{
QByteArray rawResponse;
const bool completed = sendCommandWaitBytes(command, rawResponse, msecs);
response = QString::fromUtf8(rawResponse);
return completed;
}
bool ImapConnection::sendCommandWaitBytes(const QString &command, QByteArray &response, int msecs)
{
QString tag = generateTag();
QString fullCmd = tag + " " + command + "\r\n";
bool complete = false;
bool timedOut = false;
QEventLoop loop;
QTimer timer;
QByteArray buffer;
const QByteArray tagBytes = tag.toUtf8();
// Fetch responses contain arbitrary MIME bytes and IMAP literals. The
// line-based parser cannot safely consume those bytes, so temporarily
// take ownership of readyRead and collect the complete tagged response.
QObject::disconnect(m_readyReadConnection);
auto collectResponse = [&]() {
buffer.append(m_socket->readAll());
int pos = 0;
while (pos < buffer.size()) {
const int lineEnd = buffer.indexOf("\r\n", pos);
if (lineEnd < 0) break;
const QByteArray line = buffer.mid(pos, lineEnd - pos);
const int open = line.lastIndexOf('{');
QByteArray literalSize = open >= 0 && line.endsWith('}')
? line.mid(open + 1, line.size() - open - 2) : QByteArray();
if (literalSize.endsWith('+')) literalSize.chop(1);
bool literalOk = false;
const qint64 parsedLiteralSize = literalSize.toLongLong(&literalOk);
const bool hasLiteral = open >= 0 && line.endsWith('}') && literalOk && parsedLiteralSize >= 0;
if (hasLiteral) {
const qint64 length = parsedLiteralSize;
const int dataStart = lineEnd + 2;
if (buffer.size() < dataStart + length) break;
pos = dataStart + int(length);
if (buffer.mid(pos, 2) == QByteArrayLiteral("\r\n")) pos += 2;
continue;
}
if (line.startsWith(tagBytes + QByteArrayLiteral(" "))) {
complete = true;
loop.quit();
break;
}
pos = lineEnd + 2;
}
};
QMetaObject::Connection readyConn = connect(m_socket, &QSslSocket::readyRead, &loop, collectResponse);
QMetaObject::Connection errorConn = connect(m_socket, &QAbstractSocket::errorOccurred, &loop, [&](QAbstractSocket::SocketError) {
loop.quit();
});
timer.setSingleShot(true);
QMetaObject::Connection timeoutConn = connect(&timer, &QTimer::timeout, &loop, [&]() {
timedOut = true;
loop.quit();
});
m_socket->write(fullCmd.toUtf8());
m_socket->flush();
if (m_socket->bytesAvailable() > 0) collectResponse();
timer.start(msecs);
loop.exec();
QObject::disconnect(readyConn);
QObject::disconnect(errorConn);
QObject::disconnect(timeoutConn);
m_readyReadConnection = connect(m_socket, &QSslSocket::readyRead, this, &ImapConnection::onReadyRead);
response = buffer;
if (timedOut || !complete) return false;
return true;
}
+60
View File
@@ -0,0 +1,60 @@
#ifndef IMAPCONNECTION_H
#define IMAPCONNECTION_H
#include <QSslSocket>
#include <QObject>
#include <functional>
#include <QMap>
#include <QByteArray>
class ImapConnection : public QObject
{
Q_OBJECT
public:
explicit ImapConnection(QObject *parent = nullptr);
~ImapConnection();
void connectToHost(const QString &host, int port, bool useSsl);
void login(const QString &username, const QString &password,
bool useAuthPlain,
std::function<void(bool, const QString&)> callback);
void sendCommand(const QString &command,
std::function<void(bool, const QString&)> callback);
void disconnect();
// Synchronous version: blocks until response or timeout
bool sendCommandWait(const QString &command, QString &response, int msecs);
bool sendCommandWaitBytes(const QString &command, QByteArray &response, int msecs);
signals:
void connected();
void disconnected();
void errorOccurred(const QString &message);
void untaggedResponse(const QString &line); // for * RESPONSES
void ready(); // connection ready for authentication (TCP connected for plain, encrypted for SSL)
private slots:
void onConnected();
void onEncrypted();
void onReadyRead();
void onSocketError(QAbstractSocket::SocketError error);
private:
QSslSocket *m_socket;
QString m_host;
int m_port;
bool m_useSsl;
QString m_username;
QString m_password;
int m_tagCounter;
QString m_responseBuffer;
QMap<QString, std::function<void(bool, const QString&)>> m_pendingCallbacks; // keyed by tag
QMetaObject::Connection m_readyReadConnection;
void processLine(const QString &line);
QString generateTag();
public:
void sendRaw(const QString &data);
};
#endif // IMAPCONNECTION_H
+316 -94
View File
@@ -8,6 +8,8 @@
#include "../../core/models/folder.h"
#include "../../db/dao/folderdao.h"
#include "../../db/dao/mailitemdao.h"
#include "../../services/mimestorage.h"
#include <algorithm>
ImapSynchronizer::ImapSynchronizer(QObject* parent)
: Synchronizer(parent)
@@ -16,6 +18,7 @@ ImapSynchronizer::ImapSynchronizer(QObject* parent)
bool ImapSynchronizer::initialize(const Account& account)
{
m_account = account;
const Account::ConnectionSettings& settings = account.connectionSettings();
m_host = settings.incomingHost;
m_port = settings.incomingPort;
@@ -37,9 +40,13 @@ bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
QEventLoop connectLoop;
QTimer connectTimer;
bool connectTimeout = false;
connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit);
connect(&conn, &ImapConnection::connected, &connectLoop, [&]() {
connected = true;
connectLoop.quit();
});
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
qWarning() << "IMAP connection error:" << err;
connected = false;
connectLoop.quit();
});
connectTimer.setSingleShot(true);
@@ -54,7 +61,7 @@ bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
qWarning() << "Connection timeout to" << m_host;
return false;
}
connected = true;
if (!connected) return false;
// Wait for server greeting (first untagged response)
QEventLoop greetLoop;
@@ -114,99 +121,173 @@ bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
return true;
}
bool ImapSynchronizer::syncFolder(const Folder& folder)
QVector<qint64> ImapSynchronizer::parseUidSearchResponse(const QString& response) const
{
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// Select folder
QString selectCmd = QString("SELECT \"%1\"").arg(folder.name());
QString selectResp;
if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(" OK ")) {
qWarning() << "SELECT failed for" << folder.name();
return false;
}
// Get all UIDs
QString searchCmd = "UID SEARCH ALL";
QString searchResp;
if (!conn.sendCommandWait(searchCmd, searchResp, 30000) || !searchResp.contains(" OK ")) {
qWarning() << "SEARCH failed";
return false;
}
QVector<qint64> uids;
QStringList lines = searchResp.split('\n');
QStringList lines = response.split(QStringLiteral("\n"));
for (const QString& line : lines) {
if (line.startsWith("* SEARCH")) {
QStringList parts = line.split(QRegularExpression("\\s+"));
if (line.startsWith(QStringLiteral("* SEARCH"))) {
QStringList parts = line.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
bool ok;
qint64 uid = parts[i].toLongLong(&ok);
if (ok && uid > 0) uids.append(uid);
if (ok && uid > 0)
uids.append(uid);
}
break;
}
}
// If no messages, set unread count to 0 and exit
if (uids.isEmpty()) {
Folder f = folder;
f.setUnreadCount(0);
FolderDao::update(f);
conn.disconnect();
return true;
}
// Get flags for all UIDs in one command
QStringList uidStrs;
for (qint64 uid : uids) uidStrs.append(QString::number(uid));
QString fetchCmd = "UID FETCH " + uidStrs.join(',') + " (FLAGS)";
QString fetchResp;
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000) || !fetchResp.contains(" OK ")) {
qWarning() << "FETCH FLAGS failed";
conn.disconnect();
return uids;
}
bool ImapSynchronizer::syncFolder(const Folder& folder)
{
// First, get all UIDs from server to detect deletions
QVector<qint64> allUids = fetchAllUids(QString::number(folder.id()));
if (allUids.isEmpty()) {
qWarning() << "Failed to fetch UIDs for folder" << folder.id();
return false;
}
// Parse flags and update local DB
QMap<qint64, QPair<bool,bool>> flagsMap; // uid -> (seen, flagged)
QRegularExpression flagsRx(R"(UID (\d+).*FLAGS \(([^)]*)\))");
auto it = flagsRx.globalMatch(fetchResp);
while (it.hasNext()) {
auto m = it.next();
qint64 uid = m.captured(1).toLongLong();
QString flags = m.captured(2);
bool seen = flags.contains("\\Seen");
bool flagged = flags.contains("\\Flagged");
flagsMap[uid] = qMakePair(seen, flagged);
// Get local UIDs
QVector<qint64> localUids = MailItemDao::getUidsForFolder(folder.id());
// Find deleted UIDs (exist locally but not on server)
QSet<qint64> serverUidSet(allUids.begin(), allUids.end());
QVector<qint64> deletedUids;
for (qint64 uid : localUids) {
if (!serverUidSet.contains(uid)) {
deletedUids.append(uid);
}
}
// Update local items
auto localItems = MailItemDao::findByFolderId(folder.id());
for (MailItem& item : localItems) {
if (flagsMap.contains(item.uid())) {
bool seen = flagsMap[item.uid()].first;
bool flagged = flagsMap[item.uid()].second;
if (item.isRead() != seen || item.isFlagged() != flagged) {
item.setRead(seen);
item.setFlagged(flagged);
MailItemDao::update(item);
}
// Delete locally if removed from server
bool success = true;
for (qint64 uid : deletedUids) {
if (!MailItemDao::removeByUid(folder.id(), uid)) {
qWarning() << "Failed to delete local mail item UID" << uid;
success = false;
} else {
qDebug() << "Removed local mail item UID" << uid << "(deleted on server)";
}
}
// Count unread
int unread = 0;
for (const MailItem& item : localItems) {
if (!item.isRead()) unread++;
// Now fetch new/changed messages
const qint64 sinceUid = MailItemDao::maxUidForFolder(folder.id()).value_or(0);
QVector<MailItem> items = fetchMailItems(QString::number(folder.id()), sinceUid);
for (MailItem &item : items) {
if (!persistFetchedItem(folder, item)) success = false;
}
Folder f = folder;
f.setUnreadCount(unread);
FolderDao::update(f);
// Also check for flag changes on existing messages
if (!allUids.isEmpty()) {
// Fetch flags for all known UIDs (in batches)
const int batchSize = 100;
for (int i = 0; i < allUids.size(); i += batchSize) {
QVector<qint64> batch = allUids.mid(i, qMin(batchSize, allUids.size() - i));
QStringList batchList;
for (qint64 uid : batch) batchList << QString::number(uid);
QString uidList = batchList.join(",");
ImapConnection conn;
if (!connectAndLogin(conn)) continue;
// SELECT folder
QString selectCommand = QString("SELECT \"%1\"").arg(folder.name());
QString selectResponse;
if (!conn.sendCommandWait(selectCommand, selectResponse, 30000) || !selectResponse.contains(" OK ")) {
continue;
}
// Fetch only FLAGS for this batch
QString fetchCommand = QString("UID FETCH %1 (FLAGS)").arg(uidList);
QByteArray fetchResponse;
if (!conn.sendCommandWaitBytes(fetchCommand, fetchResponse, 30000)) {
continue;
}
// Parse flags and update local
parseAndUpdateFlags(fetchResponse, folder.id());
}
}
return success;
}
conn.disconnect();
return true;
QVector<qint64> ImapSynchronizer::fetchAllUids(const QString& folderId) const
{
QVector<qint64> uids;
ImapConnection conn;
if (!connectAndLogin(conn)) {
return uids;
}
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) {
qWarning() << "Folder not found for id" << folderId;
return uids;
}
QString folderName = folderOpt->name();
// SELECT
QString selectCommand = QString("SELECT \"%1\"").arg(folderName);
QString selectResponse;
if (!conn.sendCommandWait(selectCommand, selectResponse, 30000) || !selectResponse.contains(" OK ")) {
qWarning() << "SELECT failed:" << selectResponse;
return uids;
}
// SEARCH ALL
QString searchCommand = "UID SEARCH ALL";
QString searchResponse;
if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) {
qWarning() << "SEARCH command failed:" << searchResponse;
return uids;
}
uids = parseUidSearchResponse(searchResponse);
return uids;
}
void ImapSynchronizer::parseAndUpdateFlags(const QByteArray& response, int folderId) const
{
// Parse FETCH response for FLAGS
QStringList lines = QString::fromUtf8(response).split('\n');
for (const QString& line : lines) {
if (!line.startsWith("* ") || !line.contains(" FETCH ")) continue;
// Extract UID
QRegularExpression uidRx(QStringLiteral("\\bUID\\s+(\\d+)"));
QRegularExpressionMatch uidMatch = uidRx.match(line);
if (!uidMatch.hasMatch()) continue;
qint64 uid = uidMatch.captured(1).toLongLong();
// Extract FLAGS
int flagsPos = line.indexOf("FLAGS (");
if (flagsPos < 0) continue;
int flagsEnd = line.indexOf(')', flagsPos);
if (flagsEnd < 0) continue;
QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7);
bool read = flags.contains("\\Seen");
bool flagged = flags.contains("\\Flagged");
// Update local DB if different
auto itemOpt = MailItemDao::findByUid(folderId, uid);
if (itemOpt.has_value()) {
MailItem item = itemOpt.value();
if (item.isRead() != read || item.isFlagged() != flagged) {
item.setRead(read);
item.setFlagged(flagged);
if (!MailItemDao::update(item)) {
qWarning() << "Failed to update flags for UID" << uid;
} else {
qDebug() << "Updated flags for UID" << uid << "read=" << read << "flagged=" << flagged;
}
}
}
}
}
QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid)
@@ -242,7 +323,7 @@ QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint
// SEARCH
QString searchCommand;
if (sinceUid > 0) {
searchCommand = QString("UID SEARCH %1:*").arg(sinceUid);
searchCommand = QString("UID SEARCH UID %1:*").arg(sinceUid + 1);
} else {
searchCommand = "UID SEARCH ALL";
}
@@ -283,24 +364,38 @@ QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint
}
batchList.chop(1); // remove trailing comma
// Fetch headers and flags (efficient)
QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)")
// Fetch the complete RFC822 message. Headers alone are not enough to
// reconstruct HTML, inline resources or attachments.
QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[] FLAGS INTERNALDATE)")
.arg(batchList);
QString fetchResponse;
if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) {
qWarning() << "FETCH failed for batch" << i;
QByteArray fetchResponse;
if (!conn.sendCommandWaitBytes(fetchCommand, fetchResponse, 30000)) {
qWarning() << "FETCH failed for batch" << i << "(" << batch.size() << "UIDs)"
<< "- first UID:" << batch.first() << "last UID:" << batch.last()
<< "- response:" << QString::fromUtf8(fetchResponse).left(200);
// Fallback: try fetching UIDs one by one
for (qint64 uid : batch) {
QString singleFetchCmd = QString("UID FETCH %1 (BODY.PEEK[] FLAGS INTERNALDATE)").arg(uid);
QByteArray singleResponse;
if (conn.sendCommandWaitBytes(singleFetchCmd, singleResponse, 30000)) {
QVector<MailItem> singleItems = parseFetchResponseBytes(singleResponse);
for (MailItem& item : singleItems) {
item.setFolderId(fid);
items.append(item);
}
} else {
qWarning() << " Single UID fetch failed for UID" << uid
<< "- response:" << QString::fromUtf8(singleResponse).left(200);
}
}
continue;
}
// Parse and persist
QVector<MailItem> batchItems = parseFetchResponse(fetchResponse.split('\n'));
QVector<MailItem> batchItems = parseFetchResponseBytes(fetchResponse);
for (MailItem& item : batchItems) {
item.setFolderId(fid);
if (MailItemDao::insert(item)) {
items.append(item);
} else {
qWarning() << "Failed to insert mail item uid" << item.uid();
}
items.append(item);
}
}
@@ -337,6 +432,132 @@ QVector<Folder> ImapSynchronizer::getFolders() const
return folders;
}
bool ImapSynchronizer::persistFetchedItem(const Folder& folder, MailItem& item) const
{
MimeStorageService storage;
QVector<QString> storedPaths;
ParsedMimeMessage parsed;
if (!item.rawMime().isEmpty()) {
if (!storage.parseMessage(item.rawMime(), parsed)
|| !storage.storeMessage(QString::number(m_account.id()), QString::number(folder.id()),
item, item.rawMime(), &storedPaths)) {
qWarning() << "Failed to store MIME message for UID" << item.uid();
return false;
}
}
if (!MailItemDao::upsert(item)) {
storage.deleteEmlFile(item.fileId());
return false;
}
QVector<StoredAttachmentRecord> records;
for (int i = 0; i < parsed.attachments.size(); ++i) {
const ParsedMimeAttachment &attachment = parsed.attachments.at(i);
StoredAttachmentRecord record;
record.fileName = attachment.fileName;
record.mimeType = attachment.mimeType;
record.contentId = attachment.contentId;
record.size = attachment.data.size();
if (i < storedPaths.size()) record.storedPath = storedPaths.at(i);
records.append(record);
}
if (!MailItemDao::replaceAttachments(item.id(), records)) {
storage.deleteEmlFile(item.fileId());
return false;
}
return true;
}
QVector<MailItem> ImapSynchronizer::parseFetchResponseBytes(const QByteArray& response) const
{
QVector<MailItem> items;
MimeStorageService mimeStorage;
int searchFrom = 0;
// Debug: check if response contains error
if (response.contains(" NO ") || response.contains(" BAD ")) {
qWarning() << "IMAP FETCH response contains error:" << QString::fromUtf8(response).left(500);
}
while (true) {
const int bodyPos = response.indexOf("BODY[]", searchFrom);
if (bodyPos < 0) {
// Check if we have untagged FETCH responses without BODY[]
int fetchPos = response.indexOf(" FETCH ", searchFrom);
if (fetchPos >= 0) {
int lineEnd = response.indexOf("\r\n", fetchPos);
if (lineEnd > 0) {
QByteArray line = response.mid(fetchPos, lineEnd - fetchPos);
qWarning() << "FETCH response without BODY[]:" << QString::fromLatin1(line);
}
}
break;
}
const int bracePos = response.indexOf('{', bodyPos);
const int braceEnd = response.indexOf('}', bracePos);
if (bracePos < 0 || braceEnd < 0) break;
QByteArray literalSize = response.mid(bracePos + 1, braceEnd - bracePos - 1);
if (literalSize.endsWith('+')) literalSize.chop(1);
bool sizeOk = false;
const qint64 size = literalSize.toLongLong(&sizeOk);
const int lineEnd = response.indexOf("\r\n", braceEnd);
if (!sizeOk || lineEnd < 0 || response.size() < lineEnd + 2 + size) {
qWarning() << "FETCH parse error: sizeOk=" << sizeOk << "lineEnd=" << lineEnd
<< "response.size=" << response.size() << "needed=" << (lineEnd + 2 + size);
break;
}
const QByteArray rawMime = response.mid(lineEnd + 2, int(size));
const int fetchLineStart = response.lastIndexOf("\r\n", bodyPos) + 2;
const QByteArray fetchLine = response.mid(fetchLineStart, bodyPos - fetchLineStart);
const QRegularExpression uidRx(QStringLiteral("\\bUID\\s+(\\d+)"));
const QRegularExpressionMatch uidMatch = uidRx.match(QString::fromLatin1(fetchLine));
if (!uidMatch.hasMatch()) {
qWarning() << "No UID found in FETCH line:" << QString::fromLatin1(fetchLine).left(200);
searchFrom = lineEnd + 2 + int(size);
continue;
}
ParsedMimeMessage parsed;
if (mimeStorage.parseMessage(rawMime, parsed)) {
MailItem item;
item.setUid(uidMatch.captured(1).toLongLong());
item.setMessageId(parsed.messageId);
item.setSubject(parsed.subject.isEmpty() ? QStringLiteral("(No Subject)") : parsed.subject);
item.setSender(parsed.from);
item.setRecipient(parsed.to);
item.setTo(parsed.to);
item.setCc(parsed.cc);
item.setBcc(parsed.bcc);
item.setDate(parsed.date);
item.setBodyHtml(parsed.bodyHtml);
item.setSize(rawMime.size());
item.setRawMime(rawMime);
QVector<QString> names;
for (const ParsedMimeAttachment &attachment : parsed.attachments)
names.append(attachment.fileName);
item.setAttachments(names);
// Flags are in the FETCH line, not in the MIME payload.
const int flagsPos = fetchLine.indexOf("FLAGS (");
if (flagsPos >= 0) {
const int flagsEnd = fetchLine.indexOf(')', flagsPos);
if (flagsEnd > flagsPos) {
const QString flags = QString::fromLatin1(fetchLine.mid(flagsPos + 7, flagsEnd - flagsPos - 7));
item.setRead(flags.contains(QStringLiteral("\\Seen")));
item.setFlagged(flags.contains(QStringLiteral("\\Flagged")));
}
}
items.append(item);
} else {
qWarning() << "Failed to parse MIME for UID" << uidMatch.captured(1);
}
searchFrom = lineEnd + 2 + int(size);
}
return items;
}
bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
{
// Get folder name
@@ -562,19 +783,20 @@ QVector<MailItem> ImapSynchronizer::parseFetchResponse(const QStringList& lines)
int flagsEnd = line.indexOf(')', flagsPos + 7);
if (flagsEnd != -1) {
QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7);
item.setRead(flags.contains(QStringLiteral("\\\\Seen")));
item.setFlagged(flags.contains(QStringLiteral("\\\\Flagged")));
item.setRead(flags.contains(QStringLiteral("\\Seen")));
item.setFlagged(flags.contains(QStringLiteral("\\Flagged")));
}
}
// Extract INTERNALDATE
int datePos = line.indexOf(QStringLiteral("INTERNALDATE \"\""));
int datePos = line.indexOf(QStringLiteral("INTERNALDATE \""));
if (datePos != -1) {
int dateStart = datePos + 16; // length of "INTERNALDATE \""
int dateStart = datePos + 15; // length of "INTERNALDATE \""
int dateEnd = line.indexOf('\"', dateStart);
if (dateEnd != -1) {
QString dateStr = line.mid(dateStart, dateEnd - dateStart);
QDateTime dt = QDateTime::fromString(dateStr, QStringLiteral("dd-MMM-yyyy hh:mm:ss zzz"));
dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000"
QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date);
if (dt.isValid())
item.setDate(dt);
}
@@ -582,7 +804,7 @@ QVector<MailItem> ImapSynchronizer::parseFetchResponse(const QStringList& lines)
// Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines
QString bodyData;
int bodyPos = line.indexOf(QStringLiteral("BODY["));
int bodyPos = line.indexOf(QStringLiteral("BODY[]"));
if (bodyPos != -1) {
int bracePos = line.indexOf('{', bodyPos);
if (bracePos != -1) {
@@ -0,0 +1,649 @@
#include "imapsynchronizer.h"
#include <QEventLoop>
#include <QTimer>
#include <QDebug>
#include <QRegularExpression>
#include "../../core/mailitem.h"
#include "../../core/models/account.h"
#include "../../core/models/folder.h"
#include "../../db/dao/folderdao.h"
#include "../../db/dao/mailitemdao.h"
ImapSynchronizer::ImapSynchronizer(QObject* parent)
: Synchronizer(parent)
{
}
bool ImapSynchronizer::initialize(const Account& account)
{
const Account::ConnectionSettings& settings = account.connectionSettings();
m_host = settings.incomingHost;
m_port = settings.incomingPort;
m_useSsl = settings.incomingSsl;
m_username = settings.username;
m_password = settings.password;
m_authMethod = settings.authMethod;
return true;
}
// Helper: connects and logs in to the IMAP server
bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
{
bool connected = false;
bool loggedIn = false;
QString response;
// Connect to host
QEventLoop connectLoop;
QTimer connectTimer;
bool connectTimeout = false;
connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit);
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
qWarning() << "IMAP connection error:" << err;
connectLoop.quit();
});
connectTimer.setSingleShot(true);
connect(&connectTimer, &QTimer::timeout, &connectLoop, [&](){
connectTimeout = true;
connectLoop.quit();
});
connectTimer.start(30000); // 30 seconds
conn.connectToHost(m_host, m_port, m_useSsl);
connectLoop.exec();
if (connectTimeout) {
qWarning() << "Connection timeout to" << m_host;
return false;
}
connected = true;
// Wait for server greeting (first untagged response)
QEventLoop greetLoop;
bool gotGreeting = false;
QMetaObject::Connection greetConn = connect(&conn, &ImapConnection::untaggedResponse,
[&](const QString& line) {
Q_UNUSED(line);
gotGreeting = true;
greetLoop.quit();
});
QTimer greetTimer;
greetTimer.setSingleShot(true);
connect(&greetTimer, &QTimer::timeout, &greetLoop, [&]() {
greetLoop.quit();
});
greetTimer.start(30000); // 30 seconds
greetLoop.exec();
QObject::disconnect(greetConn);
if (!gotGreeting) {
qWarning() << "Timeout waiting for server greeting";
return false;
}
// Login
QEventLoop loginLoop;
QTimer loginTimer;
bool loginTimeout = false;
bool loginSuccess = false;
// Already connected, now login
conn.login(m_username, m_password, true, [&](bool ok, const QString& resp){
if (ok) {
loginSuccess = true;
} else {
qWarning() << "Login failed:" << resp;
}
loginLoop.quit();
});
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
qWarning() << "IMAP error during login:" << err;
loginLoop.quit();
});
loginTimer.setSingleShot(true);
connect(&loginTimer, &QTimer::timeout, &loginLoop, [&](){
loginTimeout = true;
loginLoop.quit();
});
loginTimer.start(30000);
loginLoop.exec();
if (loginTimeout) {
qWarning() << "Login timeout";
return false;
}
if (!loginSuccess) {
return false;
}
loggedIn = true;
return true;
}
bool ImapSynchronizer::syncFolder(const Folder& folder)
{
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// Select folder
QString selectCmd = QString("SELECT \"%1\"").arg(folder.name());
QString selectResp;
if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(" OK ")) {
qWarning() << "SELECT failed for" << folder.name();
return false;
}
// Get all UIDs
QString searchCmd = "UID SEARCH ALL";
QString searchResp;
if (!conn.sendCommandWait(searchCmd, searchResp, 30000) || !searchResp.contains(" OK ")) {
qWarning() << "SEARCH failed";
return false;
}
QVector<qint64> uids;
QStringList lines = searchResp.split('\n');
for (const QString& line : lines) {
if (line.startsWith("* SEARCH")) {
QStringList parts = line.split(QRegularExpression("\\s+"));
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
bool ok;
qint64 uid = parts[i].toLongLong(&ok);
if (ok && uid > 0) uids.append(uid);
}
break;
}
}
// If no messages, set unread count to 0 and exit
if (uids.isEmpty()) {
Folder f = folder;
f.setUnreadCount(0);
FolderDao::update(f);
conn.disconnect();
return true;
}
// Get flags for all UIDs in one command
QStringList uidStrs;
for (qint64 uid : uids) uidStrs.append(QString::number(uid));
QString fetchCmd = "UID FETCH " + uidStrs.join(',') + " (FLAGS)";
QString fetchResp;
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000) || !fetchResp.contains(" OK ")) {
qWarning() << "FETCH FLAGS failed";
conn.disconnect();
return false;
}
// Parse flags and update local DB
QMap<qint64, QPair<bool,bool>> flagsMap; // uid -> (seen, flagged)
QRegularExpression flagsRx(R"(UID (\d+).*FLAGS \(([^)]*)\))");
auto it = flagsRx.globalMatch(fetchResp);
while (it.hasNext()) {
auto m = it.next();
qint64 uid = m.captured(1).toLongLong();
QString flags = m.captured(2);
bool seen = flags.contains("\\Seen");
bool flagged = flags.contains("\\Flagged");
flagsMap[uid] = qMakePair(seen, flagged);
}
// Update local items
auto localItems = MailItemDao::findByFolderId(folder.id());
for (MailItem& item : localItems) {
if (flagsMap.contains(item.uid())) {
bool seen = flagsMap[item.uid()].first;
bool flagged = flagsMap[item.uid()].second;
if (item.isRead() != seen || item.isFlagged() != flagged) {
item.setRead(seen);
item.setFlagged(flagged);
MailItemDao::update(item);
}
}
}
// Count unread
int unread = 0;
for (const MailItem& item : localItems) {
if (!item.isRead()) unread++;
}
Folder f = folder;
f.setUnreadCount(unread);
FolderDao::update(f);
conn.disconnect();
return true;
}
QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid)
{
QVector<MailItem> items;
ImapConnection conn;
if (!connectAndLogin(conn)) {
return items;
}
// Get folder name from DB
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) {
qWarning() << "Folder not found for id" << folderId;
return items;
}
QString folderName = folderOpt->name();
// SELECT with real folder name
QString selectCommand = QString("SELECT \"%1\"").arg(folderName);
QString selectResponse;
if (!conn.sendCommandWait(selectCommand, selectResponse, 30000)) {
qWarning() << "SELECT command failed:" << selectResponse;
return items;
}
if (!selectResponse.contains(" OK ")) {
qWarning() << "SELECT failed:" << selectResponse;
return items;
}
// SEARCH
QString searchCommand;
if (sinceUid > 0) {
searchCommand = QString("UID SEARCH %1:*").arg(sinceUid);
} else {
searchCommand = "UID SEARCH ALL";
}
QString searchResponse;
if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) {
qWarning() << "SEARCH command failed:" << searchResponse;
return items;
}
// Parse UIDs (robust: look for line starting with "* SEARCH")
QVector<qint64> uids;
QStringList lines = searchResponse.split('\n');
for (const QString& line : lines) {
if (line.startsWith("* SEARCH")) {
QStringList parts = line.split(QRegularExpression("\\s+"));
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
bool ok;
qint64 uid = parts[i].toLongLong(&ok);
if (ok && uid > 0) uids.append(uid);
}
break;
}
}
if (uids.isEmpty()) {
conn.sendCommandWait("CLOSE", searchResponse, 30000);
conn.disconnect();
return items;
}
// Fetch in batches of 50 UIDs
const int batchSize = 50;
for (int i = 0; i < uids.size(); i += batchSize) {
QVector<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); // remove trailing comma
// Fetch headers and flags (efficient)
QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)")
.arg(batchList);
QString fetchResponse;
if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) {
qWarning() << "FETCH failed for batch" << i;
continue;
}
// Parse and persist
QVector<MailItem> batchItems = parseFetchResponse(fetchResponse.split('\n'));
for (MailItem& item : batchItems) {
item.setFolderId(fid);
if (MailItemDao::insert(item)) {
items.append(item);
} else {
qWarning() << "Failed to insert mail item uid" << item.uid();
}
}
}
// Close and disconnect
QString dummy;
conn.sendCommandWait("CLOSE", dummy, 30000);
conn.disconnect();
return items;
}
QVector<Folder> ImapSynchronizer::getFolders() const
{
QVector<Folder> folders;
ImapConnection conn;
if (!connectAndLogin(conn)) {
return folders;
}
// List folders: LIST "" "*"
QString listCommand = QStringLiteral("LIST \"\" \"*\"");
QString response;
if (!conn.sendCommandWait(listCommand, response, 30000)) {
qWarning() << "LIST command failed:" << response;
return folders;
}
// Parse the response
QStringList lines = response.split('\n');
folders = parseListResponse(lines);
// Logout
conn.disconnect();
return folders;
}
bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
{
// Get folder name
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) {
qWarning() << "Folder not found for id" << folderId;
return false;
}
QString folderName = folderOpt->name();
// Build RFC822 message (headers + body)
QStringList headers;
if (!item.sender().isEmpty()) headers << "From: " + item.sender();
if (!item.recipient().isEmpty()) headers << "To: " + item.recipient();
if (!item.cc().isEmpty()) headers << "Cc: " + item.cc();
if (!item.bcc().isEmpty()) headers << "Bcc: " + item.bcc();
if (!item.subject().isEmpty()) headers << "Subject: " + item.subject();
headers << "Date: " + item.date().toString(Qt::RFC2822Date);
headers << "MIME-Version: 1.0";
headers << "Content-Type: text/html; charset=UTF-8";
headers << "Content-Transfer-Encoding: 7bit";
headers << ""; // blank line separates headers from body
headers << item.bodyHtml(); // using bodyHtml as the body
QString message = headers.join("\r\n");
QByteArray msgData = message.toUtf8();
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// SELECT folder (some servers require it)
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
QString resp;
conn.sendCommandWait(selectCmd, resp, 30000); // ignore response
// APPEND with literal: APPEND "folder" (\Seen) {size}
QString appendCmd = QString("APPEND \"%1\" (\\Seen) {%2}").arg(folderName).arg(msgData.size());
QString response;
if (!conn.sendCommandWait(appendCmd, response, 30000)) {
qWarning() << "APPEND command initial failed:" << response;
conn.disconnect();
return false;
}
// Send the literal (message data)
conn.sendRaw(QString::fromUtf8(msgData));
// Wait for the tagged response (tag + OK/NO)
if (!conn.sendCommandWait("", response, 30000)) { // dummy command to get response
qWarning() << "APPEND literal failed:" << response;
conn.disconnect();
return false;
}
conn.disconnect();
return true;
}
bool ImapSynchronizer::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged)
{
qint64 uid = itemUid.toLongLong();
if (uid <= 0) return false;
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) return false;
QString folderName = folderOpt->name();
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// SELECT
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
QString resp;
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "SELECT failed";
return false;
}
// STORE for Seen flag
QString seenFlag = read ? "+" : "-";
QString storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Seen").arg(uid).arg(seenFlag);
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "STORE Seen failed:" << resp;
conn.disconnect();
return false;
}
// STORE for Flagged flag
QString flaggedFlag = flagged ? "+" : "-";
storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Flagged").arg(uid).arg(flaggedFlag);
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "STORE Flagged failed:" << resp;
conn.disconnect();
return false;
}
conn.disconnect();
// Update local DB
auto items = MailItemDao::findByFolderId(fid);
for (MailItem& item : items) {
if (item.uid() == uid) {
item.setRead(read);
item.setFlagged(flagged);
MailItemDao::update(item);
break;
}
}
return true;
}
bool ImapSynchronizer::deleteMailItem(const QString& folderId, const QString& itemUid)
{
qint64 uid = itemUid.toLongLong();
if (uid <= 0) return false;
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) return false;
QString folderName = folderOpt->name();
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// SELECT
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
QString resp;
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "SELECT failed";
return false;
}
// Mark as \\Deleted
QString storeCmd = QString("UID STORE %1 +FLAGS.SILENT \\Deleted").arg(uid);
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "STORE Deleted failed:" << resp;
conn.disconnect();
return false;
}
// EXPUNGE (permanently delete)
QString expungeCmd = "EXPUNGE";
if (!conn.sendCommandWait(expungeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "EXPUNGE failed:" << resp;
conn.disconnect();
return false;
}
conn.disconnect();
// Remove from local DB
MailItemDao::remove(uid);
return true;
}
QString ImapSynchronizer::generateEventId() const
{
return QString::number(QDateTime::currentMSecsSinceEpoch());
}
QVector<Folder> ImapSynchronizer::parseListResponse(const QStringList& lines) const
{
QVector<Folder> folders;
QRegularExpression rx("\\* LIST \\\\([^)]*\\\\) \"([^\"]*)\" \"([^\"]*)\"");
for (const QString& line : lines) {
QRegularExpressionMatch match = rx.match(line);
if (match.hasMatch()) {
QString delimiter = match.captured(1); // unused for now
QString mailbox = match.captured(2);
Folder folder;
folder.setName(mailbox);
QString lower = mailbox.toLower();
if (lower == "inbox") folder.setInbox(true);
else if (lower == "sent") folder.setSent(true);
else if (lower == "drafts") folder.setDrafts(true);
else if (lower == "trash" || lower == "deleted items") folder.setTrash(true);
folders.append(folder);
}
}
return folders;
}
QVector<MailItem> ImapSynchronizer::parseFetchResponse(const QStringList& lines) const
{
QVector<MailItem> items;
int i = 0;
while (i < lines.size()) {
const QString& line = lines[i];
if (!line.startsWith(QStringLiteral("* "))) {
++i;
continue;
}
int fetchPos = line.indexOf(QStringLiteral(" FETCH ("));
if (fetchPos == -1) {
++i;
continue;
}
// Extract UID
int uidPos = line.indexOf(QStringLiteral("UID "));
if (uidPos == -1) {
++i;
continue;
}
int uidEnd = line.indexOf(QRegularExpression(QStringLiteral("[\\s)]")), uidPos + 4);
if (uidEnd == -1) uidEnd = line.length();
QString uidStr = line.mid(uidPos + 4, uidEnd - uidPos - 4);
bool ok;
qint64 uid = uidStr.toLongLong(&ok);
if (!ok) {
++i;
continue;
}
MailItem item;
item.setUid(uid);
// Extract FLAGS
int flagsPos = line.indexOf(QStringLiteral("FLAGS ("));
if (flagsPos != -1) {
int flagsEnd = line.indexOf(')', flagsPos + 7);
if (flagsEnd != -1) {
QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7);
item.setRead(flags.contains(QStringLiteral("\\Seen")));
item.setFlagged(flags.contains(QStringLiteral("\\Flagged")));
}
}
// Extract INTERNALDATE
int datePos = line.indexOf(QStringLiteral("INTERNALDATE \""));
if (datePos != -1) {
int dateStart = datePos + 15; // length of "INTERNALDATE \""
int dateEnd = line.indexOf('\"', dateStart);
if (dateEnd != -1) {
QString dateStr = line.mid(dateStart, dateEnd - dateStart);
dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000"
QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date);
if (dt.isValid())
item.setDate(dt);
}
}
// Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines
QString bodyData;
int bodyPos = line.indexOf(QStringLiteral("BODY["));
if (bodyPos != -1) {
int bracePos = line.indexOf('{', bodyPos);
if (bracePos != -1) {
int sizeEnd = line.indexOf('}', bracePos);
if (sizeEnd != -1) {
bool sizeOk;
int size = line.mid(bracePos + 1, sizeEnd - bracePos - 1).toInt(&sizeOk);
if (sizeOk) {
int dataStart = line.indexOf(QStringLiteral("\r\n"), sizeEnd);
if (dataStart != -1) {
dataStart += 2; // skip \r\n
int available = line.length() - dataStart;
if (available > size) available = size;
bodyData = line.mid(dataStart, available);
int received = available;
int remaining = size - received;
int j = i + 1;
while (j < lines.size() && remaining > 0) {
const QString& nextLine = lines[j];
int take = qMin(nextLine.length(), remaining);
bodyData += nextLine.left(take);
remaining -= take;
++j;
}
// we have consumed lines up to j-1
i = j - 1; // will be incremented at end of loop
}
}
}
}
}
// Parse headers from bodyData
if (!bodyData.isEmpty()) {
QRegularExpression headerRx(QStringLiteral(R"((^([^:]+):\s*(.*?)\r\n))"), QRegularExpression::MultilineOption);
auto it = headerRx.globalMatch(bodyData);
while (it.hasNext()) {
auto match = it.next();
QString key = match.captured(2).toLower().trimmed();
QString value = match.captured(3).trimmed();
if (key == QStringLiteral("subject"))
item.setSubject(value);
else if (key == QStringLiteral("from"))
item.setSender(value);
else if (key == QStringLiteral("to"))
item.setRecipient(value);
else if (key == QStringLiteral("date")) {
QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date);
if (dt.isValid())
item.setDate(dt);
}
}
}
// If subject not set, use placeholder
if (item.subject().isEmpty())
item.setSubject(QStringLiteral("(No Subject)"));
items.append(item);
++i;
}
return items;
}
@@ -0,0 +1,649 @@
#include "imapsynchronizer.h"
#include <QEventLoop>
#include <QTimer>
#include <QDebug>
#include <QRegularExpression>
#include "../../core/mailitem.h"
#include "../../core/models/account.h"
#include "../../core/models/folder.h"
#include "../../db/dao/folderdao.h"
#include "../../db/dao/mailitemdao.h"
ImapSynchronizer::ImapSynchronizer(QObject* parent)
: Synchronizer(parent)
{
}
bool ImapSynchronizer::initialize(const Account& account)
{
const Account::ConnectionSettings& settings = account.connectionSettings();
m_host = settings.incomingHost;
m_port = settings.incomingPort;
m_useSsl = settings.incomingSsl;
m_username = settings.username;
m_password = settings.password;
m_authMethod = settings.authMethod;
return true;
}
// Helper: connects and logs in to the IMAP server
bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
{
bool connected = false;
bool loggedIn = false;
QString response;
// Connect to host
QEventLoop connectLoop;
QTimer connectTimer;
bool connectTimeout = false;
connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit);
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
qWarning() << "IMAP connection error:" << err;
connectLoop.quit();
});
connectTimer.setSingleShot(true);
connect(&connectTimer, &QTimer::timeout, &connectLoop, [&](){
connectTimeout = true;
connectLoop.quit();
});
connectTimer.start(30000); // 30 seconds
conn.connectToHost(m_host, m_port, m_useSsl);
connectLoop.exec();
if (connectTimeout) {
qWarning() << "Connection timeout to" << m_host;
return false;
}
connected = true;
// Wait for server greeting (first untagged response)
QEventLoop greetLoop;
bool gotGreeting = false;
QMetaObject::Connection greetConn = connect(&conn, &ImapConnection::untaggedResponse,
[&](const QString& line) {
Q_UNUSED(line);
gotGreeting = true;
greetLoop.quit();
});
QTimer greetTimer;
greetTimer.setSingleShot(true);
connect(&greetTimer, &QTimer::timeout, &greetLoop, [&]() {
greetLoop.quit();
});
greetTimer.start(30000); // 30 seconds
greetLoop.exec();
QObject::disconnect(greetConn);
if (!gotGreeting) {
qWarning() << "Timeout waiting for server greeting";
return false;
}
// Login
QEventLoop loginLoop;
QTimer loginTimer;
bool loginTimeout = false;
bool loginSuccess = false;
// Already connected, now login
conn.login(m_username, m_password, true, [&](bool ok, const QString& resp){
if (ok) {
loginSuccess = true;
} else {
qWarning() << "Login failed:" << resp;
}
loginLoop.quit();
});
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
qWarning() << "IMAP error during login:" << err;
loginLoop.quit();
});
loginTimer.setSingleShot(true);
connect(&loginTimer, &QTimer::timeout, &loginLoop, [&](){
loginTimeout = true;
loginLoop.quit();
});
loginTimer.start(30000);
loginLoop.exec();
if (loginTimeout) {
qWarning() << "Login timeout";
return false;
}
if (!loginSuccess) {
return false;
}
loggedIn = true;
return true;
}
bool ImapSynchronizer::syncFolder(const Folder& folder)
{
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// Select folder
QString selectCmd = QString("SELECT \"%1\"").arg(folder.name());
QString selectResp;
if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(" OK ")) {
qWarning() << "SELECT failed for" << folder.name();
return false;
}
// Get all UIDs
QString searchCmd = "UID SEARCH ALL";
QString searchResp;
if (!conn.sendCommandWait(searchCmd, searchResp, 30000) || !searchResp.contains(" OK ")) {
qWarning() << "SEARCH failed";
return false;
}
QVector<qint64> uids;
QStringList lines = searchResp.split('\n');
for (const QString& line : lines) {
if (line.startsWith("* SEARCH")) {
QStringList parts = line.split(QRegularExpression("\\s+"));
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
bool ok;
qint64 uid = parts[i].toLongLong(&ok);
if (ok && uid > 0) uids.append(uid);
}
break;
}
}
// If no messages, set unread count to 0 and exit
if (uids.isEmpty()) {
Folder f = folder;
f.setUnreadCount(0);
FolderDao::update(f);
conn.disconnect();
return true;
}
// Get flags for all UIDs in one command
QStringList uidStrs;
for (qint64 uid : uids) uidStrs.append(QString::number(uid));
QString fetchCmd = "UID FETCH " + uidStrs.join(',') + " (FLAGS)";
QString fetchResp;
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000) || !fetchResp.contains(" OK ")) {
qWarning() << "FETCH FLAGS failed";
conn.disconnect();
return false;
}
// Parse flags and update local DB
QMap<qint64, QPair<bool,bool>> flagsMap; // uid -> (seen, flagged)
QRegularExpression flagsRx(R"(UID (\d+).*FLAGS \(([^)]*)\))");
auto it = flagsRx.globalMatch(fetchResp);
while (it.hasNext()) {
auto m = it.next();
qint64 uid = m.captured(1).toLongLong();
QString flags = m.captured(2);
bool seen = flags.contains("\\Seen");
bool flagged = flags.contains("\\Flagged");
flagsMap[uid] = qMakePair(seen, flagged);
}
// Update local items
auto localItems = MailItemDao::findByFolderId(folder.id());
for (MailItem& item : localItems) {
if (flagsMap.contains(item.uid())) {
bool seen = flagsMap[item.uid()].first;
bool flagged = flagsMap[item.uid()].second;
if (item.isRead() != seen || item.isFlagged() != flagged) {
item.setRead(seen);
item.setFlagged(flagged);
MailItemDao::update(item);
}
}
}
// Count unread
int unread = 0;
for (const MailItem& item : localItems) {
if (!item.isRead()) unread++;
}
Folder f = folder;
f.setUnreadCount(unread);
FolderDao::update(f);
conn.disconnect();
return true;
}
QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid)
{
QVector<MailItem> items;
ImapConnection conn;
if (!connectAndLogin(conn)) {
return items;
}
// Get folder name from DB
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) {
qWarning() << "Folder not found for id" << folderId;
return items;
}
QString folderName = folderOpt->name();
// SELECT with real folder name
QString selectCommand = QString("SELECT \"%1\"").arg(folderName);
QString selectResponse;
if (!conn.sendCommandWait(selectCommand, selectResponse, 30000)) {
qWarning() << "SELECT command failed:" << selectResponse;
return items;
}
if (!selectResponse.contains(" OK ")) {
qWarning() << "SELECT failed:" << selectResponse;
return items;
}
// SEARCH
QString searchCommand;
if (sinceUid > 0) {
searchCommand = QString("UID SEARCH %1:*").arg(sinceUid);
} else {
searchCommand = "UID SEARCH ALL";
}
QString searchResponse;
if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) {
qWarning() << "SEARCH command failed:" << searchResponse;
return items;
}
// Parse UIDs (robust: look for line starting with "* SEARCH")
QVector<qint64> uids;
QStringList lines = searchResponse.split('\n');
for (const QString& line : lines) {
if (line.startsWith("* SEARCH")) {
QStringList parts = line.split(QRegularExpression("\\s+"));
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
bool ok;
qint64 uid = parts[i].toLongLong(&ok);
if (ok && uid > 0) uids.append(uid);
}
break;
}
}
if (uids.isEmpty()) {
conn.sendCommandWait("CLOSE", searchResponse, 30000);
conn.disconnect();
return items;
}
// Fetch in batches of 50 UIDs
const int batchSize = 50;
for (int i = 0; i < uids.size(); i += batchSize) {
QVector<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); // remove trailing comma
// Fetch headers and flags (efficient)
QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)")
.arg(batchList);
QString fetchResponse;
if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) {
qWarning() << "FETCH failed for batch" << i;
continue;
}
// Parse and persist
QVector<MailItem> batchItems = parseFetchResponse(fetchResponse.split('\n'));
for (MailItem& item : batchItems) {
item.setFolderId(fid);
if (MailItemDao::insert(item)) {
items.append(item);
} else {
qWarning() << "Failed to insert mail item uid" << item.uid();
}
}
}
// Close and disconnect
QString dummy;
conn.sendCommandWait("CLOSE", dummy, 30000);
conn.disconnect();
return items;
}
QVector<Folder> ImapSynchronizer::getFolders() const
{
QVector<Folder> folders;
ImapConnection conn;
if (!connectAndLogin(conn)) {
return folders;
}
// List folders: LIST "" "*"
QString listCommand = QStringLiteral("LIST \"\" \"*\"");
QString response;
if (!conn.sendCommandWait(listCommand, response, 30000)) {
qWarning() << "LIST command failed:" << response;
return folders;
}
// Parse the response
QStringList lines = response.split('\n');
folders = parseListResponse(lines);
// Logout
conn.disconnect();
return folders;
}
bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
{
// Get folder name
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) {
qWarning() << "Folder not found for id" << folderId;
return false;
}
QString folderName = folderOpt->name();
// Build RFC822 message (headers + body)
QStringList headers;
if (!item.sender().isEmpty()) headers << "From: " + item.sender();
if (!item.recipient().isEmpty()) headers << "To: " + item.recipient();
if (!item.cc().isEmpty()) headers << "Cc: " + item.cc();
if (!item.bcc().isEmpty()) headers << "Bcc: " + item.bcc();
if (!item.subject().isEmpty()) headers << "Subject: " + item.subject();
headers << "Date: " + item.date().toString(Qt::RFC2822Date);
headers << "MIME-Version: 1.0";
headers << "Content-Type: text/html; charset=UTF-8";
headers << "Content-Transfer-Encoding: 7bit";
headers << ""; // blank line separates headers from body
headers << item.bodyHtml(); // using bodyHtml as the body
QString message = headers.join("\r\n");
QByteArray msgData = message.toUtf8();
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// SELECT folder (some servers require it)
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
QString resp;
conn.sendCommandWait(selectCmd, resp, 30000); // ignore response
// APPEND with literal: APPEND "folder" (\Seen) {size}
QString appendCmd = QString("APPEND \"%1\" (\\Seen) {%2}").arg(folderName).arg(msgData.size());
QString response;
if (!conn.sendCommandWait(appendCmd, response, 30000)) {
qWarning() << "APPEND command initial failed:" << response;
conn.disconnect();
return false;
}
// Send the literal (message data)
conn.sendRaw(QString::fromUtf8(msgData));
// Wait for the tagged response (tag + OK/NO)
if (!conn.sendCommandWait("", response, 30000)) { // dummy command to get response
qWarning() << "APPEND literal failed:" << response;
conn.disconnect();
return false;
}
conn.disconnect();
return true;
}
bool ImapSynchronizer::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged)
{
qint64 uid = itemUid.toLongLong();
if (uid <= 0) return false;
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) return false;
QString folderName = folderOpt->name();
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// SELECT
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
QString resp;
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "SELECT failed";
return false;
}
// STORE for Seen flag
QString seenFlag = read ? "+" : "-";
QString storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Seen").arg(uid).arg(seenFlag);
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "STORE Seen failed:" << resp;
conn.disconnect();
return false;
}
// STORE for Flagged flag
QString flaggedFlag = flagged ? "+" : "-";
storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Flagged").arg(uid).arg(flaggedFlag);
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "STORE Flagged failed:" << resp;
conn.disconnect();
return false;
}
conn.disconnect();
// Update local DB
auto items = MailItemDao::findByFolderId(fid);
for (MailItem& item : items) {
if (item.uid() == uid) {
item.setRead(read);
item.setFlagged(flagged);
MailItemDao::update(item);
break;
}
}
return true;
}
bool ImapSynchronizer::deleteMailItem(const QString& folderId, const QString& itemUid)
{
qint64 uid = itemUid.toLongLong();
if (uid <= 0) return false;
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) return false;
QString folderName = folderOpt->name();
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// SELECT
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
QString resp;
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "SELECT failed";
return false;
}
// Mark as \\Deleted
QString storeCmd = QString("UID STORE %1 +FLAGS.SILENT \\Deleted").arg(uid);
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "STORE Deleted failed:" << resp;
conn.disconnect();
return false;
}
// EXPUNGE (permanently delete)
QString expungeCmd = "EXPUNGE";
if (!conn.sendCommandWait(expungeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "EXPUNGE failed:" << resp;
conn.disconnect();
return false;
}
conn.disconnect();
// Remove from local DB
MailItemDao::remove(uid);
return true;
}
QString ImapSynchronizer::generateEventId() const
{
return QString::number(QDateTime::currentMSecsSinceEpoch());
}
QVector<Folder> ImapSynchronizer::parseListResponse(const QStringList& lines) const
{
QVector<Folder> folders;
QRegularExpression rx("\\* LIST \\\\([^)]*\\\\) \"([^\"]*)\" \"([^\"]*)\"");
for (const QString& line : lines) {
QRegularExpressionMatch match = rx.match(line);
if (match.hasMatch()) {
QString delimiter = match.captured(1); // unused for now
QString mailbox = match.captured(2);
Folder folder;
folder.setName(mailbox);
QString lower = mailbox.toLower();
if (lower == "inbox") folder.setInbox(true);
else if (lower == "sent") folder.setSent(true);
else if (lower == "drafts") folder.setDrafts(true);
else if (lower == "trash" || lower == "deleted items") folder.setTrash(true);
folders.append(folder);
}
}
return folders;
}
QVector<MailItem> ImapSynchronizer::parseFetchResponse(const QStringList& lines) const
{
QVector<MailItem> items;
int i = 0;
while (i < lines.size()) {
const QString& line = lines[i];
if (!line.startsWith(QStringLiteral("* "))) {
++i;
continue;
}
int fetchPos = line.indexOf(QStringLiteral(" FETCH ("));
if (fetchPos == -1) {
++i;
continue;
}
// Extract UID
int uidPos = line.indexOf(QStringLiteral("UID "));
if (uidPos == -1) {
++i;
continue;
}
int uidEnd = line.indexOf(QRegularExpression(QStringLiteral("[\\s)]")), uidPos + 4);
if (uidEnd == -1) uidEnd = line.length();
QString uidStr = line.mid(uidPos + 4, uidEnd - uidPos - 4);
bool ok;
qint64 uid = uidStr.toLongLong(&ok);
if (!ok) {
++i;
continue;
}
MailItem item;
item.setUid(uid);
// Extract FLAGS
int flagsPos = line.indexOf(QStringLiteral("FLAGS ("));
if (flagsPos != -1) {
int flagsEnd = line.indexOf(')', flagsPos + 7);
if (flagsEnd != -1) {
QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7);
item.setRead(flags.contains(QStringLiteral("\\Seen")));
item.setFlagged(flags.contains(QStringLiteral("\\Flagged")));
}
}
// Extract INTERNALDATE
int datePos = line.indexOf(QStringLiteral("INTERNALDATE \""));
if (datePos != -1) {
int dateStart = datePos + 15; // length of "INTERNALDATE \""
int dateEnd = line.indexOf('\"', dateStart);
if (dateEnd != -1) {
QString dateStr = line.mid(dateStart, dateEnd - dateStart);
dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000"
QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date);
if (dt.isValid())
item.setDate(dt);
}
}
// Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines
QString bodyData;
int bodyPos = line.indexOf(QStringLiteral("BODY["));
if (bodyPos != -1) {
int bracePos = line.indexOf('{', bodyPos);
if (bracePos != -1) {
int sizeEnd = line.indexOf('}', bracePos);
if (sizeEnd != -1) {
bool sizeOk;
int size = line.mid(bracePos + 1, sizeEnd - bracePos - 1).toInt(&sizeOk);
if (sizeOk) {
int dataStart = line.indexOf(QStringLiteral("\r\n"), sizeEnd);
if (dataStart != -1) {
dataStart += 2; // skip \r\n
int available = line.length() - dataStart;
if (available > size) available = size;
bodyData = line.mid(dataStart, available);
int received = available;
int remaining = size - received;
int j = i + 1;
while (j < lines.size() && remaining > 0) {
const QString& nextLine = lines[j];
int take = qMin(nextLine.length(), remaining);
bodyData += nextLine.left(take);
remaining -= take;
++j;
}
// we have consumed lines up to j-1
i = j - 1; // will be incremented at end of loop
}
}
}
}
}
// Parse headers from bodyData
if (!bodyData.isEmpty()) {
QRegularExpression headerRx(QStringLiteral(R"((^([^:]+):\s*(.*?)\r\n))"), QRegularExpression::MultilineOption);
auto it = headerRx.globalMatch(bodyData);
while (it.hasNext()) {
auto match = it.next();
QString key = match.captured(2).toLower().trimmed();
QString value = match.captured(3).trimmed();
if (key == QStringLiteral("subject"))
item.setSubject(value);
else if (key == QStringLiteral("from"))
item.setSender(value);
else if (key == QStringLiteral("to"))
item.setRecipient(value);
else if (key == QStringLiteral("date")) {
QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date);
if (dt.isValid())
item.setDate(dt);
}
}
}
// If subject not set, use placeholder
if (item.subject().isEmpty())
item.setSubject(QStringLiteral("(No Subject)"));
items.append(item);
++i;
}
return items;
}
+723
View File
@@ -0,0 +1,723 @@
#include "imapsynchronizer.h"
#include <QEventLoop>
#include <QTimer>
#include <QDebug>
#include <QRegularExpression>
#include "../../core/mailitem.h"
#include "../../core/models/account.h"
#include "../../core/models/folder.h"
#include "../../db/dao/folderdao.h"
#include "../../db/dao/mailitemdao.h"
#include <algorithm>
ImapSynchronizer::ImapSynchronizer(QObject* parent)
: Synchronizer(parent)
{
}
bool ImapSynchronizer::initialize(const Account& account)
{
const Account::ConnectionSettings& settings = account.connectionSettings();
m_host = settings.incomingHost;
m_port = settings.incomingPort;
m_useSsl = settings.incomingSsl;
m_username = settings.username;
m_password = settings.password;
m_authMethod = settings.authMethod;
return true;
}
// Helper: connects and logs in to the IMAP server
bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
{
bool connected = false;
bool loggedIn = false;
QString response;
// Connect to host
QEventLoop connectLoop;
QTimer connectTimer;
bool connectTimeout = false;
connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit);
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
qWarning() << "IMAP connection error:" << err;
connectLoop.quit();
});
connectTimer.setSingleShot(true);
connect(&connectTimer, &QTimer::timeout, &connectLoop, [&](){
connectTimeout = true;
connectLoop.quit();
});
connectTimer.start(30000); // 30 seconds
conn.connectToHost(m_host, m_port, m_useSsl);
connectLoop.exec();
if (connectTimeout) {
qWarning() << "Connection timeout to" << m_host;
return false;
}
connected = true;
// Wait for server greeting (first untagged response)
QEventLoop greetLoop;
bool gotGreeting = false;
QMetaObject::Connection greetConn = connect(&conn, &ImapConnection::untaggedResponse,
[&](const QString& line) {
Q_UNUSED(line);
gotGreeting = true;
greetLoop.quit();
});
QTimer greetTimer;
greetTimer.setSingleShot(true);
connect(&greetTimer, &QTimer::timeout, &greetLoop, [&]() {
greetLoop.quit();
});
greetTimer.start(30000); // 30 seconds
greetLoop.exec();
QObject::disconnect(greetConn);
if (!gotGreeting) {
qWarning() << "Timeout waiting for server greeting";
return false;
}
// Login
QEventLoop loginLoop;
QTimer loginTimer;
bool loginTimeout = false;
bool loginSuccess = false;
// Already connected, now login
conn.login(m_username, m_password, true, [&](bool ok, const QString& resp){
if (ok) {
loginSuccess = true;
} else {
qWarning() << "Login failed:" << resp;
}
loginLoop.quit();
});
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
qWarning() << "IMAP error during login:" << err;
loginLoop.quit();
});
loginTimer.setSingleShot(true);
connect(&loginTimer, &QTimer::timeout, &loginLoop, [&](){
loginTimeout = true;
loginLoop.quit();
});
loginTimer.start(30000);
loginLoop.exec();
if (loginTimeout) {
qWarning() << "Login timeout";
return false;
}
if (!loginSuccess) {
return false;
}
loggedIn = true;
return true;
}
QVector<qint64> ImapSynchronizer::parseUidSearchResponse(const QString& response) const
{
QVector<qint64> uids;
QStringList lines = response.split(QStringLiteral("\n"));
for (const QString& line : lines) {
if (line.startsWith(QStringLiteral("* SEARCH"))) {
QStringList parts = line.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
bool ok;
qint64 uid = parts[i].toLongLong(&ok);
if (ok && uid > 0)
uids.append(uid);
}
break;
}
}
return uids;
}
bool ImapSynchronizer::syncFolder(const Folder& folder)
{
ImapConnection conn;
if (!connectAndLogin(conn))
return false;
// Select folder
QString selectCmd = QStringLiteral("SELECT \"%1\"").arg(folder.name());
QString selectResp;
if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(QStringLiteral(" OK "))) {
qWarning() << "SELECT failed for" << folder.name();
conn.disconnect();
return false;
}
// Get all UIDs from server
QString searchAllCmd = QStringLiteral("UID SEARCH ALL");
QString searchAllResp;
if (!conn.sendCommandWait(searchAllCmd, searchAllResp, 30000) || !searchAllResp.contains(QStringLiteral(" OK "))) {
qWarning() << "UID SEARCH ALL failed";
conn.disconnect();
return false;
}
QVector<qint64> serverUids = parseUidSearchResponse(searchAllResp);
std::sort(serverUids.begin(), serverUids.end());
// Get local UIDs for this folder
int fid = folder.id();
QVector<qint64> localUids = MailItemDao::getUidsForFolder(fid);
std::sort(localUids.begin(), localUids.end());
// Determine last known UID (max local uid)
qint64 lastUid = 0;
if (!localUids.isEmpty())
lastUid = localUids.last();
// Fetch new UIDs (those > lastUid)
QVector<qint64> newUids;
if (lastUid == 0) {
// No local mails, treat all as new
newUids = serverUids;
} else {
for (qint64 uid : serverUids) {
if (uid > lastUid)
newUids.append(uid);
}
}
// Fetch new messages in batches
if (!newUids.isEmpty()) {
const int batchSize = 50;
for (int i = 0; i < newUids.size(); i += batchSize) {
QVector<qint64> batch = newUids.mid(i, qMin(batchSize, newUids.size() - i));
QStringList uidStrs;
for (qint64 uid : batch)
uidStrs.append(QString::number(uid));
QString batchList = uidStrs.join(QStringLiteral(","));
QString fetchCmd = QStringLiteral("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)").arg(batchList);
QString fetchResp;
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000)) {
qWarning() << "UID FETCH failed for batch" << i;
continue;
}
if (!fetchResp.contains(QStringLiteral(" OK "))) {
qWarning() << "UID FETCH did not return OK";
continue;
}
QVector<MailItem> fetched = parseFetchResponse(fetchResp.split(QStringLiteral("\n")));
for (MailItem& item : fetched) {
item.setFolderId(fid);
if (!MailItemDao::insert(item)) {
qWarning() << "Failed to insert mail item uid" << item.uid();
}
}
}
}
// Update flags for all local uids (to capture flag changes)
if (!localUids.isEmpty()) {
const int batchSize = 100;
for (int i = 0; i < localUids.size(); i += batchSize) {
QVector<qint64> batch = localUids.mid(i, qMin(batchSize, localUids.size() - i));
QStringList uidStrs;
for (qint64 uid : batch)
uidStrs.append(QString::number(uid));
QString uidList = uidStrs.join(QStringLiteral(","));
QString fetchCmd = QStringLiteral("UID FETCH %1 (FLAGS)").arg(uidList);
QString fetchResp;
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000)) {
qWarning() << "UID FETCH FLAGS failed";
continue;
}
if (!fetchResp.contains(QStringLiteral(" OK "))) {
qWarning() << "UID FETCH FLAGS not OK";
continue;
}
// Parse flags response
QRegularExpression flagsRx(QStringLiteral(R"(UID (\d+).*FLAGS \(([^)]*)\))"));
auto it = flagsRx.globalMatch(fetchResp);
while (it.hasNext()) {
auto m = it.next();
qint64 uid = m.captured(1).toLongLong();
QString flags = m.captured(2);
bool seen = flags.contains(QStringLiteral("\\Seen"));
bool flagged = flags.contains(QStringLiteral("\\Flagged"));
std::optional<MailItem> opt = MailItemDao::findById(uid);
if (opt) {
MailItem& item = *opt;
bool changed = false;
if (item.isRead() != seen) {
item.setRead(seen);
changed = true;
}
if (item.isFlagged() != flagged) {
item.setFlagged(flagged);
changed = true;
}
if (changed)
MailItemDao::update(item);
}
}
}
}
// Handle expunged messages: remove local uids not present on server
QVector<qint64> toRemove;
std::set_difference(localUids.begin(), localUids.end(),
serverUids.begin(), serverUids.end(),
std::back_inserter(toRemove));
for (qint64 uid : toRemove) {
MailItemDao::remove(uid);
}
// Update unread count
int unread = 0;
for (qint64 uid : localUids) {
std::optional<MailItem> opt = MailItemDao::findById(uid);
if (opt && !opt->isRead())
++unread;
}
Folder f = folder;
f.setUnreadCount(unread);
if (!FolderDao::update(f)) {
qWarning() << "Failed to update folder unread count";
}
conn.disconnect();
return true;
}
QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid)
{
QVector<MailItem> items;
ImapConnection conn;
if (!connectAndLogin(conn)) {
return items;
}
// Get folder name from DB
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) {
qWarning() << "Folder not found for id" << folderId;
return items;
}
QString folderName = folderOpt->name();
// SELECT with real folder name
QString selectCommand = QString("SELECT \"%1\"").arg(folderName);
QString selectResponse;
if (!conn.sendCommandWait(selectCommand, selectResponse, 30000)) {
qWarning() << "SELECT command failed:" << selectResponse;
return items;
}
if (!selectResponse.contains(" OK ")) {
qWarning() << "SELECT failed:" << selectResponse;
return items;
}
// SEARCH
QString searchCommand;
if (sinceUid > 0) {
searchCommand = QString("UID SEARCH %1:*").arg(sinceUid);
} else {
searchCommand = "UID SEARCH ALL";
}
QString searchResponse;
if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) {
qWarning() << "SEARCH command failed:" << searchResponse;
return items;
}
// Parse UIDs (robust: look for line starting with "* SEARCH")
QVector<qint64> uids;
QStringList lines = searchResponse.split('\n');
for (const QString& line : lines) {
if (line.startsWith("* SEARCH")) {
QStringList parts = line.split(QRegularExpression("\\s+"));
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
bool ok;
qint64 uid = parts[i].toLongLong(&ok);
if (ok && uid > 0) uids.append(uid);
}
break;
}
}
if (uids.isEmpty()) {
conn.sendCommandWait("CLOSE", searchResponse, 30000);
conn.disconnect();
return items;
}
// Fetch in batches of 50 UIDs
const int batchSize = 50;
for (int i = 0; i < uids.size(); i += batchSize) {
QVector<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); // remove trailing comma
// Fetch headers and flags (efficient)
QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)")
.arg(batchList);
QString fetchResponse;
if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) {
qWarning() << "FETCH failed for batch" << i;
continue;
}
// Parse and persist
QVector<MailItem> batchItems = parseFetchResponse(fetchResponse.split('\n'));
for (MailItem& item : batchItems) {
item.setFolderId(fid);
if (MailItemDao::insert(item)) {
items.append(item);
} else {
qWarning() << "Failed to insert mail item uid" << item.uid();
}
}
}
// Close and disconnect
QString dummy;
conn.sendCommandWait("CLOSE", dummy, 30000);
conn.disconnect();
return items;
}
QVector<Folder> ImapSynchronizer::getFolders() const
{
QVector<Folder> folders;
ImapConnection conn;
if (!connectAndLogin(conn)) {
return folders;
}
// List folders: LIST "" "*"
QString listCommand = QStringLiteral("LIST \"\" \"*\"");
QString response;
if (!conn.sendCommandWait(listCommand, response, 30000)) {
qWarning() << "LIST command failed:" << response;
return folders;
}
// Parse the response
QStringList lines = response.split('\n');
folders = parseListResponse(lines);
// Logout
conn.disconnect();
return folders;
}
bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
{
// Get folder name
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) {
qWarning() << "Folder not found for id" << folderId;
return false;
}
QString folderName = folderOpt->name();
// Build RFC822 message (headers + body)
QStringList headers;
if (!item.sender().isEmpty()) headers << "From: " + item.sender();
if (!item.recipient().isEmpty()) headers << "To: " + item.recipient();
if (!item.cc().isEmpty()) headers << "Cc: " + item.cc();
if (!item.bcc().isEmpty()) headers << "Bcc: " + item.bcc();
if (!item.subject().isEmpty()) headers << "Subject: " + item.subject();
headers << "Date: " + item.date().toString(Qt::RFC2822Date);
headers << "MIME-Version: 1.0";
headers << "Content-Type: text/html; charset=UTF-8";
headers << "Content-Transfer-Encoding: 7bit";
headers << ""; // blank line separates headers from body
headers << item.bodyHtml(); // using bodyHtml as the body
QString message = headers.join("\r\n");
QByteArray msgData = message.toUtf8();
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// SELECT folder (some servers require it)
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
QString resp;
conn.sendCommandWait(selectCmd, resp, 30000); // ignore response
// APPEND with literal: APPEND "folder" (\Seen) {size}
QString appendCmd = QString("APPEND \"%1\" (\\Seen) {%2}").arg(folderName).arg(msgData.size());
QString response;
if (!conn.sendCommandWait(appendCmd, response, 30000)) {
qWarning() << "APPEND command initial failed:" << response;
conn.disconnect();
return false;
}
// Send the literal (message data)
conn.sendRaw(QString::fromUtf8(msgData));
// Wait for the tagged response (tag + OK/NO)
if (!conn.sendCommandWait("", response, 30000)) { // dummy command to get response
qWarning() << "APPEND literal failed:" << response;
conn.disconnect();
return false;
}
conn.disconnect();
return true;
}
bool ImapSynchronizer::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged)
{
qint64 uid = itemUid.toLongLong();
if (uid <= 0) return false;
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) return false;
QString folderName = folderOpt->name();
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// SELECT
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
QString resp;
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "SELECT failed";
return false;
}
// STORE for Seen flag
QString seenFlag = read ? "+" : "-";
QString storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Seen").arg(uid).arg(seenFlag);
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "STORE Seen failed:" << resp;
conn.disconnect();
return false;
}
// STORE for Flagged flag
QString flaggedFlag = flagged ? "+" : "-";
storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Flagged").arg(uid).arg(flaggedFlag);
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "STORE Flagged failed:" << resp;
conn.disconnect();
return false;
}
conn.disconnect();
// Update local DB
auto items = MailItemDao::findByFolderId(fid);
for (MailItem& item : items) {
if (item.uid() == uid) {
item.setRead(read);
item.setFlagged(flagged);
MailItemDao::update(item);
break;
}
}
return true;
}
bool ImapSynchronizer::deleteMailItem(const QString& folderId, const QString& itemUid)
{
qint64 uid = itemUid.toLongLong();
if (uid <= 0) return false;
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) return false;
QString folderName = folderOpt->name();
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// SELECT
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
QString resp;
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "SELECT failed";
return false;
}
// Mark as \\Deleted
QString storeCmd = QString("UID STORE %1 +FLAGS.SILENT \\Deleted").arg(uid);
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "STORE Deleted failed:" << resp;
conn.disconnect();
return false;
}
// EXPUNGE (permanently delete)
QString expungeCmd = "EXPUNGE";
if (!conn.sendCommandWait(expungeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "EXPUNGE failed:" << resp;
conn.disconnect();
return false;
}
conn.disconnect();
// Remove from local DB
MailItemDao::remove(uid);
return true;
}
QString ImapSynchronizer::generateEventId() const
{
return QString::number(QDateTime::currentMSecsSinceEpoch());
}
QVector<Folder> ImapSynchronizer::parseListResponse(const QStringList& lines) const
{
QVector<Folder> folders;
QRegularExpression rx("\\* LIST \\\\([^)]*\\\\) \"([^\"]*)\" \"([^\"]*)\"");
for (const QString& line : lines) {
QRegularExpressionMatch match = rx.match(line);
if (match.hasMatch()) {
QString delimiter = match.captured(1); // unused for now
QString mailbox = match.captured(2);
Folder folder;
folder.setName(mailbox);
QString lower = mailbox.toLower();
if (lower == "inbox") folder.setInbox(true);
else if (lower == "sent") folder.setSent(true);
else if (lower == "drafts") folder.setDrafts(true);
else if (lower == "trash" || lower == "deleted items") folder.setTrash(true);
folders.append(folder);
}
}
return folders;
}
QVector<MailItem> ImapSynchronizer::parseFetchResponse(const QStringList& lines) const
{
QVector<MailItem> items;
int i = 0;
while (i < lines.size()) {
const QString& line = lines[i];
if (!line.startsWith(QStringLiteral("* "))) {
++i;
continue;
}
int fetchPos = line.indexOf(QStringLiteral(" FETCH ("));
if (fetchPos == -1) {
++i;
continue;
}
// Extract UID
int uidPos = line.indexOf(QStringLiteral("UID "));
if (uidPos == -1) {
++i;
continue;
}
int uidEnd = line.indexOf(QRegularExpression(QStringLiteral("[\\s)]")), uidPos + 4);
if (uidEnd == -1) uidEnd = line.length();
QString uidStr = line.mid(uidPos + 4, uidEnd - uidPos - 4);
bool ok;
qint64 uid = uidStr.toLongLong(&ok);
if (!ok) {
++i;
continue;
}
MailItem item;
item.setUid(uid);
// Extract FLAGS
int flagsPos = line.indexOf(QStringLiteral("FLAGS ("));
if (flagsPos != -1) {
int flagsEnd = line.indexOf(')', flagsPos + 7);
if (flagsEnd != -1) {
QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7);
item.setRead(flags.contains(QStringLiteral("\\Seen")));
item.setFlagged(flags.contains(QStringLiteral("\\Flagged")));
}
}
// Extract INTERNALDATE
int datePos = line.indexOf(QStringLiteral("INTERNALDATE \""));
if (datePos != -1) {
int dateStart = datePos + 15; // length of "INTERNALDATE \""
int dateEnd = line.indexOf('\"', dateStart);
if (dateEnd != -1) {
QString dateStr = line.mid(dateStart, dateEnd - dateStart);
dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000"
QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date);
if (dt.isValid())
item.setDate(dt);
}
}
// Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines
QString bodyData;
int bodyPos = line.indexOf(QStringLiteral("BODY["));
if (bodyPos != -1) {
int bracePos = line.indexOf('{', bodyPos);
if (bracePos != -1) {
int sizeEnd = line.indexOf('}', bracePos);
if (sizeEnd != -1) {
bool sizeOk;
int size = line.mid(bracePos + 1, sizeEnd - bracePos - 1).toInt(&sizeOk);
if (sizeOk) {
int dataStart = line.indexOf(QStringLiteral("\r\n"), sizeEnd);
if (dataStart != -1) {
dataStart += 2; // skip \r\n
int available = line.length() - dataStart;
if (available > size) available = size;
bodyData = line.mid(dataStart, available);
int received = available;
int remaining = size - received;
int j = i + 1;
while (j < lines.size() && remaining > 0) {
const QString& nextLine = lines[j];
int take = qMin(nextLine.length(), remaining);
bodyData += nextLine.left(take);
remaining -= take;
++j;
}
// we have consumed lines up to j-1
i = j - 1; // will be incremented at end of loop
}
}
}
}
}
// Parse headers from bodyData
if (!bodyData.isEmpty()) {
QRegularExpression headerRx(QStringLiteral(R"((^([^:]+):\s*(.*?)\r\n))"), QRegularExpression::MultilineOption);
auto it = headerRx.globalMatch(bodyData);
while (it.hasNext()) {
auto match = it.next();
QString key = match.captured(2).toLower().trimmed();
QString value = match.captured(3).trimmed();
if (key == QStringLiteral("subject"))
item.setSubject(value);
else if (key == QStringLiteral("from"))
item.setSender(value);
else if (key == QStringLiteral("to"))
item.setRecipient(value);
else if (key == QStringLiteral("date")) {
QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date);
if (dt.isValid())
item.setDate(dt);
}
}
}
// If subject not set, use placeholder
if (item.subject().isEmpty())
item.setSubject(QStringLiteral("(No Subject)"));
items.append(item);
++i;
}
return items;
}
+723
View File
@@ -0,0 +1,723 @@
#include "imapsynchronizer.h"
#include <QEventLoop>
#include <QTimer>
#include <QDebug>
#include <QRegularExpression>
#include "../../core/mailitem.h"
#include "../../core/models/account.h"
#include "../../core/models/folder.h"
#include "../../db/dao/folderdao.h"
#include "../../db/dao/mailitemdao.h"
#include <algorithm>
ImapSynchronizer::ImapSynchronizer(QObject* parent)
: Synchronizer(parent)
{
}
bool ImapSynchronizer::initialize(const Account& account)
{
const Account::ConnectionSettings& settings = account.connectionSettings();
m_host = settings.incomingHost;
m_port = settings.incomingPort;
m_useSsl = settings.incomingSsl;
m_username = settings.username;
m_password = settings.password;
m_authMethod = settings.authMethod;
return true;
}
// Helper: connects and logs in to the IMAP server
bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
{
bool connected = false;
bool loggedIn = false;
QString response;
// Connect to host
QEventLoop connectLoop;
QTimer connectTimer;
bool connectTimeout = false;
connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit);
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
qWarning() << "IMAP connection error:" << err;
connectLoop.quit();
});
connectTimer.setSingleShot(true);
connect(&connectTimer, &QTimer::timeout, &connectLoop, [&](){
connectTimeout = true;
connectLoop.quit();
});
connectTimer.start(30000); // 30 seconds
conn.connectToHost(m_host, m_port, m_useSsl);
connectLoop.exec();
if (connectTimeout) {
qWarning() << "Connection timeout to" << m_host;
return false;
}
connected = true;
// Wait for server greeting (first untagged response)
QEventLoop greetLoop;
bool gotGreeting = false;
QMetaObject::Connection greetConn = connect(&conn, &ImapConnection::untaggedResponse,
[&](const QString& line) {
Q_UNUSED(line);
gotGreeting = true;
greetLoop.quit();
});
QTimer greetTimer;
greetTimer.setSingleShot(true);
connect(&greetTimer, &QTimer::timeout, &greetLoop, [&]() {
greetLoop.quit();
});
greetTimer.start(30000); // 30 seconds
greetLoop.exec();
QObject::disconnect(greetConn);
if (!gotGreeting) {
qWarning() << "Timeout waiting for server greeting";
return false;
}
// Login
QEventLoop loginLoop;
QTimer loginTimer;
bool loginTimeout = false;
bool loginSuccess = false;
// Already connected, now login
conn.login(m_username, m_password, true, [&](bool ok, const QString& resp){
if (ok) {
loginSuccess = true;
} else {
qWarning() << "Login failed:" << resp;
}
loginLoop.quit();
});
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
qWarning() << "IMAP error during login:" << err;
loginLoop.quit();
});
loginTimer.setSingleShot(true);
connect(&loginTimer, &QTimer::timeout, &loginLoop, [&](){
loginTimeout = true;
loginLoop.quit();
});
loginTimer.start(30000);
loginLoop.exec();
if (loginTimeout) {
qWarning() << "Login timeout";
return false;
}
if (!loginSuccess) {
return false;
}
loggedIn = true;
return true;
}
QVector<qint64> ImapSynchronizer::parseUidSearchResponse(const QString& response) const
{
QVector<qint64> uids;
QStringList lines = response.split(QStringLiteral("\n"));
for (const QString& line : lines) {
if (line.startsWith(QStringLiteral("* SEARCH"))) {
QStringList parts = line.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
bool ok;
qint64 uid = parts[i].toLongLong(&ok);
if (ok && uid > 0)
uids.append(uid);
}
break;
}
}
return uids;
}
bool ImapSynchronizer::syncFolder(const Folder& folder)
{
ImapConnection conn;
if (!connectAndLogin(conn))
return false;
// Select folder
QString selectCmd = QStringLiteral("SELECT \"%1\"").arg(folder.name());
QString selectResp;
if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(QStringLiteral(" OK "))) {
qWarning() << "SELECT failed for" << folder.name();
conn.disconnect();
return false;
}
// Get all UIDs from server
QString searchAllCmd = QStringLiteral("UID SEARCH ALL");
QString searchAllResp;
if (!conn.sendCommandWait(searchAllCmd, searchAllResp, 30000) || !searchAllResp.contains(QStringLiteral(" OK "))) {
qWarning() << "UID SEARCH ALL failed";
conn.disconnect();
return false;
}
QVector<qint64> serverUids = parseUidSearchResponse(searchAllResp);
std::sort(serverUids.begin(), serverUids.end());
// Get local UIDs for this folder
int fid = folder.id();
QVector<qint64> localUids = MailItemDao::getUidsForFolder(fid);
std::sort(localUids.begin(), localUids.end());
// Determine last known UID (max local uid)
qint64 lastUid = 0;
if (!localUids.isEmpty())
lastUid = localUids.last();
// Fetch new UIDs (those > lastUid)
QVector<qint64> newUids;
if (lastUid == 0) {
// No local mails, treat all as new
newUids = serverUids;
} else {
for (qint64 uid : serverUids) {
if (uid > lastUid)
newUids.append(uid);
}
}
// Fetch new messages in batches
if (!newUids.isEmpty()) {
const int batchSize = 50;
for (int i = 0; i < newUids.size(); i += batchSize) {
QVector<qint64> batch = newUids.mid(i, qMin(batchSize, newUids.size() - i));
QStringList uidStrs;
for (qint64 uid : batch)
uidStrs.append(QString::number(uid));
QString batchList = uidStrs.join(QStringLiteral(","));
QString fetchCmd = QStringLiteral("UID FETCH %1 (BODY[] FLAGS INTERNALDATE)").arg(batchList);
QString fetchResp;
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000)) {
qWarning() << "UID FETCH failed for batch" << i;
continue;
}
if (!fetchResp.contains(QStringLiteral(" OK "))) {
qWarning() << "UID FETCH did not return OK";
continue;
}
QVector<MailItem> fetched = parseFetchResponse(fetchResp.split(QStringLiteral("\n")));
for (MailItem& item : fetched) {
item.setFolderId(fid);
if (!MailItemDao::insert(item)) {
qWarning() << "Failed to insert mail item uid" << item.uid();
}
}
}
}
// Update flags for all local uids (to capture flag changes)
if (!localUids.isEmpty()) {
const int batchSize = 100;
for (int i = 0; i < localUids.size(); i += batchSize) {
QVector<qint64> batch = localUids.mid(i, qMin(batchSize, localUids.size() - i));
QStringList uidStrs;
for (qint64 uid : batch)
uidStrs.append(QString::number(uid));
QString uidList = uidStrs.join(QStringLiteral(","));
QString fetchCmd = QStringLiteral("UID FETCH %1 (FLAGS)").arg(uidList);
QString fetchResp;
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000)) {
qWarning() << "UID FETCH FLAGS failed";
continue;
}
if (!fetchResp.contains(QStringLiteral(" OK "))) {
qWarning() << "UID FETCH FLAGS not OK";
continue;
}
// Parse flags response
QRegularExpression flagsRx(QStringLiteral(R"(UID (\d+).*FLAGS \(([^)]*)\))"));
auto it = flagsRx.globalMatch(fetchResp);
while (it.hasNext()) {
auto m = it.next();
qint64 uid = m.captured(1).toLongLong();
QString flags = m.captured(2);
bool seen = flags.contains(QStringLiteral("\\Seen"));
bool flagged = flags.contains(QStringLiteral("\\Flagged"));
std::optional<MailItem> opt = MailItemDao::findById(uid);
if (opt) {
MailItem& item = *opt;
bool changed = false;
if (item.isRead() != seen) {
item.setRead(seen);
changed = true;
}
if (item.isFlagged() != flagged) {
item.setFlagged(flagged);
changed = true;
}
if (changed)
MailItemDao::update(item);
}
}
}
}
// Handle expunged messages: remove local uids not present on server
QVector<qint64> toRemove;
std::set_difference(localUids.begin(), localUids.end(),
serverUids.begin(), serverUids.end(),
std::back_inserter(toRemove));
for (qint64 uid : toRemove) {
MailItemDao::remove(uid);
}
// Update unread count
int unread = 0;
for (qint64 uid : localUids) {
std::optional<MailItem> opt = MailItemDao::findById(uid);
if (opt && !opt->isRead())
++unread;
}
Folder f = folder;
f.setUnreadCount(unread);
if (!FolderDao::update(f)) {
qWarning() << "Failed to update folder unread count";
}
conn.disconnect();
return true;
}
QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid)
{
QVector<MailItem> items;
ImapConnection conn;
if (!connectAndLogin(conn)) {
return items;
}
// Get folder name from DB
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) {
qWarning() << "Folder not found for id" << folderId;
return items;
}
QString folderName = folderOpt->name();
// SELECT with real folder name
QString selectCommand = QString("SELECT \"%1\"").arg(folderName);
QString selectResponse;
if (!conn.sendCommandWait(selectCommand, selectResponse, 30000)) {
qWarning() << "SELECT command failed:" << selectResponse;
return items;
}
if (!selectResponse.contains(" OK ")) {
qWarning() << "SELECT failed:" << selectResponse;
return items;
}
// SEARCH
QString searchCommand;
if (sinceUid > 0) {
searchCommand = QString("UID SEARCH %1:*").arg(sinceUid);
} else {
searchCommand = "UID SEARCH ALL";
}
QString searchResponse;
if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) {
qWarning() << "SEARCH command failed:" << searchResponse;
return items;
}
// Parse UIDs (robust: look for line starting with "* SEARCH")
QVector<qint64> uids;
QStringList lines = searchResponse.split('\n');
for (const QString& line : lines) {
if (line.startsWith("* SEARCH")) {
QStringList parts = line.split(QRegularExpression("\\s+"));
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
bool ok;
qint64 uid = parts[i].toLongLong(&ok);
if (ok && uid > 0) uids.append(uid);
}
break;
}
}
if (uids.isEmpty()) {
conn.sendCommandWait("CLOSE", searchResponse, 30000);
conn.disconnect();
return items;
}
// Fetch in batches of 50 UIDs
const int batchSize = 50;
for (int i = 0; i < uids.size(); i += batchSize) {
QVector<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); // remove trailing comma
// Fetch headers and flags (efficient)
QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)")
.arg(batchList);
QString fetchResponse;
if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) {
qWarning() << "FETCH failed for batch" << i;
continue;
}
// Parse and persist
QVector<MailItem> batchItems = parseFetchResponse(fetchResponse.split('\n'));
for (MailItem& item : batchItems) {
item.setFolderId(fid);
if (MailItemDao::insert(item)) {
items.append(item);
} else {
qWarning() << "Failed to insert mail item uid" << item.uid();
}
}
}
// Close and disconnect
QString dummy;
conn.sendCommandWait("CLOSE", dummy, 30000);
conn.disconnect();
return items;
}
QVector<Folder> ImapSynchronizer::getFolders() const
{
QVector<Folder> folders;
ImapConnection conn;
if (!connectAndLogin(conn)) {
return folders;
}
// List folders: LIST "" "*"
QString listCommand = QStringLiteral("LIST \"\" \"*\"");
QString response;
if (!conn.sendCommandWait(listCommand, response, 30000)) {
qWarning() << "LIST command failed:" << response;
return folders;
}
// Parse the response
QStringList lines = response.split('\n');
folders = parseListResponse(lines);
// Logout
conn.disconnect();
return folders;
}
bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
{
// Get folder name
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) {
qWarning() << "Folder not found for id" << folderId;
return false;
}
QString folderName = folderOpt->name();
// Build RFC822 message (headers + body)
QStringList headers;
if (!item.sender().isEmpty()) headers << "From: " + item.sender();
if (!item.recipient().isEmpty()) headers << "To: " + item.recipient();
if (!item.cc().isEmpty()) headers << "Cc: " + item.cc();
if (!item.bcc().isEmpty()) headers << "Bcc: " + item.bcc();
if (!item.subject().isEmpty()) headers << "Subject: " + item.subject();
headers << "Date: " + item.date().toString(Qt::RFC2822Date);
headers << "MIME-Version: 1.0";
headers << "Content-Type: text/html; charset=UTF-8";
headers << "Content-Transfer-Encoding: 7bit";
headers << ""; // blank line separates headers from body
headers << item.bodyHtml(); // using bodyHtml as the body
QString message = headers.join("\r\n");
QByteArray msgData = message.toUtf8();
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// SELECT folder (some servers require it)
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
QString resp;
conn.sendCommandWait(selectCmd, resp, 30000); // ignore response
// APPEND with literal: APPEND "folder" (\Seen) {size}
QString appendCmd = QString("APPEND \"%1\" (\\Seen) {%2}").arg(folderName).arg(msgData.size());
QString response;
if (!conn.sendCommandWait(appendCmd, response, 30000)) {
qWarning() << "APPEND command initial failed:" << response;
conn.disconnect();
return false;
}
// Send the literal (message data)
conn.sendRaw(QString::fromUtf8(msgData));
// Wait for the tagged response (tag + OK/NO)
if (!conn.sendCommandWait("", response, 30000)) { // dummy command to get response
qWarning() << "APPEND literal failed:" << response;
conn.disconnect();
return false;
}
conn.disconnect();
return true;
}
bool ImapSynchronizer::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged)
{
qint64 uid = itemUid.toLongLong();
if (uid <= 0) return false;
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) return false;
QString folderName = folderOpt->name();
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// SELECT
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
QString resp;
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "SELECT failed";
return false;
}
// STORE for Seen flag
QString seenFlag = read ? "+" : "-";
QString storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Seen").arg(uid).arg(seenFlag);
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "STORE Seen failed:" << resp;
conn.disconnect();
return false;
}
// STORE for Flagged flag
QString flaggedFlag = flagged ? "+" : "-";
storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Flagged").arg(uid).arg(flaggedFlag);
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "STORE Flagged failed:" << resp;
conn.disconnect();
return false;
}
conn.disconnect();
// Update local DB
auto items = MailItemDao::findByFolderId(fid);
for (MailItem& item : items) {
if (item.uid() == uid) {
item.setRead(read);
item.setFlagged(flagged);
MailItemDao::update(item);
break;
}
}
return true;
}
bool ImapSynchronizer::deleteMailItem(const QString& folderId, const QString& itemUid)
{
qint64 uid = itemUid.toLongLong();
if (uid <= 0) return false;
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) return false;
QString folderName = folderOpt->name();
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// SELECT
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
QString resp;
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "SELECT failed";
return false;
}
// Mark as \\Deleted
QString storeCmd = QString("UID STORE %1 +FLAGS.SILENT \\Deleted").arg(uid);
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "STORE Deleted failed:" << resp;
conn.disconnect();
return false;
}
// EXPUNGE (permanently delete)
QString expungeCmd = "EXPUNGE";
if (!conn.sendCommandWait(expungeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "EXPUNGE failed:" << resp;
conn.disconnect();
return false;
}
conn.disconnect();
// Remove from local DB
MailItemDao::remove(uid);
return true;
}
QString ImapSynchronizer::generateEventId() const
{
return QString::number(QDateTime::currentMSecsSinceEpoch());
}
QVector<Folder> ImapSynchronizer::parseListResponse(const QStringList& lines) const
{
QVector<Folder> folders;
QRegularExpression rx("\\* LIST \\\\([^)]*\\\\) \"([^\"]*)\" \"([^\"]*)\"");
for (const QString& line : lines) {
QRegularExpressionMatch match = rx.match(line);
if (match.hasMatch()) {
QString delimiter = match.captured(1); // unused for now
QString mailbox = match.captured(2);
Folder folder;
folder.setName(mailbox);
QString lower = mailbox.toLower();
if (lower == "inbox") folder.setInbox(true);
else if (lower == "sent") folder.setSent(true);
else if (lower == "drafts") folder.setDrafts(true);
else if (lower == "trash" || lower == "deleted items") folder.setTrash(true);
folders.append(folder);
}
}
return folders;
}
QVector<MailItem> ImapSynchronizer::parseFetchResponse(const QStringList& lines) const
{
QVector<MailItem> items;
int i = 0;
while (i < lines.size()) {
const QString& line = lines[i];
if (!line.startsWith(QStringLiteral("* "))) {
++i;
continue;
}
int fetchPos = line.indexOf(QStringLiteral(" FETCH ("));
if (fetchPos == -1) {
++i;
continue;
}
// Extract UID
int uidPos = line.indexOf(QStringLiteral("UID "));
if (uidPos == -1) {
++i;
continue;
}
int uidEnd = line.indexOf(QRegularExpression(QStringLiteral("[\\s)]")), uidPos + 4);
if (uidEnd == -1) uidEnd = line.length();
QString uidStr = line.mid(uidPos + 4, uidEnd - uidPos - 4);
bool ok;
qint64 uid = uidStr.toLongLong(&ok);
if (!ok) {
++i;
continue;
}
MailItem item;
item.setUid(uid);
// Extract FLAGS
int flagsPos = line.indexOf(QStringLiteral("FLAGS ("));
if (flagsPos != -1) {
int flagsEnd = line.indexOf(')', flagsPos + 7);
if (flagsEnd != -1) {
QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7);
item.setRead(flags.contains(QStringLiteral("\\Seen")));
item.setFlagged(flags.contains(QStringLiteral("\\Flagged")));
}
}
// Extract INTERNALDATE
int datePos = line.indexOf(QStringLiteral("INTERNALDATE \""));
if (datePos != -1) {
int dateStart = datePos + 15; // length of "INTERNALDATE \""
int dateEnd = line.indexOf('\"', dateStart);
if (dateEnd != -1) {
QString dateStr = line.mid(dateStart, dateEnd - dateStart);
dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000"
QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date);
if (dt.isValid())
item.setDate(dt);
}
}
// Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines
QString bodyData;
int bodyPos = line.indexOf(QStringLiteral("BODY[]"));
if (bodyPos != -1) {
int bracePos = line.indexOf('{', bodyPos);
if (bracePos != -1) {
int sizeEnd = line.indexOf('}', bracePos);
if (sizeEnd != -1) {
bool sizeOk;
int size = line.mid(bracePos + 1, sizeEnd - bracePos - 1).toInt(&sizeOk);
if (sizeOk) {
int dataStart = line.indexOf(QStringLiteral("\r\n"), sizeEnd);
if (dataStart != -1) {
dataStart += 2; // skip \r\n
int available = line.length() - dataStart;
if (available > size) available = size;
bodyData = line.mid(dataStart, available);
int received = available;
int remaining = size - received;
int j = i + 1;
while (j < lines.size() && remaining > 0) {
const QString& nextLine = lines[j];
int take = qMin(nextLine.length(), remaining);
bodyData += nextLine.left(take);
remaining -= take;
++j;
}
// we have consumed lines up to j-1
i = j - 1; // will be incremented at end of loop
}
}
}
}
}
// Parse headers from bodyData
if (!bodyData.isEmpty()) {
QRegularExpression headerRx(QStringLiteral(R"((^([^:]+):\s*(.*?)\r\n))"), QRegularExpression::MultilineOption);
auto it = headerRx.globalMatch(bodyData);
while (it.hasNext()) {
auto match = it.next();
QString key = match.captured(2).toLower().trimmed();
QString value = match.captured(3).trimmed();
if (key == QStringLiteral("subject"))
item.setSubject(value);
else if (key == QStringLiteral("from"))
item.setSender(value);
else if (key == QStringLiteral("to"))
item.setRecipient(value);
else if (key == QStringLiteral("date")) {
QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date);
if (dt.isValid())
item.setDate(dt);
}
}
}
// If subject not set, use placeholder
if (item.subject().isEmpty())
item.setSubject(QStringLiteral("(No Subject)"));
items.append(item);
++i;
}
return items;
}
@@ -0,0 +1,649 @@
#include "imapsynchronizer.h"
#include <QEventLoop>
#include <QTimer>
#include <QDebug>
#include <QRegularExpression>
#include "../../core/mailitem.h"
#include "../../core/models/account.h"
#include "../../core/models/folder.h"
#include "../../db/dao/folderdao.h"
#include "../../db/dao/mailitemdao.h"
ImapSynchronizer::ImapSynchronizer(QObject* parent)
: Synchronizer(parent)
{
}
bool ImapSynchronizer::initialize(const Account& account)
{
const Account::ConnectionSettings& settings = account.connectionSettings();
m_host = settings.incomingHost;
m_port = settings.incomingPort;
m_useSsl = settings.incomingSsl;
m_username = settings.username;
m_password = settings.password;
m_authMethod = settings.authMethod;
return true;
}
// Helper: connects and logs in to the IMAP server
bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
{
bool connected = false;
bool loggedIn = false;
QString response;
// Connect to host
QEventLoop connectLoop;
QTimer connectTimer;
bool connectTimeout = false;
connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit);
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
qWarning() << "IMAP connection error:" << err;
connectLoop.quit();
});
connectTimer.setSingleShot(true);
connect(&connectTimer, &QTimer::timeout, &connectLoop, [&](){
connectTimeout = true;
connectLoop.quit();
});
connectTimer.start(30000); // 30 seconds
conn.connectToHost(m_host, m_port, m_useSsl);
connectLoop.exec();
if (connectTimeout) {
qWarning() << "Connection timeout to" << m_host;
return false;
}
connected = true;
// Wait for server greeting (first untagged response)
QEventLoop greetLoop;
bool gotGreeting = false;
QMetaObject::Connection greetConn = connect(&conn, &ImapConnection::untaggedResponse,
[&](const QString& line) {
Q_UNUSED(line);
gotGreeting = true;
greetLoop.quit();
});
QTimer greetTimer;
greetTimer.setSingleShot(true);
connect(&greetTimer, &QTimer::timeout, &greetLoop, [&]() {
greetLoop.quit();
});
greetTimer.start(30000); // 30 seconds
greetLoop.exec();
QObject::disconnect(greetConn);
if (!gotGreeting) {
qWarning() << "Timeout waiting for server greeting";
return false;
}
// Login
QEventLoop loginLoop;
QTimer loginTimer;
bool loginTimeout = false;
bool loginSuccess = false;
// Already connected, now login
conn.login(m_username, m_password, true, [&](bool ok, const QString& resp){
if (ok) {
loginSuccess = true;
} else {
qWarning() << "Login failed:" << resp;
}
loginLoop.quit();
});
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
qWarning() << "IMAP error during login:" << err;
loginLoop.quit();
});
loginTimer.setSingleShot(true);
connect(&loginTimer, &QTimer::timeout, &loginLoop, [&](){
loginTimeout = true;
loginLoop.quit();
});
loginTimer.start(30000);
loginLoop.exec();
if (loginTimeout) {
qWarning() << "Login timeout";
return false;
}
if (!loginSuccess) {
return false;
}
loggedIn = true;
return true;
}
bool ImapSynchronizer::syncFolder(const Folder& folder)
{
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// Select folder
QString selectCmd = QString("SELECT \"%1\"").arg(folder.name());
QString selectResp;
if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(" OK ")) {
qWarning() << "SELECT failed for" << folder.name();
return false;
}
// Get all UIDs
QString searchCmd = "UID SEARCH ALL";
QString searchResp;
if (!conn.sendCommandWait(searchCmd, searchResp, 30000) || !searchResp.contains(" OK ")) {
qWarning() << "SEARCH failed";
return false;
}
QVector<qint64> uids;
QStringList lines = searchResp.split('\n');
for (const QString& line : lines) {
if (line.startsWith("* SEARCH")) {
QStringList parts = line.split(QRegularExpression("\\s+"));
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
bool ok;
qint64 uid = parts[i].toLongLong(&ok);
if (ok && uid > 0) uids.append(uid);
}
break;
}
}
// If no messages, set unread count to 0 and exit
if (uids.isEmpty()) {
Folder f = folder;
f.setUnreadCount(0);
FolderDao::update(f);
conn.disconnect();
return true;
}
// Get flags for all UIDs in one command
QStringList uidStrs;
for (qint64 uid : uids) uidStrs.append(QString::number(uid));
QString fetchCmd = "UID FETCH " + uidStrs.join(',') + " (FLAGS)";
QString fetchResp;
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000) || !fetchResp.contains(" OK ")) {
qWarning() << "FETCH FLAGS failed";
conn.disconnect();
return false;
}
// Parse flags and update local DB
QMap<qint64, QPair<bool,bool>> flagsMap; // uid -> (seen, flagged)
QRegularExpression flagsRx(R"(UID (\d+).*FLAGS \(([^)]*)\))");
auto it = flagsRx.globalMatch(fetchResp);
while (it.hasNext()) {
auto m = it.next();
qint64 uid = m.captured(1).toLongLong();
QString flags = m.captured(2);
bool seen = flags.contains("\\Seen");
bool flagged = flags.contains("\\Flagged");
flagsMap[uid] = qMakePair(seen, flagged);
}
// Update local items
auto localItems = MailItemDao::findByFolderId(folder.id());
for (MailItem& item : localItems) {
if (flagsMap.contains(item.uid())) {
bool seen = flagsMap[item.uid()].first;
bool flagged = flagsMap[item.uid()].second;
if (item.isRead() != seen || item.isFlagged() != flagged) {
item.setRead(seen);
item.setFlagged(flagged);
MailItemDao::update(item);
}
}
}
// Count unread
int unread = 0;
for (const MailItem& item : localItems) {
if (!item.isRead()) unread++;
}
Folder f = folder;
f.setUnreadCount(unread);
FolderDao::update(f);
conn.disconnect();
return true;
}
QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid)
{
QVector<MailItem> items;
ImapConnection conn;
if (!connectAndLogin(conn)) {
return items;
}
// Get folder name from DB
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) {
qWarning() << "Folder not found for id" << folderId;
return items;
}
QString folderName = folderOpt->name();
// SELECT with real folder name
QString selectCommand = QString("SELECT \"%1\"").arg(folderName);
QString selectResponse;
if (!conn.sendCommandWait(selectCommand, selectResponse, 30000)) {
qWarning() << "SELECT command failed:" << selectResponse;
return items;
}
if (!selectResponse.contains(" OK ")) {
qWarning() << "SELECT failed:" << selectResponse;
return items;
}
// SEARCH
QString searchCommand;
if (sinceUid > 0) {
searchCommand = QString("UID SEARCH %1:*").arg(sinceUid);
} else {
searchCommand = "UID SEARCH ALL";
}
QString searchResponse;
if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) {
qWarning() << "SEARCH command failed:" << searchResponse;
return items;
}
// Parse UIDs (robust: look for line starting with "* SEARCH")
QVector<qint64> uids;
QStringList lines = searchResponse.split('\n');
for (const QString& line : lines) {
if (line.startsWith("* SEARCH")) {
QStringList parts = line.split(QRegularExpression("\\s+"));
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
bool ok;
qint64 uid = parts[i].toLongLong(&ok);
if (ok && uid > 0) uids.append(uid);
}
break;
}
}
if (uids.isEmpty()) {
conn.sendCommandWait("CLOSE", searchResponse, 30000);
conn.disconnect();
return items;
}
// Fetch in batches of 50 UIDs
const int batchSize = 50;
for (int i = 0; i < uids.size(); i += batchSize) {
QVector<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); // remove trailing comma
// Fetch headers and flags (efficient)
QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)")
.arg(batchList);
QString fetchResponse;
if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) {
qWarning() << "FETCH failed for batch" << i;
continue;
}
// Parse and persist
QVector<MailItem> batchItems = parseFetchResponse(fetchResponse.split('\n'));
for (MailItem& item : batchItems) {
item.setFolderId(fid);
if (MailItemDao::insert(item)) {
items.append(item);
} else {
qWarning() << "Failed to insert mail item uid" << item.uid();
}
}
}
// Close and disconnect
QString dummy;
conn.sendCommandWait("CLOSE", dummy, 30000);
conn.disconnect();
return items;
}
QVector<Folder> ImapSynchronizer::getFolders() const
{
QVector<Folder> folders;
ImapConnection conn;
if (!connectAndLogin(conn)) {
return folders;
}
// List folders: LIST "" "*"
QString listCommand = QStringLiteral("LIST \"\" \"*\"");
QString response;
if (!conn.sendCommandWait(listCommand, response, 30000)) {
qWarning() << "LIST command failed:" << response;
return folders;
}
// Parse the response
QStringList lines = response.split('\n');
folders = parseListResponse(lines);
// Logout
conn.disconnect();
return folders;
}
bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
{
// Get folder name
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) {
qWarning() << "Folder not found for id" << folderId;
return false;
}
QString folderName = folderOpt->name();
// Build RFC822 message (headers + body)
QStringList headers;
if (!item.sender().isEmpty()) headers << "From: " + item.sender();
if (!item.recipient().isEmpty()) headers << "To: " + item.recipient();
if (!item.cc().isEmpty()) headers << "Cc: " + item.cc();
if (!item.bcc().isEmpty()) headers << "Bcc: " + item.bcc();
if (!item.subject().isEmpty()) headers << "Subject: " + item.subject();
headers << "Date: " + item.date().toString(Qt::RFC2822Date);
headers << "MIME-Version: 1.0";
headers << "Content-Type: text/html; charset=UTF-8";
headers << "Content-Transfer-Encoding: 7bit";
headers << ""; // blank line separates headers from body
headers << item.bodyHtml(); // using bodyHtml as the body
QString message = headers.join("\r\n");
QByteArray msgData = message.toUtf8();
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// SELECT folder (some servers require it)
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
QString resp;
conn.sendCommandWait(selectCmd, resp, 30000); // ignore response
// APPEND with literal: APPEND "folder" (\Seen) {size}
QString appendCmd = QString("APPEND \"%1\" (\\Seen) {%2}").arg(folderName).arg(msgData.size());
QString response;
if (!conn.sendCommandWait(appendCmd, response, 30000)) {
qWarning() << "APPEND command initial failed:" << response;
conn.disconnect();
return false;
}
// Send the literal (message data)
conn.sendRaw(QString::fromUtf8(msgData));
// Wait for the tagged response (tag + OK/NO)
if (!conn.sendCommandWait("", response, 30000)) { // dummy command to get response
qWarning() << "APPEND literal failed:" << response;
conn.disconnect();
return false;
}
conn.disconnect();
return true;
}
bool ImapSynchronizer::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged)
{
qint64 uid = itemUid.toLongLong();
if (uid <= 0) return false;
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) return false;
QString folderName = folderOpt->name();
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// SELECT
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
QString resp;
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "SELECT failed";
return false;
}
// STORE for Seen flag
QString seenFlag = read ? "+" : "-";
QString storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Seen").arg(uid).arg(seenFlag);
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "STORE Seen failed:" << resp;
conn.disconnect();
return false;
}
// STORE for Flagged flag
QString flaggedFlag = flagged ? "+" : "-";
storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Flagged").arg(uid).arg(flaggedFlag);
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "STORE Flagged failed:" << resp;
conn.disconnect();
return false;
}
conn.disconnect();
// Update local DB
auto items = MailItemDao::findByFolderId(fid);
for (MailItem& item : items) {
if (item.uid() == uid) {
item.setRead(read);
item.setFlagged(flagged);
MailItemDao::update(item);
break;
}
}
return true;
}
bool ImapSynchronizer::deleteMailItem(const QString& folderId, const QString& itemUid)
{
qint64 uid = itemUid.toLongLong();
if (uid <= 0) return false;
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) return false;
QString folderName = folderOpt->name();
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// SELECT
QString selectCmd = QString("SELECT \"%1\"").arg(folderName);
QString resp;
if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "SELECT failed";
return false;
}
// Mark as \\Deleted
QString storeCmd = QString("UID STORE %1 +FLAGS.SILENT \\Deleted").arg(uid);
if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "STORE Deleted failed:" << resp;
conn.disconnect();
return false;
}
// EXPUNGE (permanently delete)
QString expungeCmd = "EXPUNGE";
if (!conn.sendCommandWait(expungeCmd, resp, 30000) || !resp.contains(" OK ")) {
qWarning() << "EXPUNGE failed:" << resp;
conn.disconnect();
return false;
}
conn.disconnect();
// Remove from local DB
MailItemDao::remove(uid);
return true;
}
QString ImapSynchronizer::generateEventId() const
{
return QString::number(QDateTime::currentMSecsSinceEpoch());
}
QVector<Folder> ImapSynchronizer::parseListResponse(const QStringList& lines) const
{
QVector<Folder> folders;
QRegularExpression rx("\\* LIST \\\\([^)]*\\\\) \"([^\"]*)\" \"([^\"]*)\"");
for (const QString& line : lines) {
QRegularExpressionMatch match = rx.match(line);
if (match.hasMatch()) {
QString delimiter = match.captured(1); // unused for now
QString mailbox = match.captured(2);
Folder folder;
folder.setName(mailbox);
QString lower = mailbox.toLower();
if (lower == "inbox") folder.setInbox(true);
else if (lower == "sent") folder.setSent(true);
else if (lower == "drafts") folder.setDrafts(true);
else if (lower == "trash" || lower == "deleted items") folder.setTrash(true);
folders.append(folder);
}
}
return folders;
}
QVector<MailItem> ImapSynchronizer::parseFetchResponse(const QStringList& lines) const
{
QVector<MailItem> items;
int i = 0;
while (i < lines.size()) {
const QString& line = lines[i];
if (!line.startsWith(QStringLiteral("* "))) {
++i;
continue;
}
int fetchPos = line.indexOf(QStringLiteral(" FETCH ("));
if (fetchPos == -1) {
++i;
continue;
}
// Extract UID
int uidPos = line.indexOf(QStringLiteral("UID "));
if (uidPos == -1) {
++i;
continue;
}
int uidEnd = line.indexOf(QRegularExpression(QStringLiteral("[\\s)]")), uidPos + 4);
if (uidEnd == -1) uidEnd = line.length();
QString uidStr = line.mid(uidPos + 4, uidEnd - uidPos - 4);
bool ok;
qint64 uid = uidStr.toLongLong(&ok);
if (!ok) {
++i;
continue;
}
MailItem item;
item.setUid(uid);
// Extract FLAGS
int flagsPos = line.indexOf(QStringLiteral("FLAGS ("));
if (flagsPos != -1) {
int flagsEnd = line.indexOf(')', flagsPos + 7);
if (flagsEnd != -1) {
QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7);
item.setRead(flags.contains(QStringLiteral("\\Seen")));
item.setFlagged(flags.contains(QStringLiteral("\\Flagged")));
}
}
// Extract INTERNALDATE
int datePos = line.indexOf(QStringLiteral("INTERNALDATE \""));
if (datePos != -1) {
int dateStart = datePos + 15; // length of "INTERNALDATE \""
int dateEnd = line.indexOf('\"', dateStart);
if (dateEnd != -1) {
QString dateStr = line.mid(dateStart, dateEnd - dateStart);
dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000"
QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date);
if (dt.isValid())
item.setDate(dt);
}
}
// Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines
QString bodyData;
int bodyPos = line.indexOf(QStringLiteral("BODY["));
if (bodyPos != -1) {
int bracePos = line.indexOf('{', bodyPos);
if (bracePos != -1) {
int sizeEnd = line.indexOf('}', bracePos);
if (sizeEnd != -1) {
bool sizeOk;
int size = line.mid(bracePos + 1, sizeEnd - bracePos - 1).toInt(&sizeOk);
if (sizeOk) {
int dataStart = line.indexOf(QStringLiteral("\r\n"), sizeEnd);
if (dataStart != -1) {
dataStart += 2; // skip \r\n
int available = line.length() - dataStart;
if (available > size) available = size;
bodyData = line.mid(dataStart, available);
int received = available;
int remaining = size - received;
int j = i + 1;
while (j < lines.size() && remaining > 0) {
const QString& nextLine = lines[j];
int take = qMin(nextLine.length(), remaining);
bodyData += nextLine.left(take);
remaining -= take;
++j;
}
// we have consumed lines up to j-1
i = j - 1; // will be incremented at end of loop
}
}
}
}
}
// Parse headers from bodyData
if (!bodyData.isEmpty()) {
QRegularExpression headerRx(QStringLiteral(R"((^([^:]+):\s*(.*?)\r\n))"), QRegularExpression::MultilineOption);
auto it = headerRx.globalMatch(bodyData);
while (it.hasNext()) {
auto match = it.next();
QString key = match.captured(2).toLower().trimmed();
QString value = match.captured(3).trimmed();
if (key == QStringLiteral("subject"))
item.setSubject(value);
else if (key == QStringLiteral("from"))
item.setSender(value);
else if (key == QStringLiteral("to"))
item.setRecipient(value);
else if (key == QStringLiteral("date")) {
QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date);
if (dt.isValid())
item.setDate(dt);
}
}
}
// If subject not set, use placeholder
if (item.subject().isEmpty())
item.setSubject(QStringLiteral("(No Subject)"));
items.append(item);
++i;
}
return items;
}
+16 -8
View File
@@ -2,8 +2,8 @@
#include "../synchronizer.h"
#include <QObject>
#include <QSslSocket>
#include <QStringList>
#include "imapconnection.h"
class ImapSynchronizer : public Synchronizer
{
@@ -30,20 +30,28 @@ signals:
void statusMessage(const QString& message) const;
private:
// Connection details
QVector<qint64> parseUidSearchResponse(const QString& response) const;
// Connection details (set by initialize)
mutable QString m_host;
mutable quint16 m_port{0};
mutable bool m_useSsl{false};
mutable QString m_username;
mutable QString m_password;
mutable QString m_authMethod; // "plain" or "oauth2"
mutable QByteArray m_capabilities;
// Helper para generar IDs únicos de eventos
QString generateEventId() const;
// IMAP command helpers
bool sendCommand(QSslSocket& socket, const QString& command, QString& response) const;
bool waitForResponse(QSslSocket& socket, const QString& expectedTag, QString& response) const;
QVector<Folder> parseListResponse(const QString& response) const;
};
// Helper: conecta y loguea al servidor (reutilizable)
bool connectAndLogin(ImapConnection &conn) const;
// IMAP response parsers
QVector<Folder> parseListResponse(const QStringList& lines) const;
QVector<MailItem> parseFetchResponse(const QStringList& lines) const;
QVector<MailItem> parseFetchResponseBytes(const QByteArray& response) const;
bool persistFetchedItem(const Folder& folder, MailItem& item) const;
// Sync folder helpers
QVector<qint64> fetchAllUids(const QString& folderId) const;
void parseAndUpdateFlags(const QByteArray& response, int folderId) const;
};
@@ -0,0 +1,50 @@
#pragma once
#include "../synchronizer.h"
#include <QObject>
#include <QStringList>
#include "imapconnection.h"
class ImapSynchronizer : public Synchronizer
{
Q_OBJECT
public:
explicit ImapSynchronizer(QObject* parent = nullptr);
~ImapSynchronizer() override = default;
// Synchronizer interface
bool initialize(const Account& account) override;
bool syncFolder(const Folder& folder) override;
QVector<Folder> getFolders() const override;
QVector<MailItem> fetchMailItems(const QString& folderId,
qint64 sinceUid = 0) override;
bool appendMailItem(const QString& folderId, const MailItem& item) override;
bool updateMailItemFlags(const QString& folderId,
const QString& itemUid,
bool read, bool flagged) override;
bool deleteMailItem(const QString& folderId,
const QString& itemUid) override;
signals:
void progressChanged(int percent) const;
void statusMessage(const QString& message) const;
private:
// Connection details (set by initialize)
mutable QString m_host;
mutable quint16 m_port{0};
mutable bool m_useSsl{false};
mutable QString m_username;
mutable QString m_password;
mutable QString m_authMethod; // "plain" or "oauth2"
// Helper para generar IDs únicos de eventos
QString generateEventId() const;
// Helper: conecta y loguea al servidor (reutilizable)
bool connectAndLogin(ImapConnection &conn) const;
// IMAP response parsers
QVector<Folder> parseListResponse(const QStringList& lines) const;
QVector<MailItem> parseFetchResponse(const QStringList& lines) const;
};
+389 -11
View File
@@ -3,6 +3,302 @@
#include <QFutureWatcher>
#include <QtConcurrent/QtConcurrent>
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QMimeDatabase>
#include <QRegularExpression>
#include <QDateTime>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QEventLoop>
#include <QUrl>
#include "services/mimestorage.h"
namespace {
QByteArray smtpHeader(const QString &name, const QString &value)
{
const QByteArray utf8 = value.toUtf8();
bool ascii = true;
for (char c : utf8) {
if (static_cast<unsigned char>(c) < 32 || static_cast<unsigned char>(c) > 126) {
ascii = false;
break;
}
}
const QByteArray encoded = ascii
? utf8
: QByteArray("=?UTF-8?B?") + utf8.toBase64() + QByteArray("?=");
return name.toUtf8() + QByteArray(": ") + encoded + QByteArray("\r\n");
}
QByteArray base64Lines(const QByteArray &data)
{
const QByteArray encoded = data.toBase64();
QByteArray wrapped;
for (int i = 0; i < encoded.size(); i += 76)
wrapped.append(encoded.mid(i, 76)).append("\r\n");
return wrapped;
}
QStringList smtpRecipients(const QString &value)
{
QStringList result;
for (QString part : value.split(QRegularExpression(QStringLiteral("[,;]")), Qt::SkipEmptyParts)) {
const QRegularExpressionMatch match = QRegularExpression(QStringLiteral("<([^>]+)>")).match(part);
if (match.hasMatch()) part = match.captured(1);
part = part.trimmed();
if (!part.isEmpty()) result.append(part);
}
return result;
}
bool smtpResponse(QSslSocket &socket, int expectedCode)
{
QByteArray line;
while (true) {
if (!socket.canReadLine() && !socket.waitForReadyRead(15000)) return false;
line = socket.readLine();
if (line.size() < 3) continue;
bool ok = false;
const int code = line.left(3).toInt(&ok);
if (!ok) continue;
// A space marks the final line of a multiline SMTP response.
if (line.size() > 3 && line.at(3) == '-') continue;
return code == expectedCode;
}
}
QByteArray composeMimeMessage(const MailItem &mail, const Account &account,
const QStringList &attachmentPaths, QString &error)
{
QStringList recipients = smtpRecipients(mail.to());
if (recipients.isEmpty()) recipients = smtpRecipients(mail.recipient());
recipients.append(smtpRecipients(mail.cc()));
recipients.append(smtpRecipients(mail.bcc()));
if (recipients.isEmpty()) {
error = QObject::tr("No recipients specified");
return {};
}
recipients.removeDuplicates();
QByteArray message;
const QString from = mail.sender().isEmpty() ? account.email() : mail.sender();
message.append(smtpHeader(QStringLiteral("From"), from));
message.append(smtpHeader(QStringLiteral("To"), mail.to().isEmpty() ? mail.recipient() : mail.to()));
if (!mail.cc().isEmpty()) message.append(smtpHeader(QStringLiteral("Cc"), mail.cc()));
message.append(smtpHeader(QStringLiteral("Subject"), mail.subject()));
message.append("Date: ").append((mail.date().isValid() ? mail.date() : QDateTime::currentDateTimeUtc()).toString(Qt::RFC2822Date).toUtf8()).append("\r\n");
message.append("MIME-Version: 1.0\r\n");
QByteArray body = mail.bodyHtml().toUtf8();
body.replace("\r\n", "\n");
body.replace('\r', '\n');
body.replace('\n', "\r\n");
if (attachmentPaths.isEmpty()) {
message.append("Content-Type: text/html; charset=UTF-8\r\n");
message.append("Content-Transfer-Encoding: 8bit\r\n\r\n");
message.append(body).append("\r\n");
return message;
}
const QByteArray boundary = QByteArray("----WinoMail-")
+ QByteArray::number(QDateTime::currentMSecsSinceEpoch());
message.append("Content-Type: multipart/mixed; boundary=\"").append(boundary).append("\"\r\n\r\n");
message.append("--").append(boundary).append("\r\n");
message.append("Content-Type: text/html; charset=UTF-8\r\n");
message.append("Content-Transfer-Encoding: 8bit\r\n\r\n").append(body).append("\r\n");
QMimeDatabase mimeDatabase;
for (const QString &path : attachmentPaths) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
error = QObject::tr("Could not read attachment: %1").arg(path);
return {};
}
const QFileInfo info(path);
const QString fileName = info.fileName().replace('"', '_');
const QString mimeType = mimeDatabase.mimeTypeForFile(info).name();
message.append("--").append(boundary).append("\r\n");
message.append("Content-Type: ").append(mimeType.toUtf8()).append("; name=\"").append(fileName.toUtf8()).append("\"\r\n");
message.append("Content-Transfer-Encoding: base64\r\n");
message.append("Content-Disposition: attachment; filename=\"").append(fileName.toUtf8()).append("\"\r\n\r\n");
message.append(base64Lines(file.readAll()));
}
message.append("--").append(boundary).append("--\r\n");
return message;
}
bool sendSmtp(const MailItem &mail, const Account &account,
const QStringList &attachmentPaths, QString &error)
{
const Account::ConnectionSettings settings = account.connectionSettings();
if (settings.outgoingHost.isEmpty() || settings.outgoingPort == 0) {
error = QObject::tr("SMTP server is not configured");
return false;
}
const QByteArray mime = composeMimeMessage(mail, account, attachmentPaths, error);
if (mime.isEmpty()) return false;
QSslSocket socket;
if (settings.outgoingSsl && settings.outgoingPort == 465)
socket.connectToHostEncrypted(settings.outgoingHost, settings.outgoingPort);
else
socket.connectToHost(settings.outgoingHost, settings.outgoingPort);
if (!socket.waitForConnected(15000)) { error = socket.errorString(); return false; }
if (settings.outgoingSsl && settings.outgoingPort == 465) {
if (!socket.waitForEncrypted(15000)) { error = socket.errorString(); return false; }
}
if (!smtpResponse(socket, 220)) { error = QObject::tr("SMTP greeting failed"); return false; }
auto command = [&](const QByteArray &value, int code) {
socket.write(value + QByteArray("\r\n"));
socket.flush();
return smtpResponse(socket, code);
};
if (!command("EHLO localhost", 250)) { error = QObject::tr("SMTP EHLO failed"); return false; }
if (settings.outgoingSsl && settings.outgoingPort != 465) {
if (!command("STARTTLS", 220)) { error = QObject::tr("SMTP STARTTLS failed"); return false; }
socket.startClientEncryption();
if (!socket.waitForEncrypted(15000)) { error = socket.errorString(); return false; }
if (!command("EHLO localhost", 250)) { error = QObject::tr("SMTP EHLO after TLS failed"); return false; }
}
const QString username = settings.username.isEmpty() ? account.email() : settings.username;
if (!username.isEmpty()) {
if (!command("AUTH LOGIN", 334)
|| !command(username.toUtf8().toBase64(), 334)
|| !command(settings.password.toUtf8().toBase64(), 235)) {
error = QObject::tr("SMTP authentication failed");
return false;
}
}
const QString from = mail.sender().isEmpty() ? account.email() : mail.sender();
if (!command("MAIL FROM:<" + smtpRecipients(from).value(0).toUtf8() + ">", 250)) { error = QObject::tr("SMTP MAIL FROM failed"); return false; }
const QStringList recipients = smtpRecipients(mail.to()) + smtpRecipients(mail.recipient())
+ smtpRecipients(mail.cc()) + smtpRecipients(mail.bcc());
QStringList uniqueRecipients = recipients;
uniqueRecipients.removeDuplicates();
for (const QString &recipient : uniqueRecipients) {
if (!command("RCPT TO:<" + recipient.toUtf8() + ">", 250)) { error = QObject::tr("SMTP recipient rejected: %1").arg(recipient); return false; }
}
if (!command("DATA", 354)) { error = QObject::tr("SMTP DATA failed"); return false; }
QByteArray stuffed;
const QList<QByteArray> lines = mime.split('\n');
for (QByteArray line : lines) {
if (line.startsWith('.')) stuffed.append('.');
stuffed.append(line);
if (!line.endsWith('\r')) stuffed.append('\r');
stuffed.append('\n');
}
socket.write(stuffed);
socket.write(".\r\n");
socket.flush();
if (!smtpResponse(socket, 250)) { error = QObject::tr("SMTP message rejected"); return false; }
command("QUIT", 221);
return true;
}
QByteArray base64Url(const QByteArray &data)
{
QByteArray result = data.toBase64();
result.replace('+', '-');
result.replace('/', '_');
while (result.endsWith('=')) result.chop(1);
return result;
}
bool postJson(const QUrl &url, const QByteArray &token, const QJsonObject &payload,
int expectedStatus, QString &error)
{
QNetworkAccessManager network;
QNetworkRequest request(url);
request.setRawHeader("Authorization", QByteArrayLiteral("Bearer ") + token);
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
QNetworkReply *reply = network.post(request, QJsonDocument(payload).toJson(QJsonDocument::Compact));
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
const bool ok = reply->error() == QNetworkReply::NoError && status == expectedStatus;
if (!ok) error = reply->error() == QNetworkReply::NoError
? QObject::tr("Server returned HTTP %1").arg(status) : reply->errorString();
reply->deleteLater();
return ok;
}
bool sendGmailApi(const MailItem &mail, const Account &account,
const QStringList &attachmentPaths, QString &error)
{
if (!account.isTokenValid()) {
error = QObject::tr("Gmail access token is missing or expired");
return false;
}
const QByteArray mime = composeMimeMessage(mail, account, attachmentPaths, error);
if (mime.isEmpty()) return false;
QJsonObject payload;
payload.insert(QStringLiteral("raw"), QString::fromLatin1(base64Url(mime)));
const QString user = QString::fromUtf8(QUrl::toPercentEncoding(account.email()));
const QUrl url(QStringLiteral("https://gmail.googleapis.com/gmail/v1/users/%1/messages/send").arg(user));
return postJson(url, account.accessToken().toUtf8(), payload, 200, error);
}
QJsonArray graphRecipients(const QString &value)
{
QJsonArray result;
for (const QString &address : smtpRecipients(value)) {
QJsonObject recipient;
QJsonObject email;
email.insert(QStringLiteral("address"), address);
recipient.insert(QStringLiteral("emailAddress"), email);
result.append(recipient);
}
return result;
}
bool sendOutlookApi(const MailItem &mail, const Account &account,
const QStringList &attachmentPaths, QString &error)
{
if (!account.isTokenValid()) {
error = QObject::tr("Outlook access token is missing or expired");
return false;
}
QJsonObject message;
message.insert(QStringLiteral("subject"), mail.subject());
QJsonObject body;
body.insert(QStringLiteral("contentType"), QStringLiteral("HTML"));
body.insert(QStringLiteral("content"), mail.bodyHtml());
message.insert(QStringLiteral("body"), body);
message.insert(QStringLiteral("toRecipients"), graphRecipients(mail.to().isEmpty() ? mail.recipient() : mail.to()));
message.insert(QStringLiteral("ccRecipients"), graphRecipients(mail.cc()));
message.insert(QStringLiteral("bccRecipients"), graphRecipients(mail.bcc()));
QJsonArray attachments;
QMimeDatabase mimeDatabase;
for (const QString &path : attachmentPaths) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
error = QObject::tr("Could not read attachment: %1").arg(path);
return false;
}
const QFileInfo info(path);
QJsonObject attachment;
attachment.insert(QStringLiteral("@odata.type"), QStringLiteral("#microsoft.graph.fileAttachment"));
attachment.insert(QStringLiteral("name"), info.fileName());
attachment.insert(QStringLiteral("contentType"), mimeDatabase.mimeTypeForFile(info).name());
attachment.insert(QStringLiteral("contentBytes"), QString::fromLatin1(file.readAll().toBase64()));
attachments.append(attachment);
}
message.insert(QStringLiteral("attachments"), attachments);
QJsonObject payload;
payload.insert(QStringLiteral("message"), message);
payload.insert(QStringLiteral("saveToSentItems"), true);
const QUrl url(QStringLiteral("https://graph.microsoft.com/v1.0/me/sendMail"));
return postJson(url, account.accessToken().toUtf8(), payload, 202, error);
}
}
MailService::MailService(AccountService *accountService, QObject *parent)
: QObject(parent)
@@ -18,13 +314,43 @@ QVector<MailItem> MailService::getMails(const QString &folderId)
void MailService::sendMail(const MailItem &mail, const QString &accountId)
{
// For now we just simulate sending by using EmailComposerBridge
// In a real implementation, this would use SMTP settings from the account.
Q_UNUSED(mail);
Q_UNUSED(accountId);
// Emit success immediately; actual sending would be asynchronous.
QMetaObject::invokeMethod(this, "mailSent", Qt::QueuedConnection,
Q_ARG(QString, QString()));
sendMail(mail, accountId, {});
}
void MailService::sendMail(const MailItem &mail, const QString &accountId,
const QStringList &attachmentPaths)
{
if (!m_accountService) {
emit mailSendFailed(accountId, tr("AccountService not available"));
return;
}
Account *account = m_accountService->findAccountById(accountId.toLongLong());
if (!account) {
emit mailSendFailed(accountId, tr("Account not found"));
return;
}
const Account accountCopy = *account;
delete account;
QFuture<bool> future = QtConcurrent::run([mail, accountCopy, attachmentPaths]() {
QString error;
bool ok = false;
if (accountCopy.type() == AccountType::Gmail)
ok = sendGmailApi(mail, accountCopy, attachmentPaths, error);
else if (accountCopy.type() == AccountType::Outlook)
ok = sendOutlookApi(mail, accountCopy, attachmentPaths, error);
else
ok = sendSmtp(mail, accountCopy, attachmentPaths, error);
if (!ok) qWarning() << "Send failed:" << error;
return ok;
});
auto *watcher = new QFutureWatcher<bool>(this);
connect(watcher, &QFutureWatcher<bool>::finished, this, [this, watcher, accountId]() {
const bool ok = watcher->result();
watcher->deleteLater();
if (ok) emit mailSent(accountId);
else emit mailSendFailed(accountId, tr("Mail server rejected the message"));
});
watcher->setFuture(future);
}
void MailService::fetchMails(const QString &accountId, const QString &folderId)
@@ -35,6 +361,10 @@ void MailService::fetchMails(const QString &accountId, const QString &folderId)
return;
}
Account *acc = m_accountService->findAccountById(accountId.toLongLong());
if (!acc) {
emit mailFetchError(accountId, folderId, tr("Account not found"));
return;
}
QString providerType = QStringLiteral("imap"); // fallback
if (acc) {
switch (acc->type()) {
@@ -65,10 +395,12 @@ void MailService::fetchMails(const QString &accountId, const QString &folderId)
// Initialize synchronizer with account details before using it
if (!sync->initialize(*acc)) {
delete acc;
emit mailFetchError(accountId, folderId, tr("Failed to initialize synchronizer"));
sync->deleteLater();
SynchronizerProvider::instance().unregisterSynchronizer(accountId);
return;
}
delete acc;
// Connect progress signals (if they exist) using string-based syntax for compatibility
QObject::connect(sync, SIGNAL(progressChanged(int)), this, SIGNAL(progressChanged(int)), Qt::QueuedConnection);
@@ -85,12 +417,52 @@ void MailService::fetchMails(const QString &accountId, const QString &folderId)
QObject::disconnect(sync, SIGNAL(progressChanged(int)), this, SIGNAL(progressChanged(int)));
QObject::disconnect(sync, SIGNAL(statusMessage(const QString&)), this, SIGNAL(statusMessage(const QString&)));
QVector<MailItem> items = watcher->result();
QVector<MailItem> persisted;
for (MailItem &item : items) {
if (persistFetchedItem(item, accountId, folderId)) persisted.append(item);
}
watcher->deleteLater();
emit mailFetched(accountId, folderId, items);
emit mailFetched(accountId, folderId, persisted);
});
watcher->setFuture(future);
}
bool MailService::persistFetchedItem(MailItem &item, const QString &accountId,
const QString &folderId)
{
if (item.rawMime().isEmpty()) return MailItemDao::upsert(item);
MimeStorageService storage;
ParsedMimeMessage parsed;
QVector<QString> paths;
if (!storage.parseMessage(item.rawMime(), parsed)
|| !storage.storeMessage(accountId, folderId, item, item.rawMime(), &paths)) {
qWarning() << "Failed to persist MIME message" << item.messageId();
return false;
}
if (!MailItemDao::upsert(item)) {
storage.deleteEmlFile(item.fileId());
return false;
}
QVector<StoredAttachmentRecord> records;
for (int i = 0; i < parsed.attachments.size(); ++i) {
const ParsedMimeAttachment &source = parsed.attachments.at(i);
StoredAttachmentRecord record;
record.fileName = source.fileName;
record.mimeType = source.mimeType;
record.contentId = source.contentId;
record.size = source.data.size();
if (i < paths.size()) record.storedPath = paths.at(i);
records.append(record);
}
if (!MailItemDao::replaceAttachments(item.id(), records)) {
storage.deleteEmlFile(item.fileId());
return false;
}
return true;
}
void MailService::moveMail(const QString &mailItemId, const QString &targetFolderId)
{
bool ok;
@@ -109,8 +481,14 @@ void MailService::deleteMail(const QString &mailItemId)
bool ok;
qint64 id = mailItemId.toLongLong(&ok);
if (!ok) return;
if (MailItemDao::remove(id))
const std::optional<MailItem> item = MailItemDao::findById(id);
if (MailItemDao::remove(id)) {
if (item && !item->fileId().isEmpty()) {
MimeStorageService storage;
storage.deleteEmlFile(item->fileId());
}
emit mailDeleted(mailItemId);
}
}
void MailService::markAsRead(const QString &mailItemId, bool read)
@@ -126,4 +504,4 @@ void MailService::markAsRead(const QString &mailItemId, bool read)
emit mailReadStateChanged(mailItemId, read);
}
#include "mailservice.moc"
#include "mailservice.moc"
+129
View File
@@ -0,0 +1,129 @@
#include "mailservice.h"
#include <QFuture>
#include <QFutureWatcher>
#include <QtConcurrent/QtConcurrent>
#include <QDebug>
MailService::MailService(AccountService *accountService, QObject *parent)
: QObject(parent)
, m_composer(new EmailComposerBridge(this))
, m_accountService(accountService)
{
}
QVector<MailItem> MailService::getMails(const QString &folderId)
{
return MailItemDao::findByFolderId(folderId.toInt());
}
void MailService::sendMail(const MailItem &mail, const QString &accountId)
{
// For now we just simulate sending by using EmailComposerBridge
// In a real implementation, this would use SMTP settings from the account.
Q_UNUSED(mail);
Q_UNUSED(accountId);
// Emit success immediately; actual sending would be asynchronous.
QMetaObject::invokeMethod(this, "mailSent", Qt::QueuedConnection,
Q_ARG(QString, QString()));
}
void MailService::fetchMails(const QString &accountId, const QString &folderId)
{
// Determine provider type from account
if (!m_accountService) {
emit mailFetchError(accountId, folderId, tr("AccountService not available"));
return;
}
Account *acc = m_accountService->findAccountById(accountId.toLongLong());
QString providerType = QStringLiteral("imap"); // fallback
if (acc) {
switch (acc->type()) {
case AccountType::IMAP:
providerType = QStringLiteral("imap");
break;
case AccountType::POP3:
providerType = QStringLiteral("pop3");
break;
case AccountType::Gmail:
providerType = QStringLiteral("gmail");
break;
case AccountType::Outlook:
providerType = QStringLiteral("outlook");
break;
default:
providerType = QStringLiteral("imap");
break;
}
}
Synchronizer *sync = SynchronizerProvider::instance().createSynchronizer(accountId, providerType);
if (!sync) {
emit mailFetchError(accountId, folderId, tr("Failed to create synchronizer"));
return;
}
// Initialize synchronizer with account details before using it
if (!sync->initialize(*acc)) {
emit mailFetchError(accountId, folderId, tr("Failed to initialize synchronizer"));
sync->deleteLater();
return;
}
// Connect progress signals (if they exist) using string-based syntax for compatibility
QObject::connect(sync, SIGNAL(progressChanged(int)), this, SIGNAL(progressChanged(int)), Qt::QueuedConnection);
QObject::connect(sync, SIGNAL(statusMessage(const QString&)), this, SIGNAL(statusMessage(const QString&)), Qt::QueuedConnection);
QFuture<QVector<MailItem>> future = QtConcurrent::run([=]() {
// SinceUid = 0 for full sync; could be stored per folder but omitted for simplicity
return sync->fetchMailItems(folderId, 0);
});
QFutureWatcher<QVector<MailItem>> *watcher = new QFutureWatcher<QVector<MailItem>>(this);
QObject::connect(watcher, &QFutureWatcher<QVector<MailItem>>::finished, this, [=]() {
// Disconnect progress signals
QObject::disconnect(sync, SIGNAL(progressChanged(int)), this, SIGNAL(progressChanged(int)));
QObject::disconnect(sync, SIGNAL(statusMessage(const QString&)), this, SIGNAL(statusMessage(const QString&)));
QVector<MailItem> items = watcher->result();
watcher->deleteLater();
emit mailFetched(accountId, folderId, items);
});
watcher->setFuture(future);
}
void MailService::moveMail(const QString &mailItemId, const QString &targetFolderId)
{
bool ok;
qint64 id = mailItemId.toLongLong(&ok);
if (!ok) return;
std::optional<MailItem> opt = MailItemDao::findById(id);
if (!opt) return;
MailItem item = *opt;
item.setFolderId(targetFolderId.toInt());
if (MailItemDao::update(item))
emit mailMoved(mailItemId);
}
void MailService::deleteMail(const QString &mailItemId)
{
bool ok;
qint64 id = mailItemId.toLongLong(&ok);
if (!ok) return;
if (MailItemDao::remove(id))
emit mailDeleted(mailItemId);
}
void MailService::markAsRead(const QString &mailItemId, bool read)
{
bool ok;
qint64 id = mailItemId.toLongLong(&ok);
if (!ok) return;
std::optional<MailItem> opt = MailItemDao::findById(id);
if (!opt) return;
MailItem item = *opt;
item.setRead(read);
if (MailItemDao::update(item))
emit mailReadStateChanged(mailItemId, read);
}
#include "mailservice.moc"
+5 -1
View File
@@ -22,6 +22,8 @@ public:
QVector<MailItem> getMails(const QString &folderId);
void sendMail(const MailItem &mail, const QString &accountId);
void sendMail(const MailItem &mail, const QString &accountId,
const QStringList &attachmentPaths);
void fetchMails(const QString &accountId, const QString &folderId);
void moveMail(const QString &mailItemId, const QString &targetFolderId);
void deleteMail(const QString &mailItemId);
@@ -40,8 +42,10 @@ signals:
void statusMessage(const QString &message);
private:
bool persistFetchedItem(MailItem &item, const QString &accountId,
const QString &folderId);
EmailComposerBridge *m_composer;
AccountService *m_accountService;
};
#endif // MAILSERVICE_H
#endif // MAILSERVICE_H
+296 -53
View File
@@ -2,6 +2,14 @@
#include <QStandardPaths>
#include <QDebug>
#include <QUuid>
#include <QCryptographicHash>
#include <QRegularExpression>
#include <QTextStream>
#include <QFileInfo>
#include <QUrl>
#include <QDateTime>
#include <QSaveFile>
#include <QMap>
MimeStorageService::MimeStorageService(QObject *parent)
: QObject(parent)
@@ -23,53 +31,99 @@ QString MimeStorageService::ensureDirectory(const QString &path) const
return dir.absolutePath();
}
QString MimeStorageService::safeComponent(const QString &value) const
{
QString result = value;
result.replace(QRegularExpression(QStringLiteral("[^A-Za-z0-9._-]")), QStringLiteral("_"));
if (result.isEmpty() || result == QStringLiteral(".") || result == QStringLiteral(".."))
result = QStringLiteral("unknown");
return result.left(180);
}
QString MimeStorageService::saveEmlFile(const QString &accountId, const QString &folderId, const MailItem &mail)
{
QString dir = storagePath() + "/" + accountId + "/" + folderId;
ensureDirectory(dir);
QByteArray raw;
raw.append("From: ").append(mail.sender().toUtf8()).append("\r\n");
raw.append("To: ").append(mail.recipient().toUtf8()).append("\r\n");
if (!mail.cc().isEmpty()) raw.append("Cc: ").append(mail.cc().toUtf8()).append("\r\n");
if (!mail.bcc().isEmpty()) raw.append("Bcc: ").append(mail.bcc().toUtf8()).append("\r\n");
raw.append("Subject: ").append(mail.subject().toUtf8()).append("\r\n");
raw.append("Date: ").append(mail.date().toString(Qt::RFC2822Date).toUtf8()).append("\r\n");
raw.append("MIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n");
raw.append(mail.bodyHtml().toUtf8()).append("\r\n");
return saveRawEmlFile(accountId, folderId, raw, mail.messageId());
}
QString fileName = mail.messageId();
if (fileName.isEmpty()) {
fileName = QUuid::createUuid().toString(QUuid::WithoutBraces);
}
fileName.replace('/', '_').replace('\\', '_');
QString filePath = dir + "/" + fileName + ".eml";
QString MimeStorageService::saveRawEmlFile(const QString &accountId, const QString &folderId,
const QByteArray &rawMime, const QString &stableId)
{
if (rawMime.isEmpty()) return QString();
// Write a basic .eml structure
QFile file(filePath);
if (file.open(QIODevice::WriteOnly)) {
QTextStream stream(&file);
stream << "From: " << mail.sender() << "\r\n";
stream << "To: " << mail.recipient() << "\r\n";
stream << "Subject: " << mail.subject() << "\r\n";
stream << "Date: " << mail.date().toString(Qt::RFC2822Date) << "\r\n";
stream << "MIME-Version: 1.0\r\n";
stream << "Content-Type: text/html; charset=UTF-8\r\n";
stream << "\r\n";
stream << mail.bodyHtml() << "\r\n";
file.close();
qDebug() << "[MimeStorage] Saved .eml:" << filePath;
return fileName;
const QString accountPart = safeComponent(accountId);
const QString folderPart = safeComponent(folderId);
const QByteArray digest = QCryptographicHash::hash(rawMime, QCryptographicHash::Sha256).toHex();
const QString fileName = safeComponent(stableId.isEmpty()
? QString::fromLatin1(digest)
: stableId) + QStringLiteral("_") + QString::fromLatin1(digest.left(12)) + QStringLiteral(".eml");
const QString relative = accountPart + "/" + folderPart + "/" + fileName;
const QString path = QDir(storagePath()).filePath(relative);
ensureDirectory(QFileInfo(path).absolutePath());
QSaveFile file(path);
if (!file.open(QIODevice::WriteOnly) || file.write(rawMime) != rawMime.size() || !file.commit()) {
qWarning() << "[MimeStorage] Failed to save raw MIME:" << path << file.errorString();
return QString();
}
qWarning() << "[MimeStorage] Failed to save .eml:" << filePath;
return QString();
return relative.left(relative.size() - 4); // fileId excludes .eml
}
bool MimeStorageService::storeMessage(const QString &accountId, const QString &folderId,
MailItem &mail, const QByteArray &rawMime,
QVector<QString> *storedAttachmentPaths)
{
ParsedMimeMessage parsed;
if (!parseMessage(rawMime, parsed)) return false;
if (!parsed.messageId.isEmpty()) mail.setMessageId(parsed.messageId);
if (!parsed.subject.isEmpty()) mail.setSubject(parsed.subject);
if (!parsed.from.isEmpty()) mail.setSender(parsed.from);
if (!parsed.to.isEmpty()) { mail.setTo(parsed.to); mail.setRecipient(parsed.to); }
if (!parsed.cc.isEmpty()) mail.setCc(parsed.cc);
if (!parsed.bcc.isEmpty()) mail.setBcc(parsed.bcc);
if (parsed.date.isValid()) mail.setDate(parsed.date);
mail.setBodyHtml(parsed.bodyHtml);
mail.setSize(rawMime.size());
mail.setRawMime(rawMime);
const QString fileId = saveRawEmlFile(accountId, folderId, rawMime, mail.messageId());
if (fileId.isEmpty()) return false;
mail.setFileId(fileId);
QVector<QString> names;
QVector<QString> savedPaths;
for (const ParsedMimeAttachment &attachment : parsed.attachments) {
const QString path = saveAttachment(fileId, attachment.fileName, attachment.data);
if (path.isEmpty()) {
for (const QString &savedPath : savedPaths) QFile::remove(savedPath);
deleteEmlFile(fileId);
return false;
}
savedPaths.append(path);
names.append(attachment.fileName);
if (storedAttachmentPaths) storedAttachmentPaths->append(path);
}
mail.setAttachments(names);
return true;
}
QString MimeStorageService::getEmlFilePath(const QString &fileId) const
{
// Search in all account/folder directories (simplified)
QDir baseDir(storagePath());
QStringList filters;
filters << fileId + ".eml";
QFileInfoList files = baseDir.entryInfoList(filters, QDir::Files, QDir::Name);
for (const auto &info : files) {
QStringList parts = info.absoluteFilePath().split('/');
if (parts.size() >= 2) {
return info.absoluteFilePath();
}
}
return QString();
QString relative = fileId;
if (relative.endsWith(QStringLiteral(".eml"))) relative.chop(4);
const QString base = QDir::cleanPath(storagePath());
const QString path = QDir::cleanPath(QDir(base).filePath(relative + QStringLiteral(".eml")));
if (!path.startsWith(base + QDir::separator()) || !QFileInfo::exists(path)) return QString();
return path;
}
QByteArray MimeStorageService::readEmlFile(const QString &fileId) const
@@ -88,29 +142,218 @@ bool MimeStorageService::deleteEmlFile(const QString &fileId)
{
QString path = getEmlFilePath(fileId);
if (path.isEmpty()) return false;
return QFile::remove(path);
const bool removed = QFile::remove(path);
QDir attachments(QFileInfo(path).absolutePath() + QStringLiteral("/attachments"));
if (attachments.exists()) attachments.removeRecursively();
return removed;
}
QStringList MimeStorageService::listAttachments(const QString &mailItemId) const
{
Q_UNUSED(mailItemId);
// TODO: Parse .eml to get attachments when GMime is available
return {};
const QString emlPath = getEmlFilePath(mailItemId);
if (emlPath.isEmpty()) return {};
QDir dir(QFileInfo(emlPath).absolutePath() + QStringLiteral("/attachments"));
return dir.entryList(QDir::Files, QDir::Name);
}
bool MimeStorageService::saveAttachment(const QString &mailItemId, const QString &fileName, const QByteArray &data)
QString MimeStorageService::saveAttachment(const QString &fileId, const QString &fileName, const QByteArray &data)
{
Q_UNUSED(mailItemId);
QString dir = storagePath() + "/attachments";
ensureDirectory(dir);
QFile file(dir + "/" + fileName);
if (file.open(QIODevice::WriteOnly)) {
file.write(data);
file.close();
return true;
const QString emlPath = getEmlFilePath(fileId);
if (emlPath.isEmpty()) return QString();
const QString dir = ensureDirectory(QFileInfo(emlPath).absolutePath() + QStringLiteral("/attachments"));
const QString originalName = QFileInfo(fileName).fileName();
const QString safeName = safeComponent(originalName.isEmpty()
? QStringLiteral("attachment") : originalName);
QString path = QDir(dir).filePath(safeName);
if (QFileInfo::exists(path)) {
const QFileInfo info(path);
const QString suffix = info.completeSuffix().isEmpty()
? QString() : QStringLiteral(".") + info.completeSuffix();
const QString stem = info.completeBaseName();
path = QDir(dir).filePath(stem + QStringLiteral("_")
+ QString::fromLatin1(QCryptographicHash::hash(data, QCryptographicHash::Sha256).toHex().left(12))
+ suffix);
}
return false;
QSaveFile file(path);
if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.commit()) return QString();
return path;
}
QString MimeStorageService::decodeHeaderValue(const QString &value) const
{
QString result = value;
const QRegularExpression rx(QStringLiteral("=\\?([^?]+)\\?([bBqQ])\\?([^?]+)\\?="));
QRegularExpressionMatchIterator it = rx.globalMatch(result);
while (it.hasNext()) {
const QRegularExpressionMatch match = it.next();
QByteArray encoded = match.captured(3).toLatin1();
QByteArray decoded;
if (match.captured(2).compare(QStringLiteral("b"), Qt::CaseInsensitive) == 0)
decoded = QByteArray::fromBase64(encoded);
else {
encoded.replace('_', ' ');
QByteArray out;
for (int i = 0; i < encoded.size(); ++i) {
if (encoded[i] == '=' && i + 2 < encoded.size()) {
bool ok = false;
const int value = QByteArray(encoded.mid(i + 1, 2)).toInt(&ok, 16);
if (ok) { out.append(char(value)); i += 2; continue; }
}
out.append(encoded[i]);
}
decoded = out;
}
QString decodedText = QString::fromUtf8(decoded);
if (decodedText.isEmpty() && !decoded.isEmpty()) decodedText = QString::fromLatin1(decoded);
result.replace(match.captured(0), decodedText);
}
return result.trimmed();
}
QByteArray MimeStorageService::decodeTransfer(const QByteArray &data, const QString &encoding) const
{
const QString normalized = encoding.trimmed().toLower();
if (normalized == QStringLiteral("base64")) return QByteArray::fromBase64(data);
if (normalized != QStringLiteral("quoted-printable")) return data;
QByteArray result;
for (int i = 0; i < data.size(); ++i) {
if (data[i] == '=' && i + 1 < data.size()) {
if (data[i + 1] == '\r' && i + 2 < data.size() && data[i + 2] == '\n') { i += 2; continue; }
if (data[i + 1] == '\n') { ++i; continue; }
if (i + 2 < data.size()) {
bool ok = false;
const int value = QByteArray(data.mid(i + 1, 2)).toInt(&ok, 16);
if (ok) { result.append(char(value)); i += 2; continue; }
}
}
result.append(data[i]);
}
return result;
}
void MimeStorageService::parseMimePart(const QByteArray &part, ParsedMimeMessage &message,
QString *plainText, QString *htmlText) const
{
const int crlf = part.indexOf("\r\n\r\n");
const int lf = part.indexOf("\n\n");
int split = crlf >= 0 ? crlf : lf;
const int separatorSize = crlf >= 0 ? 4 : 2;
const QByteArray headerBytes = split >= 0 ? part.left(split) : part;
const QByteArray body = split >= 0 ? part.mid(split + separatorSize) : QByteArray();
QMap<QString, QString> headers;
QString current;
for (QString line : QString::fromLatin1(headerBytes).split(QRegularExpression("\\r?\\n"))) {
if ((line.startsWith(' ') || line.startsWith('\t')) && !current.isEmpty()) {
headers[current] += QStringLiteral(" ") + line.trimmed();
continue;
}
const int colon = line.indexOf(':');
if (colon < 0) continue;
current = line.left(colon).trimmed().toLower();
headers[current] = line.mid(colon + 1).trimmed();
}
auto header = [&](const QString &name) { return headers.value(name.toLower()); };
const QString contentType = header(QStringLiteral("content-type"));
const QString disposition = header(QStringLiteral("content-disposition"));
const QString transfer = header(QStringLiteral("content-transfer-encoding"));
QRegularExpression boundaryRx(QStringLiteral("(?:^|;)\\s*boundary\\s*=\\s*(?:\\\"([^\\\"]+)\\\"|([^;\\s]+))"), QRegularExpression::CaseInsensitiveOption);
const QRegularExpressionMatch boundaryMatch = boundaryRx.match(contentType);
if (contentType.startsWith(QStringLiteral("multipart/"), Qt::CaseInsensitive) && boundaryMatch.hasMatch()) {
const QString boundary = boundaryMatch.captured(1).isEmpty() ? boundaryMatch.captured(2) : boundaryMatch.captured(1);
const QByteArray marker = QByteArrayLiteral("--") + boundary.toUtf8();
int pos = 0;
while ((pos = body.indexOf(marker, pos)) >= 0) {
int start = pos + marker.size();
if (body.mid(start, 2) == QByteArrayLiteral("--")) break;
if (body.mid(start, 2) == QByteArrayLiteral("\r\n")) start += 2;
else if (body.mid(start, 1) == QByteArrayLiteral("\n")) ++start;
int end = body.indexOf(marker, start);
if (end < 0) end = body.size();
QByteArray child = body.mid(start, end - start);
while (child.endsWith("\r\n") || child.endsWith('\n')) child.chop(child.endsWith("\r\n") ? 2 : 1);
parseMimePart(child, message, plainText, htmlText);
pos = end;
}
return;
}
const QByteArray decoded = decodeTransfer(body, transfer);
QRegularExpression parameterRx(QStringLiteral("(?:^|;)\\s*(?:filename|name)(?:\\*\\d+)?\\*?\\s*=\\s*(?:\\\"([^\\\"]+)\\\"|([^;\\s]+))"), QRegularExpression::CaseInsensitiveOption);
const QRegularExpressionMatch dispositionMatch = parameterRx.match(disposition);
const QRegularExpressionMatch typeMatch = parameterRx.match(contentType);
QString fileName = dispositionMatch.hasMatch()
? (dispositionMatch.captured(1).isEmpty() ? dispositionMatch.captured(2) : dispositionMatch.captured(1))
: (typeMatch.hasMatch() ? (typeMatch.captured(1).isEmpty() ? typeMatch.captured(2) : typeMatch.captured(1)) : QString());
const int encodedPrefix = fileName.indexOf(QStringLiteral("''"));
if (encodedPrefix >= 0) fileName = fileName.mid(encodedPrefix + 2);
fileName = QUrl::fromPercentEncoding(fileName.toUtf8());
fileName = decodeHeaderValue(fileName);
const QString mimeType = contentType.section(';', 0, 0).trimmed().toLower();
const bool isAttachment = disposition.startsWith(QStringLiteral("attachment"), Qt::CaseInsensitive)
|| !fileName.isEmpty() || (!mimeType.startsWith(QStringLiteral("text/")) && !mimeType.isEmpty());
if (isAttachment) {
ParsedMimeAttachment attachment;
attachment.fileName = fileName.isEmpty() ? QStringLiteral("attachment-%1").arg(message.attachments.size() + 1) : fileName;
attachment.mimeType = mimeType;
attachment.contentId = header(QStringLiteral("content-id")).trimmed();
attachment.contentId.remove('<');
attachment.contentId.remove('>');
attachment.data = decoded;
message.attachments.append(attachment);
return;
}
const QString text = QString::fromUtf8(decoded);
if (mimeType == QStringLiteral("text/html")) {
if (htmlText && htmlText->isEmpty()) *htmlText = text;
} else if (mimeType == QStringLiteral("text/plain") || mimeType.isEmpty()) {
if (plainText && plainText->isEmpty()) *plainText = text;
}
}
bool MimeStorageService::parseMessage(const QByteArray &rawMime, ParsedMimeMessage &message) const
{
if (rawMime.isEmpty()) return false;
message = ParsedMimeMessage();
QString plainText;
QString htmlText;
parseMimePart(rawMime, message, &plainText, &htmlText);
auto parseHeaders = [&](const QByteArray &data) {
const int split = data.indexOf("\r\n\r\n") >= 0 ? data.indexOf("\r\n\r\n") : data.indexOf("\n\n");
const QByteArray headerData = split >= 0 ? data.left(split) : data;
QMap<QString, QString> headers;
QString current;
for (QString line : QString::fromLatin1(headerData).split(QRegularExpression("\\r?\\n"))) {
if ((line.startsWith(' ') || line.startsWith('\t')) && !current.isEmpty()) { headers[current] += " " + line.trimmed(); continue; }
const int colon = line.indexOf(':');
if (colon < 0) continue;
current = line.left(colon).trimmed().toLower();
headers[current] = line.mid(colon + 1).trimmed();
}
return headers;
};
const QMap<QString, QString> headers = parseHeaders(rawMime);
message.messageId = decodeHeaderValue(headers.value(QStringLiteral("message-id"))).remove('<').remove('>');
message.subject = decodeHeaderValue(headers.value(QStringLiteral("subject")));
message.from = decodeHeaderValue(headers.value(QStringLiteral("from")));
message.to = decodeHeaderValue(headers.value(QStringLiteral("to")));
message.cc = decodeHeaderValue(headers.value(QStringLiteral("cc")));
message.bcc = decodeHeaderValue(headers.value(QStringLiteral("bcc")));
message.date = QDateTime::fromString(headers.value(QStringLiteral("date")), Qt::RFC2822Date);
if (!message.date.isValid()) message.date = QDateTime::currentDateTimeUtc();
if (!htmlText.isEmpty()) message.bodyHtml = htmlText;
else {
QString escaped = plainText.toHtmlEscaped();
escaped.replace(QStringLiteral("\r\n"), QStringLiteral("<br>"));
escaped.replace(QStringLiteral("\n"), QStringLiteral("<br>"));
message.bodyHtml = escaped;
}
return true;
}
#include "mimestorage.moc"
+36 -1
View File
@@ -5,8 +5,31 @@
#include <QString>
#include <QDir>
#include <QFile>
#include <QByteArray>
#include <QVector>
#include "core/mailitem.h"
struct ParsedMimeAttachment
{
QString fileName;
QString mimeType;
QString contentId;
QByteArray data;
};
struct ParsedMimeMessage
{
QString messageId;
QString subject;
QString from;
QString to;
QString cc;
QString bcc;
QDateTime date;
QString bodyHtml;
QVector<ParsedMimeAttachment> attachments;
};
class MimeStorageService : public QObject
{
Q_OBJECT
@@ -15,16 +38,28 @@ public:
~MimeStorageService() override = default;
QString saveEmlFile(const QString &accountId, const QString &folderId, const MailItem &mail);
QString saveRawEmlFile(const QString &accountId, const QString &folderId,
const QByteArray &rawMime, const QString &stableId = QString());
bool storeMessage(const QString &accountId, const QString &folderId,
MailItem &mail, const QByteArray &rawMime,
QVector<QString> *storedAttachmentPaths = nullptr);
bool parseMessage(const QByteArray &rawMime, ParsedMimeMessage &message) const;
QString getEmlFilePath(const QString &fileId) const;
QByteArray readEmlFile(const QString &fileId) const;
bool deleteEmlFile(const QString &fileId);
QStringList listAttachments(const QString &mailItemId) const;
bool saveAttachment(const QString &mailItemId, const QString &fileName, const QByteArray &data);
QString saveAttachment(const QString &fileId, const QString &fileName,
const QByteArray &data);
private:
QString storagePath() const;
QString ensureDirectory(const QString &path) const;
QString safeComponent(const QString &value) const;
QString decodeHeaderValue(const QString &value) const;
QByteArray decodeTransfer(const QByteArray &data, const QString &encoding) const;
void parseMimePart(const QByteArray &part, ParsedMimeMessage &message,
QString *plainText, QString *htmlText) const;
};
#endif // MIMESTORAGESERVICE_H
+165
View File
@@ -0,0 +1,165 @@
/****************************************************************************
** Meta object code from reading C++ file 'accountservice.h'
**
** Created by: The Qt Meta Object Compiler version 69 (Qt 6.10.2)
**
** WARNING! All changes made in this file will be lost!
*****************************************************************************/
#include "accountservice.h"
#include <QtCore/qmetatype.h>
#include <QtCore/qtmochelpers.h>
#include <memory>
#include <QtCore/qxptype_traits.h>
#if !defined(Q_MOC_OUTPUT_REVISION)
#error "The header file 'accountservice.h' doesn't include <QObject>."
#elif Q_MOC_OUTPUT_REVISION != 69
#error "This file was generated using the moc from 6.10.2. It"
#error "cannot be used with the include files from this version of Qt."
#error "(The moc has changed too much.)"
#endif
#ifndef Q_CONSTINIT
#define Q_CONSTINIT
#endif
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
QT_WARNING_DISABLE_GCC("-Wuseless-cast")
namespace {
struct qt_meta_tag_ZN14AccountServiceE_t {};
} // unnamed namespace
template <> constexpr inline auto AccountService::qt_create_metaobjectdata<qt_meta_tag_ZN14AccountServiceE_t>()
{
namespace QMC = QtMocConstants;
QtMocHelpers::StringRefStorage qt_stringData {
"AccountService",
"accountListChanged",
"",
"accountAdded",
"Account",
"account",
"accountRemoved",
"accountId",
"authenticationRequired",
"email",
"authUrl"
};
QtMocHelpers::UintData qt_methods {
// Signal 'accountListChanged'
QtMocHelpers::SignalData<void()>(1, 2, QMC::AccessPublic, QMetaType::Void),
// Signal 'accountAdded'
QtMocHelpers::SignalData<void(const Account &)>(3, 2, QMC::AccessPublic, QMetaType::Void, {{
{ 0x80000000 | 4, 5 },
}}),
// Signal 'accountRemoved'
QtMocHelpers::SignalData<void(int)>(6, 2, QMC::AccessPublic, QMetaType::Void, {{
{ QMetaType::Int, 7 },
}}),
// Signal 'authenticationRequired'
QtMocHelpers::SignalData<void(const QString &, const QString &)>(8, 2, QMC::AccessPublic, QMetaType::Void, {{
{ QMetaType::QString, 9 }, { QMetaType::QString, 10 },
}}),
};
QtMocHelpers::UintData qt_properties {
};
QtMocHelpers::UintData qt_enums {
};
return QtMocHelpers::metaObjectData<AccountService, qt_meta_tag_ZN14AccountServiceE_t>(QMC::MetaObjectFlag{}, qt_stringData,
qt_methods, qt_properties, qt_enums);
}
Q_CONSTINIT const QMetaObject AccountService::staticMetaObject = { {
QMetaObject::SuperData::link<QObject::staticMetaObject>(),
qt_staticMetaObjectStaticContent<qt_meta_tag_ZN14AccountServiceE_t>.stringdata,
qt_staticMetaObjectStaticContent<qt_meta_tag_ZN14AccountServiceE_t>.data,
qt_static_metacall,
nullptr,
qt_staticMetaObjectRelocatingContent<qt_meta_tag_ZN14AccountServiceE_t>.metaTypes,
nullptr
} };
void AccountService::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
auto *_t = static_cast<AccountService *>(_o);
if (_c == QMetaObject::InvokeMetaMethod) {
switch (_id) {
case 0: _t->accountListChanged(); break;
case 1: _t->accountAdded((*reinterpret_cast<std::add_pointer_t<Account>>(_a[1]))); break;
case 2: _t->accountRemoved((*reinterpret_cast<std::add_pointer_t<int>>(_a[1]))); break;
case 3: _t->authenticationRequired((*reinterpret_cast<std::add_pointer_t<QString>>(_a[1])),(*reinterpret_cast<std::add_pointer_t<QString>>(_a[2]))); break;
default: ;
}
}
if (_c == QMetaObject::IndexOfMethod) {
if (QtMocHelpers::indexOfMethod<void (AccountService::*)()>(_a, &AccountService::accountListChanged, 0))
return;
if (QtMocHelpers::indexOfMethod<void (AccountService::*)(const Account & )>(_a, &AccountService::accountAdded, 1))
return;
if (QtMocHelpers::indexOfMethod<void (AccountService::*)(int )>(_a, &AccountService::accountRemoved, 2))
return;
if (QtMocHelpers::indexOfMethod<void (AccountService::*)(const QString & , const QString & )>(_a, &AccountService::authenticationRequired, 3))
return;
}
}
const QMetaObject *AccountService::metaObject() const
{
return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}
void *AccountService::qt_metacast(const char *_clname)
{
if (!_clname) return nullptr;
if (!strcmp(_clname, qt_staticMetaObjectStaticContent<qt_meta_tag_ZN14AccountServiceE_t>.strings))
return static_cast<void*>(this);
return QObject::qt_metacast(_clname);
}
int AccountService::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
_id = QObject::qt_metacall(_c, _id, _a);
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
if (_id < 4)
qt_static_metacall(this, _c, _id, _a);
_id -= 4;
}
if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
if (_id < 4)
*reinterpret_cast<QMetaType *>(_a[0]) = QMetaType();
_id -= 4;
}
return _id;
}
// SIGNAL 0
void AccountService::accountListChanged()
{
QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}
// SIGNAL 1
void AccountService::accountAdded(const Account & _t1)
{
QMetaObject::activate<void>(this, &staticMetaObject, 1, nullptr, _t1);
}
// SIGNAL 2
void AccountService::accountRemoved(int _t1)
{
QMetaObject::activate<void>(this, &staticMetaObject, 2, nullptr, _t1);
}
// SIGNAL 3
void AccountService::authenticationRequired(const QString & _t1, const QString & _t2)
{
QMetaObject::activate<void>(this, &staticMetaObject, 3, nullptr, _t1, _t2);
}
QT_WARNING_POP
+109 -109
View File
@@ -13,6 +13,8 @@
#include <QEventLoop>
#include "../../core/events.h"
#include "../../core/eventbus.h"
#include "db/dao/folderdao.h"
#include "services/mimestorage.h"
OutlookSynchronizer::OutlookSynchronizer(QObject* parent)
: Synchronizer(parent),
@@ -84,7 +86,6 @@ bool OutlookSynchronizer::syncFolder(const Folder& folder)
// En una implementación real, aquí compararíamos con la base de datos local
// y emitiríamos las señales apropiadas para elementos nuevos/actualizados/eliminados
// Por ahora, simulamos que obtenemos algunos elementos
if (!items.isEmpty()) {
for (const MailItem& item : items) {
emit mailItemAdded(item);
@@ -139,128 +140,123 @@ QVector<MailItem> OutlookSynchronizer::fetchMailItems(const QString& folderId,
}
}
qDebug() << "Fetching mail items for folder:" << folderId;
// Construir la URL para Microsoft Graph API
QString endpoint = QString("/me/mailFolders/%1/messages").arg(folderId);
QString url = buildGraphUrl(endpoint);
// Parámetros de consulta
qDebug() << "Fetching complete Outlook messages for folder:" << folderId;
QString remoteFolderId = folderId;
bool localFolderId = false;
folderId.toInt(&localFolderId);
if (localFolderId) {
const auto folder = FolderDao::findById(folderId.toInt());
if (folder) {
remoteFolderId = folder->parentFolderId();
if (remoteFolderId.isEmpty()) {
const QString name = folder->name().toLower();
if (name == QStringLiteral("inbox")) remoteFolderId = QStringLiteral("inbox");
else if (name == QStringLiteral("sent") || name == QStringLiteral("sent items")) remoteFolderId = QStringLiteral("sentitems");
else if (name == QStringLiteral("drafts")) remoteFolderId = QStringLiteral("drafts");
else if (name == QStringLiteral("trash") || name == QStringLiteral("deleted items")) remoteFolderId = QStringLiteral("deleteditems");
else remoteFolderId = folder->name();
}
}
}
QNetworkAccessManager network;
auto get = [&](const QUrl &url, QByteArray &data) {
QNetworkRequest request = createAuthRequest(url.toString());
request.setRawHeader("Accept", "message/rfc822, application/json");
QNetworkReply *reply = network.get(request);
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
const bool ok = reply->error() == QNetworkReply::NoError;
if (ok) data = reply->readAll();
else qWarning() << "Outlook request failed:" << reply->errorString() << url;
reply->deleteLater();
return ok;
};
QVector<QJsonObject> messageRefs;
QUrl nextUrl(buildGraphUrl(QStringLiteral("/me/mailFolders/%1/messages").arg(remoteFolderId)));
QUrlQuery query;
query.addQueryItem("$top", "50"); // Limitar a 50 mensajes por petición
query.addQueryItem("$orderby", "receivedDateTime DESC");
url += "?" + query.toString();
QNetworkRequest request = createAuthRequest(url);
QNetworkReply* reply = m_networkManager->get(request);
// Nota: En una implementación real, esperaríamos la respuesta asíncronamente
// pero por simplicidad en este stub, simulamos una respuesta
// Simular respuesta para desarrollo
query.addQueryItem(QStringLiteral("$top"), QStringLiteral("100"));
query.addQueryItem(QStringLiteral("$select"), QStringLiteral("id,isRead,importance"));
query.addQueryItem(QStringLiteral("$orderby"), QStringLiteral("receivedDateTime DESC"));
nextUrl.setQuery(query);
while (nextUrl.isValid() && !nextUrl.toString().isEmpty()) {
QByteArray response;
if (!get(nextUrl, response)) return {};
QJsonParseError parseError;
const QJsonDocument document = QJsonDocument::fromJson(response, &parseError);
if (parseError.error != QJsonParseError::NoError || !document.isObject()) return {};
const QJsonObject object = document.object();
for (const QJsonValue &value : object.value(QStringLiteral("value")).toArray())
if (value.isObject()) messageRefs.append(value.toObject());
const QString continuation = object.value(QStringLiteral("@odata.nextLink")).toString();
nextUrl = continuation.isEmpty() ? QUrl() : QUrl(continuation);
}
QVector<MailItem> items;
items.append(MailItem(1, folderId.toInt(), "Reunión de proyecto",
"juan.perez@empresa.com", m_account.email(),
QDateTime::currentDateTime().addSecs(-3600),
false, false));
items.append(MailItem(2, folderId.toInt(), "Entrega de documentación",
"ana.gomez@cliente.com", m_account.email(),
QDateTime::currentDateTime().addSecs(-7200),
true, false));
MimeStorageService mimeStorage;
for (const QJsonObject &reference : messageRefs) {
const QString messageId = reference.value(QStringLiteral("id")).toString();
if (messageId.isEmpty()) continue;
const QString encodedId = QString::fromUtf8(QUrl::toPercentEncoding(messageId));
const QUrl rawUrl(buildGraphUrl(QStringLiteral("/me/messages/%1/$value").arg(encodedId)));
QByteArray rawMime;
if (!get(rawUrl, rawMime) || rawMime.isEmpty()) continue;
ParsedMimeMessage parsed;
if (!mimeStorage.parseMessage(rawMime, parsed)) continue;
MailItem item;
item.setFolderId(folderId.toInt());
item.setMessageId(messageId);
item.setRawMime(rawMime);
item.setSubject(parsed.subject.isEmpty() ? QStringLiteral("(No Subject)") : parsed.subject);
item.setSender(parsed.from);
item.setRecipient(parsed.to);
item.setTo(parsed.to);
item.setCc(parsed.cc);
item.setBcc(parsed.bcc);
item.setDate(parsed.date);
item.setBodyHtml(parsed.bodyHtml);
item.setSize(rawMime.size());
item.setRead(reference.value(QStringLiteral("isRead")).toBool(true));
item.setFlagged(reference.value(QStringLiteral("importance")).toString() == QStringLiteral("high"));
QVector<QString> attachmentNames;
for (const ParsedMimeAttachment &attachment : parsed.attachments) attachmentNames.append(attachment.fileName);
item.setAttachments(attachmentNames);
items.append(item);
}
return items;
}
bool OutlookSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
{
if (!m_account.isTokenValid()) {
if (!refreshAccessToken()) {
qWarning() << "Failed to refresh token for appending mail item";
return false;
}
}
qDebug() << "Appending mail item to folder:" << folderId;
// En una implementación real, llamaríamos a Microsoft Graph API
// para crear un mensaje en la carpeta especificada
// Por ahora, simulamos éxito
emit mailItemAdded(item);
// Publish MailItemAddedEvent
WinoMail::Events::MailItemAddedEvent mailEvent;
mailEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
mailEvent.timestamp = QDateTime::currentDateTimeUtc();
mailEvent.item = item;
PUBLISH(mailEvent);
return true;
Q_UNUSED(folderId);
Q_UNUSED(item);
qWarning() << "Outlook appendMailItem is not used for sending; MailService sends via Graph API";
return false;
}
bool OutlookSynchronizer::updateMailItemFlags(const QString& folderId,
const QString& itemUid,
bool read, bool flagged)
{
if (!m_account.isTokenValid()) {
if (!refreshAccessToken()) {
qWarning() << "Failed to refresh token for updating mail item flags";
return false;
}
}
qDebug() << "Updating mail item flags:" << itemUid
<< "read:" << read << "flagged:" << flagged;
// En una implementación real, llamaríamos a Microsoft Graph API
// para actualizar las flags del mensaje
// Por ahora, simulamos éxito
MailItem updatedItem;
updatedItem.setId(itemUid.toLongLong());
updatedItem.setFolderId(folderId.toInt());
updatedItem.setRead(read);
updatedItem.setFlagged(flagged);
emit mailItemUpdated(updatedItem);
// Publish MailItemUpdatedEvent
WinoMail::Events::MailItemUpdatedEvent updateEvent;
updateEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
updateEvent.timestamp = QDateTime::currentDateTimeUtc();
updateEvent.item = updatedItem;
updateEvent.changedFields = QStringList() << "read" << "flagged"; // Simplified
PUBLISH(updateEvent);
return true;
Q_UNUSED(folderId);
Q_UNUSED(itemUid);
Q_UNUSED(read);
Q_UNUSED(flagged);
qWarning() << "Outlook flag update is not available through this synchronizer";
return false;
}
bool OutlookSynchronizer::deleteMailItem(const QString& folderId,
const QString& itemUid)
{
if (!m_account.isTokenValid()) {
if (!refreshAccessToken()) {
qWarning() << "Failed to refresh token for deleting mail item";
return false;
}
}
qDebug() << "Deleting mail item:" << itemUid << "from folder:" << folderId;
// En una implementación real, llamaríamos a Microsoft Graph API
// para eliminar el mensaje
// Por ahora, simulamos éxito
emit mailItemRemoved(itemUid);
// Publish MailItemRemovedEvent
WinoMail::Events::MailItemRemovedEvent removeEvent;
removeEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
removeEvent.timestamp = QDateTime::currentDateTimeUtc();
removeEvent.itemUid = itemUid;
removeEvent.folderId = folderId.toInt();
PUBLISH(removeEvent);
return true;
Q_UNUSED(folderId);
Q_UNUSED(itemUid);
qWarning() << "Outlook delete is not available through this synchronizer";
return false;
}
void OutlookSynchronizer::onGraphReplyFinished(QNetworkReply* reply)
@@ -340,9 +336,12 @@ bool OutlookSynchronizer::refreshAccessToken()
return false;
}
// TODO: These should come from secure configuration, not hardcoded
const QString clientId = "YOUR_CLIENT_ID_HERE"; // Replace with actual client ID
const QString clientSecret = "YOUR_CLIENT_SECRET_HERE"; // Replace with actual client secret
const QString clientId = qEnvironmentVariable("WINO_OUTLOOK_CLIENT_ID");
const QString clientSecret = qEnvironmentVariable("WINO_OUTLOOK_CLIENT_SECRET");
if (clientId.isEmpty() || clientSecret.isEmpty()) {
qWarning() << "Outlook token expired and WINO_OUTLOOK_CLIENT_ID/SECRET are not configured";
return false;
}
const QString tokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
QNetworkRequest networkRequest{QUrl(tokenUrl)};
@@ -355,7 +354,8 @@ bool OutlookSynchronizer::refreshAccessToken()
postData.addQueryItem("grant_type", "refresh_token");
postData.addQueryItem("client_secret", clientSecret);
QNetworkReply* reply = m_networkManager->post(networkRequest, postData.toString(QUrl::FullyEncoded).toUtf8());
QNetworkAccessManager network;
QNetworkReply* reply = network.post(networkRequest, postData.toString(QUrl::FullyEncoded).toUtf8());
// Wait for reply synchronously for simplicity in this context
QEventLoop loop;
+318
View File
@@ -0,0 +1,318 @@
#include "pop3synchronizer.h"
#include <QDebug>
#include <QDateTime>
#include <QRegularExpression>
#include <QSqlQuery>
#include <QAbstractSocket>
#include <QMap>
#include <QCryptographicHash>
#include "../../core/mailitem.h"
#include "../../db/dao/mailitemdao.h"
#include "../../db/dao/folderdao.h"
#include "../../core/models/account.h"
#include "../../services/mimestorage.h"
Pop3Synchronizer::Pop3Synchronizer(QObject *parent)
: Synchronizer(parent)
, m_socket(nullptr)
{
}
bool Pop3Synchronizer::initialize(const Account &account)
{
m_account = account;
const auto &settings = account.connectionSettings();
m_host = settings.incomingHost;
m_port = settings.incomingPort;
m_useSsl = settings.incomingSsl;
m_username = settings.username;
m_password = settings.password;
if (m_socket) {
m_socket->deleteLater();
m_socket = nullptr;
}
m_socket = new QSslSocket(this);
qDebug() << "POP3 synchronizer initialized for" << m_username << "at" << m_host;
return true;
}
bool Pop3Synchronizer::syncFolder(const Folder &folder)
{
Q_UNUSED(folder);
return true;
}
QVector<Folder> Pop3Synchronizer::getFolders() const
{
QVector<Folder> folders;
Folder inbox;
inbox.setId(0);
inbox.setAccountId(m_account.id());
inbox.setName(QStringLiteral("Inbox"));
inbox.setParentFolderId(QString());
folders.append(inbox);
return folders;
}
QVector<MailItem> Pop3Synchronizer::fetchMailItems(const QString &folderId, qint64 sinceUid)
{
QVector<MailItem> items;
QSslSocket socket;
if (m_useSsl) socket.connectToHostEncrypted(m_host, m_port);
else socket.connectToHost(m_host, m_port);
if (!socket.waitForConnected(15000)) {
qWarning() << "Failed to connect to POP3 server:" << socket.errorString();
return items;
}
if (m_useSsl && !socket.waitForEncrypted(15000)) {
qWarning() << "TLS handshake failed:" << socket.errorString();
return items;
}
auto readLine = [&](QByteArray &line) {
if (!socket.canReadLine() && !socket.waitForReadyRead(15000)) return false;
line = socket.readLine();
return !line.isEmpty();
};
auto command = [&](const QByteArray &commandText, QByteArray &response, bool multiline) {
socket.write(commandText + QByteArrayLiteral("\r\n"));
socket.flush();
if (!readLine(response) || !response.trimmed().startsWith('+')) return false;
if (multiline) {
while (true) {
QByteArray line;
if (!readLine(line)) return false;
if (line == QByteArrayLiteral(".\r\n") || line == QByteArrayLiteral(".\n")) break;
if (line.startsWith("..")) line.remove(0, 1);
response.append(line);
}
}
return true;
};
QByteArray response;
if (!readLine(response) || !response.trimmed().startsWith('+')) return items;
if (!command(QByteArrayLiteral("USER ") + m_username.toUtf8(), response, false)
|| !command(QByteArrayLiteral("PASS ") + m_password.toUtf8(), response, false)) {
qWarning() << "POP3 authentication failed:" << response;
return items;
}
if (!command(QByteArrayLiteral("UIDL"), response, true)) {
qWarning() << "UIDL failed:" << response;
return items;
}
m_uidToMsgNum.clear();
m_maxUid = 0;
QList<qint64> uidsToFetch;
const QStringList lines = QString::fromUtf8(response).split(QRegularExpression("\\r?\\n"));
for (const QString &line : lines) {
const QStringList parts = line.trimmed().split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
if (parts.size() < 2) continue;
bool okMsgId = false, okUid = false;
const int msgNum = parts.at(0).toInt(&okMsgId);
qint64 uid = parts.at(1).toLongLong(&okUid);
if (!okUid) {
const QByteArray digest = QCryptographicHash::hash(parts.at(1).toUtf8(), QCryptographicHash::Sha256).toHex().left(15);
uid = digest.toLongLong(&okUid, 16);
}
if (!okMsgId || !okUid || uid <= 0) continue;
m_uidToMsgNum[QString::number(uid)] = msgNum;
m_maxUid = qMax(m_maxUid, uid);
if (uid > sinceUid) uidsToFetch.append(uid);
}
MimeStorageService mimeStorage;
int processed = 0;
for (const qint64 uid : uidsToFetch) {
const int msgNum = m_uidToMsgNum.value(QString::number(uid));
QByteArray rawResponse;
if (!command(QByteArrayLiteral("RETR ") + QByteArray::number(msgNum), rawResponse, true)) {
qWarning() << "RETR failed for message" << msgNum;
continue;
}
const int firstLineEnd = rawResponse.indexOf("\r\n");
const QByteArray rawMime = firstLineEnd >= 0 ? rawResponse.mid(firstLineEnd + 2) : QByteArray();
ParsedMimeMessage parsed;
if (rawMime.isEmpty() || !mimeStorage.parseMessage(rawMime, parsed)) {
qWarning() << "Failed to parse POP3 MIME message with UID" << uid;
continue;
}
MailItem item;
item.setFolderId(folderId.toInt());
item.setMessageId(parsed.messageId);
item.setSubject(parsed.subject.isEmpty() ? QStringLiteral("(No Subject)") : parsed.subject);
item.setSender(parsed.from);
item.setRecipient(parsed.to);
item.setTo(parsed.to);
item.setCc(parsed.cc);
item.setBcc(parsed.bcc);
item.setDate(parsed.date);
item.setBodyHtml(parsed.bodyHtml);
item.setSize(rawMime.size());
item.setRawMime(rawMime);
item.setUid(uid);
QVector<QString> attachmentNames;
for (const ParsedMimeAttachment &attachment : parsed.attachments) attachmentNames.append(attachment.fileName);
item.setAttachments(attachmentNames);
items.append(item);
++processed;
emit progressChanged(uidsToFetch.isEmpty() ? 100 : static_cast<int>((100.0 * processed) / uidsToFetch.size()));
emit statusMessage(tr("Fetched %1 of %2 messages").arg(processed).arg(uidsToFetch.size()));
}
command(QByteArrayLiteral("QUIT"), response, false);
socket.disconnectFromHost();
return items;
}
bool Pop3Synchronizer::appendMailItem(const QString &folderId, const MailItem &item)
{
Q_UNUSED(folderId);
Q_UNUSED(item);
qWarning() << "POP3 appendMailItem is not a send transport; MailService owns SMTP sending";
return false;
}
bool Pop3Synchronizer::updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged)
{
QVector<MailItem> items = MailItemDao::findByFolderId(folderId.toInt());
for (MailItem &it : items) {
if (it.uid() == itemUid.toLongLong()) {
it.setRead(read);
it.setFlagged(flagged);
if (MailItemDao::update(it))
return true;
}
}
return false;
}
bool Pop3Synchronizer::deleteMailItem(const QString &folderId, const QString &itemUid)
{
Q_UNUSED(folderId);
if (!m_socket) {
qWarning() << "POP3 socket not initialized";
return false;
}
if (m_socket->state() != QAbstractSocket::UnconnectedState) {
m_socket->disconnectFromHost();
if (!m_socket->waitForDisconnected(3000)) {
qWarning() << "Failed to disconnect from previous connection";
return false;
}
}
if (m_useSsl)
m_socket->connectToHostEncrypted(m_host, m_port);
else
m_socket->connectToHost(m_host, m_port);
if (!m_socket->waitForConnected(5000)) {
qWarning() << "Failed to connect to POP3 server:" << m_socket->errorString();
return false;
}
if (m_useSsl && !m_socket->waitForEncrypted(5000)) {
qWarning() << "TLS handshake failed:" << m_socket->errorString();
m_socket->disconnectFromHost();
return false;
}
QString greeting;
if (!m_socket->waitForReadyRead(5000)) {
qWarning() << "No greeting from POP3 server";
m_socket->disconnectFromHost();
return false;
}
greeting = QString::fromUtf8(m_socket->readLine()).trimmed();
if (!greeting.startsWith(QLatin1Char('+'))) {
qWarning() << "Invalid POP3 greeting:" << greeting;
m_socket->disconnectFromHost();
return false;
}
QString userCmd = QStringLiteral("USER %1\r\n").arg(m_username);
QString passCmd = QStringLiteral("PASS %1\r\n").arg(m_password);
QString resp;
if (!sendCommand(userCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
qWarning() << "USER command failed:" << resp;
m_socket->disconnectFromHost();
return false;
}
if (!sendCommand(passCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
qWarning() << "PASS command failed:" << resp;
m_socket->disconnectFromHost();
return false;
}
// Get UIDL to map uid to message number
QString uidlResp;
if (!sendCommand(QStringLiteral("UIDL\r\n"), uidlResp) || !uidlResp.startsWith(QLatin1Char('+'))) {
qWarning() << "UIDL failed:" << uidlResp;
m_socket->disconnectFromHost();
return false;
}
int msgNum = -1;
QStringList lines = uidlResp.split(QRegularExpression("\\r?\\n"));
for (const QString &line : lines) {
if (line.startsWith(QLatin1Char('.')) || line.isEmpty())
continue;
QString l = line;
if (l.startsWith(QLatin1Char('+')))
l.remove(0, 1);
QStringList parts = l.split(QRegularExpression("\\s+"));
if (parts.size() < 2)
continue;
bool okUid;
qint64 uid = parts[1].toLongLong(&okUid);
if (!okUid)
continue;
if (uid == itemUid.toLongLong()) {
bool okMsgId;
msgNum = parts[0].toInt(&okMsgId);
if (!okMsgId)
msgNum = -1;
break;
}
}
if (msgNum == -1) {
qWarning() << "Could not find message number for UID" << itemUid;
m_socket->disconnectFromHost();
return false;
}
QString deleCmd = QStringLiteral("DELE %1\r\n").arg(msgNum);
if (!sendCommand(deleCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
qWarning() << "DELE failed for msg" << msgNum << ":" << resp;
m_socket->disconnectFromHost();
return false;
}
// Deletion takes effect on QUIT
sendCommand(QStringLiteral("QUIT\r\n"), resp);
m_socket->disconnectFromHost();
return true;
}
QString Pop3Synchronizer::readResponse()
{
if (!m_socket)
return QString();
if (!m_socket->waitForReadyRead(5000))
return QString();
return QString::fromUtf8(m_socket->readLine()).trimmed();
}
bool Pop3Synchronizer::sendCommand(const QString &cmd, QString &response)
{
if (!m_socket)
return false;
m_socket->write(cmd.toUtf8());
if (!m_socket->waitForBytesWritten(5000))
return false;
response = readResponse();
return !response.isEmpty();
}
@@ -0,0 +1,230 @@
#include "pop3synchronizer.h"
#include <QDebug>
#include <QDateTime>
#include <QRegularExpression>
#include "../../core/mailitem.h"
#include "../../db/dao/mailitemdao.h"
#include "../../db/dao/folderdao.h"
#include "../../core/models/account.h"
Pop3Synchronizer::Pop3Synchronizer(QObject *parent)
: Synchronizer(parent)
{
}
bool Pop3Synchronizer::initialize(const Account &account)
{
m_account = account;
const auto &settings = account.connectionSettings();
m_host = settings.incomingHost;
m_port = settings.incomingPort;
m_useSsl = settings.incomingSsl;
m_username = settings.username;
m_password = settings.password;
// For simplicity, we do not actually connect here; connection will be made per operation.
qDebug() << "POP3 synchronizer initialized for" << m_username << "at" << m_host;
return true;
}
bool Pop3Synchronizer::syncFolder(const Folder &folder)
{
Q_UNUSED(folder);
// POP3 does not have folder sync; we just return true.
return true;
}
QVector<Folder> Pop3Synchronizer::getFolders() const
{
QVector<Folder> folders;
Folder inbox;
inbox.setId(0); // Assuming folder ID 0 for Inbox (should be looked up)
inbox.setAccountId(m_account.id());
inbox.setName(QStringLiteral("Inbox"));
inbox.setParentFolderId(QString());
folders.append(inbox);
return folders;
}
QVector<MailItem> Pop3Synchronizer::fetchMailItems(const QString &folderId, qint64 sinceUid)
{
Q_UNUSED(sinceUid);
QVector<MailItem> items;
// Connect to POP3 server
QSslSocket socket;
if (m_useSsl)
socket.connectToHostEncrypted(m_host, m_port);
else
socket.connectToHost(m_host, m_port);
if (!socket.waitForConnected(5000)) {
qWarning() << "Failed to connect to POP3 server:" << socket.errorString();
return items;
}
if (m_useSsl && !socket.waitForEncrypted(5000)) {
qWarning() << "TLS handshake failed:" << socket.errorString();
socket.disconnectFromHost();
return items;
}
// Read greeting
QString greeting;
if (!socket.waitForReadyRead(5000)) {
qWarning() << "No greeting from POP3 server";
socket.disconnectFromHost();
return items;
}
greeting = socket.readLine();
if (!greeting.startsWith(QLatin1Char('+'))) {
qWarning() << "Invalid POP3 greeting:" << greeting;
socket.disconnectFromHost();
return items;
}
// Login
QString userCmd = QStringLiteral("USER %1\r\n").arg(m_username);
QString passCmd = QStringLiteral("PASS %1\r\n").arg(m_password);
QString resp;
if (!sendCommand(userCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
qWarning() << "USER command failed:" << resp;
socket.disconnectFromHost();
return items;
}
if (!sendCommand(passCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
qWarning() << "PASS command failed:" << resp;
socket.disconnectFromHost();
return items;
}
// Get list of messages (STAT and LIST)
QString statResp;
if (!sendCommand(QStringLiteral("STAT\r\n"), statResp) || !statResp.startsWith(QLatin1Char('+'))) {
qWarning() << "STAT failed:" << statResp;
socket.disconnectFromHost();
return items;
}
// Parse STAT: "+<count> <size>"
QStringList statParts = statResp.split(QRegularExpression("\\s+"));
int count = 0;
if (statParts.size() >= 2)
count = statParts[1].toInt();
// For simplicity, we fetch all messages (since POP3 doesn't support UID since easily without UIDL mapping)
// We'll use UIDL to get unique IDs, then RETR for each.
QString uidlResp;
if (!sendCommand(QStringLiteral("UIDL\r\n"), uidlResp) || !uidlResp.startsWith(QLatin1Char('+'))) {
qWarning() << "UIDL failed:" << uidlResp;
socket.disconnectFromHost();
return items;
}
// UIDL response lines: each line: "+<msg-id> <uid>"
QStringList lines = uidlResp.split(QRegularExpression("\\r?\\n"));
for (const QString &line : lines) {
if (line.startsWith(QLatin1Char('.')) || line.isEmpty())
continue;
// Remove leading '+'
QString l = line;
if (l.startsWith(QLatin1Char('+')))
l.remove(0,1);
QStringList parts = l.split(QRegularExpression("\\s+"));
if (parts.size() < 2)
continue;
QString msgIdStr = parts[0];
QString uid = parts[1];
bool ok;
int msgId = msgIdStr.toInt(&ok);
if (!ok)
continue;
// Retrieve message
QString retrCmd = QStringLiteral("RETR %1\r\n").arg(msgIdStr);
QString retrResp;
if (!sendCommand(retrCmd, retrResp) || !retrResp.startsWith(QLatin1Char('+'))) {
qWarning() << "RETR failed for msg" << msgIdStr;
continue;
}
// Read message lines until a line with a single dot.
QByteArray msgData;
while (true) {
if (!socket.waitForReadyRead(5000))
break;
QByteArray line = socket.readLine();
if (line == QByteArray(".\r\n"))
break;
msgData.append(line);
}
// Parse rudimentary headers: we can extract Subject, From, Date.
// For simplicity, we'll set placeholder values.
QString subject = QStringLiteral("(No Subject)");
QString from = QStringLiteral("unknown@example.com");
QDateTime date = QDateTime::currentDateTimeUtc();
bool seen = false; // POP3 doesn't have read flag; we assume unread unless we store locally.
bool flagged = false;
MailItem item;
item.setFolderId(folderId.toInt());
item.setSubject(subject);
item.setSender(from);
item.setDate(date);
item.setRead(seen);
item.setFlagged(flagged);
// We could store the UID in a custom field, but MailItem has uid field (qint64) - we can store the numeric UID if possible.
// For now, we leave uid 0.
// Insert into DB
if (MailItemDao::insert(item)) {
QSqlQuery qry;
qry.exec(QStringLiteral("SELECT last_insert_rowid()"));
qint64 newId = -1;
if (qry.next())
newId = qry.value(0).toLongLong();
item.setId(newId);
items.append(item);
qDebug() << "Fetched and stored msgId" << msgIdStr << "as mail id" << newId;
} else {
qWarning() << "Failed to insert mail item for msgId" << msgIdStr;
}
}
// Quit
sendCommand(QStringLiteral("QUIT\r\n"), resp);
socket.disconnectFromHost();
return items;
}
bool Pop3Synchronizer::appendMailItem(const QString &folderId, const MailItem &item)
{
Q_UNUSED(folderId);
Q_UNUSED(item);
// POP3 does not support uploading; we can implement via SMTP elsewhere.
return false;
}
bool Pop3Synchronizer::updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged)
{
Q_UNUSED(folderId);
Q_UNUSED(itemUid);
Q_UNUSED(read);
Q_UNUSED(flagged);
// POP3 does not support flag updates.
return false;
}
bool Pop3Synchronizer::deleteMailItem(const QString &folderId, const QString &itemUid)
{
Q_UNUSED(folderId);
Q_UNUSED(itemUid);
// POP3 does not support deleting by UID easily without mapping; we could implement DELE by message number.
// For simplicity, we return false.
return false;
}
QString Pop3Synchronizer::readResponse()
{
if (!m_socket)
return QString();
if (!m_socket->waitForReadyRead(5000))
return QString();
return QString::fromUtf8(m_socket->readLine()).trimmed();
}
bool Pop3Synchronizer::sendCommand(const QString &cmd, QString &response)
{
if (!m_socket)
return false;
m_socket->write(cmd.toUtf8());
if (!m_socket->waitForBytesWritten(5000))
return false;
response = readResponse();
return !response.isEmpty();
}
+229
View File
@@ -0,0 +1,229 @@
#include "pop3synchronizer.h"
#include <QDebug>
#include <QDateTime>
#include "../../core/mailitem.h"
#include "../../db/dao/mailitemdao.h"
#include "../../db/dao/folderdao.h"
#include "../../core/models/account.h"
Pop3Synchronizer::Pop3Synchronizer(QObject *parent)
: Synchronizer(parent)
{
}
bool Pop3Synchronizer::initialize(const Account &account)
{
m_account = account;
const auto &settings = account.connectionSettings();
m_host = settings.incomingHost;
m_port = settings.incomingPort;
m_useSsl = settings.incomingSsl;
m_username = settings.username;
m_password = settings.password;
// For simplicity, we do not actually connect here; connection will be made per operation.
qDebug() << "POP3 synchronizer initialized for" << m_username << "at" << m_host;
return true;
}
bool Pop3Synchronizer::syncFolder(const Folder &folder)
{
Q_UNUSED(folder);
// POP3 does not have folder sync; we just return true.
return true;
}
QVector<Folder> Pop3Synchronizer::getFolders() const
{
QVector<Folder> folders;
Folder inbox;
inbox.setId(0); // Assuming folder ID 0 for Inbox (should be looked up)
inbox.setAccountId(m_account.id());
inbox.setName(QStringLiteral("Inbox"));
inbox.setParentId(-1);
folders.append(inbox);
return folders;
}
QVector<MailItem> Pop3Synchronizer::fetchMailItems(const QString &folderId, qint64 sinceUid)
{
Q_UNUSED(sinceUid);
QVector<MailItem> items;
// Connect to POP3 server
QSslSocket socket;
if (m_useSsl)
socket.connectToHostEncrypted(m_host, m_port);
else
socket.connectToHost(m_host, m_port);
if (!socket.waitForConnected(5000)) {
qWarning() << "Failed to connect to POP3 server:" << socket.errorString();
return items;
}
if (m_useSsl && !socket.waitForEncrypted(5000)) {
qWarning() << "TLS handshake failed:" << socket.errorString();
socket.disconnectFromHost();
return items;
}
// Read greeting
QString greeting;
if (!socket.waitForReadyRead(5000)) {
qWarning() << "No greeting from POP3 server";
socket.disconnectFromHost();
return items;
}
greeting = socket.readLine();
if (!greeting.startsWith(QLatin1Char('+'))) {
qWarning() << "Invalid POP3 greeting:" << greeting;
socket.disconnectFromHost();
return items;
}
// Login
QString userCmd = QStringLiteral("USER %1\r\n").arg(m_username);
QString passCmd = QStringLiteral("PASS %1\r\n").arg(m_password);
QString resp;
if (!sendCommand(userCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
qWarning() << "USER command failed:" << resp;
socket.disconnectFromHost();
return items;
}
if (!sendCommand(passCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
qWarning() << "PASS command failed:" << resp;
socket.disconnectFromHost();
return items;
}
// Get list of messages (STAT and LIST)
QString statResp;
if (!sendCommand(QStringLiteral("STAT\r\n"), statResp) || !statResp.startsWith(QLatin1Char('+'))) {
qWarning() << "STAT failed:" << statResp;
socket.disconnectFromHost();
return items;
}
// Parse STAT: "+<count> <size>"
QStringList statParts = statResp.split(QRegExp("\\s+"));
int count = 0;
if (statParts.size() >= 2)
count = statParts[1].toInt();
// For simplicity, we fetch all messages (since POP3 doesn't support UID since easily without UIDL mapping)
// We'll use UIDL to get unique IDs, then RETR for each.
QString uidlResp;
if (!sendCommand(QStringLiteral("UIDL\r\n"), uidlResp) || !uidlResp.startsWith(QLatin1Char('+'))) {
qWarning() << "UIDL failed:" << uidlResp;
socket.disconnectFromHost();
return items;
}
// UIDL response lines: each line: "+<msg-id> <uid>"
QStringList lines = uidlResp.split(QRegExp("\\r?\\n"));
for (const QString &line : lines) {
if (line.startsWith(QLatin1Char('.')) || line.isEmpty())
continue;
// Remove leading '+'
QString l = line;
if (l.startsWith(QLatin1Char('+')))
l.remove(0,1);
QStringList parts = l.split(QRegExp("\\s+"));
if (parts.size() < 2)
continue;
QString msgIdStr = parts[0];
QString uid = parts[1];
bool ok;
int msgId = msgIdStr.toInt(&ok);
if (!ok)
continue;
// Retrieve message
QString retrCmd = QStringLiteral("RETR %1\r\n").arg(msgIdStr);
QString retrResp;
if (!sendCommand(retrCmd, retrResp) || !retrResp.startsWith(QLatin1Char('+'))) {
qWarning() << "RETR failed for msg" << msgIdStr;
continue;
}
// Read message lines until a line with a single dot.
QByteArray msgData;
while (true) {
if (!socket.waitForReadyRead(5000))
break;
QByteArray line = socket.readLine();
if (line == QByteArray(".\r\n"))
break;
msgData.append(line);
}
// Parse rudimentary headers: we can extract Subject, From, Date.
// For simplicity, we'll set placeholder values.
QString subject = QStringLiteral("(No Subject)");
QString from = QStringLiteral("unknown@example.com");
QDateTime date = QDateTime::currentDateTimeUtc();
bool seen = false; // POP3 doesn't have read flag; we assume unread unless we store locally.
bool flagged = false;
MailItem item;
item.setFolderId(folderId.toInt());
item.setSubject(subject);
item.setFrom(from);
item.setDate(date);
item.setRead(seen);
item.setFlagged(flagged);
// We could store the UID in a custom field, but MailItem has uid field (qint64) - we can store the numeric UID if possible.
// For now, we leave uid 0.
// Insert into DB
if (MailItemDao::insert(item)) {
QSqlQuery qry;
qry.exec(QStringLiteral("SELECT last_insert_rowid()"));
qint64 newId = -1;
if (qry.next())
newId = qry.value(0).toLongLong();
item.setId(newId);
items.append(item);
qDebug() << "Fetched and stored msgId" << msgIdStr << "as mail id" << newId;
} else {
qWarning() << "Failed to insert mail item for msgId" << msgIdStr;
}
}
// Quit
sendCommand(QStringLiteral("QUIT\r\n"), resp);
socket.disconnectFromHost();
return items;
}
bool Pop3Synchronizer::appendMailItem(const QString &folderId, const MailItem &item)
{
Q_UNUSED(folderId);
Q_UNUSED(item);
// POP3 does not support uploading; we can implement via SMTP elsewhere.
return false;
}
bool Pop3Synchronizer::updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged)
{
Q_UNUSED(folderId);
Q_UNUSED(itemUid);
Q_UNUSED(read);
Q_UNUSED(flagged);
// POP3 does not support flag updates.
return false;
}
bool Pop3Synchronizer::deleteMailItem(const QString &folderId, const QString &itemUid)
{
Q_UNUSED(folderId);
Q_UNUSED(itemUid);
// POP3 does not support deleting by UID easily without mapping; we could implement DELE by message number.
// For simplicity, we return false.
return false;
}
QString Pop3Synchronizer::readResponse()
{
if (!m_socket)
return QString();
if (!m_socket->waitForReadyRead(5000))
return QString();
return QString::fromUtf8(m_socket->readLine()).trimmed();
}
bool Pop3Synchronizer::sendCommand(const QString &cmd, QString &response)
{
if (!m_socket)
return false;
m_socket->write(cmd.toUtf8());
if (!m_socket->waitForBytesWritten(5000))
return false;
response = readResponse();
return !response.isEmpty();
}
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#include "../synchronizer.h"
#include <QObject>
#include <QSslSocket>
#include <QStringList>
#include <QMap>
class Pop3Synchronizer : public Synchronizer
{
Q_OBJECT
public:
explicit Pop3Synchronizer(QObject *parent = nullptr);
~Pop3Synchronizer() override = default;
// Synchronizer interface
bool initialize(const Account &account) override;
bool syncFolder(const Folder &folder) override;
QVector<Folder> getFolders() const override;
QVector<MailItem> fetchMailItems(const QString &folderId, qint64 sinceUid = 0) override;
bool appendMailItem(const QString &folderId, const MailItem &item) override;
bool updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged) override;
bool deleteMailItem(const QString &folderId, const QString &itemUid) override;
signals:
void progressChanged(int percent);
void statusMessage(const QString &message);
private:
QString m_host;
quint16 m_port{0};
bool m_useSsl{false};
QString m_username;
QString m_password;
QSslSocket *m_socket{nullptr};
// Mapping from UIDL (as string) to message number for DELE
QMap<QString, int> m_uidToMsgNum;
// Highest UID seen (used for sinceUid)
qint64 m_maxUid{0};
QString readResponse();
bool sendCommand(const QString &cmd, QString &response);
};