#include "mailservice.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "services/mimestorage.h" namespace { QByteArray smtpHeader(const QString &name, const QString &value) { const QByteArray utf8 = value.toUtf8(); bool ascii = true; for (char c : utf8) { if (static_cast(c) < 32 || static_cast(c) > 126) { ascii = false; break; } } const QByteArray encoded = ascii ? utf8 : QByteArray("=?UTF-8?B?") + utf8.toBase64() + QByteArray("?="); return name.toUtf8() + QByteArray(": ") + encoded + QByteArray("\r\n"); } QByteArray base64Lines(const QByteArray &data) { const QByteArray encoded = data.toBase64(); QByteArray wrapped; for (int i = 0; i < encoded.size(); i += 76) wrapped.append(encoded.mid(i, 76)).append("\r\n"); return wrapped; } QStringList smtpRecipients(const QString &value) { QStringList result; for (QString part : value.split(QRegularExpression(QStringLiteral("[,;]")), Qt::SkipEmptyParts)) { const QRegularExpressionMatch match = QRegularExpression(QStringLiteral("<([^>]+)>")).match(part); if (match.hasMatch()) part = match.captured(1); part = part.trimmed(); if (!part.isEmpty()) result.append(part); } return result; } bool smtpResponse(QSslSocket &socket, int expectedCode) { QByteArray line; while (true) { if (!socket.canReadLine() && !socket.waitForReadyRead(15000)) return false; line = socket.readLine(); if (line.size() < 3) continue; bool ok = false; const int code = line.left(3).toInt(&ok); if (!ok) continue; // A space marks the final line of a multiline SMTP response. if (line.size() > 3 && line.at(3) == '-') continue; return code == expectedCode; } } QByteArray composeMimeMessage(const MailItem &mail, const Account &account, const QStringList &attachmentPaths, QString &error) { QStringList recipients = smtpRecipients(mail.to()); if (recipients.isEmpty()) recipients = smtpRecipients(mail.recipient()); recipients.append(smtpRecipients(mail.cc())); recipients.append(smtpRecipients(mail.bcc())); if (recipients.isEmpty()) { error = QObject::tr("No recipients specified"); return {}; } recipients.removeDuplicates(); QByteArray message; const QString from = mail.sender().isEmpty() ? account.email() : mail.sender(); message.append(smtpHeader(QStringLiteral("From"), from)); message.append(smtpHeader(QStringLiteral("To"), mail.to().isEmpty() ? mail.recipient() : mail.to())); if (!mail.cc().isEmpty()) message.append(smtpHeader(QStringLiteral("Cc"), mail.cc())); message.append(smtpHeader(QStringLiteral("Subject"), mail.subject())); message.append("Date: ").append((mail.date().isValid() ? mail.date() : QDateTime::currentDateTimeUtc()).toString(Qt::RFC2822Date).toUtf8()).append("\r\n"); message.append("MIME-Version: 1.0\r\n"); QByteArray body = mail.bodyHtml().toUtf8(); body.replace("\r\n", "\n"); body.replace('\r', '\n'); body.replace('\n', "\r\n"); if (attachmentPaths.isEmpty()) { message.append("Content-Type: text/html; charset=UTF-8\r\n"); message.append("Content-Transfer-Encoding: 8bit\r\n\r\n"); message.append(body).append("\r\n"); return message; } const QByteArray boundary = QByteArray("----WinoMail-") + QByteArray::number(QDateTime::currentMSecsSinceEpoch()); message.append("Content-Type: multipart/mixed; boundary=\"").append(boundary).append("\"\r\n\r\n"); message.append("--").append(boundary).append("\r\n"); message.append("Content-Type: text/html; charset=UTF-8\r\n"); message.append("Content-Transfer-Encoding: 8bit\r\n\r\n").append(body).append("\r\n"); QMimeDatabase mimeDatabase; for (const QString &path : attachmentPaths) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { error = QObject::tr("Could not read attachment: %1").arg(path); return {}; } const QFileInfo info(path); const QString fileName = info.fileName().replace('"', '_'); const QString mimeType = mimeDatabase.mimeTypeForFile(info).name(); message.append("--").append(boundary).append("\r\n"); message.append("Content-Type: ").append(mimeType.toUtf8()).append("; name=\"").append(fileName.toUtf8()).append("\"\r\n"); message.append("Content-Transfer-Encoding: base64\r\n"); message.append("Content-Disposition: attachment; filename=\"").append(fileName.toUtf8()).append("\"\r\n\r\n"); message.append(base64Lines(file.readAll())); } message.append("--").append(boundary).append("--\r\n"); return message; } bool sendSmtp(const MailItem &mail, const Account &account, const QStringList &attachmentPaths, QString &error) { const Account::ConnectionSettings settings = account.connectionSettings(); if (settings.outgoingHost.isEmpty() || settings.outgoingPort == 0) { error = QObject::tr("SMTP server is not configured"); return false; } const QByteArray mime = composeMimeMessage(mail, account, attachmentPaths, error); if (mime.isEmpty()) return false; QSslSocket socket; if (settings.outgoingSsl && settings.outgoingPort == 465) socket.connectToHostEncrypted(settings.outgoingHost, settings.outgoingPort); else socket.connectToHost(settings.outgoingHost, settings.outgoingPort); if (!socket.waitForConnected(15000)) { error = socket.errorString(); return false; } if (settings.outgoingSsl && settings.outgoingPort == 465) { if (!socket.waitForEncrypted(15000)) { error = socket.errorString(); return false; } } if (!smtpResponse(socket, 220)) { error = QObject::tr("SMTP greeting failed"); return false; } auto command = [&](const QByteArray &value, int code) { socket.write(value + QByteArray("\r\n")); socket.flush(); return smtpResponse(socket, code); }; if (!command("EHLO localhost", 250)) { error = QObject::tr("SMTP EHLO failed"); return false; } if (settings.outgoingSsl && settings.outgoingPort != 465) { if (!command("STARTTLS", 220)) { error = QObject::tr("SMTP STARTTLS failed"); return false; } socket.startClientEncryption(); if (!socket.waitForEncrypted(15000)) { error = socket.errorString(); return false; } if (!command("EHLO localhost", 250)) { error = QObject::tr("SMTP EHLO after TLS failed"); return false; } } const QString username = settings.username.isEmpty() ? account.email() : settings.username; if (!username.isEmpty()) { if (!command("AUTH LOGIN", 334) || !command(username.toUtf8().toBase64(), 334) || !command(settings.password.toUtf8().toBase64(), 235)) { error = QObject::tr("SMTP authentication failed"); return false; } } const QString from = mail.sender().isEmpty() ? account.email() : mail.sender(); if (!command("MAIL FROM:<" + smtpRecipients(from).value(0).toUtf8() + ">", 250)) { error = QObject::tr("SMTP MAIL FROM failed"); return false; } const QStringList recipients = smtpRecipients(mail.to()) + smtpRecipients(mail.recipient()) + smtpRecipients(mail.cc()) + smtpRecipients(mail.bcc()); QStringList uniqueRecipients = recipients; uniqueRecipients.removeDuplicates(); for (const QString &recipient : uniqueRecipients) { if (!command("RCPT TO:<" + recipient.toUtf8() + ">", 250)) { error = QObject::tr("SMTP recipient rejected: %1").arg(recipient); return false; } } if (!command("DATA", 354)) { error = QObject::tr("SMTP DATA failed"); return false; } QByteArray stuffed; const QList lines = mime.split('\n'); for (QByteArray line : lines) { if (line.startsWith('.')) stuffed.append('.'); stuffed.append(line); if (!line.endsWith('\r')) stuffed.append('\r'); stuffed.append('\n'); } socket.write(stuffed); socket.write(".\r\n"); socket.flush(); if (!smtpResponse(socket, 250)) { error = QObject::tr("SMTP message rejected"); return false; } command("QUIT", 221); return true; } QByteArray base64Url(const QByteArray &data) { QByteArray result = data.toBase64(); result.replace('+', '-'); result.replace('/', '_'); while (result.endsWith('=')) result.chop(1); return result; } bool postJson(const QUrl &url, const QByteArray &token, const QJsonObject &payload, int expectedStatus, QString &error) { QNetworkAccessManager network; QNetworkRequest request(url); request.setRawHeader("Authorization", QByteArrayLiteral("Bearer ") + token); request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); QNetworkReply *reply = network.post(request, QJsonDocument(payload).toJson(QJsonDocument::Compact)); QEventLoop loop; QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); loop.exec(); const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); const bool ok = reply->error() == QNetworkReply::NoError && status == expectedStatus; if (!ok) error = reply->error() == QNetworkReply::NoError ? QObject::tr("Server returned HTTP %1").arg(status) : reply->errorString(); reply->deleteLater(); return ok; } bool sendGmailApi(const MailItem &mail, const Account &account, const QStringList &attachmentPaths, QString &error) { if (!account.isTokenValid()) { error = QObject::tr("Gmail access token is missing or expired"); return false; } const QByteArray mime = composeMimeMessage(mail, account, attachmentPaths, error); if (mime.isEmpty()) return false; QJsonObject payload; payload.insert(QStringLiteral("raw"), QString::fromLatin1(base64Url(mime))); const QString user = QString::fromUtf8(QUrl::toPercentEncoding(account.email())); const QUrl url(QStringLiteral("https://gmail.googleapis.com/gmail/v1/users/%1/messages/send").arg(user)); return postJson(url, account.accessToken().toUtf8(), payload, 200, error); } QJsonArray graphRecipients(const QString &value) { QJsonArray result; for (const QString &address : smtpRecipients(value)) { QJsonObject recipient; QJsonObject email; email.insert(QStringLiteral("address"), address); recipient.insert(QStringLiteral("emailAddress"), email); result.append(recipient); } return result; } bool sendOutlookApi(const MailItem &mail, const Account &account, const QStringList &attachmentPaths, QString &error) { if (!account.isTokenValid()) { error = QObject::tr("Outlook access token is missing or expired"); return false; } QJsonObject message; message.insert(QStringLiteral("subject"), mail.subject()); QJsonObject body; body.insert(QStringLiteral("contentType"), QStringLiteral("HTML")); body.insert(QStringLiteral("content"), mail.bodyHtml()); message.insert(QStringLiteral("body"), body); message.insert(QStringLiteral("toRecipients"), graphRecipients(mail.to().isEmpty() ? mail.recipient() : mail.to())); message.insert(QStringLiteral("ccRecipients"), graphRecipients(mail.cc())); message.insert(QStringLiteral("bccRecipients"), graphRecipients(mail.bcc())); QJsonArray attachments; QMimeDatabase mimeDatabase; for (const QString &path : attachmentPaths) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { error = QObject::tr("Could not read attachment: %1").arg(path); return false; } const QFileInfo info(path); QJsonObject attachment; attachment.insert(QStringLiteral("@odata.type"), QStringLiteral("#microsoft.graph.fileAttachment")); attachment.insert(QStringLiteral("name"), info.fileName()); attachment.insert(QStringLiteral("contentType"), mimeDatabase.mimeTypeForFile(info).name()); attachment.insert(QStringLiteral("contentBytes"), QString::fromLatin1(file.readAll().toBase64())); attachments.append(attachment); } message.insert(QStringLiteral("attachments"), attachments); QJsonObject payload; payload.insert(QStringLiteral("message"), message); payload.insert(QStringLiteral("saveToSentItems"), true); const QUrl url(QStringLiteral("https://graph.microsoft.com/v1.0/me/sendMail")); return postJson(url, account.accessToken().toUtf8(), payload, 202, error); } } MailService::MailService(AccountService *accountService, QObject *parent) : QObject(parent) , m_composer(new EmailComposerBridge(this)) , m_accountService(accountService) { } QVector MailService::getMails(const QString &folderId) { return MailItemDao::findByFolderId(folderId.toInt()); } void MailService::sendMail(const MailItem &mail, const QString &accountId) { sendMail(mail, accountId, {}); } void MailService::sendMail(const MailItem &mail, const QString &accountId, const QStringList &attachmentPaths) { if (!m_accountService) { emit mailSendFailed(accountId, tr("AccountService not available")); return; } Account *account = m_accountService->findAccountById(accountId.toLongLong()); if (!account) { emit mailSendFailed(accountId, tr("Account not found")); return; } const Account accountCopy = *account; delete account; QFuture future = QtConcurrent::run([mail, accountCopy, attachmentPaths]() { QString error; bool ok = false; if (accountCopy.type() == AccountType::Gmail) ok = sendGmailApi(mail, accountCopy, attachmentPaths, error); else if (accountCopy.type() == AccountType::Outlook) ok = sendOutlookApi(mail, accountCopy, attachmentPaths, error); else ok = sendSmtp(mail, accountCopy, attachmentPaths, error); if (!ok) qWarning() << "Send failed:" << error; return ok; }); auto *watcher = new QFutureWatcher(this); connect(watcher, &QFutureWatcher::finished, this, [this, watcher, accountId]() { const bool ok = watcher->result(); watcher->deleteLater(); if (ok) emit mailSent(accountId); else emit mailSendFailed(accountId, tr("Mail server rejected the message")); }); watcher->setFuture(future); } void MailService::fetchMails(const QString &accountId, const QString &folderId) { // Determine provider type from account if (!m_accountService) { emit mailFetchError(accountId, folderId, tr("AccountService not available")); return; } Account *acc = m_accountService->findAccountById(accountId.toLongLong()); if (!acc) { emit mailFetchError(accountId, folderId, tr("Account not found")); return; } QString providerType = QStringLiteral("imap"); // fallback if (acc) { switch (acc->type()) { case AccountType::IMAP: providerType = QStringLiteral("imap"); break; case AccountType::POP3: providerType = QStringLiteral("pop3"); break; case AccountType::Gmail: providerType = QStringLiteral("gmail"); break; case AccountType::Outlook: providerType = QStringLiteral("outlook"); break; default: providerType = QStringLiteral("imap"); break; } } Synchronizer *sync = SynchronizerProvider::instance().createSynchronizer(accountId, providerType); if (!sync) { emit mailFetchError(accountId, folderId, tr("Failed to create synchronizer")); return; } // Initialize synchronizer with account details before using it if (!sync->initialize(*acc)) { delete acc; emit mailFetchError(accountId, folderId, tr("Failed to initialize synchronizer")); SynchronizerProvider::instance().unregisterSynchronizer(accountId); return; } delete acc; // Connect progress signals (if they exist) using string-based syntax for compatibility QObject::connect(sync, SIGNAL(progressChanged(int)), this, SIGNAL(progressChanged(int)), Qt::QueuedConnection); QObject::connect(sync, SIGNAL(statusMessage(const QString&)), this, SIGNAL(statusMessage(const QString&)), Qt::QueuedConnection); QFuture> future = QtConcurrent::run([this, sync, folderId]() { // SinceUid = 0 for full sync; could be stored per folder but omitted for simplicity return sync->fetchMailItems(folderId, 0); }); QFutureWatcher> *watcher = new QFutureWatcher>(this); QObject::connect(watcher, &QFutureWatcher>::finished, this, [this, sync, watcher, accountId, folderId]() { // Disconnect progress signals QObject::disconnect(sync, SIGNAL(progressChanged(int)), this, SIGNAL(progressChanged(int))); QObject::disconnect(sync, SIGNAL(statusMessage(const QString&)), this, SIGNAL(statusMessage(const QString&))); QVector items = watcher->result(); QVector persisted; for (MailItem &item : items) { if (persistFetchedItem(item, accountId, folderId)) persisted.append(item); } watcher->deleteLater(); emit mailFetched(accountId, folderId, persisted); }); watcher->setFuture(future); } bool MailService::persistFetchedItem(MailItem &item, const QString &accountId, const QString &folderId) { if (item.rawMime().isEmpty()) return MailItemDao::upsert(item); MimeStorageService storage; ParsedMimeMessage parsed; QVector paths; if (!storage.parseMessage(item.rawMime(), parsed) || !storage.storeMessage(accountId, folderId, item, item.rawMime(), &paths)) { qWarning() << "Failed to persist MIME message" << item.messageId(); return false; } if (!MailItemDao::upsert(item)) { storage.deleteEmlFile(item.fileId()); return false; } QVector records; for (int i = 0; i < parsed.attachments.size(); ++i) { const ParsedMimeAttachment &source = parsed.attachments.at(i); StoredAttachmentRecord record; record.fileName = source.fileName; record.mimeType = source.mimeType; record.contentId = source.contentId; record.size = source.data.size(); if (i < paths.size()) record.storedPath = paths.at(i); records.append(record); } if (!MailItemDao::replaceAttachments(item.id(), records)) { storage.deleteEmlFile(item.fileId()); return false; } return true; } void MailService::moveMail(const QString &mailItemId, const QString &targetFolderId) { bool ok; qint64 id = mailItemId.toLongLong(&ok); if (!ok) return; std::optional opt = MailItemDao::findById(id); if (!opt) return; MailItem item = *opt; item.setFolderId(targetFolderId.toInt()); if (MailItemDao::update(item)) emit mailMoved(mailItemId); } void MailService::deleteMail(const QString &mailItemId) { bool ok; qint64 id = mailItemId.toLongLong(&ok); if (!ok) return; const std::optional item = MailItemDao::findById(id); if (MailItemDao::remove(id)) { if (item && !item->fileId().isEmpty()) { MimeStorageService storage; storage.deleteEmlFile(item->fileId()); } emit mailDeleted(mailItemId); } } void MailService::markAsRead(const QString &mailItemId, bool read) { bool ok; qint64 id = mailItemId.toLongLong(&ok); if (!ok) return; std::optional opt = MailItemDao::findById(id); if (!opt) return; MailItem item = *opt; item.setRead(read); if (MailItemDao::update(item)) emit mailReadStateChanged(mailItemId, read); } MailService::~MailService() = default;