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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user