723 lines
24 KiB
Plaintext
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;
|
||
|
|
}
|