Files
wino-mail-dtkqt/src/services/imap/imapsynchronizer.cpp.bak3
T
javier a004bdd60f 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
2026-08-17 15:34:49 +02:00

723 lines
24 KiB
Plaintext

#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;
}