#include "imapsynchronizer.h" #include #include #include #include #include "../../core/mailitem.h" #include "../../core/models/account.h" #include "../../core/models/folder.h" #include "../../db/dao/folderdao.h" #include "../../db/dao/mailitemdao.h" #include ImapSynchronizer::ImapSynchronizer(QObject* parent) : Synchronizer(parent) { } bool ImapSynchronizer::initialize(const Account& account) { const Account::ConnectionSettings& settings = account.connectionSettings(); m_host = settings.incomingHost; m_port = settings.incomingPort; m_useSsl = settings.incomingSsl; m_username = settings.username; m_password = settings.password; m_authMethod = settings.authMethod; return true; } // Helper: connects and logs in to the IMAP server bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const { bool connected = false; bool loggedIn = false; QString response; // Connect to host QEventLoop connectLoop; QTimer connectTimer; bool connectTimeout = false; connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit); connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){ qWarning() << "IMAP connection error:" << err; connectLoop.quit(); }); connectTimer.setSingleShot(true); connect(&connectTimer, &QTimer::timeout, &connectLoop, [&](){ connectTimeout = true; connectLoop.quit(); }); connectTimer.start(30000); // 30 seconds conn.connectToHost(m_host, m_port, m_useSsl); connectLoop.exec(); if (connectTimeout) { qWarning() << "Connection timeout to" << m_host; return false; } connected = true; // Wait for server greeting (first untagged response) QEventLoop greetLoop; bool gotGreeting = false; QMetaObject::Connection greetConn = connect(&conn, &ImapConnection::untaggedResponse, [&](const QString& line) { Q_UNUSED(line); gotGreeting = true; greetLoop.quit(); }); QTimer greetTimer; greetTimer.setSingleShot(true); connect(&greetTimer, &QTimer::timeout, &greetLoop, [&]() { greetLoop.quit(); }); greetTimer.start(30000); // 30 seconds greetLoop.exec(); QObject::disconnect(greetConn); if (!gotGreeting) { qWarning() << "Timeout waiting for server greeting"; return false; } // Login QEventLoop loginLoop; QTimer loginTimer; bool loginTimeout = false; bool loginSuccess = false; // Already connected, now login conn.login(m_username, m_password, true, [&](bool ok, const QString& resp){ if (ok) { loginSuccess = true; } else { qWarning() << "Login failed:" << resp; } loginLoop.quit(); }); connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){ qWarning() << "IMAP error during login:" << err; loginLoop.quit(); }); loginTimer.setSingleShot(true); connect(&loginTimer, &QTimer::timeout, &loginLoop, [&](){ loginTimeout = true; loginLoop.quit(); }); loginTimer.start(30000); loginLoop.exec(); if (loginTimeout) { qWarning() << "Login timeout"; return false; } if (!loginSuccess) { return false; } loggedIn = true; return true; } QVector ImapSynchronizer::parseUidSearchResponse(const QString& response) const { QVector uids; QStringList lines = response.split(QStringLiteral("\n")); for (const QString& line : lines) { if (line.startsWith(QStringLiteral("* SEARCH"))) { QStringList parts = line.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts); for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH" bool ok; qint64 uid = parts[i].toLongLong(&ok); if (ok && uid > 0) uids.append(uid); } break; } } return uids; } bool ImapSynchronizer::syncFolder(const Folder& folder) { ImapConnection conn; if (!connectAndLogin(conn)) return false; // Select folder QString selectCmd = QStringLiteral("SELECT \"%1\"").arg(folder.name()); QString selectResp; if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(QStringLiteral(" OK "))) { qWarning() << "SELECT failed for" << folder.name(); conn.disconnect(); return false; } // Get all UIDs from server QString searchAllCmd = QStringLiteral("UID SEARCH ALL"); QString searchAllResp; if (!conn.sendCommandWait(searchAllCmd, searchAllResp, 30000) || !searchAllResp.contains(QStringLiteral(" OK "))) { qWarning() << "UID SEARCH ALL failed"; conn.disconnect(); return false; } QVector serverUids = parseUidSearchResponse(searchAllResp); std::sort(serverUids.begin(), serverUids.end()); // Get local UIDs for this folder int fid = folder.id(); QVector localUids = MailItemDao::getUidsForFolder(fid); std::sort(localUids.begin(), localUids.end()); // Determine last known UID (max local uid) qint64 lastUid = 0; if (!localUids.isEmpty()) lastUid = localUids.last(); // Fetch new UIDs (those > lastUid) QVector newUids; if (lastUid == 0) { // No local mails, treat all as new newUids = serverUids; } else { for (qint64 uid : serverUids) { if (uid > lastUid) newUids.append(uid); } } // Fetch new messages in batches if (!newUids.isEmpty()) { const int batchSize = 50; for (int i = 0; i < newUids.size(); i += batchSize) { QVector batch = newUids.mid(i, qMin(batchSize, newUids.size() - i)); QStringList uidStrs; for (qint64 uid : batch) uidStrs.append(QString::number(uid)); QString batchList = uidStrs.join(QStringLiteral(",")); QString fetchCmd = QStringLiteral("UID FETCH %1 (BODY[] FLAGS INTERNALDATE)").arg(batchList); QString fetchResp; if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000)) { qWarning() << "UID FETCH failed for batch" << i; continue; } if (!fetchResp.contains(QStringLiteral(" OK "))) { qWarning() << "UID FETCH did not return OK"; continue; } QVector fetched = parseFetchResponse(fetchResp.split(QStringLiteral("\n"))); for (MailItem& item : fetched) { item.setFolderId(fid); if (!MailItemDao::insert(item)) { qWarning() << "Failed to insert mail item uid" << item.uid(); } } } } // Update flags for all local uids (to capture flag changes) if (!localUids.isEmpty()) { const int batchSize = 100; for (int i = 0; i < localUids.size(); i += batchSize) { QVector batch = localUids.mid(i, qMin(batchSize, localUids.size() - i)); QStringList uidStrs; for (qint64 uid : batch) uidStrs.append(QString::number(uid)); QString uidList = uidStrs.join(QStringLiteral(",")); QString fetchCmd = QStringLiteral("UID FETCH %1 (FLAGS)").arg(uidList); QString fetchResp; if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000)) { qWarning() << "UID FETCH FLAGS failed"; continue; } if (!fetchResp.contains(QStringLiteral(" OK "))) { qWarning() << "UID FETCH FLAGS not OK"; continue; } // Parse flags response QRegularExpression flagsRx(QStringLiteral(R"(UID (\d+).*FLAGS \(([^)]*)\))")); auto it = flagsRx.globalMatch(fetchResp); while (it.hasNext()) { auto m = it.next(); qint64 uid = m.captured(1).toLongLong(); QString flags = m.captured(2); bool seen = flags.contains(QStringLiteral("\\Seen")); bool flagged = flags.contains(QStringLiteral("\\Flagged")); std::optional opt = MailItemDao::findById(uid); if (opt) { MailItem& item = *opt; bool changed = false; if (item.isRead() != seen) { item.setRead(seen); changed = true; } if (item.isFlagged() != flagged) { item.setFlagged(flagged); changed = true; } if (changed) MailItemDao::update(item); } } } } // Handle expunged messages: remove local uids not present on server QVector toRemove; std::set_difference(localUids.begin(), localUids.end(), serverUids.begin(), serverUids.end(), std::back_inserter(toRemove)); for (qint64 uid : toRemove) { MailItemDao::remove(uid); } // Update unread count int unread = 0; for (qint64 uid : localUids) { std::optional opt = MailItemDao::findById(uid); if (opt && !opt->isRead()) ++unread; } Folder f = folder; f.setUnreadCount(unread); if (!FolderDao::update(f)) { qWarning() << "Failed to update folder unread count"; } conn.disconnect(); return true; } QVector ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid) { QVector items; ImapConnection conn; if (!connectAndLogin(conn)) { return items; } // Get folder name from DB int fid = folderId.toInt(); auto folderOpt = FolderDao::findById(fid); if (!folderOpt) { qWarning() << "Folder not found for id" << folderId; return items; } QString folderName = folderOpt->name(); // SELECT with real folder name QString selectCommand = QString("SELECT \"%1\"").arg(folderName); QString selectResponse; if (!conn.sendCommandWait(selectCommand, selectResponse, 30000)) { qWarning() << "SELECT command failed:" << selectResponse; return items; } if (!selectResponse.contains(" OK ")) { qWarning() << "SELECT failed:" << selectResponse; return items; } // SEARCH QString searchCommand; if (sinceUid > 0) { searchCommand = QString("UID SEARCH %1:*").arg(sinceUid); } else { searchCommand = "UID SEARCH ALL"; } QString searchResponse; if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) { qWarning() << "SEARCH command failed:" << searchResponse; return items; } // Parse UIDs (robust: look for line starting with "* SEARCH") QVector uids; QStringList lines = searchResponse.split('\n'); for (const QString& line : lines) { if (line.startsWith("* SEARCH")) { QStringList parts = line.split(QRegularExpression("\\s+")); for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH" bool ok; qint64 uid = parts[i].toLongLong(&ok); if (ok && uid > 0) uids.append(uid); } break; } } if (uids.isEmpty()) { conn.sendCommandWait("CLOSE", searchResponse, 30000); conn.disconnect(); return items; } // Fetch in batches of 50 UIDs const int batchSize = 50; for (int i = 0; i < uids.size(); i += batchSize) { QVector batch = uids.mid(i, qMin(batchSize, uids.size() - i)); QString batchList; for (qint64 uid : batch) { batchList.append(QString::number(uid)).append(","); } batchList.chop(1); // remove trailing comma // Fetch headers and flags (efficient) QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)") .arg(batchList); QString fetchResponse; if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) { qWarning() << "FETCH failed for batch" << i; continue; } // Parse and persist QVector batchItems = parseFetchResponse(fetchResponse.split('\n')); for (MailItem& item : batchItems) { item.setFolderId(fid); if (MailItemDao::insert(item)) { items.append(item); } else { qWarning() << "Failed to insert mail item uid" << item.uid(); } } } // Close and disconnect QString dummy; conn.sendCommandWait("CLOSE", dummy, 30000); conn.disconnect(); return items; } QVector ImapSynchronizer::getFolders() const { QVector folders; ImapConnection conn; if (!connectAndLogin(conn)) { return folders; } // List folders: LIST "" "*" QString listCommand = QStringLiteral("LIST \"\" \"*\""); QString response; if (!conn.sendCommandWait(listCommand, response, 30000)) { qWarning() << "LIST command failed:" << response; return folders; } // Parse the response QStringList lines = response.split('\n'); folders = parseListResponse(lines); // Logout conn.disconnect(); return folders; } bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item) { // Get folder name int fid = folderId.toInt(); auto folderOpt = FolderDao::findById(fid); if (!folderOpt) { qWarning() << "Folder not found for id" << folderId; return false; } QString folderName = folderOpt->name(); // Build RFC822 message (headers + body) QStringList headers; if (!item.sender().isEmpty()) headers << "From: " + item.sender(); if (!item.recipient().isEmpty()) headers << "To: " + item.recipient(); if (!item.cc().isEmpty()) headers << "Cc: " + item.cc(); if (!item.bcc().isEmpty()) headers << "Bcc: " + item.bcc(); if (!item.subject().isEmpty()) headers << "Subject: " + item.subject(); headers << "Date: " + item.date().toString(Qt::RFC2822Date); headers << "MIME-Version: 1.0"; headers << "Content-Type: text/html; charset=UTF-8"; headers << "Content-Transfer-Encoding: 7bit"; headers << ""; // blank line separates headers from body headers << item.bodyHtml(); // using bodyHtml as the body QString message = headers.join("\r\n"); QByteArray msgData = message.toUtf8(); ImapConnection conn; if (!connectAndLogin(conn)) return false; // SELECT folder (some servers require it) QString selectCmd = QString("SELECT \"%1\"").arg(folderName); QString resp; conn.sendCommandWait(selectCmd, resp, 30000); // ignore response // APPEND with literal: APPEND "folder" (\Seen) {size} QString appendCmd = QString("APPEND \"%1\" (\\Seen) {%2}").arg(folderName).arg(msgData.size()); QString response; if (!conn.sendCommandWait(appendCmd, response, 30000)) { qWarning() << "APPEND command initial failed:" << response; conn.disconnect(); return false; } // Send the literal (message data) conn.sendRaw(QString::fromUtf8(msgData)); // Wait for the tagged response (tag + OK/NO) if (!conn.sendCommandWait("", response, 30000)) { // dummy command to get response qWarning() << "APPEND literal failed:" << response; conn.disconnect(); return false; } conn.disconnect(); return true; } bool ImapSynchronizer::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged) { qint64 uid = itemUid.toLongLong(); if (uid <= 0) return false; int fid = folderId.toInt(); auto folderOpt = FolderDao::findById(fid); if (!folderOpt) return false; QString folderName = folderOpt->name(); ImapConnection conn; if (!connectAndLogin(conn)) return false; // SELECT QString selectCmd = QString("SELECT \"%1\"").arg(folderName); QString resp; if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) { qWarning() << "SELECT failed"; return false; } // STORE for Seen flag QString seenFlag = read ? "+" : "-"; QString storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Seen").arg(uid).arg(seenFlag); if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) { qWarning() << "STORE Seen failed:" << resp; conn.disconnect(); return false; } // STORE for Flagged flag QString flaggedFlag = flagged ? "+" : "-"; storeCmd = QString("UID STORE %1 %2FLAGS.SILENT \\Flagged").arg(uid).arg(flaggedFlag); if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) { qWarning() << "STORE Flagged failed:" << resp; conn.disconnect(); return false; } conn.disconnect(); // Update local DB auto items = MailItemDao::findByFolderId(fid); for (MailItem& item : items) { if (item.uid() == uid) { item.setRead(read); item.setFlagged(flagged); MailItemDao::update(item); break; } } return true; } bool ImapSynchronizer::deleteMailItem(const QString& folderId, const QString& itemUid) { qint64 uid = itemUid.toLongLong(); if (uid <= 0) return false; int fid = folderId.toInt(); auto folderOpt = FolderDao::findById(fid); if (!folderOpt) return false; QString folderName = folderOpt->name(); ImapConnection conn; if (!connectAndLogin(conn)) return false; // SELECT QString selectCmd = QString("SELECT \"%1\"").arg(folderName); QString resp; if (!conn.sendCommandWait(selectCmd, resp, 30000) || !resp.contains(" OK ")) { qWarning() << "SELECT failed"; return false; } // Mark as \\Deleted QString storeCmd = QString("UID STORE %1 +FLAGS.SILENT \\Deleted").arg(uid); if (!conn.sendCommandWait(storeCmd, resp, 30000) || !resp.contains(" OK ")) { qWarning() << "STORE Deleted failed:" << resp; conn.disconnect(); return false; } // EXPUNGE (permanently delete) QString expungeCmd = "EXPUNGE"; if (!conn.sendCommandWait(expungeCmd, resp, 30000) || !resp.contains(" OK ")) { qWarning() << "EXPUNGE failed:" << resp; conn.disconnect(); return false; } conn.disconnect(); // Remove from local DB MailItemDao::remove(uid); return true; } QString ImapSynchronizer::generateEventId() const { return QString::number(QDateTime::currentMSecsSinceEpoch()); } QVector ImapSynchronizer::parseListResponse(const QStringList& lines) const { QVector folders; QRegularExpression rx("\\* LIST \\\\([^)]*\\\\) \"([^\"]*)\" \"([^\"]*)\""); for (const QString& line : lines) { QRegularExpressionMatch match = rx.match(line); if (match.hasMatch()) { QString delimiter = match.captured(1); // unused for now QString mailbox = match.captured(2); Folder folder; folder.setName(mailbox); QString lower = mailbox.toLower(); if (lower == "inbox") folder.setInbox(true); else if (lower == "sent") folder.setSent(true); else if (lower == "drafts") folder.setDrafts(true); else if (lower == "trash" || lower == "deleted items") folder.setTrash(true); folders.append(folder); } } return folders; } QVector ImapSynchronizer::parseFetchResponse(const QStringList& lines) const { QVector items; int i = 0; while (i < lines.size()) { const QString& line = lines[i]; if (!line.startsWith(QStringLiteral("* "))) { ++i; continue; } int fetchPos = line.indexOf(QStringLiteral(" FETCH (")); if (fetchPos == -1) { ++i; continue; } // Extract UID int uidPos = line.indexOf(QStringLiteral("UID ")); if (uidPos == -1) { ++i; continue; } int uidEnd = line.indexOf(QRegularExpression(QStringLiteral("[\\s)]")), uidPos + 4); if (uidEnd == -1) uidEnd = line.length(); QString uidStr = line.mid(uidPos + 4, uidEnd - uidPos - 4); bool ok; qint64 uid = uidStr.toLongLong(&ok); if (!ok) { ++i; continue; } MailItem item; item.setUid(uid); // Extract FLAGS int flagsPos = line.indexOf(QStringLiteral("FLAGS (")); if (flagsPos != -1) { int flagsEnd = line.indexOf(')', flagsPos + 7); if (flagsEnd != -1) { QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7); item.setRead(flags.contains(QStringLiteral("\\Seen"))); item.setFlagged(flags.contains(QStringLiteral("\\Flagged"))); } } // Extract INTERNALDATE int datePos = line.indexOf(QStringLiteral("INTERNALDATE \"")); if (datePos != -1) { int dateStart = datePos + 15; // length of "INTERNALDATE \"" int dateEnd = line.indexOf('\"', dateStart); if (dateEnd != -1) { QString dateStr = line.mid(dateStart, dateEnd - dateStart); dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000" QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date); if (dt.isValid()) item.setDate(dt); } } // Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines QString bodyData; int bodyPos = line.indexOf(QStringLiteral("BODY[]")); if (bodyPos != -1) { int bracePos = line.indexOf('{', bodyPos); if (bracePos != -1) { int sizeEnd = line.indexOf('}', bracePos); if (sizeEnd != -1) { bool sizeOk; int size = line.mid(bracePos + 1, sizeEnd - bracePos - 1).toInt(&sizeOk); if (sizeOk) { int dataStart = line.indexOf(QStringLiteral("\r\n"), sizeEnd); if (dataStart != -1) { dataStart += 2; // skip \r\n int available = line.length() - dataStart; if (available > size) available = size; bodyData = line.mid(dataStart, available); int received = available; int remaining = size - received; int j = i + 1; while (j < lines.size() && remaining > 0) { const QString& nextLine = lines[j]; int take = qMin(nextLine.length(), remaining); bodyData += nextLine.left(take); remaining -= take; ++j; } // we have consumed lines up to j-1 i = j - 1; // will be incremented at end of loop } } } } } // Parse headers from bodyData if (!bodyData.isEmpty()) { QRegularExpression headerRx(QStringLiteral(R"((^([^:]+):\s*(.*?)\r\n))"), QRegularExpression::MultilineOption); auto it = headerRx.globalMatch(bodyData); while (it.hasNext()) { auto match = it.next(); QString key = match.captured(2).toLower().trimmed(); QString value = match.captured(3).trimmed(); if (key == QStringLiteral("subject")) item.setSubject(value); else if (key == QStringLiteral("from")) item.setSender(value); else if (key == QStringLiteral("to")) item.setRecipient(value); else if (key == QStringLiteral("date")) { QDateTime dt = QDateTime::fromString(value, Qt::RFC2822Date); if (dt.isValid()) item.setDate(dt); } } } // If subject not set, use placeholder if (item.subject().isEmpty()) item.setSubject(QStringLiteral("(No Subject)")); items.append(item); ++i; } return items; }