diff --git a/CMakeLists.txt b/CMakeLists.txt index fab2ceb..6f8efb8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -127,6 +127,7 @@ set(SRC_FILES if (USE_MAILIO_IMAP) list(APPEND SRC_FILES src/services/imap/imapsynchronizer_mailio.cpp + src/services/pop3/pop3synchronizer_mailio.cpp ) endif() # Executable diff --git a/src/core/synchronizerprovider.cpp b/src/core/synchronizerprovider.cpp index 8252475..a9254a0 100644 --- a/src/core/synchronizerprovider.cpp +++ b/src/core/synchronizerprovider.cpp @@ -4,6 +4,7 @@ #include "services/imap/imapsynchronizer.h" #include "services/imap/imapsynchronizer_mailio.h" #include "services/pop3/pop3synchronizer.h" +#include "services/pop3/pop3synchronizer_mailio.h" #include SynchronizerProvider::SynchronizerProvider(QObject *parent) @@ -60,7 +61,12 @@ Synchronizer* SynchronizerProvider::createSynchronizer(const QString &accountId, sync = new ImapSynchronizer(this); #endif } else if (providerType == "pop3" || providerType == "pop") { +#if defined(USE_MAILIO_IMAP) && USE_MAILIO_IMAP + // Use mailio-based POP3 synchronizer + sync = new Pop3SynchronizerMailio(this); +#else sync = new Pop3Synchronizer(this); +#endif } else { qWarning() << "[SynchronizerProvider] Unknown provider type:" << providerType; return nullptr; diff --git a/src/services/pop3/pop3synchronizer_mailio.cpp b/src/services/pop3/pop3synchronizer_mailio.cpp new file mode 100644 index 0000000..12b9115 --- /dev/null +++ b/src/services/pop3/pop3synchronizer_mailio.cpp @@ -0,0 +1,295 @@ +// src/services/pop3/pop3synchronizer_mailio.cpp +// mailio-based POP3 synchronizer - replaces custom POP3 implementation + +#if defined(USE_MAILIO_IMAP) && USE_MAILIO_IMAP + +#include "pop3synchronizer_mailio.h" +#include +#include +#include +#include +#include +#include +#include "synchronizer.h" +#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 "services/mimestorage.h" + +using namespace mailio; + +Pop3SynchronizerMailio::Pop3SynchronizerMailio(QObject* parent) + : Synchronizer(parent) +{} + +bool Pop3SynchronizerMailio::initialize(const Account& account) +{ + m_account = account; + const Account::ConnectionSettings& settings = account.connectionSettings(); + m_host = settings.incomingHost.toStdString(); + m_port = settings.incomingPort; + m_useSsl = settings.incomingSsl; + m_username = settings.username.toStdString(); + m_password = settings.password.toStdString(); // password or OAuth2 access token + m_authMethod = settings.authMethod.toStdString(); + return true; +} + +bool Pop3SynchronizerMailio::syncFolder(const Folder& folder) +{ + Q_UNUSED(folder); + // POP3 only has INBOX, so we just fetch from the default mailbox + return true; +} + +QVector Pop3SynchronizerMailio::getFolders() const +{ + QVector folders; + Folder inbox; + inbox.setName("INBOX"); + inbox.setInbox(true); + folders.append(inbox); + return folders; +} + +QVector Pop3SynchronizerMailio::fetchMailItems(const QString& folderId, qint64 sinceUid) +{ + Q_UNUSED(folderId); + QVector items; + + try { + // Connect to POP3 server + pop3 conn(m_host, m_port); + if (m_useSsl) { + conn.connect_ssl(); + } else { + conn.connect(); + } + + // Authenticate + if (m_authMethod == "oauth2" || m_authMethod == "xoauth2") { + conn.authenticate("XOAUTH2", m_username, m_password); + } else { + conn.authenticate(m_username, m_password, pop3::auth_method_t::USER_PASS); + } + + // Get message list with UIDs + std::vector msgInfos = conn.list_messages(); + + std::vector newMsgIndices; + for (const auto& info : msgInfos) { + // mailio's pop3::message_info has uid() method + if (static_cast(info.uid()) > sinceUid) { + newMsgIndices.push_back(info.index()); + } + } + + if (!newMsgIndices.empty()) { + const size_t batchSize = 20; + for (size_t i = 0; i < newMsgIndices.size(); i += batchSize) { + size_t end = std::min(i + batchSize, newMsgIndices.size()); + std::vector batch(newMsgIndices.begin() + i, newMsgIndices.begin() + end); + + // Fetch full messages + std::vector messages = conn.fetch_messages(batch); + + for (const auto& msg : messages) { + MailItem item = parseMailioMessage(msg); + item.setFolderId(0); // POP3 only has INBOX (folderId 0 or 1) + items.append(item); + } + } + } + + conn.close(); + } catch (const std::exception& e) { + qWarning() << "POP3 fetchMailItems error:" << QString::fromStdString(e.what()); + } + + return items; +} + +bool Pop3SynchronizerMailio::appendMailItem(const QString& folderId, const MailItem& item) +{ + Q_UNUSED(folderId); + Q_UNUSED(item); + // POP3 doesn't support APPEND; sending is done via SMTP + qWarning() << "POP3 appendMailItem not supported; use SMTP for sending"; + return false; +} + +bool Pop3SynchronizerMailio::updateMailItemFlags(const QString& folderId, const QString& itemUid, bool read, bool flagged) +{ + Q_UNUSED(folderId); + Q_UNUSED(itemUid); + Q_UNUSED(read); + Q_UNUSED(flagged); + // POP3 doesn't support flag updates on server (no STORE command) + // Local DB update only + return true; +} + +bool Pop3SynchronizerMailio::deleteMailItem(const QString& folderId, const QString& itemUid) +{ + Q_UNUSED(folderId); + + try { + pop3 conn(m_host, m_port); + if (m_useSsl) conn.connect_ssl(); else conn.connect(); + + if (m_authMethod == "oauth2" || m_authMethod == "xoauth2") { + conn.authenticate("XOAUTH2", m_username, m_password); + } else { + conn.authenticate(m_username, m_password, pop3::auth_method_t::USER_PASS); + } + + // Delete message by index (need to map UID to index) + // This is a limitation of POP3 - we'd need to list messages first + // For now, just close connection + conn.close(); + return false; + } catch (const std::exception& e) { + qWarning() << "POP3 deleteMailItem error:" << QString::fromStdString(e.what()); + return false; + } +} + +// Private helpers + +bool Pop3SynchronizerMailio::persistFetchedMessage(const Folder& folder, const mailio::message& msg) +{ + try { + MailItem item = parseMailioMessage(msg); + item.setFolderId(folder.id()); + + MimeStorageService storage; + QVector storedPaths; + ParsedMimeMessage parsed; + + std::string rawMime = msg.to_string(); + QByteArray rawMimeData = QByteArray::fromStdString(rawMime); + + if (!storage.parseMessage(rawMimeData, parsed) + || !storage.storeMessage(QString::number(m_account.id()), QString::number(folder.id()), + item, rawMimeData, &storedPaths)) { + qWarning() << "Failed to store POP3 MIME message"; + return false; + } + + if (!MailItemDao::upsert(item)) { + storage.deleteEmlFile(item.fileId()); + return false; + } + + QVector records; + for (size_t i = 0; i < parsed.attachments.size(); ++i) { + const auto& att = parsed.attachments[i]; + StoredAttachmentRecord record; + record.fileName = QString::fromStdString(att.fileName); + record.mimeType = QString::fromStdString(att.mimeType); + record.contentId = QString::fromStdString(att.contentId); + record.size = att.data.size(); + if (i < storedPaths.size()) record.storedPath = storedPaths[i]; + records.append(record); + } + + if (!MailItemDao::replaceAttachments(item.id(), records)) { + storage.deleteEmlFile(item.fileId()); + return false; + } + + return true; + } catch (const std::exception& e) { + qWarning() << "persistFetchedMessage error:" << QString::fromStdString(e.what()); + return false; + } +} + +MailItem Pop3SynchronizerMailio::parseMailioMessage(const mailio::message& msg) +{ + MailItem item; + + // UID will be set by caller + item.setUid(0); + + item.setMessageId(QString::fromStdString(msg.header().message_id())); + item.setSubject(QString::fromStdString(msg.header().subject()).isEmpty() ? "(No Subject)" : QString::fromStdString(msg.header().subject())); + + // From + std::vector fromList = msg.header().from(); + if (!fromList.empty()) { + item.setSender(QString::fromStdString(fromList[0].to_string())); + } + + // To + std::vector toList = msg.header().to(); + QStringList toStrs; + for (const auto& addr : toList) toStrs << QString::fromStdString(addr.to_string()); + item.setTo(toStrs.join(", ")); + item.setRecipient(item.to()); + + // CC + std::vector ccList = msg.header().cc(); + QStringList ccStrs; + for (const auto& addr : ccList) ccStrs << QString::fromStdString(addr.to_string()); + item.setCc(ccStrs.join(", ")); + + item.setBcc(""); + + // Date + std::time_t dateTime = msg.header().date(); + item.setDate(QDateTime::fromSecsSinceEpoch(dateTime)); + + // Body + std::string htmlBody = msg.content_text(text_format_t::HTML); + if (!htmlBody.empty()) { + item.setBodyHtml(QString::fromStdString(htmlBody)); + } else { + std::string plainBody = msg.content_text(text_format_t::PLAIN); + item.setBodyHtml(QString("
%1
").arg(QString::fromStdString(plainBody).toHtmlEscaped())); + } + + item.setSize(msg.to_string().size()); + + // Attachments + QStringList attachmentNames; + for (const auto& att : msg.attachments()) { + attachmentNames << QString::fromStdString(att.descriptor().filename); + } + item.setAttachments(attachmentNames); + + item.setRead(false); + item.setFlagged(false); + + return item; +} + +mailio::message Pop3SynchronizerMailio::createMailioMessage(const MailItem& item) +{ + mailio::message msg; + + msg.header().subject(item.subject().toStdString()); + msg.header().from({mailio::address(item.sender().toStdString())}); + + QStringList toList = item.to().split(',', Qt::SkipEmptyParts); + std::vector toAddrs; + for (const QString& addr : toList) toAddrs.push_back(mailio::address(addr.trimmed().toStdString())); + msg.header().to(toAddrs); + + QStringList ccList = item.cc().split(',', Qt::SkipEmptyParts); + std::vector ccAddrs; + for (const QString& addr : ccList) ccAddrs.push_back(mailio::address(addr.trimmed().toStdString())); + msg.header().cc(ccAddrs); + + msg.header().date(QDateTime::currentDateTimeUtc().toSecsSinceEpoch()); + + if (!item.bodyHtml().isEmpty()) { + msg.content_text(item.bodyHtml().toStdString(), text_format_t::HTML); + } + + return msg; +} + +#endif // USE_MAILIO_IMAP \ No newline at end of file diff --git a/src/services/pop3/pop3synchronizer_mailio.h b/src/services/pop3/pop3synchronizer_mailio.h new file mode 100644 index 0000000..8a3c692 --- /dev/null +++ b/src/services/pop3/pop3synchronizer_mailio.h @@ -0,0 +1,49 @@ +// src/services/pop3/pop3synchronizer_mailio.h +// mailio-based POP3 synchronizer + +#pragma once + +#include "synchronizer.h" +#include +#include + +#if defined(USE_MAILIO_IMAP) && USE_MAILIO_IMAP +#include +#endif + +class Pop3SynchronizerMailio : public Synchronizer +{ + Q_OBJECT +public: + explicit Pop3SynchronizerMailio(QObject* parent = nullptr); + ~Pop3SynchronizerMailio() override = default; + + // Synchronizer interface + bool initialize(const Account& account) override; + bool syncFolder(const Folder& folder) override; + QVector getFolders() const override; + QVector 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 std::string m_host; + mutable quint16 m_port{0}; + mutable bool m_useSsl{false}; + mutable std::string m_username; + mutable std::string m_password; + mutable std::string m_authMethod; // "plain", "xoauth2" + + // Helpers +#if defined(USE_MAILIO_IMAP) && USE_MAILIO_IMAP + bool persistFetchedMessage(const Folder& folder, const mailio::message& msg); + MailItem parseMailioMessage(const mailio::message& msg); + mailio::message createMailioMessage(const MailItem& item); +#endif +}; \ No newline at end of file