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:
2026-08-17 15:34:49 +02:00
parent 364624aada
commit a004bdd60f
94 changed files with 19408 additions and 1047 deletions
+8 -6
View File
@@ -17,24 +17,26 @@ void AccountSetupDialogLauncher::setupAccountManually(const Account &account)
{
qDebug() << "[AccountSetupDialogLauncher] Setting up account:" << account.email();
// Save to database
bool created = AccountDao::insert(account);
if (!created) {
// Save to database and retain the generated id for the synchronizer/event.
const qint64 id = AccountDao::insert(account);
if (id < 0) {
qWarning() << "[AccountSetupDialogLauncher] Failed to save account to DB";
emit accountSetupFailed("Failed to save account");
return;
}
Account storedAccount = account;
storedAccount.setId(static_cast<int>(id));
// Initialize synchronizer
initializeSynchronizer(account);
initializeSynchronizer(storedAccount);
// Publish event
WinoMail::Events::AccountAddedEvent event;
event.account = account;
event.account = storedAccount;
EVENT_BUS.publish(event);
qDebug() << "[AccountSetupDialogLauncher] Account created successfully:" << account.email();
emit accountSetupCompleted(account);
emit accountSetupCompleted(storedAccount);
}
void AccountSetupDialogLauncher::initializeSynchronizer(const Account &account)
+18 -12
View File
@@ -1,6 +1,9 @@
#include "emailmanager.h"
#include <QDebug>
#include <QFile>
#include <QStandardPaths>
#include "db/dao/mailitemdao.h"
#include "services/mimestorage.h"
EmailManager::EmailManager(QObject *parent)
: QObject(parent)
@@ -9,29 +12,33 @@ EmailManager::EmailManager(QObject *parent)
MailItem EmailManager::getMailItemById(qint64 id) const
{
Q_UNUSED(id);
qDebug() << "[EmailManager] getMailItemById (stub)";
return MailItem();
const auto item = MailItemDao::findById(id);
return item.value_or(MailItem());
}
QString EmailManager::getStorageDirectory() const
{
qDebug() << "[EmailManager] getStorageDirectory (stub)";
return QString();
return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/mails");
}
QString EmailManager::getEmailHtmlById(qint64 id) const
{
Q_UNUSED(id);
qDebug() << "[EmailManager] getEmailHtmlById (stub)";
return QString("<html><body><p>GMime not available</p></body></html>");
const auto item = MailItemDao::findById(id);
if (!item) return QString();
if (!item->bodyHtml().isEmpty()) return item->bodyHtml();
if (item->fileId().isEmpty()) return QString();
MimeStorageService storage;
ParsedMimeMessage parsed;
return storage.parseMessage(storage.readEmlFile(item->fileId()), parsed) ? parsed.bodyHtml : QString();
}
QString EmailManager::convertEmlToHtml(const QString& emlFilePath) const
{
Q_UNUSED(emlFilePath);
qDebug() << "[EmailManager] convertEmlToHtml (stub)";
return QString();
QFile file(emlFilePath);
if (!file.open(QIODevice::ReadOnly)) return QString();
MimeStorageService storage;
ParsedMimeMessage parsed;
return storage.parseMessage(file.readAll(), parsed) ? parsed.bodyHtml : QString();
}
bool EmailManager::sendEmail(const QString& to, const QString& subject, const QString& body)
@@ -39,7 +46,6 @@ bool EmailManager::sendEmail(const QString& to, const QString& subject, const QS
Q_UNUSED(to);
Q_UNUSED(subject);
Q_UNUSED(body);
qDebug() << "[EmailManager] sendEmail (stub)";
return false;
}
+7 -5
View File
@@ -4,9 +4,7 @@
#include <QDesktopServices>
#include <QNetworkReply>
// Gmail OAuth2 configuration
static const char* GMAIL_CLIENT_ID = "your-gmail-client-id.apps.googleusercontent.com";
static const char* GMAIL_CLIENT_SECRET = "your-gmail-client-secret";
// Configure the OAuth application outside the source tree.
static const char* GMAIL_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
static const char* GMAIL_TOKEN_URL = "https://oauth2.googleapis.com/token";
static const char* GMAIL_SCOPES = "https://www.googleapis.com/auth/gmail.modify https://www.googleapis.com/auth/gmail.readonly email profile";
@@ -15,8 +13,8 @@ GmailAuthenticator::GmailAuthenticator(QObject *parent)
: Authenticator(parent)
, m_callbackServer(new OAuthCallbackServer(8080, this))
{
m_clientId = GMAIL_CLIENT_ID;
m_clientSecret = GMAIL_CLIENT_SECRET;
m_clientId = qEnvironmentVariable("WINO_GMAIL_CLIENT_ID");
m_clientSecret = qEnvironmentVariable("WINO_GMAIL_CLIENT_SECRET");
m_authEndpoint = GMAIL_AUTH_URL;
m_tokenEndpoint = GMAIL_TOKEN_URL;
m_scopes = GMAIL_SCOPES;
@@ -31,6 +29,10 @@ GmailAuthenticator::GmailAuthenticator(QObject *parent)
void GmailAuthenticator::authenticate(const QString &email)
{
if (m_clientId.isEmpty() || m_clientSecret.isEmpty()) {
emit authenticationFailed("Configure WINO_GMAIL_CLIENT_ID and WINO_GMAIL_CLIENT_SECRET first");
return;
}
m_email = email;
m_redirectUri = m_callbackServer->redirectUri();
qDebug() << "[GmailAuthenticator] Starting authentication for:" << email;
+8
View File
@@ -3,6 +3,7 @@
#include <QString>
#include <QDateTime>
#include <QVector>
#include <QByteArray>
class MailItem
{
@@ -68,6 +69,12 @@ public:
QString bcc() const { return m_bcc; }
void setBcc(const QString& bcc) { m_bcc = bcc; }
// The original message is kept transiently while it is being persisted.
// It is intentionally not stored in SQLite; MimeStorageService writes it
// verbatim to the account/folder mail store.
QByteArray rawMime() const { return m_rawMime; }
void setRawMime(const QByteArray& rawMime) { m_rawMime = rawMime; }
private:
qint64 m_id{0};
int m_folderId{0};
@@ -86,4 +93,5 @@ private:
QString m_to;
QString m_cc;
QString m_bcc;
QByteArray m_rawMime;
};
+68 -17
View File
@@ -1,4 +1,35 @@
#include "account.h"
#include <QCryptographicHash>
#include <QSysInfo>
#include <QCoreApplication>
#ifdef Q_OS_WIN
#include <windows.h>
#include <wincrypt.h>
#endif
namespace {
QByteArray localPasswordKey()
{
QByteArray seed = QSysInfo::machineUniqueId();
seed.append('|').append(qgetenv("USERNAME"));
seed.append('|').append(qgetenv("USER"));
seed.append('|').append(QCoreApplication::organizationName().toUtf8());
seed.append('|').append(QCoreApplication::applicationName().toUtf8());
return QCryptographicHash::hash(seed, QCryptographicHash::Sha256);
}
QString decryptLegacyPassword(const QString &encrypted)
{
const QByteArray data = QByteArray::fromBase64(encrypted.toUtf8());
static const QByteArray key = QByteArray::fromHex("0123456789ABCDEF0123456789ABCDEF");
QByteArray result(data.size(), 0);
for (int i = 0; i < data.size(); ++i) result[i] = data[i] ^ key.at(i % key.size());
return QString::fromUtf8(result);
}
}
Account::Account(int id, const QString& email, const QString& displayName,
const QString& signature,
@@ -18,27 +49,47 @@ Account::Account(int id, const QString& email, const QString& displayName,
QString Account::encryptPassword(const QString& plainText)
{
// Simple XOR encryption with a fixed key, then base64 encode
// Note: This is for obfuscation only, not strong security.
static const char keyHex[] = "0123456789ABCDEF"; // 16-byte key
QByteArray key;
bool ok;
for (int i = 0; i < 16; ++i) {
char c = QString::fromLatin1(keyHex + i * 2, 2).toInt(&ok, 16);
if (!ok) c = 0;
key.append(c);
const QByteArray input = plainText.toUtf8();
#ifdef Q_OS_WIN
DATA_BLOB source{static_cast<DWORD>(input.size()), reinterpret_cast<BYTE*>(const_cast<char*>(input.constData()))};
DATA_BLOB protectedData{};
if (CryptProtectData(&source, L"Wino Mail password", nullptr, nullptr, nullptr,
CRYPTPROTECT_UI_FORBIDDEN, &protectedData)) {
const QByteArray output(reinterpret_cast<const char*>(protectedData.pbData), protectedData.cbData);
LocalFree(protectedData.pbData);
return QStringLiteral("dpapi:") + QString::fromLatin1(output.toBase64());
}
QByteArray input = plainText.toUtf8();
#endif
const QByteArray key = localPasswordKey();
QByteArray result(input.size(), 0);
for (int i = 0; i < input.size(); ++i) {
result[i] = input[i] ^ key[i % key.size()];
}
return result.toBase64();
for (int i = 0; i < input.size(); ++i) result[i] = input[i] ^ key.at(i % key.size());
return QStringLiteral("local:") + QString::fromLatin1(result.toBase64());
}
QString Account::decryptPassword(const QString& encrypted)
{
// Same as encryption since XOR is symmetric
return encryptPassword(encrypted);
if (encrypted.startsWith(QStringLiteral("dpapi:"))) {
#ifdef Q_OS_WIN
const QByteArray data = QByteArray::fromBase64(encrypted.mid(6).toLatin1());
DATA_BLOB source{static_cast<DWORD>(data.size()), reinterpret_cast<BYTE*>(const_cast<char*>(data.constData()))};
DATA_BLOB plain{};
if (CryptUnprotectData(&source, nullptr, nullptr, nullptr, nullptr,
CRYPTPROTECT_UI_FORBIDDEN, &plain)) {
const QString result = QString::fromUtf8(reinterpret_cast<const char*>(plain.pbData), plain.cbData);
LocalFree(plain.pbData);
return result;
}
#endif
return QString();
}
if (encrypted.startsWith(QStringLiteral("local:"))) {
const QByteArray data = QByteArray::fromBase64(encrypted.mid(6).toLatin1());
const QByteArray key = localPasswordKey();
QByteArray result(data.size(), 0);
for (int i = 0; i < data.size(); ++i) result[i] = data[i] ^ key.at(i % key.size());
return QString::fromUtf8(result);
}
// Read passwords written by older versions once; all new writes use the
// user-bound envelope above.
return decryptLegacyPassword(encrypted);
}
+2 -1
View File
@@ -67,7 +67,8 @@ public:
const ConnectionSettings& connectionSettings() const { return m_connectionSettings; }
void setConnectionSettings(const ConnectionSettings& settings) { m_connectionSettings = settings; }
/// Simple encryption/decryption for passwords (XOR with fixed key, base64 encoded)
/// Protects passwords with the Windows user-bound data protection API.
/// Non-Windows builds use a machine/user-derived compatibility envelope.
static QString encryptPassword(const QString& plainText);
static QString decryptPassword(const QString& encrypted);
+80
View File
@@ -0,0 +1,80 @@
#pragma once
#include <QString>
#include <QDateTime>
#include <QtGlobal>
#include <QByteArray>
enum class AccountType {
POP3,
Outlook,
Gmail,
IMAP
};
class Account
{
public:
Account() = default;
Account(int id, const QString& email, const QString& displayName,
AccountType type, const QString& accessToken = QString(),
const QString& refreshToken = QString(),
const QDateTime& tokenExpires = QDateTime());
int id() const { return m_id; }
void setId(int id) { m_id = id; }
QString email() const { return m_email; }
void setEmail(const QString& email) { m_email = email; }
QString displayName() const { return m_displayName; }
void setDisplayName(const QString& displayName) { m_displayName = displayName; }
AccountType type() const { return m_type; }
void setType(AccountType type) { m_type = type; }
QString accessToken() const { return m_accessToken; }
void setAccessToken(const QString& token) { m_accessToken = token; }
QString refreshToken() const { return m_refreshToken; }
void setRefreshToken(const QString& token) { m_refreshToken = token; }
QDateTime tokenExpires() const { return m_tokenExpires; }
void setTokenExpires(const QDateTime& expires) { m_tokenExpires = expires; }
bool isTokenValid() const {
return !m_accessToken.isEmpty() && m_tokenExpires.isValid() &&
m_tokenExpires > QDateTime::currentDateTimeUtc();
}
/// Connection settings for incoming and outgoing servers
struct ConnectionSettings {
QString type; // "imap" or "pop3"
QString incomingHost;
quint16 incomingPort = 0;
bool incomingSsl = false;
QString outgoingHost;
quint16 outgoingPort = 0;
bool outgoingSsl = false;
QString username;
QString password;
QString authMethod; // "plain" or "oauth2"
};
const ConnectionSettings& connectionSettings() const { return m_connectionSettings; }
void setConnectionSettings(const ConnectionSettings& settings) { m_connectionSettings = settings; }
/// Simple encryption/decryption for passwords (XOR with fixed key, base64 encoded)
static QString encryptPassword(const QString& plainText);
static QString decryptPassword(const QString& encrypted);
private:
int m_id{0};
QString m_email;
QString m_displayName;
AccountType m_type{AccountType::IMAP};
QString m_accessToken;
QString m_refreshToken;
QDateTime m_tokenExpires;
ConnectionSettings m_connectionSettings;
};
+84
View File
@@ -0,0 +1,84 @@
#pragma once
#include <QString>
#include <QDateTime>
#include <QtGlobal>
#include <QByteArray>
enum class AccountType {
POP3,
Outlook,
Gmail,
IMAP
};
class Account
{
public:
Account() = default;
Account(int id, const QStringAccount(int id, const QString& email, const QString& displayName, email, const QStringAccount(int id, const QString& email, const QString& displayName, displayName, const QStringAccount(int id, const QString& email, const QString& displayName, signature,
AccountType type, const QString& accessToken = QString(),
const QString& refreshToken = QString(),
const QDateTime& tokenExpires = QDateTime());
int id() const { return m_id; }
void setId(int id) { m_id = id; }
QString email() const { return m_email; }
void setEmail(const QString& email) { m_email = email; }
QString displayName() const { return m_displayName; }
void setDisplayName(const QString& displayName) { m_displayName = displayName; }
QString signature() const { return m_signature; }
void setSignature(const QString& signature) { m_signature = signature; }
AccountType type() const { return m_type; }
void setType(AccountType type) { m_type = type; }
QString accessToken() const { return m_accessToken; }
void setAccessToken(const QString& token) { m_accessToken = token; }
QString refreshToken() const { return m_refreshToken; }
void setRefreshToken(const QString& token) { m_refreshToken = token; }
QDateTime tokenExpires() const { return m_tokenExpires; }
void setTokenExpires(const QDateTime& expires) { m_tokenExpires = expires; }
bool isTokenValid() const {
return !m_accessToken.isEmpty() && m_tokenExpires.isValid() &&
m_tokenExpires > QDateTime::currentDateTimeUtc();
}
/// Connection settings for incoming and outgoing servers
struct ConnectionSettings {
QString type; // "imap" or "pop3"
QString incomingHost;
quint16 incomingPort = 0;
bool incomingSsl = false;
QString outgoingHost;
quint16 outgoingPort = 0;
bool outgoingSsl = false;
QString username;
QString password;
QString authMethod; // "plain" or "oauth2"
};
const ConnectionSettings& connectionSettings() const { return m_connectionSettings; }
void setConnectionSettings(const ConnectionSettings& settings) { m_connectionSettings = settings; }
/// Simple encryption/decryption for passwords (XOR with fixed key, base64 encoded)
static QString encryptPassword(const QString& plainText);
static QString decryptPassword(const QString& encrypted);
private:
int m_id{0};
QString m_email;
QString m_displayName;
QString m_signature;
QString m_signature;
AccountType m_type{AccountType::IMAP};
QString m_accessToken;
QString m_refreshToken;
QDateTime m_tokenExpires;
ConnectionSettings m_connectionSettings;
};
+7 -5
View File
@@ -4,9 +4,7 @@
#include <QDesktopServices>
#include <QNetworkReply>
// Microsoft Graph OAuth2 configuration
static const char* OUTLOOK_CLIENT_ID = "your-outlook-client-id";
static const char* OUTLOOK_CLIENT_SECRET = "your-outlook-client-secret";
// Configure the OAuth application outside the source tree.
static const char* OUTLOOK_AUTH_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
static const char* OUTLOOK_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
static const char* OUTLOOK_SCOPES = "offline_access Mail.ReadWrite Mail.Send User.Read";
@@ -15,8 +13,8 @@ OutlookAuthenticator::OutlookAuthenticator(QObject *parent)
: Authenticator(parent)
, m_callbackServer(new OAuthCallbackServer(8080, this))
{
m_clientId = OUTLOOK_CLIENT_ID;
m_clientSecret = OUTLOOK_CLIENT_SECRET;
m_clientId = qEnvironmentVariable("WINO_OUTLOOK_CLIENT_ID");
m_clientSecret = qEnvironmentVariable("WINO_OUTLOOK_CLIENT_SECRET");
m_authEndpoint = OUTLOOK_AUTH_URL;
m_tokenEndpoint = OUTLOOK_TOKEN_URL;
m_scopes = OUTLOOK_SCOPES;
@@ -31,6 +29,10 @@ OutlookAuthenticator::OutlookAuthenticator(QObject *parent)
void OutlookAuthenticator::authenticate(const QString &email)
{
if (m_clientId.isEmpty() || m_clientSecret.isEmpty()) {
emit authenticationFailed("Configure WINO_OUTLOOK_CLIENT_ID and WINO_OUTLOOK_CLIENT_SECRET first");
return;
}
m_email = email;
m_redirectUri = m_callbackServer->redirectUri();
qDebug() << "[OutlookAuthenticator] Starting authentication for:" << email;
+12 -9
View File
@@ -2,7 +2,7 @@
qint64 AccountDao::insert(const Account& account)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"INSERT INTO Account (email, displayName, signature, type, accessToken, refreshToken, tokenExpires, "
@@ -39,12 +39,13 @@ qint64 AccountDao::insert(const Account& account)
bool AccountDao::update(const Account& account)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"UPDATE Account SET "
"email = :email, "
"displayName = :displayName, "
"signature = :signature, "
"type = :type, "
"accessToken = :accessToken, "
"refreshToken = :refreshToken, "
@@ -66,6 +67,7 @@ bool AccountDao::update(const Account& account)
query.bindValue(":type", static_cast<int>(account.type()));
query.bindValue(":accessToken", account.accessToken());
query.bindValue(":refreshToken", account.refreshToken());
query.bindValue(":tokenExpires", account.tokenExpires());
const auto& settings = account.connectionSettings();
query.bindValue(":incomingHost", settings.incomingHost);
query.bindValue(":incomingPort", settings.incomingPort);
@@ -86,7 +88,7 @@ bool AccountDao::update(const Account& account)
bool AccountDao::remove(int id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM Account WHERE id = :id");
query.bindValue(":id", id);
@@ -100,10 +102,10 @@ bool AccountDao::remove(int id)
Account* AccountDao::findById(int id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"SELECT id, email, displayName, type, accessToken, refreshToken, tokenExpires, "
"SELECT id, email, displayName, signature, type, accessToken, refreshToken, tokenExpires, "
"incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, "
"connUsername, connPassword, authMethod "
"FROM Account WHERE id = :id");
@@ -144,10 +146,10 @@ Account* AccountDao::findById(int id)
QVector<Account> AccountDao::findAll()
{
QVector<Account> accounts;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (!query.exec(
"SELECT id, email, displayName, type, accessToken, refreshToken, tokenExpires, "
"SELECT id, email, displayName, signature, type, accessToken, refreshToken, tokenExpires, "
"incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, "
"connUsername, connPassword, authMethod "
"FROM Account")) {
@@ -160,6 +162,7 @@ QVector<Account> AccountDao::findAll()
acc.setId(query.value("id").toInt());
acc.setEmail(query.value("email").toString());
acc.setDisplayName(query.value("displayName").toString());
acc.setSignature(query.value("signature").toString());
acc.setType(static_cast<AccountType>(query.value("type").toInt()));
acc.setAccessToken(query.value("accessToken").toString());
acc.setRefreshToken(query.value("refreshToken").toString());
@@ -182,10 +185,10 @@ QVector<Account> AccountDao::findAll()
Account* AccountDao::findByEmail(const QString& email)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"SELECT id, email, displayName, type, accessToken, refreshToken, tokenExpires, "
"SELECT id, email, displayName, signature, type, accessToken, refreshToken, tokenExpires, "
"incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, "
"connUsername, connPassword, authMethod "
"FROM Account WHERE email = :email");
+221
View File
@@ -0,0 +1,221 @@
#include "accountdao.h"
bool AccountDao::insert(const Account& account)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"INSERT INTO Account (email, displayName, type, accessToken, refreshToken, tokenExpires, "
"incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, "
"connUsername, connPassword, authMethod) "
"VALUES (:email, :displayName, :type, :accessToken, :refreshToken, :tokenExpires, "
":incomingHost, :incomingPort, :incomingSsl, :outgoingHost, :outgoingPort, :outgoingSsl, "
":connUsername, :connPassword, :authMethod)");
query.bindValue(":email", account.email());
query.bindValue(":displayName", account.displayName());
query.bindValue(":type", static_cast<int>(account.type()));
query.bindValue(":accessToken", account.accessToken());
query.bindValue(":refreshToken", account.refreshToken());
query.bindValue(":tokenExpires", account.tokenExpires());
const auto& settings = account.connectionSettings();
query.bindValue(":incomingHost", settings.incomingHost);
query.bindValue(":incomingPort", settings.incomingPort);
query.bindValue(":incomingSsl", settings.incomingSsl ? 1 : 0);
query.bindValue(":outgoingHost", settings.outgoingHost);
query.bindValue(":outgoingPort", settings.outgoingPort);
query.bindValue(":outgoingSsl", settings.outgoingSsl ? 1 : 0);
query.bindValue(":connUsername", settings.username);
query.bindValue(":connPassword", Account::encryptPassword(settings.password));
query.bindValue(":authMethod", settings.authMethod);
if (!query.exec()) {
qWarning() << "Failed to insert account:" << query.lastError().text();
return false;
}
return true;
}
bool AccountDao::update(const Account& account)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"UPDATE Account SET "
"email = :email, "
"displayName = :displayName, "
"type = :type, "
"accessToken = :accessToken, "
"refreshToken = :refreshToken, "
"tokenExpires = :tokenExpires, "
"incomingHost = :incomingHost, "
"incomingPort = :incomingPort, "
"incomingSsl = :incomingSsl, "
"outgoingHost = :outgoingHost, "
"outgoingPort = :outgoingPort, "
"outgoingSsl = :outgoingSsl, "
"connUsername = :connUsername, "
"connPassword = :connPassword, "
"authMethod = :authMethod "
"WHERE id = :id");
query.bindValue(":id", account.id());
query.bindValue(":email", account.email());
query.bindValue(":displayName", account.displayName());
query.bindValue(":type", static_cast<int>(account.type()));
query.bindValue(":accessToken", account.accessToken());
query.bindValue(":refreshToken", account.refreshToken());
query.bindValue(":tokenExpires", account.tokenExpires());
const auto& settings = account.connectionSettings();
query.bindValue(":incomingHost", settings.incomingHost);
query.bindValue(":incomingPort", settings.incomingPort);
query.bindValue(":incomingSsl", settings.incomingSsl ? 1 : 0);
query.bindValue(":outgoingHost", settings.outgoingHost);
query.bindValue(":outgoingPort", settings.outgoingPort);
query.bindValue(":outgoingSsl", settings.outgoingSsl ? 1 : 0);
query.bindValue(":connUsername", settings.username);
query.bindValue(":connPassword", Account::encryptPassword(settings.password));
query.bindValue(":authMethod", settings.authMethod);
if (!query.exec()) {
qWarning() << "Failed to update account:" << query.lastError().text();
return false;
}
return true;
}
bool AccountDao::remove(int id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM Account WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to delete account:" << query.lastError().text();
return false;
}
return true;
}
Account* AccountDao::findById(int id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"SELECT id, email, displayName, type, accessToken, refreshToken, tokenExpires, "
"incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, "
"connUsername, connPassword, authMethod "
"FROM Account WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to find account by id:" << query.lastError().text();
return nullptr;
}
if (query.next()) {
Account* account = new Account();
account->setId(query.value("id").toInt());
account->setEmail(query.value("email").toString());
account->setDisplayName(query.value("displayName").toString());
account->setType(static_cast<AccountType>(query.value("type").toInt()));
account->setAccessToken(query.value("accessToken").toString());
account->setRefreshToken(query.value("refreshToken").toString());
account->setTokenExpires(query.value("tokenExpires").toDateTime());
Account::ConnectionSettings settings;
settings.incomingHost = query.value("incomingHost").toString();
settings.incomingPort = query.value("incomingPort").toInt();
settings.incomingSsl = query.value("incomingSsl").toBool();
settings.outgoingHost = query.value("outgoingHost").toString();
settings.outgoingPort = query.value("outgoingPort").toInt();
settings.outgoingSsl = query.value("outgoingSsl").toBool();
settings.username = query.value("connUsername").toString();
settings.password = Account::decryptPassword(query.value("connPassword").toString());
settings.authMethod = query.value("authMethod").toString();
account->setConnectionSettings(settings);
return account;
}
return nullptr;
}
QVector<Account> AccountDao::findAll()
{
QVector<Account> accounts;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (!query.exec(
"SELECT id, email, displayName, type, accessToken, refreshToken, tokenExpires, "
"incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, "
"connUsername, connPassword, authMethod "
"FROM Account")) {
qWarning() << "Failed to fetch all accounts:" << query.lastError().text();
return accounts;
}
while (query.next()) {
Account acc;
acc.setId(query.value("id").toInt());
acc.setEmail(query.value("email").toString());
acc.setDisplayName(query.value("displayName").toString());
acc.setType(static_cast<AccountType>(query.value("type").toInt()));
acc.setAccessToken(query.value("accessToken").toString());
acc.setRefreshToken(query.value("refreshToken").toString());
acc.setTokenExpires(query.value("tokenExpires").toDateTime());
Account::ConnectionSettings settings;
settings.incomingHost = query.value("incomingHost").toString();
settings.incomingPort = query.value("incomingPort").toInt();
settings.incomingSsl = query.value("incomingSsl").toBool();
settings.outgoingHost = query.value("outgoingHost").toString();
settings.outgoingPort = query.value("outgoingPort").toInt();
settings.outgoingSsl = query.value("outgoingSsl").toBool();
settings.username = query.value("connUsername").toString();
settings.password = Account::decryptPassword(query.value("connPassword").toString());
settings.authMethod = query.value("authMethod").toString();
acc.setConnectionSettings(settings);
accounts.append(acc);
}
return accounts;
}
Account* AccountDao::findByEmail(const QString& email)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"SELECT id, email, displayName, type, accessToken, refreshToken, tokenExpires, "
"incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, "
"connUsername, connPassword, authMethod "
"FROM Account WHERE email = :email");
query.bindValue(":email", email);
if (!query.exec()) {
qWarning() << "Failed to find account by email:" << query.lastError().text();
return nullptr;
}
if (query.next()) {
Account* acc = new Account();
acc->setId(query.value("id").toInt());
acc->setEmail(query.value("email").toString());
acc->setDisplayName(query.value("displayName").toString());
acc->setType(static_cast<AccountType>(query.value("type").toInt()));
acc->setAccessToken(query.value("accessToken").toString());
acc->setRefreshToken(query.value("refreshToken").toString());
acc->setTokenExpires(query.value("tokenExpires").toDateTime());
Account::ConnectionSettings settings;
settings.incomingHost = query.value("incomingHost").toString();
settings.incomingPort = query.value("incomingPort").toInt();
settings.incomingSsl = query.value("incomingSsl").toBool();
settings.outgoingHost = query.value("outgoingHost").toString();
settings.outgoingPort = query.value("outgoingPort").toInt();
settings.outgoingSsl = query.value("outgoingSsl").toBool();
settings.username = query.value("connUsername").toString();
settings.password = Account::decryptPassword(query.value("connPassword").toString());
settings.authMethod = query.value("authMethod").toString();
acc->setConnectionSettings(settings);
return acc;
}
return nullptr;
}
+224
View File
@@ -0,0 +1,224 @@
#include "accountdao.h"
qint64 AccountDao::insert(const Account& account)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"INSERT INTO Account (email, displayName, signature, type, accessToken, refreshToken, tokenExpires, "
"incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, "
"connUsername, connPassword, authMethod) "
"VALUES (:email, :displayName, :signature, :type, :accessToken, :refreshToken, :tokenExpires, "
":incomingHost, :incomingPort, :incomingSsl, :outgoingHost, :outgoingPort, :outgoingSsl, "
":connUsername, :connPassword, :authMethod)");
query.bindValue(":email", account.email());
query.bindValue(":displayName", account.displayName());
query.bindValue(":signature", account.signature());
query.bindValue(":type", static_cast<int>(account.type()));
query.bindValue(":accessToken", account.accessToken());
query.bindValue(":refreshToken", account.refreshToken());
query.bindValue(":tokenExpires", account.tokenExpires());
const auto& settings = account.connectionSettings();
query.bindValue(":incomingHost", settings.incomingHost);
query.bindValue(":incomingPort", settings.incomingPort);
query.bindValue(":incomingSsl", settings.incomingSsl ? 1 : 0);
query.bindValue(":outgoingHost", settings.outgoingHost);
query.bindValue(":outgoingPort", settings.outgoingPort);
query.bindValue(":outgoingSsl", settings.outgoingSsl ? 1 : 0);
query.bindValue(":connUsername", settings.username);
query.bindValue(":connPassword", Account::encryptPassword(settings.password));
query.bindValue(":authMethod", settings.authMethod);
if (!query.exec()) {
qWarning() << "Failed to insert account:" << query.lastError().text();
return -1;
}
return query.lastInsertId().toLongLong();
}
bool AccountDao::update(const Account& account)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"UPDATE Account SET "
"email = :email, "
"displayName = :displayName, "
"type = :type, "
"accessToken = :accessToken, "
"refreshToken = :refreshToken, "
"tokenExpires = :tokenExpires, "
"incomingHost = :incomingHost, "
"incomingPort = :incomingPort, "
"incomingSsl = :incomingSsl, "
"outgoingHost = :outgoingHost, "
"outgoingPort = :outgoingPort, "
"outgoingSsl = :outgoingSsl, "
"connUsername = :connUsername, "
"connPassword = :connPassword, "
"authMethod = :authMethod "
"WHERE id = :id");
query.bindValue(":id", account.id());
query.bindValue(":email", account.email());
query.bindValue(":displayName", account.displayName());
query.bindValue(":signature", account.signature());
query.bindValue(":type", static_cast<int>(account.type()));
query.bindValue(":accessToken", account.accessToken());
query.bindValue(":refreshToken", account.refreshToken());
const auto& settings = account.connectionSettings();
query.bindValue(":incomingHost", settings.incomingHost);
query.bindValue(":incomingPort", settings.incomingPort);
query.bindValue(":incomingSsl", settings.incomingSsl ? 1 : 0);
query.bindValue(":outgoingHost", settings.outgoingHost);
query.bindValue(":outgoingPort", settings.outgoingPort);
query.bindValue(":outgoingSsl", settings.outgoingSsl ? 1 : 0);
query.bindValue(":connUsername", settings.username);
query.bindValue(":connPassword", Account::encryptPassword(settings.password));
query.bindValue(":authMethod", settings.authMethod);
if (!query.exec()) {
qWarning() << "Failed to update account:" << query.lastError().text();
return false;
}
return true;
}
bool AccountDao::remove(int id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM Account WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to delete account:" << query.lastError().text();
return false;
}
return true;
}
Account* AccountDao::findById(int id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"SELECT id, email, displayName, type, accessToken, refreshToken, tokenExpires, "
"incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, "
"connUsername, connPassword, authMethod "
"FROM Account WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to find account by id:" << query.lastError().text();
return nullptr;
}
if (query.next()) {
Account* account = new Account();
account->setId(query.value("id").toInt());
account->setEmail(query.value("email").toString());
account->setDisplayName(query.value("displayName").toString());
account->setSignature(query.value("signature").toString());
account->setType(static_cast<AccountType>(query.value("type").toInt()));
account->setAccessToken(query.value("accessToken").toString());
account->setRefreshToken(query.value("refreshToken").toString());
account->setTokenExpires(query.value("tokenExpires").toDateTime());
Account::ConnectionSettings settings;
settings.incomingHost = query.value("incomingHost").toString();
settings.incomingPort = query.value("incomingPort").toInt();
settings.incomingSsl = query.value("incomingSsl").toBool();
settings.outgoingHost = query.value("outgoingHost").toString();
settings.outgoingPort = query.value("outgoingPort").toInt();
settings.outgoingSsl = query.value("outgoingSsl").toBool();
settings.username = query.value("connUsername").toString();
settings.password = Account::decryptPassword(query.value("connPassword").toString());
settings.authMethod = query.value("authMethod").toString();
account->setConnectionSettings(settings);
return account;
}
return nullptr;
}
QVector<Account> AccountDao::findAll()
{
QVector<Account> accounts;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (!query.exec(
"SELECT id, email, displayName, type, accessToken, refreshToken, tokenExpires, "
"incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, "
"connUsername, connPassword, authMethod "
"FROM Account")) {
qWarning() << "Failed to fetch all accounts:" << query.lastError().text();
return accounts;
}
while (query.next()) {
Account acc;
acc.setId(query.value("id").toInt());
acc.setEmail(query.value("email").toString());
acc.setDisplayName(query.value("displayName").toString());
acc.setType(static_cast<AccountType>(query.value("type").toInt()));
acc.setAccessToken(query.value("accessToken").toString());
acc.setRefreshToken(query.value("refreshToken").toString());
acc.setTokenExpires(query.value("tokenExpires").toDateTime());
Account::ConnectionSettings settings;
settings.incomingHost = query.value("incomingHost").toString();
settings.incomingPort = query.value("incomingPort").toInt();
settings.incomingSsl = query.value("incomingSsl").toBool();
settings.outgoingHost = query.value("outgoingHost").toString();
settings.outgoingPort = query.value("outgoingPort").toInt();
settings.outgoingSsl = query.value("outgoingSsl").toBool();
settings.username = query.value("connUsername").toString();
settings.password = Account::decryptPassword(query.value("connPassword").toString());
settings.authMethod = query.value("authMethod").toString();
acc.setConnectionSettings(settings);
accounts.append(acc);
}
return accounts;
}
Account* AccountDao::findByEmail(const QString& email)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"SELECT id, email, displayName, type, accessToken, refreshToken, tokenExpires, "
"incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, "
"connUsername, connPassword, authMethod "
"FROM Account WHERE email = :email");
query.bindValue(":email", email);
if (!query.exec()) {
qWarning() << "Failed to find account by email:" << query.lastError().text();
return nullptr;
}
if (query.next()) {
Account* acc = new Account();
acc->setId(query.value("id").toInt());
acc->setEmail(query.value("email").toString());
acc->setDisplayName(query.value("displayName").toString());
acc->setSignature(query.value("signature").toString());
acc->setType(static_cast<AccountType>(query.value("type").toInt()));
acc->setAccessToken(query.value("accessToken").toString());
acc->setRefreshToken(query.value("refreshToken").toString());
acc->setTokenExpires(query.value("tokenExpires").toDateTime());
Account::ConnectionSettings settings;
settings.incomingHost = query.value("incomingHost").toString();
settings.incomingPort = query.value("incomingPort").toInt();
settings.incomingSsl = query.value("incomingSsl").toBool();
settings.outgoingHost = query.value("outgoingHost").toString();
settings.outgoingPort = query.value("outgoingPort").toInt();
settings.outgoingSsl = query.value("outgoingSsl").toBool();
settings.username = query.value("connUsername").toString();
settings.password = Account::decryptPassword(query.value("connPassword").toString());
settings.authMethod = query.value("authMethod").toString();
acc->setConnectionSettings(settings);
return acc;
}
return nullptr;
}
+31 -31
View File
@@ -4,34 +4,9 @@
#include <QSqlError>
#include <optional>
bool FolderDao::insert(const Folder& folder)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"INSERT INTO Folder (accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced) "
"VALUES (:accountId, :name, :parentFolderId, :isInbox, :isSent, :isDrafts, :isTrash, :unreadCount, :lastSynced)"
);
query.bindValue(":accountId", folder.accountId());
query.bindValue(":name", folder.name());
query.bindValue(":parentFolderId", folder.parentFolderId());
query.bindValue(":isInbox", folder.isInbox() ? 1 : 0);
query.bindValue(":isSent", folder.isSent() ? 1 : 0);
query.bindValue(":isDrafts", folder.isDrafts() ? 1 : 0);
query.bindValue(":isTrash", folder.isTrash() ? 1 : 0);
query.bindValue(":unreadCount", folder.unreadCount());
query.bindValue(":lastSynced", folder.lastSynced());
if (!query.exec()) {
qWarning() << "Failed to insert folder:" << query.lastError().text();
return false;
}
return true;
}
bool FolderDao::update(const Folder& folder)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"UPDATE Folder SET "
@@ -66,7 +41,7 @@ bool FolderDao::update(const Folder& folder)
bool FolderDao::remove(int id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM Folder WHERE id = :id");
query.bindValue(":id", id);
@@ -80,7 +55,7 @@ bool FolderDao::remove(int id)
std::optional<Folder> FolderDao::findById(int id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder WHERE id = :id");
query.bindValue(":id", id);
@@ -110,7 +85,7 @@ std::optional<Folder> FolderDao::findById(int id)
QVector<Folder> FolderDao::findAll()
{
QVector<Folder> folders;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (!query.exec("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder")) {
qWarning() << "Failed to fetch all folders:" << query.lastError().text();
@@ -137,7 +112,7 @@ QVector<Folder> FolderDao::findAll()
QVector<Folder> FolderDao::findByAccountId(int accountId)
{
QVector<Folder> folders;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder WHERE accountId = :accountId");
query.bindValue(":accountId", accountId);
@@ -166,7 +141,7 @@ QVector<Folder> FolderDao::findByAccountId(int accountId)
bool FolderDao::removeByAccountId(int accountId)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM Folder WHERE accountId = :accountId");
query.bindValue(":accountId", accountId);
@@ -177,3 +152,28 @@ bool FolderDao::removeByAccountId(int accountId)
}
return true;
}
qint64 FolderDao::insert(const Folder& folder)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"INSERT INTO Folder (accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced) "
"VALUES (:accountId, :name, :parentFolderId, :isInbox, :isSent, :isDrafts, :isTrash, :unreadCount, :lastSynced)"
);
query.bindValue(":accountId", folder.accountId());
query.bindValue(":name", folder.name());
query.bindValue(":parentFolderId", folder.parentFolderId());
query.bindValue(":isInbox", folder.isInbox() ? 1 : 0);
query.bindValue(":isSent", folder.isSent() ? 1 : 0);
query.bindValue(":isDrafts", folder.isDrafts() ? 1 : 0);
query.bindValue(":isTrash", folder.isTrash() ? 1 : 0);
query.bindValue(":unreadCount", folder.unreadCount());
query.bindValue(":lastSynced", folder.lastSynced());
if (!query.exec()) {
qWarning() << "Failed to insert folder:" << query.lastError().text();
return -1;
}
return query.lastInsertId().toLongLong();
}
+179
View File
@@ -0,0 +1,179 @@
#include <QSqlQuery>
#include "folderdao.h"
#include <QSqlError>
#include <QSqlError>
#include <optional>
bool FolderDao::insert(const Folder& folder)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"INSERT INTO Folder (accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced) "
"VALUES (:accountId, :name, :parentFolderId, :isInbox, :isSent, :isDrafts, :isTrash, :unreadCount, :lastSynced)"
);
query.bindValue(":accountId", folder.accountId());
query.bindValue(":name", folder.name());
query.bindValue(":parentFolderId", folder.parentFolderId());
query.bindValue(":isInbox", folder.isInbox() ? 1 : 0);
query.bindValue(":isSent", folder.isSent() ? 1 : 0);
query.bindValue(":isDrafts", folder.isDrafts() ? 1 : 0);
query.bindValue(":isTrash", folder.isTrash() ? 1 : 0);
query.bindValue(":unreadCount", folder.unreadCount());
query.bindValue(":lastSynced", folder.lastSynced());
if (!query.exec()) {
qWarning() << "Failed to insert folder:" << query.lastError().text();
return false;
}
return true;
}
bool FolderDao::update(const Folder& folder)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"UPDATE Folder SET "
"accountId = :accountId, "
"name = :name, "
"parentFolderId = :parentFolderId, "
"isInbox = :isInbox, "
"isSent = :isSent, "
"isDrafts = :isDrafts, "
"isTrash = :isTrash, "
"unreadCount = :unreadCount, "
"lastSynced = :lastSynced "
"WHERE id = :id"
);
query.bindValue(":id", folder.id());
query.bindValue(":accountId", folder.accountId());
query.bindValue(":name", folder.name());
query.bindValue(":parentFolderId", folder.parentFolderId());
query.bindValue(":isInbox", folder.isInbox() ? 1 : 0);
query.bindValue(":isSent", folder.isSent() ? 1 : 0);
query.bindValue(":isDrafts", folder.isDrafts() ? 1 : 0);
query.bindValue(":isTrash", folder.isTrash() ? 1 : 0);
query.bindValue(":unreadCount", folder.unreadCount());
query.bindValue(":lastSynced", folder.lastSynced());
if (!query.exec()) {
qWarning() << "Failed to update folder:" << query.lastError().text();
return false;
}
return true;
}
bool FolderDao::remove(int id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM Folder WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to delete folder:" << query.lastError().text();
return false;
}
return true;
}
std::optional<Folder> FolderDao::findById(int id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to find folder by id:" << query.lastError().text();
return std::nullopt;
}
if (query.next()) {
Folder fld;
fld.setId(query.value(0).toInt());
fld.setAccountId(query.value(1).toInt());
fld.setName(query.value(2).toString());
fld.setParentFolderId(query.value(3).toString());
fld.setInbox(query.value(4).toBool());
fld.setSent(query.value(5).toBool());
fld.setDrafts(query.value(6).toBool());
fld.setTrash(query.value(7).toBool());
fld.setUnreadCount(query.value(8).toInt());
fld.setLastSynced(query.value(9).toDateTime());
return fld;
}
return std::nullopt;
}
QVector<Folder> FolderDao::findAll()
{
QVector<Folder> folders;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (!query.exec("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder")) {
qWarning() << "Failed to fetch all folders:" << query.lastError().text();
return folders;
}
while (query.next()) {
Folder fld;
fld.setId(query.value(0).toInt());
fld.setAccountId(query.value(1).toInt());
fld.setName(query.value(2).toString());
fld.setParentFolderId(query.value(3).toString());
fld.setInbox(query.value(4).toBool());
fld.setSent(query.value(5).toBool());
fld.setDrafts(query.value(6).toBool());
fld.setTrash(query.value(7).toBool());
fld.setUnreadCount(query.value(8).toInt());
fld.setLastSynced(query.value(9).toDateTime());
folders.push_back(fld);
}
return folders;
}
QVector<Folder> FolderDao::findByAccountId(int accountId)
{
QVector<Folder> folders;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder WHERE accountId = :accountId");
query.bindValue(":accountId", accountId);
if (!query.exec()) {
qWarning() << "Failed to fetch folders by account id:" << query.lastError().text();
return folders;
}
while (query.next()) {
Folder fld;
fld.setId(query.value(0).toInt());
fld.setAccountId(query.value(1).toInt());
fld.setName(query.value(2).toString());
fld.setParentFolderId(query.value(3).toString());
fld.setInbox(query.value(4).toBool());
fld.setSent(query.value(5).toBool());
fld.setDrafts(query.value(6).toBool());
fld.setTrash(query.value(7).toBool());
fld.setUnreadCount(query.value(8).toInt());
fld.setLastSynced(query.value(9).toDateTime());
folders.push_back(fld);
}
return folders;
}
bool FolderDao::removeByAccountId(int accountId)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM Folder WHERE accountId = :accountId");
query.bindValue(":accountId", accountId);
if (!query.exec()) {
qWarning() << "Failed to delete folders by account id:" << query.lastError().text();
return false;
}
return true;
}
+180
View File
@@ -0,0 +1,180 @@
#include <QSqlQuery>
#include "folderdao.h"
#include <QSqlError>
#include <QSqlError>
#include <optional>
qint64 FolderDao::insert(Folder& folder)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"INSERT INTO Folder (accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced) "
"VALUES (:accountId, :name, :parentFolderId, :isInbox, :isSent, :isDrafts, :isTrash, :unreadCount, :lastSynced)"
);
query.bindValue(":accountId", folder.accountId());
query.bindValue(":name", folder.name());
query.bindValue(":parentFolderId", folder.parentFolderId());
query.bindValue(":isInbox", folder.isInbox() ? 1 : 0);
query.bindValue(":isSent", folder.isSent() ? 1 : 0);
query.bindValue(":isDrafts", folder.isDrafts() ? 1 : 0);
query.bindValue(":isTrash", folder.isTrash() ? 1 : 0);
query.bindValue(":unreadCount", folder.unreadCount());
query.bindValue(":lastSynced", folder.lastSynced());
if (!query.exec()) {
qWarning() << "Failed to insert folder:" << query.lastError().text();
return -1;
}
folder.setId(query.lastInsertId().toLongLong());
return folder.id();
}
bool FolderDao::update(const Folder& folder)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"UPDATE Folder SET "
"accountId = :accountId, "
"name = :name, "
"parentFolderId = :parentFolderId, "
"isInbox = :isInbox, "
"isSent = :isSent, "
"isDrafts = :isDrafts, "
"isTrash = :isTrash, "
"unreadCount = :unreadCount, "
"lastSynced = :lastSynced "
"WHERE id = :id"
);
query.bindValue(":id", folder.id());
query.bindValue(":accountId", folder.accountId());
query.bindValue(":name", folder.name());
query.bindValue(":parentFolderId", folder.parentFolderId());
query.bindValue(":isInbox", folder.isInbox() ? 1 : 0);
query.bindValue(":isSent", folder.isSent() ? 1 : 0);
query.bindValue(":isDrafts", folder.isDrafts() ? 1 : 0);
query.bindValue(":isTrash", folder.isTrash() ? 1 : 0);
query.bindValue(":unreadCount", folder.unreadCount());
query.bindValue(":lastSynced", folder.lastSynced());
if (!query.exec()) {
qWarning() << "Failed to update folder:" << query.lastError().text();
return false;
}
return true;
}
bool FolderDao::remove(int id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM Folder WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to delete folder:" << query.lastError().text();
return false;
}
return true;
}
std::optional<Folder> FolderDao::findById(int id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to find folder by id:" << query.lastError().text();
return std::nullopt;
}
if (query.next()) {
Folder fld;
fld.setId(query.value(0).toInt());
fld.setAccountId(query.value(1).toInt());
fld.setName(query.value(2).toString());
fld.setParentFolderId(query.value(3).toString());
fld.setInbox(query.value(4).toBool());
fld.setSent(query.value(5).toBool());
fld.setDrafts(query.value(6).toBool());
fld.setTrash(query.value(7).toBool());
fld.setUnreadCount(query.value(8).toInt());
fld.setLastSynced(query.value(9).toDateTime());
return fld;
}
return std::nullopt;
}
QVector<Folder> FolderDao::findAll()
{
QVector<Folder> folders;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (!query.exec("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder")) {
qWarning() << "Failed to fetch all folders:" << query.lastError().text();
return folders;
}
while (query.next()) {
Folder fld;
fld.setId(query.value(0).toInt());
fld.setAccountId(query.value(1).toInt());
fld.setName(query.value(2).toString());
fld.setParentFolderId(query.value(3).toString());
fld.setInbox(query.value(4).toBool());
fld.setSent(query.value(5).toBool());
fld.setDrafts(query.value(6).toBool());
fld.setTrash(query.value(7).toBool());
fld.setUnreadCount(query.value(8).toInt());
fld.setLastSynced(query.value(9).toDateTime());
folders.push_back(fld);
}
return folders;
}
QVector<Folder> FolderDao::findByAccountId(int accountId)
{
QVector<Folder> folders;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder WHERE accountId = :accountId");
query.bindValue(":accountId", accountId);
if (!query.exec()) {
qWarning() << "Failed to fetch folders by account id:" << query.lastError().text();
return folders;
}
while (query.next()) {
Folder fld;
fld.setId(query.value(0).toInt());
fld.setAccountId(query.value(1).toInt());
fld.setName(query.value(2).toString());
fld.setParentFolderId(query.value(3).toString());
fld.setInbox(query.value(4).toBool());
fld.setSent(query.value(5).toBool());
fld.setDrafts(query.value(6).toBool());
fld.setTrash(query.value(7).toBool());
fld.setUnreadCount(query.value(8).toInt());
fld.setLastSynced(query.value(9).toDateTime());
folders.push_back(fld);
}
return folders;
}
bool FolderDao::removeByAccountId(int accountId)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM Folder WHERE accountId = :accountId");
query.bindValue(":accountId", accountId);
if (!query.exec()) {
qWarning() << "Failed to delete folders by account id:" << query.lastError().text();
return false;
}
return true;
}
+1 -1
View File
@@ -8,7 +8,7 @@
class FolderDao
{
public:
static bool insert(const Folder& folder);
static qint64 insert(const Folder& folder);
static bool update(const Folder& folder);
static bool remove(int id);
static bool removeByAccountId(int accountId);
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include "../databasemanager.h"
#include "../../core/models/folder.h"
#include <QVector>
#include <optional>
class FolderDao
{
public:
static bool insert(const Folder& folder);
static bool update(const Folder& folder);
static bool remove(int id);
static bool removeByAccountId(int accountId);
static std::optional<Folder> findById(int id);
static QVector<Folder> findAll();
static QVector<Folder> findByAccountId(int accountId);
};
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include "../databasemanager.h"
#include "../../core/models/folder.h"
#include <QVector>
#include <optional>
class FolderDao
{
public:
static qint64 insert(const Folder& folder);
static bool update(const Folder& folder);
static bool remove(int id);
static bool removeByAccountId(int accountId);
static std::optional<Folder> findById(int id);
static QVector<Folder> findAll();
static QVector<Folder> findByAccountId(int accountId);
};
+201 -17
View File
@@ -1,18 +1,38 @@
#include <QSqlQuery>
#include <QSqlError>
#include "mailitemdao.h"
#include <QDebug>
#include <optional>
bool MailItemDao::insert(const MailItem& item)
bool MailItemDao::insert(MailItem& item)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
// IMAP UIDs and provider message IDs are stable across synchronizations.
// Update the existing row instead of creating duplicates on every sync.
if (!item.messageId().isEmpty() || item.uid() > 0) {
QSqlQuery existing(db);
if (!item.messageId().isEmpty()) {
existing.prepare("SELECT id FROM MailCopy WHERE messageId = :messageId LIMIT 1");
existing.bindValue(":messageId", item.messageId());
} else {
existing.prepare("SELECT id FROM MailCopy WHERE folderId = :folderId AND uid = :uid LIMIT 1");
existing.bindValue(":folderId", item.folderId());
existing.bindValue(":uid", item.uid());
}
if (existing.exec() && existing.next()) {
item.setId(existing.value(0).toLongLong());
return update(item);
}
}
QSqlQuery query(db);
query.prepare(
"INSERT INTO MailCopy (folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid) "
"VALUES (:folderId, :messageId, :subject, :sender, :recipient, :date, :read, :flagged, :hasAttachment, :size, :fileId, :uid)"
"INSERT INTO MailCopy (folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr) "
"VALUES (:folderId, :messageId, :subject, :sender, :recipient, :date, :read, :flagged, :hasAttachment, :size, :fileId, :uid, :bodyHtml, :toAddr, :ccAddr, :bccAddr)"
);
query.bindValue(":folderId", item.folderId());
query.bindValue(":messageId", item.messageId());
query.bindValue(":messageId", item.messageId().isEmpty() ? QVariant() : QVariant(item.messageId()));
query.bindValue(":subject", item.subject());
query.bindValue(":sender", item.sender());
query.bindValue(":recipient", item.recipient());
@@ -23,17 +43,27 @@ bool MailItemDao::insert(const MailItem& item)
query.bindValue(":size", item.size());
query.bindValue(":fileId", item.fileId());
query.bindValue(":uid", item.uid());
query.bindValue(":bodyHtml", item.bodyHtml());
query.bindValue(":toAddr", item.to());
query.bindValue(":ccAddr", item.cc());
query.bindValue(":bccAddr", item.bcc());
if (!query.exec()) {
qWarning() << "Failed to insert mail item:" << query.lastError().text();
return false;
}
item.setId(query.lastInsertId().toLongLong());
return true;
}
bool MailItemDao::upsert(MailItem& item)
{
return insert(item);
}
bool MailItemDao::update(const MailItem& item)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"UPDATE MailCopy SET "
@@ -48,12 +78,16 @@ bool MailItemDao::update(const MailItem& item)
"hasAttachment = :hasAttachment, "
"size = :size, "
"fileId = :fileId, "
"uid = :uid "
"uid = :uid, "
"bodyHtml = :bodyHtml, "
"toAddr = :toAddr, "
"ccAddr = :ccAddr, "
"bccAddr = :bccAddr "
"WHERE id = :id"
);
query.bindValue(":id", item.id());
query.bindValue(":folderId", item.folderId());
query.bindValue(":messageId", item.messageId());
query.bindValue(":messageId", item.messageId().isEmpty() ? QVariant() : QVariant(item.messageId()));
query.bindValue(":subject", item.subject());
query.bindValue(":sender", item.sender());
query.bindValue(":recipient", item.recipient());
@@ -64,6 +98,10 @@ bool MailItemDao::update(const MailItem& item)
query.bindValue(":size", item.size());
query.bindValue(":fileId", item.fileId());
query.bindValue(":uid", item.uid());
query.bindValue(":bodyHtml", item.bodyHtml());
query.bindValue(":toAddr", item.to());
query.bindValue(":ccAddr", item.cc());
query.bindValue(":bccAddr", item.bcc());
if (!query.exec()) {
qWarning() << "Failed to update mail item:" << query.lastError().text();
@@ -74,7 +112,7 @@ bool MailItemDao::update(const MailItem& item)
bool MailItemDao::remove(qint64 id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM MailCopy WHERE id = :id");
query.bindValue(":id", id);
@@ -88,9 +126,9 @@ bool MailItemDao::remove(qint64 id)
std::optional<MailItem> MailItemDao::findById(qint64 id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid FROM MailCopy WHERE id = :id");
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
@@ -112,6 +150,13 @@ std::optional<MailItem> MailItemDao::findById(qint64 id)
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
item.setTo(query.value(14).toString());
item.setCc(query.value(15).toString());
item.setBcc(query.value(16).toString());
QVector<QString> names;
for (const auto &attachment : attachmentsForMail(item.id())) names.append(attachment.fileName);
item.setAttachments(names);
return item;
}
return std::nullopt;
@@ -120,9 +165,9 @@ std::optional<MailItem> MailItemDao::findById(qint64 id)
QVector<MailItem> MailItemDao::findAll()
{
QVector<MailItem> items;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (!query.exec("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid FROM MailCopy")) {
if (!query.exec("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy")) {
qWarning() << "Failed to fetch all mail items:" << query.lastError().text();
return items;
}
@@ -141,6 +186,13 @@ QVector<MailItem> MailItemDao::findAll()
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
item.setTo(query.value(14).toString());
item.setCc(query.value(15).toString());
item.setBcc(query.value(16).toString());
QVector<QString> names;
for (const auto &attachment : attachmentsForMail(item.id())) names.append(attachment.fileName);
item.setAttachments(names);
items.append(item);
}
return items;
@@ -149,9 +201,9 @@ QVector<MailItem> MailItemDao::findAll()
QVector<MailItem> MailItemDao::findByFolderId(int folderId)
{
QVector<MailItem> items;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid FROM MailCopy WHERE folderId = :folderId");
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
@@ -173,6 +225,13 @@ QVector<MailItem> MailItemDao::findByFolderId(int folderId)
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
item.setTo(query.value(14).toString());
item.setCc(query.value(15).toString());
item.setBcc(query.value(16).toString());
QVector<QString> names;
for (const auto &attachment : attachmentsForMail(item.id())) names.append(attachment.fileName);
item.setAttachments(names);
items.append(item);
}
return items;
@@ -181,9 +240,9 @@ QVector<MailItem> MailItemDao::findByFolderId(int folderId)
QVector<MailItem> MailItemDao::findByFolderIdSinceUid(int folderId, qint64 sinceUid)
{
QVector<MailItem> items;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid FROM MailCopy WHERE folderId = :folderId AND id > :sinceUid");
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE folderId = :folderId AND uid > :sinceUid");
query.bindValue(":folderId", folderId);
query.bindValue(":sinceUid", sinceUid);
@@ -206,7 +265,132 @@ QVector<MailItem> MailItemDao::findByFolderIdSinceUid(int folderId, qint64 since
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
item.setTo(query.value(14).toString());
item.setCc(query.value(15).toString());
item.setBcc(query.value(16).toString());
QVector<QString> names;
for (const auto &attachment : attachmentsForMail(item.id())) names.append(attachment.fileName);
item.setAttachments(names);
items.append(item);
}
return items;
}
std::optional<qint64> MailItemDao::maxUidForFolder(int folderId)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT MAX(uid) FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
qWarning() << "Failed to get max uid for folder:" << query.lastError().text();
return std::nullopt;
}
if (query.next()) {
if (query.isNull(0)) {
return std::nullopt;
}
return query.value(0).toLongLong();
}
return std::nullopt;
}
QVector<qint64> MailItemDao::getUidsForFolder(int folderId)
{
QVector<qint64> uids;
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT uid FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
qWarning() << "Failed to get uids for folder:" << query.lastError().text();
return uids;
}
while (query.next()) {
qint64 uid = query.value(0).toLongLong();
uids.append(uid);
}
return uids;
}
QVector<qint64> MailItemDao::uidsForFolder(int folderId)
{
return getUidsForFolder(folderId);
}
std::optional<MailItem> MailItemDao::findByUid(int folderId, qint64 uid)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id FROM MailCopy WHERE folderId = :folderId AND uid = :uid LIMIT 1");
query.bindValue(":folderId", folderId);
query.bindValue(":uid", uid);
if (!query.exec() || !query.next()) return std::nullopt;
return findById(query.value(0).toLongLong());
}
bool MailItemDao::removeByUid(int folderId, qint64 uid)
{
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM MailCopy WHERE folderId = :folderId AND uid = :uid");
query.bindValue(":folderId", folderId);
query.bindValue(":uid", uid);
if (!query.exec()) {
qWarning() << "Failed to delete mail item by UID:" << query.lastError().text();
return false;
}
return true;
}
bool MailItemDao::replaceAttachments(qint64 mailItemId,
const QVector<StoredAttachmentRecord>& attachments)
{
QSqlDatabase db = DatabaseManager::instance().database();
if (!db.transaction()) return false;
QSqlQuery remove(db);
remove.prepare("DELETE FROM Attachment WHERE mailCopyId = :mailCopyId");
remove.bindValue(":mailCopyId", mailItemId);
if (!remove.exec()) { db.rollback(); return false; }
QSqlQuery insert(db);
insert.prepare("INSERT INTO Attachment (mailCopyId, filename, mimeType, size, contentId, storedPath) "
"VALUES (:mailCopyId, :filename, :mimeType, :size, :contentId, :storedPath)");
for (const StoredAttachmentRecord &attachment : attachments) {
insert.bindValue(":mailCopyId", mailItemId);
insert.bindValue(":filename", attachment.fileName);
insert.bindValue(":mimeType", attachment.mimeType);
insert.bindValue(":size", attachment.size);
insert.bindValue(":contentId", attachment.contentId);
insert.bindValue(":storedPath", attachment.storedPath);
if (!insert.exec()) { db.rollback(); return false; }
}
return db.commit();
}
QVector<StoredAttachmentRecord> MailItemDao::attachmentsForMail(qint64 mailItemId)
{
QVector<StoredAttachmentRecord> result;
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT filename, mimeType, size, contentId, storedPath "
"FROM Attachment WHERE mailCopyId = :mailCopyId ORDER BY id");
query.bindValue(":mailCopyId", mailItemId);
if (!query.exec()) return result;
while (query.next()) {
StoredAttachmentRecord attachment;
attachment.fileName = query.value(0).toString();
attachment.mimeType = query.value(1).toString();
attachment.size = query.value(2).toLongLong();
attachment.contentId = query.value(3).toString();
attachment.storedPath = query.value(4).toString();
result.append(attachment);
}
return result;
}
+254
View File
@@ -0,0 +1,254 @@
#include <QSqlQuery>
#include <QSqlError>
#include "mailitemdao.h"
#include <optional>
bool MailItemDao::insert(const MailItem& item)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"INSERT INTO MailCopy (folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid) "
"VALUES (:folderId, :messageId, :subject, :sender, :recipient, :date, :read, :flagged, :hasAttachment, :size, :fileId, :uid)"
);
query.bindValue(":folderId", item.folderId());
query.bindValue(":messageId", item.messageId());
query.bindValue(":subject", item.subject());
query.bindValue(":sender", item.sender());
query.bindValue(":recipient", item.recipient());
query.bindValue(":date", item.date());
query.bindValue(":read", item.isRead() ? 1 : 0);
query.bindValue(":flagged", item.isFlagged() ? 1 : 0);
query.bindValue(":hasAttachment", !item.attachments().isEmpty() ? 1 : 0);
query.bindValue(":size", item.size());
query.bindValue(":fileId", item.fileId());
query.bindValue(":uid", item.uid());
if (!query.exec()) {
qWarning() << "Failed to insert mail item:" << query.lastError().text();
return false;
}
return true;
}
bool MailItemDao::update(const MailItem& item)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"UPDATE MailCopy SET "
"folderId = :folderId, "
"messageId = :messageId, "
"subject = :subject, "
"sender = :sender, "
"recipient = :recipient, "
"date = :date, "
"read = :read, "
"flagged = :flagged, "
"hasAttachment = :hasAttachment, "
"size = :size, "
"fileId = :fileId, "
"uid = :uid "
"WHERE id = :id"
);
query.bindValue(":id", item.id());
query.bindValue(":folderId", item.folderId());
query.bindValue(":messageId", item.messageId());
query.bindValue(":subject", item.subject());
query.bindValue(":sender", item.sender());
query.bindValue(":recipient", item.recipient());
query.bindValue(":date", item.date());
query.bindValue(":read", item.isRead() ? 1 : 0);
query.bindValue(":flagged", item.isFlagged() ? 1 : 0);
query.bindValue(":hasAttachment", !item.attachments().isEmpty() ? 1 : 0);
query.bindValue(":size", item.size());
query.bindValue(":fileId", item.fileId());
query.bindValue(":uid", item.uid());
if (!query.exec()) {
qWarning() << "Failed to update mail item:" << query.lastError().text();
return false;
}
return true;
}
bool MailItemDao::remove(qint64 id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM MailCopy WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to delete mail item:" << query.lastError().text();
return false;
}
return true;
}
std::optional<MailItem> MailItemDao::findById(qint64 id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid FROM MailCopy WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to find mail item by id:" << query.lastError().text();
return std::nullopt;
}
if (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
return item;
}
return std::nullopt;
}
QVector<MailItem> MailItemDao::findAll()
{
QVector<MailItem> items;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (!query.exec("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid FROM MailCopy")) {
qWarning() << "Failed to fetch all mail items:" << query.lastError().text();
return items;
}
while (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
items.append(item);
}
return items;
}
QVector<MailItem> MailItemDao::findByFolderId(int folderId)
{
QVector<MailItem> items;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
qWarning() << "Failed to fetch mail items by folder id:" << query.lastError().text();
return items;
}
while (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
items.append(item);
}
return items;
}
QVector<MailItem> MailItemDao::findByFolderIdSinceUid(int folderId, qint64 sinceUid)
{
QVector<MailItem> items;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid FROM MailCopy WHERE folderId = :folderId AND uid > :sinceUid");
query.bindValue(":folderId", folderId);
query.bindValue(":sinceUid", sinceUid);
if (!query.exec()) {
qWarning() << "Failed to fetch mail items by folder id since uid:" << query.lastError().text();
return items;
}
while (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
items.append(item);
}
return items;
}
std::optional<qint64> MailItemDao::maxUidForFolder(int folderId)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT MAX(uid) FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
qWarning() << "Failed to get max uid for folder:" << query.lastError().text();
return std::nullopt;
}
if (query.next()) {
if (query.isNull(0)) {
return std::nullopt;
}
return query.value(0).toLongLong();
}
return std::nullopt;
}
QVector<qint64> MailItemDao::uidsForFolder(int folderId)
{
QVector<qint64> uids;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT uid FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
qWarning() << "Failed to get uids for folder:" << query.lastError().text();
return uids;
}
while (query.next()) {
qint64 uid = query.value(0).toLongLong();
if (uid > 0) {
uids.append(uid);
}
}
return uids;
}
+285
View File
@@ -0,0 +1,285 @@
#include <QSqlQuery>
#include <QSqlError>
#include "mailitemdao.h"
#include <QDebug>
#include <optional>
bool MailItemDao::insert(MailItem& item)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"INSERT INTO MailCopy (folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr) "
"VALUES (:folderId, :messageId, :subject, :sender, :recipient, :date, :read, :flagged, :hasAttachment, :size, :fileId, :uid, :bodyHtml, :toAddr, :ccAddr, :bccAddr)"
);
query.bindValue(":folderId", item.folderId());
query.bindValue(":messageId", item.messageId());
query.bindValue(":subject", item.subject());
query.bindValue(":sender", item.sender());
query.bindValue(":recipient", item.recipient());
query.bindValue(":date", item.date());
query.bindValue(":read", item.isRead() ? 1 : 0);
query.bindValue(":flagged", item.isFlagged() ? 1 : 0);
query.bindValue(":hasAttachment", !item.attachments().isEmpty() ? 1 : 0);
query.bindValue(":size", item.size());
query.bindValue(":fileId", item.fileId());
query.bindValue(":uid", item.uid());
query.bindValue(":bodyHtml", item.bodyHtml());
query.bindValue(":toAddr", item.to());
query.bindValue(":ccAddr", item.cc());
query.bindValue(":bccAddr", item.bcc());
if (!query.exec()) {
qWarning() << "Failed to insert mail item:" << query.lastError().text();
return false;
}
qInfo() << "Last insert id:" << query.lastInsertId();
item.setId(query.lastInsertId().toLongLong());
return true;
}
bool MailItemDao::update(const MailItem& item)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"UPDATE MailCopy SET "
"folderId = :folderId, "
"messageId = :messageId, "
"subject = :subject, "
"sender = :sender, "
"recipient = :recipient, "
"date = :date, "
"read = :read, "
"flagged = :flagged, "
"hasAttachment = :hasAttachment, "
"size = :size, "
"fileId = :fileId, "
"uid = :uid, "
"bodyHtml = :bodyHtml "
"WHERE id = :id"
);
query.bindValue(":id", item.id());
query.bindValue(":folderId", item.folderId());
query.bindValue(":messageId", item.messageId());
query.bindValue(":subject", item.subject());
query.bindValue(":sender", item.sender());
query.bindValue(":recipient", item.recipient());
query.bindValue(":date", item.date());
query.bindValue(":read", item.isRead() ? 1 : 0);
query.bindValue(":flagged", item.isFlagged() ? 1 : 0);
query.bindValue(":hasAttachment", !item.attachments().isEmpty() ? 1 : 0);
query.bindValue(":size", item.size());
query.bindValue(":fileId", item.fileId());
query.bindValue(":uid", item.uid());
query.bindValue(":bodyHtml", item.bodyHtml());
query.bindValue(":toAddr", item.to());
query.bindValue(":ccAddr", item.cc());
query.bindValue(":bccAddr", item.bcc());
query.bindValue(":toAddr", item.to());
query.bindValue(":ccAddr", item.cc());
query.bindValue(":bccAddr", item.bcc());
if (!query.exec()) {
qWarning() << "Failed to update mail item:" << query.lastError().text();
return false;
}
return true;
}
bool MailItemDao::remove(qint64 id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM MailCopy WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to delete mail item:" << query.lastError().text();
return false;
}
return true;
}
std::optional<MailItem> MailItemDao::findById(qint64 id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to find mail item by id:" << query.lastError().text();
return std::nullopt;
}
if (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
item.setTo(query.value(14).toString());
item.setCc(query.value(15).toString());
item.setBcc(query.value(16).toString());
qDebug() << "Debug findById: toAddr=" << query.value(14).toString() << " ccAddr=" << query.value(15).toString() << " bccAddr=" << query.value(16).toString();
return item;
}
return std::nullopt;
}
QVector<MailItem> MailItemDao::findAll()
{
QVector<MailItem> items;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (!query.exec("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy")) {
qWarning() << "Failed to fetch all mail items:" << query.lastError().text();
return items;
}
while (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
item.setTo(query.value(14).toString());
item.setCc(query.value(15).toString());
item.setBcc(query.value(16).toString());
items.append(item);
}
return items;
}
QVector<MailItem> MailItemDao::findByFolderId(int folderId)
{
QVector<MailItem> items;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
qWarning() << "Failed to fetch mail items by folder id:" << query.lastError().text();
return items;
}
while (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
item.setTo(query.value(14).toString());
item.setCc(query.value(15).toString());
item.setBcc(query.value(16).toString());
items.append(item);
}
return items;
}
QVector<MailItem> MailItemDao::findByFolderIdSinceUid(int folderId, qint64 sinceUid)
{
QVector<MailItem> items;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE folderId = :folderId AND uid > :sinceUid");
query.bindValue(":folderId", folderId);
query.bindValue(":sinceUid", sinceUid);
if (!query.exec()) {
qWarning() << "Failed to fetch mail items by folder id since uid:" << query.lastError().text();
return items;
}
while (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
item.setTo(query.value(14).toString());
item.setCc(query.value(15).toString());
item.setBcc(query.value(16).toString());
items.append(item);
}
return items;
}
std::optional<qint64> MailItemDao::maxUidForFolder(int folderId)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT MAX(uid) FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
qWarning() << "Failed to get max uid for folder:" << query.lastError().text();
return std::nullopt;
}
if (query.next()) {
if (query.isNull(0)) {
return std::nullopt;
}
return query.value(0).toLongLong();
}
return std::nullopt;
}
QVector<qint64> MailItemDao::getUidsForFolder(int folderId)
{
QVector<qint64> uids;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT uid FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
qWarning() << "Failed to get uids for folder:" << query.lastError().text();
return uids;
}
while (query.next()) {
qint64 uid = query.value(0).toLongLong();
uids.append(uid);
}
return uids;
}
+260
View File
@@ -0,0 +1,260 @@
#include <QSqlQuery>
#include <QSqlError>
#include "mailitemdao.h"
#include <optional>
bool MailItemDao::insert(const MailItem& item)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"INSERT INTO MailCopy (folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml) "
"VALUES (:folderId, :messageId, :subject, :sender, :recipient, :date, :read, :flagged, :hasAttachment, :size, :fileId, :uid, :bodyHtml)"
);
query.bindValue(":folderId", item.folderId());
query.bindValue(":messageId", item.messageId());
query.bindValue(":subject", item.subject());
query.bindValue(":sender", item.sender());
query.bindValue(":recipient", item.recipient());
query.bindValue(":date", item.date());
query.bindValue(":read", item.isRead() ? 1 : 0);
query.bindValue(":flagged", item.isFlagged() ? 1 : 0);
query.bindValue(":hasAttachment", !item.attachments().isEmpty() ? 1 : 0);
query.bindValue(":size", item.size());
query.bindValue(":fileId", item.fileId());
query.bindValue(":uid", item.uid());
query.bindValue(":bodyHtml", item.bodyHtml());
if (!query.exec()) {
qWarning() << "Failed to insert mail item:" << query.lastError().text();
return false;
}
return true;
}
bool MailItemDao::update(const MailItem& item)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"UPDATE MailCopy SET "
"folderId = :folderId, "
"messageId = :messageId, "
"subject = :subject, "
"sender = :sender, "
"recipient = :recipient, "
"date = :date, "
"read = :read, "
"flagged = :flagged, "
"hasAttachment = :hasAttachment, "
"size = :size, "
"fileId = :fileId, "
"uid = :uid, "
"bodyHtml = :bodyHtml "
"WHERE id = :id"
);
query.bindValue(":id", item.id());
query.bindValue(":folderId", item.folderId());
query.bindValue(":messageId", item.messageId());
query.bindValue(":subject", item.subject());
query.bindValue(":sender", item.sender());
query.bindValue(":recipient", item.recipient());
query.bindValue(":date", item.date());
query.bindValue(":read", item.isRead() ? 1 : 0);
query.bindValue(":flagged", item.isFlagged() ? 1 : 0);
query.bindValue(":hasAttachment", !item.attachments().isEmpty() ? 1 : 0);
query.bindValue(":size", item.size());
query.bindValue(":fileId", item.fileId());
query.bindValue(":uid", item.uid());
query.bindValue(":bodyHtml", item.bodyHtml());
if (!query.exec()) {
qWarning() << "Failed to update mail item:" << query.lastError().text();
return false;
}
return true;
}
bool MailItemDao::remove(qint64 id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM MailCopy WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to delete mail item:" << query.lastError().text();
return false;
}
return true;
}
std::optional<MailItem> MailItemDao::findById(qint64 id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to find mail item by id:" << query.lastError().text();
return std::nullopt;
}
if (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
return item;
}
return std::nullopt;
}
QVector<MailItem> MailItemDao::findAll()
{
QVector<MailItem> items;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (!query.exec("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy")) {
qWarning() << "Failed to fetch all mail items:" << query.lastError().text();
return items;
}
while (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
items.append(item);
}
return items;
}
QVector<MailItem> MailItemDao::findByFolderId(int folderId)
{
QVector<MailItem> items;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
qWarning() << "Failed to fetch mail items by folder id:" << query.lastError().text();
return items;
}
while (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
items.append(item);
}
return items;
}
QVector<MailItem> MailItemDao::findByFolderIdSinceUid(int folderId, qint64 sinceUid)
{
QVector<MailItem> items;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy WHERE folderId = :folderId AND uid > :sinceUid");
query.bindValue(":folderId", folderId);
query.bindValue(":sinceUid", sinceUid);
if (!query.exec()) {
qWarning() << "Failed to fetch mail items by folder id since uid:" << query.lastError().text();
return items;
}
while (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
items.append(item);
}
return items;
}
std::optional<qint64> MailItemDao::maxUidForFolder(int folderId)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT MAX(uid) FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
qWarning() << "Failed to get max uid for folder:" << query.lastError().text();
return std::nullopt;
}
if (query.next()) {
if (query.isNull(0)) {
return std::nullopt;
}
return query.value(0).toLongLong();
}
return std::nullopt;
}
QVector<qint64> MailItemDao::getUidsForFolder(int folderId)
{
QVector<qint64> uids;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT uid FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
qWarning() << "Failed to get uids for folder:" << query.lastError().text();
return uids;
}
while (query.next()) {
qint64 uid = query.value(0).toLongLong();
uids.append(uid);
}
return uids;
}
+263
View File
@@ -0,0 +1,263 @@
#include <QSqlQuery>
#include <QSqlError>
#include "mailitemdao.h"
#include <optional>
bool MailItemDao::insert(const MailItem& item)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"INSERT INTO MailCopy (folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr) "
"VALUES (:folderId, :messageId, :subject, :sender, :recipient, :date, :read, :flagged, :hasAttachment, :size, :fileId, :uid, :bodyHtml, :toAddr, :ccAddr, :bccAddr)"
);
query.bindValue(":folderId", item.folderId());
query.bindValue(":messageId", item.messageId());
query.bindValue(":subject", item.subject());
query.bindValue(":sender", item.sender());
query.bindValue(":recipient", item.recipient());
query.bindValue(":date", item.date());
query.bindValue(":read", item.isRead() ? 1 : 0);
query.bindValue(":flagged", item.isFlagged() ? 1 : 0);
query.bindValue(":hasAttachment", !item.attachments().isEmpty() ? 1 : 0);
query.bindValue(":size", item.size());
query.bindValue(":fileId", item.fileId());
query.bindValue(":uid", item.uid());
query.bindValue(":bodyHtml", item.bodyHtml());
query.bindValue(":toAddr", item.to());
query.bindValue(":ccAddr", item.cc());
query.bindValue(":bccAddr", item.bcc());
if (!query.exec()) {
qWarning() << "Failed to insert mail item:" << query.lastError().text();
return false;
}
return true;
}
bool MailItemDao::update(const MailItem& item)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare(
"UPDATE MailCopy SET "
"folderId = :folderId, "
"messageId = :messageId, "
"subject = :subject, "
"sender = :sender, "
"recipient = :recipient, "
"date = :date, "
"read = :read, "
"flagged = :flagged, "
"hasAttachment = :hasAttachment, "
"size = :size, "
"fileId = :fileId, "
"uid = :uid, "
"bodyHtml = :bodyHtml "
"WHERE id = :id"
);
query.bindValue(":id", item.id());
query.bindValue(":folderId", item.folderId());
query.bindValue(":messageId", item.messageId());
query.bindValue(":subject", item.subject());
query.bindValue(":sender", item.sender());
query.bindValue(":recipient", item.recipient());
query.bindValue(":date", item.date());
query.bindValue(":read", item.isRead() ? 1 : 0);
query.bindValue(":flagged", item.isFlagged() ? 1 : 0);
query.bindValue(":hasAttachment", !item.attachments().isEmpty() ? 1 : 0);
query.bindValue(":size", item.size());
query.bindValue(":fileId", item.fileId());
query.bindValue(":uid", item.uid());
query.bindValue(":bodyHtml", item.bodyHtml());
if (!query.exec()) {
qWarning() << "Failed to update mail item:" << query.lastError().text();
return false;
}
return true;
}
bool MailItemDao::remove(qint64 id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("DELETE FROM MailCopy WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to delete mail item:" << query.lastError().text();
return false;
}
return true;
}
std::optional<MailItem> MailItemDao::findById(qint64 id)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy WHERE id = :id");
query.bindValue(":id", id);
if (!query.exec()) {
qWarning() << "Failed to find mail item by id:" << query.lastError().text();
return std::nullopt;
}
if (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
return item;
}
return std::nullopt;
}
QVector<MailItem> MailItemDao::findAll()
{
QVector<MailItem> items;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
if (!query.exec("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy")) {
qWarning() << "Failed to fetch all mail items:" << query.lastError().text();
return items;
}
while (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
items.append(item);
}
return items;
}
QVector<MailItem> MailItemDao::findByFolderId(int folderId)
{
QVector<MailItem> items;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
qWarning() << "Failed to fetch mail items by folder id:" << query.lastError().text();
return items;
}
while (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
items.append(item);
}
return items;
}
QVector<MailItem> MailItemDao::findByFolderIdSinceUid(int folderId, qint64 sinceUid)
{
QVector<MailItem> items;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy WHERE folderId = :folderId AND uid > :sinceUid");
query.bindValue(":folderId", folderId);
query.bindValue(":sinceUid", sinceUid);
if (!query.exec()) {
qWarning() << "Failed to fetch mail items by folder id since uid:" << query.lastError().text();
return items;
}
while (query.next()) {
MailItem item;
item.setId(query.value(0).toInt());
item.setFolderId(query.value(1).toInt());
item.setMessageId(query.value(2).toString());
item.setSubject(query.value(3).toString());
item.setSender(query.value(4).toString());
item.setRecipient(query.value(5).toString());
item.setDate(query.value(6).toDateTime());
item.setRead(query.value(7).toBool());
item.setFlagged(query.value(8).toBool());
item.setSize(query.value(10).toLongLong());
item.setFileId(query.value(11).toString());
item.setUid(query.value(12).toLongLong());
item.setBodyHtml(query.value(13).toString());
items.append(item);
}
return items;
}
std::optional<qint64> MailItemDao::maxUidForFolder(int folderId)
{
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT MAX(uid) FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
qWarning() << "Failed to get max uid for folder:" << query.lastError().text();
return std::nullopt;
}
if (query.next()) {
if (query.isNull(0)) {
return std::nullopt;
}
return query.value(0).toLongLong();
}
return std::nullopt;
}
QVector<qint64> MailItemDao::getUidsForFolder(int folderId)
{
QVector<qint64> uids;
QSqlDatabase& db = DatabaseManager::instance().database();
QSqlQuery query(db);
query.prepare("SELECT uid FROM MailCopy WHERE folderId = :folderId");
query.bindValue(":folderId", folderId);
if (!query.exec()) {
qWarning() << "Failed to get uids for folder:" << query.lastError().text();
return uids;
}
while (query.next()) {
qint64 uid = query.value(0).toLongLong();
uids.append(uid);
}
return uids;
}
+18 -1
View File
@@ -5,14 +5,31 @@
#include <QVector>
#include <optional>
struct StoredAttachmentRecord
{
QString fileName;
QString mimeType;
QString contentId;
qint64 size{0};
QString storedPath;
};
class MailItemDao
{
public:
static bool insert(const MailItem& item);
static bool insert(MailItem& item);
static bool upsert(MailItem& item);
static bool update(const MailItem& item);
static bool remove(qint64 id);
static std::optional<MailItem> findById(qint64 id);
static QVector<MailItem> findAll();
static QVector<MailItem> findByFolderId(int folderId);
static QVector<MailItem> findByFolderIdSinceUid(int folderId, qint64 sinceUid);
static std::optional<qint64> maxUidForFolder(int folderId);
static QVector<qint64> getUidsForFolder(int folderId);
static QVector<qint64> uidsForFolder(int folderId);
static std::optional<MailItem> findByUid(int folderId, qint64 uid);
static bool removeByUid(int folderId, qint64 uid);
static bool replaceAttachments(qint64 mailItemId, const QVector<StoredAttachmentRecord>& attachments);
static QVector<StoredAttachmentRecord> attachmentsForMail(qint64 mailItemId);
};
+59
View File
@@ -0,0 +1,59 @@
import sys
import re
filename = sys.argv[1]
with open(filename, 'r') as f:
content = f.read()
# Helper to replace SELECT clause and add mapping after setBodyHtml
def replace_select_and_add_mapping(content, func_name):
# Pattern to find the function (simplistic but works for our file)
# We'll replace the SELECT string inside the function.
# Since each function has a unique SELECT, we can replace directly.
if func_name == 'findById':
# Replace SELECT line
pattern = r'(query\.prepare\\(\s*"SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy WHERE id = :id"\s*\\);)'
replacement = '''query.prepare(
"SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE id = :id"
);'''
content = re.sub(pattern, replacement, content)
# Add mapping after setBodyHtml line
# Find line with item.setBodyHtml(query.value(13).toString());
pattern2 = r'(item\.setBodyHtml\(query\.value\(13\)\.toString\(\);\s*\n\s*)'
replacement2 = r'\1 item.setTo(query.value(14).toString());\n item.setCc(query.value(15).toString());\n item.setBcc(query.value(16).toString());\n'
content = re.sub(pattern2, replacement2, content)
elif func_name == 'findAll':
# SELECT line inside exec
pattern = r'(query\.exec\(\s*"SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy"\s*\)\s*)'
# We need to replace the whole argument; easier: replace the string inside.
# We'll replace the quoted string.
pattern = r'"SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy"'
replacement = '"SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy"'
content = re.sub(pattern, replacement, content)
# Add mapping after setBodyHtml line in the while loop
pattern2 = r'(item\.setBodyHtml\(query\.value\(13\)\.toString\(\);\s*\n\s*)'
replacement2 = r'\1 item.setTo(query.value(14).toString());\n item.setCc(query.value(15).toString());\n item.setBcc(query.value(16).toString());\n'
content = re.sub(pattern2, replacement2, content)
elif func_name == 'findByFolderId':
pattern = r'"SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy WHERE folderId = :folderId"'
replacement = '"SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE folderId = :folderId"'
content = re.sub(pattern, replacement, content)
pattern2 = r'(item\.setBodyHtml\(query\.value\(13\)\.toString\(\);\s*\n\s*)'
replacement2 = r'\1 item.setTo(query.value(14).toString());\n item.setCc(query.value(15).toString());\n item.setBcc(query.value(16).toString());\n'
content = re.sub(pattern2, replacement2, content)
elif func_name == 'findByFolderIdSinceUid':
pattern = r'"SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml FROM MailCopy WHERE folderId = :folderId AND uid > :sinceUid"'
replacement = '"SELECT id, folderId, messageId, subject, sender, recipient, date, read, flagged, hasAttachment, size, fileId, uid, bodyHtml, toAddr, ccAddr, bccAddr FROM MailCopy WHERE folderId = :folderId AND uid > :sinceUid"'
content = re.sub(pattern, replacement, content)
pattern2 = r'(item\.setBodyHtml\(query\.value\(13\)\.toString\(\);\s*\n\s*)'
replacement2 = r'\1 item.setTo(query.value(14).toString());\n item.setCc(query.value(15).toString());\n item.setBcc(query.value(16).toString());\n'
content = re.sub(pattern2, replacement2, content)
return content
# Apply to each function
for func in ['findById', 'findAll', 'findByFolderId', 'findByFolderIdSinceUid']:
content = replace_select_and_add_mapping(content, func)
with open(filename, 'w') as f:
f.write(content)
print('Modified', filename)
+50 -2
View File
@@ -37,6 +37,11 @@ bool DatabaseManager::initialize(const QString& databasePath)
dbPath = dir.filePath("wino-mail.sqlite");
}
m_databasePath = dbPath;
m_ownerThread = QThread::currentThreadId();
qDebug() << "[DatabaseManager] Opening database at:" << dbPath;
m_db = QSqlDatabase::addDatabase("QSQLITE", "wino_mail_connection");
m_db.setDatabaseName(dbPath);
@@ -45,18 +50,22 @@ bool DatabaseManager::initialize(const QString& databasePath)
return false;
}
qDebug() << "[DatabaseManager] Database opened successfully";
QSqlQuery query(m_db);
// Enable foreign keys
qDebug() << "[DatabaseManager] Enabling foreign keys";
query.exec("PRAGMA foreign_keys = ON;");
// Create tables if they don't exist
qDebug() << "[DatabaseManager] Creating Account table";
// Account table
if (!query.exec(
"CREATE TABLE IF NOT EXISTS Account ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"email TEXT NOT NULL, "
"displayName TEXT, ""signature TEXT, "
"displayName TEXT, signature TEXT, "
"type INTEGER NOT NULL, " // 0=Outlook,1=Gmail,2=IMAP
"accessToken TEXT, "
"refreshToken TEXT, "
@@ -74,6 +83,7 @@ bool DatabaseManager::initialize(const QString& databasePath)
qWarning() << "Failed to create Account table:" << query.lastError().text();
return false;
}
qDebug() << "[DatabaseManager] Account table created successfully";
// Folder table
if (!query.exec(
@@ -110,6 +120,10 @@ bool DatabaseManager::initialize(const QString& databasePath)
"size INTEGER, "
"uid INTEGER, "
"fileId TEXT, " // references the .eml file in storage
"bodyHtml TEXT, "
"toAddr TEXT, "
"ccAddr TEXT, "
"bccAddr TEXT, "
"FOREIGN KEY(folderId) REFERENCES Folder(id) ON DELETE CASCADE"
");")) {
qWarning() << "Failed to create MailCopy table:" << query.lastError().text();
@@ -125,18 +139,52 @@ bool DatabaseManager::initialize(const QString& databasePath)
"mimeType TEXT, "
"size INTEGER, "
"contentId TEXT, "
"storedPath TEXT, "
"FOREIGN KEY(mailCopyId) REFERENCES MailCopy(id) ON DELETE CASCADE"
");")) {
qWarning() << "Failed to create Attachment table:" << query.lastError().text();
return false;
}
// Existing installations were created before storedPath was introduced.
// SQLite reports an error when the column already exists, which is safe to
// ignore here because the CREATE TABLE above covers new databases.
query.exec("ALTER TABLE Account ADD COLUMN signature TEXT");
query.exec("ALTER TABLE MailCopy ADD COLUMN toAddr TEXT");
query.exec("ALTER TABLE MailCopy ADD COLUMN ccAddr TEXT");
query.exec("ALTER TABLE MailCopy ADD COLUMN bccAddr TEXT");
query.exec("ALTER TABLE MailCopy ADD COLUMN bodyHtml TEXT");
query.exec("ALTER TABLE MailCopy ADD COLUMN uid INTEGER");
query.exec("ALTER TABLE MailCopy ADD COLUMN fileId TEXT");
query.exec("ALTER TABLE Attachment ADD COLUMN storedPath TEXT");
query.exec("CREATE INDEX IF NOT EXISTS idx_mailcopy_folder_uid ON MailCopy(folderId, uid)");
query.exec("CREATE INDEX IF NOT EXISTS idx_attachment_mailcopy ON Attachment(mailCopyId)");
m_initialized = true;
qDebug() << "Database initialized at:" << dbPath;
return true;
}
QSqlDatabase& DatabaseManager::database()
QSqlDatabase DatabaseManager::database()
{
if (QThread::currentThreadId() == m_ownerThread)
return m_db;
const QString connectionName = QStringLiteral("wino_mail_connection_%1")
.arg(reinterpret_cast<quintptr>(QThread::currentThreadId()), 0, 16);
if (QSqlDatabase::contains(connectionName))
return QSqlDatabase::database(connectionName);
QMutexLocker locker(&m_mutex);
if (QSqlDatabase::contains(connectionName))
return QSqlDatabase::database(connectionName);
QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), connectionName);
db.setDatabaseName(m_databasePath);
if (!db.open()) {
qWarning() << "Failed to open thread-local database:" << db.lastError().text();
return db;
}
QSqlQuery pragma(db);
pragma.exec(QStringLiteral("PRAGMA foreign_keys = ON"));
return db;
}
+142
View File
@@ -0,0 +1,142 @@
#include "databasemanager.h"
#include <QSqlQuery>
#include <QSqlError>
#include <QDebug>
#include <QStandardPaths>
#include <QDir>
DatabaseManager& DatabaseManager::instance()
{
static DatabaseManager instance;
return instance;
}
DatabaseManager::DatabaseManager() = default;
DatabaseManager::~DatabaseManager()
{
if (m_db.isOpen()) {
m_db.close();
}
}
bool DatabaseManager::initialize(const QString& databasePath)
{
QMutexLocker locker(&m_mutex);
if (m_initialized) {
return true;
}
QString dbPath = databasePath;
if (dbPath.isEmpty()) {
// Use application data location
QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
if (!dir.exists()) {
dir.mkpath(".");
}
dbPath = dir.filePath("wino-mail.sqlite");
}
m_db = QSqlDatabase::addDatabase("QSQLITE", "wino_mail_connection");
m_db.setDatabaseName(dbPath);
if (!m_db.open()) {
qWarning() << "Failed to open database:" << m_db.lastError().text();
return false;
}
QSqlQuery query(m_db);
// Enable foreign keys
query.exec("PRAGMA foreign_keys = ON;");
// Create tables if they don't exist
// Account table
if (!query.exec(
"CREATE TABLE IF NOT EXISTS Account ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"email TEXT NOT NULL, "
"displayName TEXT, signature TEXT, "
"type INTEGER NOT NULL, " // 0=Outlook,1=Gmail,2=IMAP
"accessToken TEXT, "
"refreshToken TEXT, "
"tokenExpires DATETIME, "
"incomingHost TEXT, "
"incomingPort INTEGER, "
"incomingSsl INTEGER, "
"outgoingHost TEXT, "
"outgoingPort INTEGER, "
"outgoingSsl INTEGER, "
"connUsername TEXT, "
"connPassword TEXT, "
"authMethod TEXT"
");")) {
qWarning() << "Failed to create Account table:" << query.lastError().text();
return false;
}
// Folder table
if (!query.exec(
"CREATE TABLE IF NOT EXISTS Folder ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"accountId INTEGER NOT NULL, "
"name TEXT NOT NULL, "
"parentFolderId TEXT, "
"isInbox BOOLEAN DEFAULT 0, "
"isSent BOOLEAN DEFAULT 0, "
"isDrafts BOOLEAN DEFAULT 0, "
"isTrash BOOLEAN DEFAULT 0, "
"unreadCount INTEGER DEFAULT 0, "
"lastSynced DATETIME, "
"FOREIGN KEY(accountId) REFERENCES Account(id) ON DELETE CASCADE"
");")) {
qWarning() << "Failed to create Folder table:" << query.lastError().text();
return false;
}
// MailCopy table (simplified)
if (!query.exec(
"CREATE TABLE IF NOT EXISTS MailCopy ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"folderId INTEGER NOT NULL, "
"messageId TEXT UNIQUE, "
"subject TEXT, "
"sender TEXT, "
"recipient TEXT, "
"date DATETIME, "
"read BOOLEAN DEFAULT 0, "
"flagged BOOLEAN DEFAULT 0, "
"hasAttachment BOOLEAN DEFAULT 0, "
"size INTEGER, "
"uid INTEGER, "
"fileId TEXT, " // references the .eml file in storage
"FOREIGN KEY(folderId) REFERENCES Folder(id) ON DELETE CASCADE"
");")) {
qWarning() << "Failed to create MailCopy table:" << query.lastError().text();
return false;
}
// Attachments table (optional)
if (!query.exec(
"CREATE TABLE IF NOT EXISTS Attachment ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"mailCopyId INTEGER NOT NULL, "
"filename TEXT, "
"mimeType TEXT, "
"size INTEGER, "
"contentId TEXT, "
"FOREIGN KEY(mailCopyId) REFERENCES MailCopy(id) ON DELETE CASCADE"
");")) {
qWarning() << "Failed to create Attachment table:" << query.lastError().text();
return false;
}
m_initialized = true;
qDebug() << "Database initialized at:" << dbPath;
return true;
}
QSqlDatabase& DatabaseManager::database()
{
return m_db;
}
+7 -2
View File
@@ -3,6 +3,7 @@
#include <QSqlDatabase>
#include <QString>
#include <QMutex>
#include <QThread>
class DatabaseManager
{
@@ -10,8 +11,10 @@ public:
static DatabaseManager& instance();
~DatabaseManager();
bool initialize(const QString& databasePath = QStringLiteral("wino-mail.sqlite"));
QSqlDatabase& database();
bool initialize(const QString& databasePath = QString());
// Returns a connection owned by the calling thread. SQLite/Qt database
// connections must never be shared across threads.
QSqlDatabase database();
private:
DatabaseManager();
@@ -19,5 +22,7 @@ private:
QSqlDatabase m_db;
QMutex m_mutex;
QString m_databasePath;
Qt::HANDLE m_ownerThread{nullptr};
bool m_initialized{false};
};
+2 -1
View File
@@ -123,7 +123,8 @@ void DbChangeProcessor::processBatch()
if (!m_mailItemAddedQueue.isEmpty()) {
qDebug() << "DbChangeProcessor: Processing" << m_mailItemAddedQueue.size() << "MailItemAdded events";
for (const auto& event : m_mailItemAddedQueue) {
MailItemDao::insert(event.item);
MailItem item = event.item;
MailItemDao::insert(item);
}
m_mailItemAddedQueue.clear();
}
+219
View File
@@ -0,0 +1,219 @@
#include "dbchangeprocessor.h"
#include <QDebug>
#include <QDateTime>
#include "../db/dao/mailitemdao.h"
#include "../db/dao/folderdao.h"
#include "../db/dao/accountdao.h"
DbChangeProcessor::DbChangeProcessor(QObject* parent)
: QObject(parent),
m_batchTimer(new QTimer(this))
{
// Configurar el timer para procesar batch cada 5 segundos
m_batchTimer->setInterval(5000); // 5 segundos
connect(m_batchTimer, &QTimer::timeout, this, &DbChangeProcessor::processBatch);
m_batchTimer->start();
// Suscribirse a todos los eventos relevantes
SUBSCRIBE(WinoMail::Events::MailItemAddedEvent,
[this](const WinoMail::Events::MailItemAddedEvent& event) {
handleMailItemAdded(event);
});
SUBSCRIBE(WinoMail::Events::MailItemRemovedEvent,
[this](const WinoMail::Events::MailItemRemovedEvent& event) {
handleMailItemRemoved(event);
});
SUBSCRIBE(WinoMail::Events::MailItemUpdatedEvent,
[this](const WinoMail::Events::MailItemUpdatedEvent& event) {
handleMailItemUpdated(event);
});
SUBSCRIBE(WinoMail::Events::FolderAddedEvent,
[this](const WinoMail::Events::FolderAddedEvent& event) {
handleFolderAdded(event);
});
SUBSCRIBE(WinoMail::Events::FolderRemovedEvent,
[this](const WinoMail::Events::FolderRemovedEvent& event) {
handleFolderRemoved(event);
});
SUBSCRIBE(WinoMail::Events::FolderUpdatedEvent,
[this](const WinoMail::Events::FolderUpdatedEvent& event) {
handleFolderUpdated(event);
});
SUBSCRIBE(WinoMail::Events::AccountAddedEvent,
[this](const WinoMail::Events::AccountAddedEvent& event) {
handleAccountAdded(event);
});
SUBSCRIBE(WinoMail::Events::AccountRemovedEvent,
[this](const WinoMail::Events::AccountRemovedEvent& event) {
handleAccountRemoved(event);
});
SUBSCRIBE(WinoMail::Events::AccountUpdatedEvent,
[this](const WinoMail::Events::AccountUpdatedEvent& event) {
handleAccountUpdated(event);
});
}
void DbChangeProcessor::handleMailItemAdded(const WinoMail::Events::MailItemAddedEvent& event)
{
qDebug() << "DbChangeProcessor: Queuing MailItemAddedEvent for ID:" << event.item.id();
m_mailItemAddedQueue.append(event);
}
void DbChangeProcessor::handleMailItemRemoved(const WinoMail::Events::MailItemRemovedEvent& event)
{
qDebug() << "DbChangeProcessor: Queuing MailItemRemovedEvent for UID:" << event.itemUid;
m_mailItemRemovedQueue.append(event);
}
void DbChangeProcessor::handleMailItemUpdated(const WinoMail::Events::MailItemUpdatedEvent& event)
{
qDebug() << "DbChangeProcessor: Queuing MailItemUpdatedEvent for ID:" << event.item.id();
m_mailItemUpdatedQueue.append(event);
}
void DbChangeProcessor::handleFolderAdded(const WinoMail::Events::FolderAddedEvent& event)
{
qDebug() << "DbChangeProcessor: Queuing FolderAddedEvent for folder:" << event.folder.name();
m_folderAddedQueue.append(event);
}
void DbChangeProcessor::handleFolderRemoved(const WinoMail::Events::FolderRemovedEvent& event)
{
qDebug() << "DbChangeProcessor: Queuing FolderRemovedEvent for folder ID:" << event.folderId;
m_folderRemovedQueue.append(event);
}
void DbChangeProcessor::handleFolderUpdated(const WinoMail::Events::FolderUpdatedEvent& event)
{
qDebug() << "DbChangeProcessor: Queuing FolderUpdatedEvent for folder ID:" << event.folder.id();
m_folderUpdatedQueue.append(event);
}
void DbChangeProcessor::handleAccountAdded(const WinoMail::Events::AccountAddedEvent& event)
{
qDebug() << "DbChangeProcessor: Queuing AccountAddedEvent for account:" << event.account.email();
m_accountAddedQueue.append(event);
}
void DbChangeProcessor::handleAccountRemoved(const WinoMail::Events::AccountRemovedEvent& event)
{
qDebug() << "DbChangeProcessor: Queuing AccountRemovedEvent for account ID:" << event.accountId;
m_accountRemovedQueue.append(event);
}
void DbChangeProcessor::handleAccountUpdated(const WinoMail::Events::AccountUpdatedEvent& event)
{
qDebug() << "DbChangeProcessor: Queuing AccountUpdatedEvent for account ID:" << event.account.id();
m_accountUpdatedQueue.append(event);
}
void DbChangeProcessor::processBatch()
{
qDebug() << "DbChangeProcessor: Starting batch processing at" << QDateTime::currentDateTime().toString();
// Procesar eventos de MailItem añadidos
if (!m_mailItemAddedQueue.isEmpty()) {
qDebug() << "DbChangeProcessor: Processing" << m_mailItemAddedQueue.size() << "MailItemAdded events";
for (auto for (autoconst auto& event event : m_mailItemAddedQueue) { event : m_mailItemAddedQueue) {
MailItemDao::insert(event.item);
}
m_mailItemAddedQueue.clear();
}
// Procesar eventos de MailItem eliminados
if (!m_mailItemRemovedQueue.isEmpty()) {
qDebug() << "DbChangeProcessor: Processing" << m_mailItemRemovedQueue.size() << "MailItemRemoved events";
for (const auto& event : m_mailItemRemovedQueue) {
// We don't have removeByUid in MailItemDao, so we skip and log a warning.
// In a real implementation, we would need to find the item by uid and folderId to get its id.
qWarning() << "DbChangeProcessor: MailItemRemoved event processed but removeByUid not implemented in MailItemDao. Skipping removal for UID:" << event.itemUid;
}
m_mailItemRemovedQueue.clear();
}
// Procesar eventos de MailItem actualizados
if (!m_mailItemUpdatedQueue.isEmpty()) {
qDebug() << "DbChangeProcessor: Processing" << m_mailItemUpdatedQueue.size() << "MailItemUpdated events";
for (const auto& event : m_mailItemUpdatedQueue) {
// We ignore changedFields and update the whole item for simplicity.
MailItemDao::update(event.item);
}
m_mailItemUpdatedQueue.clear();
}
// Procesar eventos de carpetas añadidas
if (!m_folderAddedQueue.isEmpty()) {
qDebug() << "DbChangeProcessor: Processing" << m_folderAddedQueue.size() << "FolderAdded events";
for (const auto& event : m_folderAddedQueue) {
FolderDao::insert(event.folder);
}
m_folderAddedQueue.clear();
}
// Procesar eventos de carpetas eliminadas
if (!m_folderRemovedQueue.isEmpty()) {
qDebug() << "DbChangeProcessor: Processing" << m_folderRemovedQueue.size() << "FolderRemoved events";
for (const auto& event : m_folderRemovedQueue) {
// We don't have a way to get the folder id from the event without a query, so we skip and log a warning.
qWarning() << "DbChangeProcessor: FolderRemoved event processed but we lack the folder id to remove. Skipping removal for folder ID:" << event.folderId;
}
m_folderRemovedQueue.clear();
}
// Procesar eventos de carpetas actualizadas
if (!m_folderUpdatedQueue.isEmpty()) {
qDebug() << "DbChangeProcessor: Processing" << m_folderUpdatedQueue.size() << "FolderUpdated events";
for (const auto& event : m_folderUpdatedQueue) {
// We ignore changedFields and update the whole folder for simplicity.
FolderDao::update(event.folder);
}
m_folderUpdatedQueue.clear();
}
// Procesar eventos de cuentas añadidas
if (!m_accountAddedQueue.isEmpty()) {
qDebug() << "DbChangeProcessor: Processing" << m_accountAddedQueue.size() << "AccountAdded events";
for (const auto& event : m_accountAddedQueue) {
AccountDao::insert(event.account);
}
m_accountAddedQueue.clear();
}
// Procesar eventos de cuentas eliminadas
if (!m_accountRemovedQueue.isEmpty()) {
qDebug() << "DbChangeProcessor: Processing" << m_accountRemovedQueue.size() << "AccountRemoved events";
for (const auto& event : m_accountRemovedQueue) {
AccountDao::remove(event.accountId);
}
m_accountRemovedQueue.clear();
}
// Procesar eventos de cuentas actualizadas
if (!m_accountUpdatedQueue.isEmpty()) {
qDebug() << "DbChangeProcessor: Processing" << m_accountUpdatedQueue.size() << "AccountUpdated events";
for (const auto& event : m_accountUpdatedQueue) {
// We ignore changedFields and update the whole account for simplicity.
AccountDao::update(event.account);
}
m_accountUpdatedQueue.clear();
}
qDebug() << "DbChangeProcessor: Batch processing completed at" << QDateTime::currentDateTime().toString();
}
DbChangeProcessor::~DbChangeProcessor()
{
if (m_batchTimer->isActive()) {
m_batchTimer->stop();
}
// Procesar cualquier evento restante antes de destruir
processBatch();
}
+17 -2
View File
@@ -19,19 +19,34 @@ int main(int argc, char *argv[])
app.setApplicationVersion("1.0.0");
app.setOrganizationName("Wino");
qDebug() << "[main] Starting Wino Mail";
// Initialize subsystems
DatabaseManager::instance().initialize();
qDebug() << "[main] Initializing DatabaseManager";
if (!DatabaseManager::instance().initialize()) {
qCritical() << "[main] Database initialization failed";
return 1;
}
qDebug() << "[main] DatabaseManager initialized";
EventBus &bus = EventBus::instance();
qDebug() << "[main] EventBus created";
NotificationManager notificationManager(&bus);
qDebug() << "[main] NotificationManager created";
// Start background sync
qDebug() << "[main] Creating SyncScheduler";
SyncScheduler syncScheduler(&bus);
syncScheduler.start();
qDebug() << "[main] Starting SyncScheduler";
// syncScheduler.start(); // Temporarily disabled for debugging
qDebug() << "[main] SyncScheduler created (not started)";
// Create main window
qDebug() << "[main] Creating MainMainWindow";
MainMainWindow window;
qDebug() << "[main] MainMainWindow created, showing window";
window.show();
qDebug() << "[main] Window shown, entering event loop";
qDebug() << "Wino Mail started successfully";
+11 -6
View File
@@ -3,6 +3,8 @@
#include <QSslSocket>
#include "db/dao/accountdao.h"
#include "db/dao/folderdao.h"
#include "db/dao/mailitemdao.h"
#include "services/mimestorage.h"
#include "core/authenticator.h"
#include "core/gmailauthenticator.h"
#include "core/outlookauthenticator.h"
@@ -79,7 +81,7 @@ void AccountService::addAccount(const Account &account)
else if (folderName == "Sent") folder.setSent(true);
else if (folderName == "Drafts") folder.setDrafts(true);
else if (folderName == "Trash") folder.setTrash(true);
if (!FolderDao::insert(folder)) {
if (FolderDao::insert(folder) == -1) {
qWarning() << "[AccountService] Failed to insert folder" << folder.name()
<< "for account ID" << accToAdd.id();
}
@@ -100,6 +102,12 @@ void AccountService::removeAccount(int accountId)
{
Account *account = AccountDao::findById(accountId);
if (account) {
MimeStorageService storage;
for (const Folder &folder : FolderDao::findByAccountId(accountId)) {
for (const MailItem &item : MailItemDao::findByFolderId(folder.id())) {
if (!item.fileId().isEmpty()) storage.deleteEmlFile(item.fileId());
}
}
SynchronizerProvider::instance().unregisterSynchronizer(QString::number(accountId));
AccountDao::remove(accountId);
publishAccountEvent(*account, false);
@@ -124,10 +132,7 @@ void AccountService::startAuthentication(const QString &email, const QString &pr
Authenticator *auth = createAuthenticator(providerType);
if (auth) {
connect(auth, &Authenticator::authenticationCompleted, this, [this](const Account &account) {
addAccount(account.email(),
account.type() == AccountType::Gmail ? "gmail"
: account.type() == AccountType::Outlook ? "outlook" : "imap",
account.accessToken(), account.refreshToken());
addAccount(account);
});
connect(auth, &Authenticator::authenticationFailed, this, [](const QString &error) {
qWarning() << "[AccountService] Authentication failed:" << error;
@@ -239,7 +244,7 @@ void AccountService::syncFoldersForAccount(const Account &account)
folder.setAccountId(account.id());
// Ensure the folder has an invalid ID (0) so DB assigns new one
folder.setId(0);
if (!FolderDao::insert(folder)) {
if (FolderDao::insert(folder) == -1) {
qWarning() << QStringLiteral("[AccountService] Failed to insert folder") << folder.name() << QStringLiteral("for account") << accountIdStr;
}
}
+255
View File
@@ -0,0 +1,255 @@
#include "accountservice.h"
#include <QDebug>
#include <QSslSocket>
#include "db/dao/accountdao.h"
#include "db/dao/folderdao.h"
#include "core/authenticator.h"
#include "core/gmailauthenticator.h"
#include "core/outlookauthenticator.h"
#include "core/imapauthenticator.h"
#include "core/synchronizerprovider.h"
#include "core/eventbus.h"
#include "core/events.h"
#include "db/databasemanager.h"
#include <QSqlQuery>
AccountService::AccountService(QObject *parent)
: QObject(parent)
{
}
QVector<Account> AccountService::getAllAccounts()
{
return AccountDao::findAll();
}
Account* AccountService::findAccountById(int id)
{
return AccountDao::findById(id);
}
Account* AccountService::findAccountByEmail(const QString &email)
{
return AccountDao::findByEmail(email);
}
void AccountService::addAccount(const QString &email, const QString &providerType,
const QString &accessToken, const QString &refreshToken)
{
Account account;
account.setEmail(email);
account.setDisplayName(email);
if (providerType == "gmail") account.setType(AccountType::Gmail);
else if (providerType == "outlook") account.setType(AccountType::Outlook);
else account.setType(AccountType::IMAP);
account.setAccessToken(accessToken);
account.setRefreshToken(refreshToken);
qint64 id = AccountDao::insert(account);
if (id != -1) {
account.setId(id);
qDebug() << "[AccountService] Account added:" << email;
publishAccountEvent(account, true);
emit accountAdded(account);
emit accountListChanged();
} else {
qWarning() << "[AccountService] Failed to add account:" << email;
}
}
void AccountService::addAccount(const Account &account)
{
qint64 id = AccountDao::insert(account);
if (id != -1) {
Account accToAdd = account; // make a copy we can modify
accToAdd.setId(id);
qDebug() << "[AccountService] Account added:" << accToAdd.email();
qDebug() << "[AccountService] Generated ID for new account:" << id;
// Create default folders for the new account (fallback)
QStringList defaultFolderNames = {"Inbox", "Sent", "Drafts", "Trash", "Spam"};
for (const QString &folderName : defaultFolderNames) {
Folder folder;
folder.setName(folderName);
folder.setAccountId(accToAdd.id());
// Set special folder flags
if (folderName == "Inbox") folder.setInbox(true);
else if (folderName == "Sent") folder.setSent(true);
else if (folderName == "Drafts") folder.setDrafts(true);
else if (folderName == "Trash") folder.setTrash(true);
if (!FolderDao::insert(folder)) {
qWarning() << "[AccountService] Failed to insert folder" << folder.name()
<< "for account ID" << accToAdd.id();
}
}
// Attempt to sync real folders from the server; if successful, replace defaults
syncFoldersForAccount(accToAdd);
publishAccountEvent(accToAdd, true);
emit accountAdded(accToAdd);
emit accountListChanged();
} else {
qWarning() << "[AccountService] Failed to add account:" << account.email();
}
}
void AccountService::removeAccount(int accountId)
{
Account *account = AccountDao::findById(accountId);
if (account) {
SynchronizerProvider::instance().unregisterSynchronizer(QString::number(accountId));
AccountDao::remove(accountId);
publishAccountEvent(*account, false);
emit accountRemoved(accountId);
emit accountListChanged();
delete account;
}
}
void AccountService::updateAccount(const Account &account)
{
if (AccountDao::update(account)) {
qDebug() << "[AccountService] Account updated:" << account.email();
// Ensure synchronizer is reinitialized with new connection settings
syncFoldersForAccount(account);
emit accountListChanged();
}
}
void AccountService::startAuthentication(const QString &email, const QString &providerType)
{
Authenticator *auth = createAuthenticator(providerType);
if (auth) {
connect(auth, &Authenticator::authenticationCompleted, this, [this](const Account &account) {
addAccount(account.email(),
account.type() == AccountType::Gmail ? "gmail"
: account.type() == AccountType::Outlook ? "outlook" : "imap",
account.accessToken(), account.refreshToken());
});
connect(auth, &Authenticator::authenticationFailed, this, [](const QString &error) {
qWarning() << "[AccountService] Authentication failed:" << error;
});
connect(auth, &QObject::destroyed, auth, [auth]() { /* cleanup handled by Qt */ });
auth->authenticate(email);
}
}
Authenticator* AccountService::createAuthenticator(const QString &providerType)
{
if (providerType == "gmail") return new GmailAuthenticator(this);
if (providerType == "outlook") return new OutlookAuthenticator(this);
if (providerType == "imap") return new ImapAuthenticator(this);
qWarning() << "[AccountService] Unknown provider:" << providerType;
return nullptr;
}
bool AccountService::testConnection(const Account::ConnectionSettings &settings, QString &errorMessage)
{
QSslSocket socket;
if (settings.incomingSsl) {
socket.connectToHostEncrypted(settings.incomingHost, settings.incomingPort);
if (!socket.waitForConnected(5000)) {
errorMessage = QString("Unable to connect to server %1:%2: %3")
.arg(settings.incomingHost)
.arg(settings.incomingPort)
.arg(socket.errorString());
return false;
}
if (!socket.waitForEncrypted(5000)) {
errorMessage = QString("TLS handshake failed: %1").arg(socket.errorString());
return false;
}
} else {
socket.connectToHost(settings.incomingHost, settings.incomingPort);
if (!socket.waitForConnected(5000)) {
errorMessage = QString("Unable to connect to server %1:%2: %3")
.arg(settings.incomingHost)
.arg(settings.incomingPort)
.arg(socket.errorString());
return false;
}
}
// Optionally read greeting (ignore)
if (!socket.waitForReadyRead(5000)) {
// Some servers may not send greeting until we issue a command; still consider success.
}
return true;
}
void AccountService::publishAccountEvent(const Account &account, bool added)
{
if (added) {
WinoMail::Events::AccountAddedEvent event;
event.account = account;
EVENT_BUS.publish(event);
} else {
WinoMail::Events::AccountRemovedEvent event;
event.accountId = account.id();
EVENT_BUS.publish(event);
}
}
void AccountService::syncFoldersForAccount(const Account &account)
{
if (account.email().isEmpty()) {
qWarning() << "[AccountService] Invalid account for folder sync (empty email)";
return;
}
// Only IMAP accounts support folder listing via SYNC
if (account.type() != AccountType::IMAP) {
qDebug() << QStringLiteral("[AccountService] Folder sync only supported for IMAP accounts; skipping");
return;
}
QString accountIdStr = QString::number(account.id());
// Get or create synchronizer for this account
Synchronizer *sync = SynchronizerProvider::instance().getSynchronizer(accountIdStr);
if (!sync) {
sync = SynchronizerProvider::instance().createSynchronizer(accountIdStr, QStringLiteral("imap"));
if (!sync) {
qWarning() << QStringLiteral("[AccountService] Failed to create IMAP synchronizer for account") << accountIdStr;
return;
}
}
// Initialize the synchronizer with the account (sets m_account)
if (!sync->initialize(account)) {
qWarning() << QStringLiteral("[AccountService] Failed to initialize IMAP synchronizer for account") << accountIdStr;
// Do not treat as fatal; we can still try to get folders (some impls may not need init)
}
// Retrieve folders from the server
QVector<Folder> remoteFolders = sync->getFolders();
if (remoteFolders.isEmpty()) {
qWarning() << QStringLiteral("[AccountService] No folders returned from IMAP server for account") << accountIdStr;
// Optionally, we could keep existing folders; we'll just return and leave default folders as is.
return;
}
// Remove existing folders for this account to avoid duplicates
if (!FolderDao::removeByAccountId(account.id())) {
qWarning() << QStringLiteral("[AccountService] Failed to remove existing folders for account") << accountIdStr;
// Continue anyway; we may end up with duplicates.
}
// Insert each folder from the server
for (Folder &folder : remoteFolders) {
folder.setAccountId(account.id());
// Ensure the folder has an invalid ID (0) so DB assigns new one
folder.setId(0);
if (!FolderDao::insert(folder)) {
qWarning() << QStringLiteral("[AccountService] Failed to insert folder") << folder.name() << QStringLiteral("for account") << accountIdStr;
}
}
qDebug() << QStringLiteral("[AccountService] Synced %1 folders for account %2").arg(remoteFolders.size()).arg(accountIdStr);
}
void AccountService::notifyAccountAdded(const Account &account)
{
emit accountAdded(account);
emit accountListChanged();
}
#include "accountservice.moc"
+155 -105
View File
@@ -11,8 +11,11 @@
#include <QDateTime>
#include <QTimer>
#include <QEventLoop>
#include <QNetworkRequest>
#include "../../core/events.h"
#include "../../core/eventbus.h"
#include "db/dao/folderdao.h"
#include "services/mimestorage.h"
GmailSynchronizer::GmailSynchronizer(QObject* parent)
: Synchronizer(parent),
@@ -84,7 +87,6 @@ bool GmailSynchronizer::syncFolder(const Folder& folder)
// En una implementación real, aquí compararíamos con la base de datos local
// y emitiríamos las señales apropiadas para elementos nuevos/actualizados/eliminados
// Por ahora, simulamos que obtenemos algunos elementos
if (!items.isEmpty()) {
for (const MailItem& item : items) {
emit mailItemAdded(item);
@@ -142,130 +144,147 @@ QVector<MailItem> GmailSynchronizer::fetchMailItems(const QString& folderId,
}
}
qDebug() << "Fetching mail items for folder (label):" << folderId;
qDebug() << "Fetching complete Gmail messages for label:" << folderId;
// Construir la URL para Gmail API
// Nota: En Gmail, las carpetas se identifican por su nombre de label (ej: INBOX, DRAFTS, etc.)
QString endpoint = QString("/me/mailFolders/%1/messages").arg(folderId);
QString url = buildGmailUrl(endpoint);
QString labelId = folderId;
bool localFolderId = false;
folderId.toInt(&localFolderId);
if (localFolderId) {
const auto folder = FolderDao::findById(folderId.toInt());
if (folder) {
labelId = folder->parentFolderId();
if (labelId.isEmpty()) {
const QString name = folder->name().toLower();
if (name == QStringLiteral("inbox")) labelId = QStringLiteral("INBOX");
else if (name == QStringLiteral("sent") || name == QStringLiteral("sent mail")) labelId = QStringLiteral("SENT");
else if (name == QStringLiteral("drafts")) labelId = QStringLiteral("DRAFT");
else if (name == QStringLiteral("trash")) labelId = QStringLiteral("TRASH");
else if (name == QStringLiteral("spam")) labelId = QStringLiteral("SPAM");
else labelId = folder->name();
}
}
}
// Parámetros de consulta
QNetworkAccessManager network;
auto get = [&](const QUrl &url, QByteArray &data, QString *contentType = nullptr) {
QNetworkReply *reply = network.get(createAuthRequest(url.toString()));
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
const bool ok = reply->error() == QNetworkReply::NoError;
if (ok) {
if (contentType) *contentType = QString::fromLatin1(reply->header(QNetworkRequest::ContentTypeHeader).toByteArray());
data = reply->readAll();
} else {
qWarning() << "Gmail request failed:" << reply->errorString() << url;
}
reply->deleteLater();
return ok;
};
QVector<QJsonObject> messageRefs;
QString pageToken;
do {
QUrl url(buildGmailUrl(QStringLiteral("messages")));
QUrlQuery query;
query.addQueryItem("maxResults", "50"); // Limitar a 50 mensajes por petición
query.addQueryItem("q", "in:" + folderId); // Filtrar por label
url += "?" + query.toString();
query.addQueryItem(QStringLiteral("labelIds"), labelId);
query.addQueryItem(QStringLiteral("maxResults"), QStringLiteral("100"));
if (!pageToken.isEmpty()) query.addQueryItem(QStringLiteral("pageToken"), pageToken);
url.setQuery(query);
QNetworkRequest request = createAuthRequest(url);
QNetworkReply* reply = m_networkManager->get(request);
QByteArray response;
if (!get(url, response)) return {};
QJsonParseError parseError;
const QJsonDocument document = QJsonDocument::fromJson(response, &parseError);
if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
qWarning() << "Invalid Gmail message list:" << parseError.errorString();
return {};
}
const QJsonObject object = document.object();
for (const QJsonValue &value : object.value(QStringLiteral("messages")).toArray())
if (value.isObject()) messageRefs.append(value.toObject());
pageToken = object.value(QStringLiteral("nextPageToken")).toString();
} while (!pageToken.isEmpty());
// Nota: En una implementación real, esperaríamos la respuesta asíncronamente
// pero por simplicidad en este stub, simulamos una respuesta
// Simular respuesta para desarrollo
QVector<MailItem> items;
items.append(MailItem(1, folderId.toInt(), "Actualización de proyecto",
"carlos.rodriguez@proveedor.com", m_account.email(),
QDateTime::currentDateTime().addSecs(-1800),
false, false));
items.append(MailItem(2, folderId.toInt(), "Factura adjunta",
"facturacion@servicios.com", m_account.email(),
QDateTime::currentDateTime().addSecs(-5400),
true, true)); // Marcada como importante y leída
MimeStorageService mimeStorage;
for (const QJsonObject &reference : messageRefs) {
const QString gmailId = reference.value(QStringLiteral("id")).toString();
if (gmailId.isEmpty()) continue;
QUrl rawUrl(buildGmailUrl(QStringLiteral("messages/%1").arg(gmailId)));
QUrlQuery rawQuery;
rawQuery.addQueryItem(QStringLiteral("format"), QStringLiteral("raw"));
rawUrl.setQuery(rawQuery);
QByteArray response;
if (!get(rawUrl, response)) continue;
QJsonParseError parseError;
const QJsonDocument document = QJsonDocument::fromJson(response, &parseError);
if (parseError.error != QJsonParseError::NoError || !document.isObject()) continue;
const QJsonObject object = document.object();
QByteArray encoded = object.value(QStringLiteral("raw")).toString().toLatin1();
encoded.replace('-', '+');
encoded.replace('_', '/');
while (encoded.size() % 4) encoded.append('=');
const QByteArray rawMime = QByteArray::fromBase64(encoded);
ParsedMimeMessage parsed;
if (rawMime.isEmpty() || !mimeStorage.parseMessage(rawMime, parsed)) continue;
MailItem item;
item.setFolderId(folderId.toInt());
item.setMessageId(gmailId);
item.setRawMime(rawMime);
item.setSubject(parsed.subject.isEmpty() ? QStringLiteral("(No Subject)") : parsed.subject);
item.setSender(parsed.from);
item.setRecipient(parsed.to);
item.setTo(parsed.to);
item.setCc(parsed.cc);
item.setBcc(parsed.bcc);
item.setDate(parsed.date);
item.setBodyHtml(parsed.bodyHtml);
item.setSize(rawMime.size());
item.setRead(true);
QVector<QString> attachmentNames;
for (const ParsedMimeAttachment &attachment : parsed.attachments) attachmentNames.append(attachment.fileName);
item.setAttachments(attachmentNames);
const QJsonArray labels = object.value(QStringLiteral("labelIds")).toArray();
for (const QJsonValue &label : labels) {
if (label.toString() == QStringLiteral("UNREAD")) item.setRead(false);
if (label.toString() == QStringLiteral("STARRED")) item.setFlagged(true);
}
items.append(item);
}
return items;
}
bool GmailSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
{
if (!m_account.isTokenValid()) {
if (!refreshAccessToken()) {
qWarning() << "Failed to refresh token for appending mail item";
Q_UNUSED(folderId);
Q_UNUSED(item);
qWarning() << "Gmail appendMailItem is not used for sending; MailService sends via Gmail API";
return false;
}
}
qDebug() << "Appending mail item to folder (label):" << folderId;
// En una implementación real, llamaríamos a Gmail API
// para crear un mensaje en la etiqueta especificada
// Por ahora, simulamos éxito
emit mailItemAdded(item);
// Publish MailItemAddedEvent
WinoMail::Events::MailItemAddedEvent mailEvent;
mailEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
mailEvent.timestamp = QDateTime::currentDateTimeUtc();
mailEvent.item = item;
PUBLISH(mailEvent);
return true;
}
bool GmailSynchronizer::updateMailItemFlags(const QString& folderId,
const QString& itemUid,
bool read, bool flagged)
{
if (!m_account.isTokenValid()) {
if (!refreshAccessToken()) {
qWarning() << "Failed to refresh token for updating mail item flags";
Q_UNUSED(folderId);
Q_UNUSED(itemUid);
Q_UNUSED(read);
Q_UNUSED(flagged);
qWarning() << "Gmail flag update is not available through this synchronizer";
return false;
}
}
qDebug() << "Updating mail item flags:" << itemUid
<< "read:" << read << "flagged:" << flagged;
// En una implementación real, llamaríamos a Gmail API
// para actualizar las etiquetas del mensaje (leído, importante, etc.)
// Por ahora, simulamos éxito
MailItem updatedItem;
updatedItem.setId(itemUid.toLongLong());
updatedItem.setFolderId(folderId.toInt());
updatedItem.setRead(read);
updatedItem.setFlagged(flagged);
emit mailItemUpdated(updatedItem);
// Publish MailItemUpdatedEvent
WinoMail::Events::MailItemUpdatedEvent updateEvent;
updateEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
updateEvent.timestamp = QDateTime::currentDateTimeUtc();
updateEvent.item = updatedItem;
updateEvent.changedFields = QStringList() << "read" << "flagged"; // Simplified
PUBLISH(updateEvent);
return true;
}
bool GmailSynchronizer::deleteMailItem(const QString& folderId,
const QString& itemUid)
{
if (!m_account.isTokenValid()) {
if (!refreshAccessToken()) {
qWarning() << "Failed to refresh token for deleting mail item";
Q_UNUSED(folderId);
Q_UNUSED(itemUid);
qWarning() << "Gmail delete is not available through this synchronizer";
return false;
}
}
qDebug() << "Deleting mail item:" << itemUid << "from folder (label):" << folderId;
// En una implementación real, llamaríamos a Gmail API
// para eliminar el mensaje (moverlo a la papelera)
// Por ahora, simulamos éxito
emit mailItemRemoved(itemUid);
// Publish MailItemRemovedEvent
WinoMail::Events::MailItemRemovedEvent removeEvent;
removeEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
removeEvent.timestamp = QDateTime::currentDateTimeUtc();
removeEvent.itemUid = itemUid;
removeEvent.folderId = folderId.toInt();
PUBLISH(removeEvent);
return true;
}
void GmailSynchronizer::onGmailReplyFinished(QNetworkReply* reply)
{
@@ -321,9 +340,11 @@ void GmailSynchronizer::onHistoryTimer()
QString GmailSynchronizer::buildGmailUrl(const QString& endpoint) const
{
QString normalized = endpoint;
while (normalized.startsWith('/')) normalized.remove(0, 1);
return QStringLiteral("https://gmail.googleapis.com/gmail/v1/users/%1/%2")
.arg(m_account.email())
.arg(endpoint);
.arg(QString::fromUtf8(QUrl::toPercentEncoding(m_account.email())))
.arg(normalized);
}
QNetworkRequest GmailSynchronizer::createAuthRequest(const QString& url) const
@@ -338,12 +359,41 @@ QNetworkRequest GmailSynchronizer::createAuthRequest(const QString& url) const
bool GmailSynchronizer::refreshAccessToken()
{
// En una implementación real, aquí usaríamos el refresh token
// para obtener un nuevo access token desde Google OAuth2
if (m_refreshToken.isEmpty()) return false;
const QString clientId = qEnvironmentVariable("WINO_GMAIL_CLIENT_ID");
const QString clientSecret = qEnvironmentVariable("WINO_GMAIL_CLIENT_SECRET");
if (clientId.isEmpty() || clientSecret.isEmpty()) {
qWarning() << "Gmail token expired and WINO_GMAIL_CLIENT_ID/SECRET are not configured";
return false;
}
qWarning() << "Token refresh not implemented - using stub";
// Simulamos éxito por ahora
return !m_refreshToken.isEmpty();
QNetworkAccessManager network;
QNetworkRequest request(QUrl(QStringLiteral("https://oauth2.googleapis.com/token")));
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded"));
QUrlQuery form;
form.addQueryItem(QStringLiteral("client_id"), clientId);
form.addQueryItem(QStringLiteral("client_secret"), clientSecret);
form.addQueryItem(QStringLiteral("refresh_token"), m_refreshToken);
form.addQueryItem(QStringLiteral("grant_type"), QStringLiteral("refresh_token"));
QNetworkReply *reply = network.post(request, form.toString(QUrl::FullyEncoded).toUtf8());
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "Gmail token refresh failed:" << reply->errorString();
reply->deleteLater();
return false;
}
const QJsonDocument document = QJsonDocument::fromJson(reply->readAll());
reply->deleteLater();
const QString token = document.object().value(QStringLiteral("access_token")).toString();
if (token.isEmpty()) return false;
m_accessToken = token;
const int expiresIn = document.object().value(QStringLiteral("expires_in")).toInt(3600);
m_tokenExpires = QDateTime::currentDateTimeUtc().addSecs(expiresIn);
m_account.setAccessToken(m_accessToken);
m_account.setTokenExpires(m_tokenExpires);
return true;
}
qint64 GmailSynchronizer::getHistoryId(const QString& folderId) const
+217
View File
@@ -0,0 +1,217 @@
#include "imapconnection.h"
#include <QDebug>
#include <QEventLoop>
#include <QTimer>
ImapConnection::ImapConnection(QObject *parent) : QObject(parent),
m_socket(new QSslSocket(this)),
m_tagCounter(1)
{
connect(m_socket, &QAbstractSocket::connected, this, &ImapConnection::onConnected);
connect(m_socket, &QSslSocket::encrypted, this, &ImapConnection::onEncrypted);
m_readyReadConnection = connect(m_socket, &QSslSocket::readyRead, this, &ImapConnection::onReadyRead);
connect(m_socket, &QAbstractSocket::errorOccurred, this, &ImapConnection::onSocketError);
}
ImapConnection::~ImapConnection()
{
disconnect();
}
void ImapConnection::connectToHost(const QString &host, int port, bool useSsl)
{
m_host = host;
m_port = port;
m_useSsl = useSsl;
if (useSsl) {
m_socket->connectToHostEncrypted(host, port);
} else {
m_socket->connectToHost(host, port);
}
}
void ImapConnection::login(const QString &username, const QString &password,
bool useAuthPlain,
std::function<void(bool, const QString&)> callback)
{
if (!m_socket->isEncrypted() && m_useSsl) {
callback(false, "Not encrypted");
return;
}
QString cmd;
if (useAuthPlain) {
QByteArray auth;
auth.append('\0');
auth.append(username.toUtf8());
auth.append('\0');
auth.append(password.toUtf8());
cmd = QString("AUTHENTICATE PLAIN %1").arg(QString::fromLatin1(auth.toBase64()));
} else {
cmd = QString("LOGIN %1 %2").arg(username, password);
}
sendCommand(cmd, callback);
}
void ImapConnection::sendCommand(const QString &command,
std::function<void(bool, const QString&)> callback)
{
QString tag = generateTag();
QString fullCmd = tag + " " + command + "\r\n";
m_socket->write(fullCmd.toUtf8());
m_socket->flush();
m_pendingCallbacks[tag] = callback;
}
void ImapConnection::disconnect()
{
if (m_socket->state() != QAbstractSocket::UnconnectedState) {
m_socket->disconnectFromHost();
m_socket->waitForDisconnected(1000);
}
m_pendingCallbacks.clear();
}
void ImapConnection::onConnected()
{
// For non-SSL connections, signal connected immediately.
// For SSL connections, onEncrypted() fires instead after the SSL handshake.
if (!m_useSsl) {
emit connected();
}
}
void ImapConnection::onEncrypted()
{
emit connected();
}
void ImapConnection::onReadyRead()
{
while (m_socket->canReadLine()) {
QString line = QString::fromUtf8(m_socket->readLine()).trimmed();
processLine(line);
}
}
void ImapConnection::onSocketError(QAbstractSocket::SocketError error)
{
emit errorOccurred(m_socket->errorString());
}
void ImapConnection::processLine(const QString &line)
{
// If it's an untagged response (starts with *), emit signal
if (line.startsWith('*')) {
emit untaggedResponse(line);
return;
}
// Extract the tag prefix (first word) and look it up in pending callbacks
int spacePos = line.indexOf(' ');
if (spacePos == -1) return; // malformed line
QString tagFromLine = line.left(spacePos);
auto it = m_pendingCallbacks.find(tagFromLine);
if (it != m_pendingCallbacks.end()) {
bool ok = line.contains(" OK ");
auto cb = it.value();
m_pendingCallbacks.erase(it);
cb(ok, line);
return;
}
// If we get here, it's an unhandled response
qWarning() << "Unhandled IMAP response:" << line;
}
void ImapConnection::sendRaw(const QString &data)
{
m_socket->write(data.toUtf8() + "\r\n");
m_socket->flush();
}
QString ImapConnection::generateTag()
{
// Simple tag: A001, A002, ... (like traditional IMAP clients)
return QString("A%1").arg(m_tagCounter++, 4, 10, QChar('0'));
}
bool ImapConnection::sendCommandWait(const QString &command, QString &response, int msecs)
{
QByteArray rawResponse;
const bool completed = sendCommandWaitBytes(command, rawResponse, msecs);
response = QString::fromUtf8(rawResponse);
return completed;
}
bool ImapConnection::sendCommandWaitBytes(const QString &command, QByteArray &response, int msecs)
{
QString tag = generateTag();
QString fullCmd = tag + " " + command + "\r\n";
bool complete = false;
bool timedOut = false;
QEventLoop loop;
QTimer timer;
QByteArray buffer;
const QByteArray tagBytes = tag.toUtf8();
// Fetch responses contain arbitrary MIME bytes and IMAP literals. The
// line-based parser cannot safely consume those bytes, so temporarily
// take ownership of readyRead and collect the complete tagged response.
QObject::disconnect(m_readyReadConnection);
auto collectResponse = [&]() {
buffer.append(m_socket->readAll());
int pos = 0;
while (pos < buffer.size()) {
const int lineEnd = buffer.indexOf("\r\n", pos);
if (lineEnd < 0) break;
const QByteArray line = buffer.mid(pos, lineEnd - pos);
const int open = line.lastIndexOf('{');
QByteArray literalSize = open >= 0 && line.endsWith('}')
? line.mid(open + 1, line.size() - open - 2) : QByteArray();
if (literalSize.endsWith('+')) literalSize.chop(1);
bool literalOk = false;
const qint64 parsedLiteralSize = literalSize.toLongLong(&literalOk);
const bool hasLiteral = open >= 0 && line.endsWith('}') && literalOk && parsedLiteralSize >= 0;
if (hasLiteral) {
const qint64 length = parsedLiteralSize;
const int dataStart = lineEnd + 2;
if (buffer.size() < dataStart + length) break;
pos = dataStart + int(length);
if (buffer.mid(pos, 2) == QByteArrayLiteral("\r\n")) pos += 2;
continue;
}
if (line.startsWith(tagBytes + QByteArrayLiteral(" "))) {
complete = true;
loop.quit();
break;
}
pos = lineEnd + 2;
}
};
QMetaObject::Connection readyConn = connect(m_socket, &QSslSocket::readyRead, &loop, collectResponse);
QMetaObject::Connection errorConn = connect(m_socket, &QAbstractSocket::errorOccurred, &loop, [&](QAbstractSocket::SocketError) {
loop.quit();
});
timer.setSingleShot(true);
QMetaObject::Connection timeoutConn = connect(&timer, &QTimer::timeout, &loop, [&]() {
timedOut = true;
loop.quit();
});
m_socket->write(fullCmd.toUtf8());
m_socket->flush();
if (m_socket->bytesAvailable() > 0) collectResponse();
timer.start(msecs);
loop.exec();
QObject::disconnect(readyConn);
QObject::disconnect(errorConn);
QObject::disconnect(timeoutConn);
m_readyReadConnection = connect(m_socket, &QSslSocket::readyRead, this, &ImapConnection::onReadyRead);
response = buffer;
if (timedOut || !complete) return false;
return true;
}
+60
View File
@@ -0,0 +1,60 @@
#ifndef IMAPCONNECTION_H
#define IMAPCONNECTION_H
#include <QSslSocket>
#include <QObject>
#include <functional>
#include <QMap>
#include <QByteArray>
class ImapConnection : public QObject
{
Q_OBJECT
public:
explicit ImapConnection(QObject *parent = nullptr);
~ImapConnection();
void connectToHost(const QString &host, int port, bool useSsl);
void login(const QString &username, const QString &password,
bool useAuthPlain,
std::function<void(bool, const QString&)> callback);
void sendCommand(const QString &command,
std::function<void(bool, const QString&)> callback);
void disconnect();
// Synchronous version: blocks until response or timeout
bool sendCommandWait(const QString &command, QString &response, int msecs);
bool sendCommandWaitBytes(const QString &command, QByteArray &response, int msecs);
signals:
void connected();
void disconnected();
void errorOccurred(const QString &message);
void untaggedResponse(const QString &line); // for * RESPONSES
void ready(); // connection ready for authentication (TCP connected for plain, encrypted for SSL)
private slots:
void onConnected();
void onEncrypted();
void onReadyRead();
void onSocketError(QAbstractSocket::SocketError error);
private:
QSslSocket *m_socket;
QString m_host;
int m_port;
bool m_useSsl;
QString m_username;
QString m_password;
int m_tagCounter;
QString m_responseBuffer;
QMap<QString, std::function<void(bool, const QString&)>> m_pendingCallbacks; // keyed by tag
QMetaObject::Connection m_readyReadConnection;
void processLine(const QString &line);
QString generateTag();
public:
void sendRaw(const QString &data);
};
#endif // IMAPCONNECTION_H
+313 -91
View File
@@ -8,6 +8,8 @@
#include "../../core/models/folder.h"
#include "../../db/dao/folderdao.h"
#include "../../db/dao/mailitemdao.h"
#include "../../services/mimestorage.h"
#include <algorithm>
ImapSynchronizer::ImapSynchronizer(QObject* parent)
: Synchronizer(parent)
@@ -16,6 +18,7 @@ ImapSynchronizer::ImapSynchronizer(QObject* parent)
bool ImapSynchronizer::initialize(const Account& account)
{
m_account = account;
const Account::ConnectionSettings& settings = account.connectionSettings();
m_host = settings.incomingHost;
m_port = settings.incomingPort;
@@ -37,9 +40,13 @@ bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
QEventLoop connectLoop;
QTimer connectTimer;
bool connectTimeout = false;
connect(&conn, &ImapConnection::connected, &connectLoop, &QEventLoop::quit);
connect(&conn, &ImapConnection::connected, &connectLoop, [&]() {
connected = true;
connectLoop.quit();
});
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
qWarning() << "IMAP connection error:" << err;
connected = false;
connectLoop.quit();
});
connectTimer.setSingleShot(true);
@@ -54,7 +61,7 @@ bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
qWarning() << "Connection timeout to" << m_host;
return false;
}
connected = true;
if (!connected) return false;
// Wait for server greeting (first untagged response)
QEventLoop greetLoop;
@@ -114,99 +121,173 @@ bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
return true;
}
bool ImapSynchronizer::syncFolder(const Folder& folder)
QVector<qint64> ImapSynchronizer::parseUidSearchResponse(const QString& response) const
{
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// Select folder
QString selectCmd = QString("SELECT \"%1\"").arg(folder.name());
QString selectResp;
if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(" OK ")) {
qWarning() << "SELECT failed for" << folder.name();
return false;
}
// Get all UIDs
QString searchCmd = "UID SEARCH ALL";
QString searchResp;
if (!conn.sendCommandWait(searchCmd, searchResp, 30000) || !searchResp.contains(" OK ")) {
qWarning() << "SEARCH failed";
return false;
}
QVector<qint64> uids;
QStringList lines = searchResp.split('\n');
QStringList lines = response.split(QStringLiteral("\n"));
for (const QString& line : lines) {
if (line.startsWith("* SEARCH")) {
QStringList parts = line.split(QRegularExpression("\\s+"));
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);
if (ok && uid > 0)
uids.append(uid);
}
break;
}
}
// If no messages, set unread count to 0 and exit
if (uids.isEmpty()) {
Folder f = folder;
f.setUnreadCount(0);
FolderDao::update(f);
conn.disconnect();
return true;
return uids;
}
// Get flags for all UIDs in one command
QStringList uidStrs;
for (qint64 uid : uids) uidStrs.append(QString::number(uid));
QString fetchCmd = "UID FETCH " + uidStrs.join(',') + " (FLAGS)";
QString fetchResp;
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000) || !fetchResp.contains(" OK ")) {
qWarning() << "FETCH FLAGS failed";
conn.disconnect();
bool ImapSynchronizer::syncFolder(const Folder& folder)
{
// First, get all UIDs from server to detect deletions
QVector<qint64> allUids = fetchAllUids(QString::number(folder.id()));
if (allUids.isEmpty()) {
qWarning() << "Failed to fetch UIDs for folder" << folder.id();
return false;
}
// Parse flags and update local DB
QMap<qint64, QPair<bool,bool>> flagsMap; // uid -> (seen, flagged)
QRegularExpression flagsRx(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("\\Seen");
// Get local UIDs
QVector<qint64> localUids = MailItemDao::getUidsForFolder(folder.id());
// Find deleted UIDs (exist locally but not on server)
QSet<qint64> serverUidSet(allUids.begin(), allUids.end());
QVector<qint64> deletedUids;
for (qint64 uid : localUids) {
if (!serverUidSet.contains(uid)) {
deletedUids.append(uid);
}
}
// Delete locally if removed from server
bool success = true;
for (qint64 uid : deletedUids) {
if (!MailItemDao::removeByUid(folder.id(), uid)) {
qWarning() << "Failed to delete local mail item UID" << uid;
success = false;
} else {
qDebug() << "Removed local mail item UID" << uid << "(deleted on server)";
}
}
// Now fetch new/changed messages
const qint64 sinceUid = MailItemDao::maxUidForFolder(folder.id()).value_or(0);
QVector<MailItem> items = fetchMailItems(QString::number(folder.id()), sinceUid);
for (MailItem &item : items) {
if (!persistFetchedItem(folder, item)) success = false;
}
// Also check for flag changes on existing messages
if (!allUids.isEmpty()) {
// Fetch flags for all known UIDs (in batches)
const int batchSize = 100;
for (int i = 0; i < allUids.size(); i += batchSize) {
QVector<qint64> batch = allUids.mid(i, qMin(batchSize, allUids.size() - i));
QStringList batchList;
for (qint64 uid : batch) batchList << QString::number(uid);
QString uidList = batchList.join(",");
ImapConnection conn;
if (!connectAndLogin(conn)) continue;
// SELECT folder
QString selectCommand = QString("SELECT \"%1\"").arg(folder.name());
QString selectResponse;
if (!conn.sendCommandWait(selectCommand, selectResponse, 30000) || !selectResponse.contains(" OK ")) {
continue;
}
// Fetch only FLAGS for this batch
QString fetchCommand = QString("UID FETCH %1 (FLAGS)").arg(uidList);
QByteArray fetchResponse;
if (!conn.sendCommandWaitBytes(fetchCommand, fetchResponse, 30000)) {
continue;
}
// Parse flags and update local
parseAndUpdateFlags(fetchResponse, folder.id());
}
}
return success;
}
QVector<qint64> ImapSynchronizer::fetchAllUids(const QString& folderId) const
{
QVector<qint64> uids;
ImapConnection conn;
if (!connectAndLogin(conn)) {
return uids;
}
int fid = folderId.toInt();
auto folderOpt = FolderDao::findById(fid);
if (!folderOpt) {
qWarning() << "Folder not found for id" << folderId;
return uids;
}
QString folderName = folderOpt->name();
// SELECT
QString selectCommand = QString("SELECT \"%1\"").arg(folderName);
QString selectResponse;
if (!conn.sendCommandWait(selectCommand, selectResponse, 30000) || !selectResponse.contains(" OK ")) {
qWarning() << "SELECT failed:" << selectResponse;
return uids;
}
// SEARCH ALL
QString searchCommand = "UID SEARCH ALL";
QString searchResponse;
if (!conn.sendCommandWait(searchCommand, searchResponse, 30000)) {
qWarning() << "SEARCH command failed:" << searchResponse;
return uids;
}
uids = parseUidSearchResponse(searchResponse);
return uids;
}
void ImapSynchronizer::parseAndUpdateFlags(const QByteArray& response, int folderId) const
{
// Parse FETCH response for FLAGS
QStringList lines = QString::fromUtf8(response).split('\n');
for (const QString& line : lines) {
if (!line.startsWith("* ") || !line.contains(" FETCH ")) continue;
// Extract UID
QRegularExpression uidRx(QStringLiteral("\\bUID\\s+(\\d+)"));
QRegularExpressionMatch uidMatch = uidRx.match(line);
if (!uidMatch.hasMatch()) continue;
qint64 uid = uidMatch.captured(1).toLongLong();
// Extract FLAGS
int flagsPos = line.indexOf("FLAGS (");
if (flagsPos < 0) continue;
int flagsEnd = line.indexOf(')', flagsPos);
if (flagsEnd < 0) continue;
QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7);
bool read = flags.contains("\\Seen");
bool flagged = flags.contains("\\Flagged");
flagsMap[uid] = qMakePair(seen, flagged);
}
// Update local items
auto localItems = MailItemDao::findByFolderId(folder.id());
for (MailItem& item : localItems) {
if (flagsMap.contains(item.uid())) {
bool seen = flagsMap[item.uid()].first;
bool flagged = flagsMap[item.uid()].second;
if (item.isRead() != seen || item.isFlagged() != flagged) {
item.setRead(seen);
// Update local DB if different
auto itemOpt = MailItemDao::findByUid(folderId, uid);
if (itemOpt.has_value()) {
MailItem item = itemOpt.value();
if (item.isRead() != read || item.isFlagged() != flagged) {
item.setRead(read);
item.setFlagged(flagged);
MailItemDao::update(item);
if (!MailItemDao::update(item)) {
qWarning() << "Failed to update flags for UID" << uid;
} else {
qDebug() << "Updated flags for UID" << uid << "read=" << read << "flagged=" << flagged;
}
}
}
// Count unread
int unread = 0;
for (const MailItem& item : localItems) {
if (!item.isRead()) unread++;
}
Folder f = folder;
f.setUnreadCount(unread);
FolderDao::update(f);
conn.disconnect();
return true;
}
QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid)
@@ -242,7 +323,7 @@ QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint
// SEARCH
QString searchCommand;
if (sinceUid > 0) {
searchCommand = QString("UID SEARCH %1:*").arg(sinceUid);
searchCommand = QString("UID SEARCH UID %1:*").arg(sinceUid + 1);
} else {
searchCommand = "UID SEARCH ALL";
}
@@ -283,24 +364,38 @@ QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint
}
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)")
// Fetch the complete RFC822 message. Headers alone are not enough to
// reconstruct HTML, inline resources or attachments.
QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[] FLAGS INTERNALDATE)")
.arg(batchList);
QString fetchResponse;
if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) {
qWarning() << "FETCH failed for batch" << i;
QByteArray fetchResponse;
if (!conn.sendCommandWaitBytes(fetchCommand, fetchResponse, 30000)) {
qWarning() << "FETCH failed for batch" << i << "(" << batch.size() << "UIDs)"
<< "- first UID:" << batch.first() << "last UID:" << batch.last()
<< "- response:" << QString::fromUtf8(fetchResponse).left(200);
// Fallback: try fetching UIDs one by one
for (qint64 uid : batch) {
QString singleFetchCmd = QString("UID FETCH %1 (BODY.PEEK[] FLAGS INTERNALDATE)").arg(uid);
QByteArray singleResponse;
if (conn.sendCommandWaitBytes(singleFetchCmd, singleResponse, 30000)) {
QVector<MailItem> singleItems = parseFetchResponseBytes(singleResponse);
for (MailItem& item : singleItems) {
item.setFolderId(fid);
items.append(item);
}
} else {
qWarning() << " Single UID fetch failed for UID" << uid
<< "- response:" << QString::fromUtf8(singleResponse).left(200);
}
}
continue;
}
// Parse and persist
QVector<MailItem> batchItems = parseFetchResponse(fetchResponse.split('\n'));
QVector<MailItem> batchItems = parseFetchResponseBytes(fetchResponse);
for (MailItem& item : batchItems) {
item.setFolderId(fid);
if (MailItemDao::insert(item)) {
items.append(item);
} else {
qWarning() << "Failed to insert mail item uid" << item.uid();
}
}
}
@@ -337,6 +432,132 @@ QVector<Folder> ImapSynchronizer::getFolders() const
return folders;
}
bool ImapSynchronizer::persistFetchedItem(const Folder& folder, MailItem& item) const
{
MimeStorageService storage;
QVector<QString> storedPaths;
ParsedMimeMessage parsed;
if (!item.rawMime().isEmpty()) {
if (!storage.parseMessage(item.rawMime(), parsed)
|| !storage.storeMessage(QString::number(m_account.id()), QString::number(folder.id()),
item, item.rawMime(), &storedPaths)) {
qWarning() << "Failed to store MIME message for UID" << item.uid();
return false;
}
}
if (!MailItemDao::upsert(item)) {
storage.deleteEmlFile(item.fileId());
return false;
}
QVector<StoredAttachmentRecord> records;
for (int i = 0; i < parsed.attachments.size(); ++i) {
const ParsedMimeAttachment &attachment = parsed.attachments.at(i);
StoredAttachmentRecord record;
record.fileName = attachment.fileName;
record.mimeType = attachment.mimeType;
record.contentId = attachment.contentId;
record.size = attachment.data.size();
if (i < storedPaths.size()) record.storedPath = storedPaths.at(i);
records.append(record);
}
if (!MailItemDao::replaceAttachments(item.id(), records)) {
storage.deleteEmlFile(item.fileId());
return false;
}
return true;
}
QVector<MailItem> ImapSynchronizer::parseFetchResponseBytes(const QByteArray& response) const
{
QVector<MailItem> items;
MimeStorageService mimeStorage;
int searchFrom = 0;
// Debug: check if response contains error
if (response.contains(" NO ") || response.contains(" BAD ")) {
qWarning() << "IMAP FETCH response contains error:" << QString::fromUtf8(response).left(500);
}
while (true) {
const int bodyPos = response.indexOf("BODY[]", searchFrom);
if (bodyPos < 0) {
// Check if we have untagged FETCH responses without BODY[]
int fetchPos = response.indexOf(" FETCH ", searchFrom);
if (fetchPos >= 0) {
int lineEnd = response.indexOf("\r\n", fetchPos);
if (lineEnd > 0) {
QByteArray line = response.mid(fetchPos, lineEnd - fetchPos);
qWarning() << "FETCH response without BODY[]:" << QString::fromLatin1(line);
}
}
break;
}
const int bracePos = response.indexOf('{', bodyPos);
const int braceEnd = response.indexOf('}', bracePos);
if (bracePos < 0 || braceEnd < 0) break;
QByteArray literalSize = response.mid(bracePos + 1, braceEnd - bracePos - 1);
if (literalSize.endsWith('+')) literalSize.chop(1);
bool sizeOk = false;
const qint64 size = literalSize.toLongLong(&sizeOk);
const int lineEnd = response.indexOf("\r\n", braceEnd);
if (!sizeOk || lineEnd < 0 || response.size() < lineEnd + 2 + size) {
qWarning() << "FETCH parse error: sizeOk=" << sizeOk << "lineEnd=" << lineEnd
<< "response.size=" << response.size() << "needed=" << (lineEnd + 2 + size);
break;
}
const QByteArray rawMime = response.mid(lineEnd + 2, int(size));
const int fetchLineStart = response.lastIndexOf("\r\n", bodyPos) + 2;
const QByteArray fetchLine = response.mid(fetchLineStart, bodyPos - fetchLineStart);
const QRegularExpression uidRx(QStringLiteral("\\bUID\\s+(\\d+)"));
const QRegularExpressionMatch uidMatch = uidRx.match(QString::fromLatin1(fetchLine));
if (!uidMatch.hasMatch()) {
qWarning() << "No UID found in FETCH line:" << QString::fromLatin1(fetchLine).left(200);
searchFrom = lineEnd + 2 + int(size);
continue;
}
ParsedMimeMessage parsed;
if (mimeStorage.parseMessage(rawMime, parsed)) {
MailItem item;
item.setUid(uidMatch.captured(1).toLongLong());
item.setMessageId(parsed.messageId);
item.setSubject(parsed.subject.isEmpty() ? QStringLiteral("(No Subject)") : parsed.subject);
item.setSender(parsed.from);
item.setRecipient(parsed.to);
item.setTo(parsed.to);
item.setCc(parsed.cc);
item.setBcc(parsed.bcc);
item.setDate(parsed.date);
item.setBodyHtml(parsed.bodyHtml);
item.setSize(rawMime.size());
item.setRawMime(rawMime);
QVector<QString> names;
for (const ParsedMimeAttachment &attachment : parsed.attachments)
names.append(attachment.fileName);
item.setAttachments(names);
// Flags are in the FETCH line, not in the MIME payload.
const int flagsPos = fetchLine.indexOf("FLAGS (");
if (flagsPos >= 0) {
const int flagsEnd = fetchLine.indexOf(')', flagsPos);
if (flagsEnd > flagsPos) {
const QString flags = QString::fromLatin1(fetchLine.mid(flagsPos + 7, flagsEnd - flagsPos - 7));
item.setRead(flags.contains(QStringLiteral("\\Seen")));
item.setFlagged(flags.contains(QStringLiteral("\\Flagged")));
}
}
items.append(item);
} else {
qWarning() << "Failed to parse MIME for UID" << uidMatch.captured(1);
}
searchFrom = lineEnd + 2 + int(size);
}
return items;
}
bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
{
// Get folder name
@@ -562,19 +783,20 @@ QVector<MailItem> ImapSynchronizer::parseFetchResponse(const QStringList& lines)
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")));
item.setRead(flags.contains(QStringLiteral("\\Seen")));
item.setFlagged(flags.contains(QStringLiteral("\\Flagged")));
}
}
// Extract INTERNALDATE
int datePos = line.indexOf(QStringLiteral("INTERNALDATE \"\""));
int datePos = line.indexOf(QStringLiteral("INTERNALDATE \""));
if (datePos != -1) {
int dateStart = datePos + 16; // length of "INTERNALDATE \""
int dateStart = datePos + 15; // length of "INTERNALDATE \""
int dateEnd = line.indexOf('\"', dateStart);
if (dateEnd != -1) {
QString dateStr = line.mid(dateStart, dateEnd - dateStart);
QDateTime dt = QDateTime::fromString(dateStr, QStringLiteral("dd-MMM-yyyy hh:mm:ss zzz"));
dateStr.replace('-', ' '); // ahora "9 Jul 2025 14:42:04 +0000"
QDateTime dt = QDateTime::fromString(dateStr, Qt::RFC2822Date);
if (dt.isValid())
item.setDate(dt);
}
@@ -582,7 +804,7 @@ QVector<MailItem> ImapSynchronizer::parseFetchResponse(const QStringList& lines)
// Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines
QString bodyData;
int bodyPos = line.indexOf(QStringLiteral("BODY["));
int bodyPos = line.indexOf(QStringLiteral("BODY[]"));
if (bodyPos != -1) {
int bracePos = line.indexOf('{', bodyPos);
if (bracePos != -1) {
@@ -0,0 +1,649 @@
#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"
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;
}
bool ImapSynchronizer::syncFolder(const Folder& folder)
{
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// Select folder
QString selectCmd = QString("SELECT \"%1\"").arg(folder.name());
QString selectResp;
if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(" OK ")) {
qWarning() << "SELECT failed for" << folder.name();
return false;
}
// Get all UIDs
QString searchCmd = "UID SEARCH ALL";
QString searchResp;
if (!conn.sendCommandWait(searchCmd, searchResp, 30000) || !searchResp.contains(" OK ")) {
qWarning() << "SEARCH failed";
return false;
}
QVector<qint64> uids;
QStringList lines = searchResp.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 no messages, set unread count to 0 and exit
if (uids.isEmpty()) {
Folder f = folder;
f.setUnreadCount(0);
FolderDao::update(f);
conn.disconnect();
return true;
}
// Get flags for all UIDs in one command
QStringList uidStrs;
for (qint64 uid : uids) uidStrs.append(QString::number(uid));
QString fetchCmd = "UID FETCH " + uidStrs.join(',') + " (FLAGS)";
QString fetchResp;
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000) || !fetchResp.contains(" OK ")) {
qWarning() << "FETCH FLAGS failed";
conn.disconnect();
return false;
}
// Parse flags and update local DB
QMap<qint64, QPair<bool,bool>> flagsMap; // uid -> (seen, flagged)
QRegularExpression flagsRx(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("\\Seen");
bool flagged = flags.contains("\\Flagged");
flagsMap[uid] = qMakePair(seen, flagged);
}
// Update local items
auto localItems = MailItemDao::findByFolderId(folder.id());
for (MailItem& item : localItems) {
if (flagsMap.contains(item.uid())) {
bool seen = flagsMap[item.uid()].first;
bool flagged = flagsMap[item.uid()].second;
if (item.isRead() != seen || item.isFlagged() != flagged) {
item.setRead(seen);
item.setFlagged(flagged);
MailItemDao::update(item);
}
}
}
// Count unread
int unread = 0;
for (const MailItem& item : localItems) {
if (!item.isRead()) unread++;
}
Folder f = folder;
f.setUnreadCount(unread);
FolderDao::update(f);
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;
}
@@ -0,0 +1,649 @@
#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"
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;
}
bool ImapSynchronizer::syncFolder(const Folder& folder)
{
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// Select folder
QString selectCmd = QString("SELECT \"%1\"").arg(folder.name());
QString selectResp;
if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(" OK ")) {
qWarning() << "SELECT failed for" << folder.name();
return false;
}
// Get all UIDs
QString searchCmd = "UID SEARCH ALL";
QString searchResp;
if (!conn.sendCommandWait(searchCmd, searchResp, 30000) || !searchResp.contains(" OK ")) {
qWarning() << "SEARCH failed";
return false;
}
QVector<qint64> uids;
QStringList lines = searchResp.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 no messages, set unread count to 0 and exit
if (uids.isEmpty()) {
Folder f = folder;
f.setUnreadCount(0);
FolderDao::update(f);
conn.disconnect();
return true;
}
// Get flags for all UIDs in one command
QStringList uidStrs;
for (qint64 uid : uids) uidStrs.append(QString::number(uid));
QString fetchCmd = "UID FETCH " + uidStrs.join(',') + " (FLAGS)";
QString fetchResp;
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000) || !fetchResp.contains(" OK ")) {
qWarning() << "FETCH FLAGS failed";
conn.disconnect();
return false;
}
// Parse flags and update local DB
QMap<qint64, QPair<bool,bool>> flagsMap; // uid -> (seen, flagged)
QRegularExpression flagsRx(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("\\Seen");
bool flagged = flags.contains("\\Flagged");
flagsMap[uid] = qMakePair(seen, flagged);
}
// Update local items
auto localItems = MailItemDao::findByFolderId(folder.id());
for (MailItem& item : localItems) {
if (flagsMap.contains(item.uid())) {
bool seen = flagsMap[item.uid()].first;
bool flagged = flagsMap[item.uid()].second;
if (item.isRead() != seen || item.isFlagged() != flagged) {
item.setRead(seen);
item.setFlagged(flagged);
MailItemDao::update(item);
}
}
}
// Count unread
int unread = 0;
for (const MailItem& item : localItems) {
if (!item.isRead()) unread++;
}
Folder f = folder;
f.setUnreadCount(unread);
FolderDao::update(f);
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;
}
+723
View File
@@ -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.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] 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;
}
+723
View File
@@ -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;
}
@@ -0,0 +1,649 @@
#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"
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;
}
bool ImapSynchronizer::syncFolder(const Folder& folder)
{
ImapConnection conn;
if (!connectAndLogin(conn)) return false;
// Select folder
QString selectCmd = QString("SELECT \"%1\"").arg(folder.name());
QString selectResp;
if (!conn.sendCommandWait(selectCmd, selectResp, 30000) || !selectResp.contains(" OK ")) {
qWarning() << "SELECT failed for" << folder.name();
return false;
}
// Get all UIDs
QString searchCmd = "UID SEARCH ALL";
QString searchResp;
if (!conn.sendCommandWait(searchCmd, searchResp, 30000) || !searchResp.contains(" OK ")) {
qWarning() << "SEARCH failed";
return false;
}
QVector<qint64> uids;
QStringList lines = searchResp.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 no messages, set unread count to 0 and exit
if (uids.isEmpty()) {
Folder f = folder;
f.setUnreadCount(0);
FolderDao::update(f);
conn.disconnect();
return true;
}
// Get flags for all UIDs in one command
QStringList uidStrs;
for (qint64 uid : uids) uidStrs.append(QString::number(uid));
QString fetchCmd = "UID FETCH " + uidStrs.join(',') + " (FLAGS)";
QString fetchResp;
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000) || !fetchResp.contains(" OK ")) {
qWarning() << "FETCH FLAGS failed";
conn.disconnect();
return false;
}
// Parse flags and update local DB
QMap<qint64, QPair<bool,bool>> flagsMap; // uid -> (seen, flagged)
QRegularExpression flagsRx(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("\\Seen");
bool flagged = flags.contains("\\Flagged");
flagsMap[uid] = qMakePair(seen, flagged);
}
// Update local items
auto localItems = MailItemDao::findByFolderId(folder.id());
for (MailItem& item : localItems) {
if (flagsMap.contains(item.uid())) {
bool seen = flagsMap[item.uid()].first;
bool flagged = flagsMap[item.uid()].second;
if (item.isRead() != seen || item.isFlagged() != flagged) {
item.setRead(seen);
item.setFlagged(flagged);
MailItemDao::update(item);
}
}
}
// Count unread
int unread = 0;
for (const MailItem& item : localItems) {
if (!item.isRead()) unread++;
}
Folder f = folder;
f.setUnreadCount(unread);
FolderDao::update(f);
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;
}
+15 -7
View File
@@ -2,8 +2,8 @@
#include "../synchronizer.h"
#include <QObject>
#include <QSslSocket>
#include <QStringList>
#include "imapconnection.h"
class ImapSynchronizer : public Synchronizer
{
@@ -30,20 +30,28 @@ signals:
void statusMessage(const QString& message) const;
private:
// Connection details
QVector<qint64> parseUidSearchResponse(const QString& response) const;
// Connection details (set by initialize)
mutable QString m_host;
mutable quint16 m_port{0};
mutable bool m_useSsl{false};
mutable QString m_username;
mutable QString m_password;
mutable QString m_authMethod; // "plain" or "oauth2"
mutable QByteArray m_capabilities;
// Helper para generar IDs únicos de eventos
QString generateEventId() const;
// IMAP command helpers
bool sendCommand(QSslSocket& socket, const QString& command, QString& response) const;
bool waitForResponse(QSslSocket& socket, const QString& expectedTag, QString& response) const;
QVector<Folder> parseListResponse(const QString& response) const;
// Helper: conecta y loguea al servidor (reutilizable)
bool connectAndLogin(ImapConnection &conn) const;
// IMAP response parsers
QVector<Folder> parseListResponse(const QStringList& lines) const;
QVector<MailItem> parseFetchResponse(const QStringList& lines) const;
QVector<MailItem> parseFetchResponseBytes(const QByteArray& response) const;
bool persistFetchedItem(const Folder& folder, MailItem& item) const;
// Sync folder helpers
QVector<qint64> fetchAllUids(const QString& folderId) const;
void parseAndUpdateFlags(const QByteArray& response, int folderId) const;
};
@@ -0,0 +1,50 @@
#pragma once
#include "../synchronizer.h"
#include <QObject>
#include <QStringList>
#include "imapconnection.h"
class ImapSynchronizer : public Synchronizer
{
Q_OBJECT
public:
explicit ImapSynchronizer(QObject* parent = nullptr);
~ImapSynchronizer() override = default;
// Synchronizer interface
bool initialize(const Account& account) override;
bool syncFolder(const Folder& folder) override;
QVector<Folder> getFolders() const override;
QVector<MailItem> 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 QString m_host;
mutable quint16 m_port{0};
mutable bool m_useSsl{false};
mutable QString m_username;
mutable QString m_password;
mutable QString m_authMethod; // "plain" or "oauth2"
// Helper para generar IDs únicos de eventos
QString generateEventId() const;
// Helper: conecta y loguea al servidor (reutilizable)
bool connectAndLogin(ImapConnection &conn) const;
// IMAP response parsers
QVector<Folder> parseListResponse(const QStringList& lines) const;
QVector<MailItem> parseFetchResponse(const QStringList& lines) const;
};
+388 -10
View File
@@ -3,6 +3,302 @@
#include <QFutureWatcher>
#include <QtConcurrent/QtConcurrent>
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QMimeDatabase>
#include <QRegularExpression>
#include <QDateTime>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QEventLoop>
#include <QUrl>
#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<unsigned char>(c) < 32 || static_cast<unsigned char>(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<QByteArray> 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)
@@ -18,13 +314,43 @@ QVector<MailItem> MailService::getMails(const QString &folderId)
void MailService::sendMail(const MailItem &mail, const QString &accountId)
{
// For now we just simulate sending by using EmailComposerBridge
// In a real implementation, this would use SMTP settings from the account.
Q_UNUSED(mail);
Q_UNUSED(accountId);
// Emit success immediately; actual sending would be asynchronous.
QMetaObject::invokeMethod(this, "mailSent", Qt::QueuedConnection,
Q_ARG(QString, QString()));
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<bool> 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<bool>(this);
connect(watcher, &QFutureWatcher<bool>::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)
@@ -35,6 +361,10 @@ void MailService::fetchMails(const QString &accountId, const QString &folderId)
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()) {
@@ -65,10 +395,12 @@ void MailService::fetchMails(const QString &accountId, const QString &folderId)
// Initialize synchronizer with account details before using it
if (!sync->initialize(*acc)) {
delete acc;
emit mailFetchError(accountId, folderId, tr("Failed to initialize synchronizer"));
sync->deleteLater();
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);
@@ -85,12 +417,52 @@ void MailService::fetchMails(const QString &accountId, const QString &folderId)
QObject::disconnect(sync, SIGNAL(progressChanged(int)), this, SIGNAL(progressChanged(int)));
QObject::disconnect(sync, SIGNAL(statusMessage(const QString&)), this, SIGNAL(statusMessage(const QString&)));
QVector<MailItem> items = watcher->result();
QVector<MailItem> persisted;
for (MailItem &item : items) {
if (persistFetchedItem(item, accountId, folderId)) persisted.append(item);
}
watcher->deleteLater();
emit mailFetched(accountId, folderId, items);
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<QString> 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<StoredAttachmentRecord> 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;
@@ -109,9 +481,15 @@ void MailService::deleteMail(const QString &mailItemId)
bool ok;
qint64 id = mailItemId.toLongLong(&ok);
if (!ok) return;
if (MailItemDao::remove(id))
const std::optional<MailItem> 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)
{
+129
View File
@@ -0,0 +1,129 @@
#include "mailservice.h"
#include <QFuture>
#include <QFutureWatcher>
#include <QtConcurrent/QtConcurrent>
#include <QDebug>
MailService::MailService(AccountService *accountService, QObject *parent)
: QObject(parent)
, m_composer(new EmailComposerBridge(this))
, m_accountService(accountService)
{
}
QVector<MailItem> MailService::getMails(const QString &folderId)
{
return MailItemDao::findByFolderId(folderId.toInt());
}
void MailService::sendMail(const MailItem &mail, const QString &accountId)
{
// For now we just simulate sending by using EmailComposerBridge
// In a real implementation, this would use SMTP settings from the account.
Q_UNUSED(mail);
Q_UNUSED(accountId);
// Emit success immediately; actual sending would be asynchronous.
QMetaObject::invokeMethod(this, "mailSent", Qt::QueuedConnection,
Q_ARG(QString, QString()));
}
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());
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)) {
emit mailFetchError(accountId, folderId, tr("Failed to initialize synchronizer"));
sync->deleteLater();
return;
}
// 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<QVector<MailItem>> future = QtConcurrent::run([=]() {
// SinceUid = 0 for full sync; could be stored per folder but omitted for simplicity
return sync->fetchMailItems(folderId, 0);
});
QFutureWatcher<QVector<MailItem>> *watcher = new QFutureWatcher<QVector<MailItem>>(this);
QObject::connect(watcher, &QFutureWatcher<QVector<MailItem>>::finished, this, [=]() {
// 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<MailItem> items = watcher->result();
watcher->deleteLater();
emit mailFetched(accountId, folderId, items);
});
watcher->setFuture(future);
}
void MailService::moveMail(const QString &mailItemId, const QString &targetFolderId)
{
bool ok;
qint64 id = mailItemId.toLongLong(&ok);
if (!ok) return;
std::optional<MailItem> 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;
if (MailItemDao::remove(id))
emit mailDeleted(mailItemId);
}
void MailService::markAsRead(const QString &mailItemId, bool read)
{
bool ok;
qint64 id = mailItemId.toLongLong(&ok);
if (!ok) return;
std::optional<MailItem> opt = MailItemDao::findById(id);
if (!opt) return;
MailItem item = *opt;
item.setRead(read);
if (MailItemDao::update(item))
emit mailReadStateChanged(mailItemId, read);
}
#include "mailservice.moc"
+4
View File
@@ -22,6 +22,8 @@ public:
QVector<MailItem> getMails(const QString &folderId);
void sendMail(const MailItem &mail, const QString &accountId);
void sendMail(const MailItem &mail, const QString &accountId,
const QStringList &attachmentPaths);
void fetchMails(const QString &accountId, const QString &folderId);
void moveMail(const QString &mailItemId, const QString &targetFolderId);
void deleteMail(const QString &mailItemId);
@@ -40,6 +42,8 @@ signals:
void statusMessage(const QString &message);
private:
bool persistFetchedItem(MailItem &item, const QString &accountId,
const QString &folderId);
EmailComposerBridge *m_composer;
AccountService *m_accountService;
};
+296 -53
View File
@@ -2,6 +2,14 @@
#include <QStandardPaths>
#include <QDebug>
#include <QUuid>
#include <QCryptographicHash>
#include <QRegularExpression>
#include <QTextStream>
#include <QFileInfo>
#include <QUrl>
#include <QDateTime>
#include <QSaveFile>
#include <QMap>
MimeStorageService::MimeStorageService(QObject *parent)
: QObject(parent)
@@ -23,53 +31,99 @@ QString MimeStorageService::ensureDirectory(const QString &path) const
return dir.absolutePath();
}
QString MimeStorageService::safeComponent(const QString &value) const
{
QString result = value;
result.replace(QRegularExpression(QStringLiteral("[^A-Za-z0-9._-]")), QStringLiteral("_"));
if (result.isEmpty() || result == QStringLiteral(".") || result == QStringLiteral(".."))
result = QStringLiteral("unknown");
return result.left(180);
}
QString MimeStorageService::saveEmlFile(const QString &accountId, const QString &folderId, const MailItem &mail)
{
QString dir = storagePath() + "/" + accountId + "/" + folderId;
ensureDirectory(dir);
QString fileName = mail.messageId();
if (fileName.isEmpty()) {
fileName = QUuid::createUuid().toString(QUuid::WithoutBraces);
QByteArray raw;
raw.append("From: ").append(mail.sender().toUtf8()).append("\r\n");
raw.append("To: ").append(mail.recipient().toUtf8()).append("\r\n");
if (!mail.cc().isEmpty()) raw.append("Cc: ").append(mail.cc().toUtf8()).append("\r\n");
if (!mail.bcc().isEmpty()) raw.append("Bcc: ").append(mail.bcc().toUtf8()).append("\r\n");
raw.append("Subject: ").append(mail.subject().toUtf8()).append("\r\n");
raw.append("Date: ").append(mail.date().toString(Qt::RFC2822Date).toUtf8()).append("\r\n");
raw.append("MIME-Version: 1.0\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n");
raw.append(mail.bodyHtml().toUtf8()).append("\r\n");
return saveRawEmlFile(accountId, folderId, raw, mail.messageId());
}
fileName.replace('/', '_').replace('\\', '_');
QString filePath = dir + "/" + fileName + ".eml";
// Write a basic .eml structure
QFile file(filePath);
if (file.open(QIODevice::WriteOnly)) {
QTextStream stream(&file);
stream << "From: " << mail.sender() << "\r\n";
stream << "To: " << mail.recipient() << "\r\n";
stream << "Subject: " << mail.subject() << "\r\n";
stream << "Date: " << mail.date().toString(Qt::RFC2822Date) << "\r\n";
stream << "MIME-Version: 1.0\r\n";
stream << "Content-Type: text/html; charset=UTF-8\r\n";
stream << "\r\n";
stream << mail.bodyHtml() << "\r\n";
file.close();
qDebug() << "[MimeStorage] Saved .eml:" << filePath;
return fileName;
}
qWarning() << "[MimeStorage] Failed to save .eml:" << filePath;
QString MimeStorageService::saveRawEmlFile(const QString &accountId, const QString &folderId,
const QByteArray &rawMime, const QString &stableId)
{
if (rawMime.isEmpty()) return QString();
const QString accountPart = safeComponent(accountId);
const QString folderPart = safeComponent(folderId);
const QByteArray digest = QCryptographicHash::hash(rawMime, QCryptographicHash::Sha256).toHex();
const QString fileName = safeComponent(stableId.isEmpty()
? QString::fromLatin1(digest)
: stableId) + QStringLiteral("_") + QString::fromLatin1(digest.left(12)) + QStringLiteral(".eml");
const QString relative = accountPart + "/" + folderPart + "/" + fileName;
const QString path = QDir(storagePath()).filePath(relative);
ensureDirectory(QFileInfo(path).absolutePath());
QSaveFile file(path);
if (!file.open(QIODevice::WriteOnly) || file.write(rawMime) != rawMime.size() || !file.commit()) {
qWarning() << "[MimeStorage] Failed to save raw MIME:" << path << file.errorString();
return QString();
}
return relative.left(relative.size() - 4); // fileId excludes .eml
}
bool MimeStorageService::storeMessage(const QString &accountId, const QString &folderId,
MailItem &mail, const QByteArray &rawMime,
QVector<QString> *storedAttachmentPaths)
{
ParsedMimeMessage parsed;
if (!parseMessage(rawMime, parsed)) return false;
if (!parsed.messageId.isEmpty()) mail.setMessageId(parsed.messageId);
if (!parsed.subject.isEmpty()) mail.setSubject(parsed.subject);
if (!parsed.from.isEmpty()) mail.setSender(parsed.from);
if (!parsed.to.isEmpty()) { mail.setTo(parsed.to); mail.setRecipient(parsed.to); }
if (!parsed.cc.isEmpty()) mail.setCc(parsed.cc);
if (!parsed.bcc.isEmpty()) mail.setBcc(parsed.bcc);
if (parsed.date.isValid()) mail.setDate(parsed.date);
mail.setBodyHtml(parsed.bodyHtml);
mail.setSize(rawMime.size());
mail.setRawMime(rawMime);
const QString fileId = saveRawEmlFile(accountId, folderId, rawMime, mail.messageId());
if (fileId.isEmpty()) return false;
mail.setFileId(fileId);
QVector<QString> names;
QVector<QString> savedPaths;
for (const ParsedMimeAttachment &attachment : parsed.attachments) {
const QString path = saveAttachment(fileId, attachment.fileName, attachment.data);
if (path.isEmpty()) {
for (const QString &savedPath : savedPaths) QFile::remove(savedPath);
deleteEmlFile(fileId);
return false;
}
savedPaths.append(path);
names.append(attachment.fileName);
if (storedAttachmentPaths) storedAttachmentPaths->append(path);
}
mail.setAttachments(names);
return true;
}
QString MimeStorageService::getEmlFilePath(const QString &fileId) const
{
// Search in all account/folder directories (simplified)
QDir baseDir(storagePath());
QStringList filters;
filters << fileId + ".eml";
QFileInfoList files = baseDir.entryInfoList(filters, QDir::Files, QDir::Name);
for (const auto &info : files) {
QStringList parts = info.absoluteFilePath().split('/');
if (parts.size() >= 2) {
return info.absoluteFilePath();
}
}
return QString();
QString relative = fileId;
if (relative.endsWith(QStringLiteral(".eml"))) relative.chop(4);
const QString base = QDir::cleanPath(storagePath());
const QString path = QDir::cleanPath(QDir(base).filePath(relative + QStringLiteral(".eml")));
if (!path.startsWith(base + QDir::separator()) || !QFileInfo::exists(path)) return QString();
return path;
}
QByteArray MimeStorageService::readEmlFile(const QString &fileId) const
@@ -88,29 +142,218 @@ bool MimeStorageService::deleteEmlFile(const QString &fileId)
{
QString path = getEmlFilePath(fileId);
if (path.isEmpty()) return false;
return QFile::remove(path);
const bool removed = QFile::remove(path);
QDir attachments(QFileInfo(path).absolutePath() + QStringLiteral("/attachments"));
if (attachments.exists()) attachments.removeRecursively();
return removed;
}
QStringList MimeStorageService::listAttachments(const QString &mailItemId) const
{
Q_UNUSED(mailItemId);
// TODO: Parse .eml to get attachments when GMime is available
return {};
const QString emlPath = getEmlFilePath(mailItemId);
if (emlPath.isEmpty()) return {};
QDir dir(QFileInfo(emlPath).absolutePath() + QStringLiteral("/attachments"));
return dir.entryList(QDir::Files, QDir::Name);
}
bool MimeStorageService::saveAttachment(const QString &mailItemId, const QString &fileName, const QByteArray &data)
QString MimeStorageService::saveAttachment(const QString &fileId, const QString &fileName, const QByteArray &data)
{
Q_UNUSED(mailItemId);
QString dir = storagePath() + "/attachments";
ensureDirectory(dir);
QFile file(dir + "/" + fileName);
if (file.open(QIODevice::WriteOnly)) {
file.write(data);
file.close();
return true;
const QString emlPath = getEmlFilePath(fileId);
if (emlPath.isEmpty()) return QString();
const QString dir = ensureDirectory(QFileInfo(emlPath).absolutePath() + QStringLiteral("/attachments"));
const QString originalName = QFileInfo(fileName).fileName();
const QString safeName = safeComponent(originalName.isEmpty()
? QStringLiteral("attachment") : originalName);
QString path = QDir(dir).filePath(safeName);
if (QFileInfo::exists(path)) {
const QFileInfo info(path);
const QString suffix = info.completeSuffix().isEmpty()
? QString() : QStringLiteral(".") + info.completeSuffix();
const QString stem = info.completeBaseName();
path = QDir(dir).filePath(stem + QStringLiteral("_")
+ QString::fromLatin1(QCryptographicHash::hash(data, QCryptographicHash::Sha256).toHex().left(12))
+ suffix);
}
return false;
QSaveFile file(path);
if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.commit()) return QString();
return path;
}
QString MimeStorageService::decodeHeaderValue(const QString &value) const
{
QString result = value;
const QRegularExpression rx(QStringLiteral("=\\?([^?]+)\\?([bBqQ])\\?([^?]+)\\?="));
QRegularExpressionMatchIterator it = rx.globalMatch(result);
while (it.hasNext()) {
const QRegularExpressionMatch match = it.next();
QByteArray encoded = match.captured(3).toLatin1();
QByteArray decoded;
if (match.captured(2).compare(QStringLiteral("b"), Qt::CaseInsensitive) == 0)
decoded = QByteArray::fromBase64(encoded);
else {
encoded.replace('_', ' ');
QByteArray out;
for (int i = 0; i < encoded.size(); ++i) {
if (encoded[i] == '=' && i + 2 < encoded.size()) {
bool ok = false;
const int value = QByteArray(encoded.mid(i + 1, 2)).toInt(&ok, 16);
if (ok) { out.append(char(value)); i += 2; continue; }
}
out.append(encoded[i]);
}
decoded = out;
}
QString decodedText = QString::fromUtf8(decoded);
if (decodedText.isEmpty() && !decoded.isEmpty()) decodedText = QString::fromLatin1(decoded);
result.replace(match.captured(0), decodedText);
}
return result.trimmed();
}
QByteArray MimeStorageService::decodeTransfer(const QByteArray &data, const QString &encoding) const
{
const QString normalized = encoding.trimmed().toLower();
if (normalized == QStringLiteral("base64")) return QByteArray::fromBase64(data);
if (normalized != QStringLiteral("quoted-printable")) return data;
QByteArray result;
for (int i = 0; i < data.size(); ++i) {
if (data[i] == '=' && i + 1 < data.size()) {
if (data[i + 1] == '\r' && i + 2 < data.size() && data[i + 2] == '\n') { i += 2; continue; }
if (data[i + 1] == '\n') { ++i; continue; }
if (i + 2 < data.size()) {
bool ok = false;
const int value = QByteArray(data.mid(i + 1, 2)).toInt(&ok, 16);
if (ok) { result.append(char(value)); i += 2; continue; }
}
}
result.append(data[i]);
}
return result;
}
void MimeStorageService::parseMimePart(const QByteArray &part, ParsedMimeMessage &message,
QString *plainText, QString *htmlText) const
{
const int crlf = part.indexOf("\r\n\r\n");
const int lf = part.indexOf("\n\n");
int split = crlf >= 0 ? crlf : lf;
const int separatorSize = crlf >= 0 ? 4 : 2;
const QByteArray headerBytes = split >= 0 ? part.left(split) : part;
const QByteArray body = split >= 0 ? part.mid(split + separatorSize) : QByteArray();
QMap<QString, QString> headers;
QString current;
for (QString line : QString::fromLatin1(headerBytes).split(QRegularExpression("\\r?\\n"))) {
if ((line.startsWith(' ') || line.startsWith('\t')) && !current.isEmpty()) {
headers[current] += QStringLiteral(" ") + line.trimmed();
continue;
}
const int colon = line.indexOf(':');
if (colon < 0) continue;
current = line.left(colon).trimmed().toLower();
headers[current] = line.mid(colon + 1).trimmed();
}
auto header = [&](const QString &name) { return headers.value(name.toLower()); };
const QString contentType = header(QStringLiteral("content-type"));
const QString disposition = header(QStringLiteral("content-disposition"));
const QString transfer = header(QStringLiteral("content-transfer-encoding"));
QRegularExpression boundaryRx(QStringLiteral("(?:^|;)\\s*boundary\\s*=\\s*(?:\\\"([^\\\"]+)\\\"|([^;\\s]+))"), QRegularExpression::CaseInsensitiveOption);
const QRegularExpressionMatch boundaryMatch = boundaryRx.match(contentType);
if (contentType.startsWith(QStringLiteral("multipart/"), Qt::CaseInsensitive) && boundaryMatch.hasMatch()) {
const QString boundary = boundaryMatch.captured(1).isEmpty() ? boundaryMatch.captured(2) : boundaryMatch.captured(1);
const QByteArray marker = QByteArrayLiteral("--") + boundary.toUtf8();
int pos = 0;
while ((pos = body.indexOf(marker, pos)) >= 0) {
int start = pos + marker.size();
if (body.mid(start, 2) == QByteArrayLiteral("--")) break;
if (body.mid(start, 2) == QByteArrayLiteral("\r\n")) start += 2;
else if (body.mid(start, 1) == QByteArrayLiteral("\n")) ++start;
int end = body.indexOf(marker, start);
if (end < 0) end = body.size();
QByteArray child = body.mid(start, end - start);
while (child.endsWith("\r\n") || child.endsWith('\n')) child.chop(child.endsWith("\r\n") ? 2 : 1);
parseMimePart(child, message, plainText, htmlText);
pos = end;
}
return;
}
const QByteArray decoded = decodeTransfer(body, transfer);
QRegularExpression parameterRx(QStringLiteral("(?:^|;)\\s*(?:filename|name)(?:\\*\\d+)?\\*?\\s*=\\s*(?:\\\"([^\\\"]+)\\\"|([^;\\s]+))"), QRegularExpression::CaseInsensitiveOption);
const QRegularExpressionMatch dispositionMatch = parameterRx.match(disposition);
const QRegularExpressionMatch typeMatch = parameterRx.match(contentType);
QString fileName = dispositionMatch.hasMatch()
? (dispositionMatch.captured(1).isEmpty() ? dispositionMatch.captured(2) : dispositionMatch.captured(1))
: (typeMatch.hasMatch() ? (typeMatch.captured(1).isEmpty() ? typeMatch.captured(2) : typeMatch.captured(1)) : QString());
const int encodedPrefix = fileName.indexOf(QStringLiteral("''"));
if (encodedPrefix >= 0) fileName = fileName.mid(encodedPrefix + 2);
fileName = QUrl::fromPercentEncoding(fileName.toUtf8());
fileName = decodeHeaderValue(fileName);
const QString mimeType = contentType.section(';', 0, 0).trimmed().toLower();
const bool isAttachment = disposition.startsWith(QStringLiteral("attachment"), Qt::CaseInsensitive)
|| !fileName.isEmpty() || (!mimeType.startsWith(QStringLiteral("text/")) && !mimeType.isEmpty());
if (isAttachment) {
ParsedMimeAttachment attachment;
attachment.fileName = fileName.isEmpty() ? QStringLiteral("attachment-%1").arg(message.attachments.size() + 1) : fileName;
attachment.mimeType = mimeType;
attachment.contentId = header(QStringLiteral("content-id")).trimmed();
attachment.contentId.remove('<');
attachment.contentId.remove('>');
attachment.data = decoded;
message.attachments.append(attachment);
return;
}
const QString text = QString::fromUtf8(decoded);
if (mimeType == QStringLiteral("text/html")) {
if (htmlText && htmlText->isEmpty()) *htmlText = text;
} else if (mimeType == QStringLiteral("text/plain") || mimeType.isEmpty()) {
if (plainText && plainText->isEmpty()) *plainText = text;
}
}
bool MimeStorageService::parseMessage(const QByteArray &rawMime, ParsedMimeMessage &message) const
{
if (rawMime.isEmpty()) return false;
message = ParsedMimeMessage();
QString plainText;
QString htmlText;
parseMimePart(rawMime, message, &plainText, &htmlText);
auto parseHeaders = [&](const QByteArray &data) {
const int split = data.indexOf("\r\n\r\n") >= 0 ? data.indexOf("\r\n\r\n") : data.indexOf("\n\n");
const QByteArray headerData = split >= 0 ? data.left(split) : data;
QMap<QString, QString> headers;
QString current;
for (QString line : QString::fromLatin1(headerData).split(QRegularExpression("\\r?\\n"))) {
if ((line.startsWith(' ') || line.startsWith('\t')) && !current.isEmpty()) { headers[current] += " " + line.trimmed(); continue; }
const int colon = line.indexOf(':');
if (colon < 0) continue;
current = line.left(colon).trimmed().toLower();
headers[current] = line.mid(colon + 1).trimmed();
}
return headers;
};
const QMap<QString, QString> headers = parseHeaders(rawMime);
message.messageId = decodeHeaderValue(headers.value(QStringLiteral("message-id"))).remove('<').remove('>');
message.subject = decodeHeaderValue(headers.value(QStringLiteral("subject")));
message.from = decodeHeaderValue(headers.value(QStringLiteral("from")));
message.to = decodeHeaderValue(headers.value(QStringLiteral("to")));
message.cc = decodeHeaderValue(headers.value(QStringLiteral("cc")));
message.bcc = decodeHeaderValue(headers.value(QStringLiteral("bcc")));
message.date = QDateTime::fromString(headers.value(QStringLiteral("date")), Qt::RFC2822Date);
if (!message.date.isValid()) message.date = QDateTime::currentDateTimeUtc();
if (!htmlText.isEmpty()) message.bodyHtml = htmlText;
else {
QString escaped = plainText.toHtmlEscaped();
escaped.replace(QStringLiteral("\r\n"), QStringLiteral("<br>"));
escaped.replace(QStringLiteral("\n"), QStringLiteral("<br>"));
message.bodyHtml = escaped;
}
return true;
}
#include "mimestorage.moc"
+36 -1
View File
@@ -5,8 +5,31 @@
#include <QString>
#include <QDir>
#include <QFile>
#include <QByteArray>
#include <QVector>
#include "core/mailitem.h"
struct ParsedMimeAttachment
{
QString fileName;
QString mimeType;
QString contentId;
QByteArray data;
};
struct ParsedMimeMessage
{
QString messageId;
QString subject;
QString from;
QString to;
QString cc;
QString bcc;
QDateTime date;
QString bodyHtml;
QVector<ParsedMimeAttachment> attachments;
};
class MimeStorageService : public QObject
{
Q_OBJECT
@@ -15,16 +38,28 @@ public:
~MimeStorageService() override = default;
QString saveEmlFile(const QString &accountId, const QString &folderId, const MailItem &mail);
QString saveRawEmlFile(const QString &accountId, const QString &folderId,
const QByteArray &rawMime, const QString &stableId = QString());
bool storeMessage(const QString &accountId, const QString &folderId,
MailItem &mail, const QByteArray &rawMime,
QVector<QString> *storedAttachmentPaths = nullptr);
bool parseMessage(const QByteArray &rawMime, ParsedMimeMessage &message) const;
QString getEmlFilePath(const QString &fileId) const;
QByteArray readEmlFile(const QString &fileId) const;
bool deleteEmlFile(const QString &fileId);
QStringList listAttachments(const QString &mailItemId) const;
bool saveAttachment(const QString &mailItemId, const QString &fileName, const QByteArray &data);
QString saveAttachment(const QString &fileId, const QString &fileName,
const QByteArray &data);
private:
QString storagePath() const;
QString ensureDirectory(const QString &path) const;
QString safeComponent(const QString &value) const;
QString decodeHeaderValue(const QString &value) const;
QByteArray decodeTransfer(const QByteArray &data, const QString &encoding) const;
void parseMimePart(const QByteArray &part, ParsedMimeMessage &message,
QString *plainText, QString *htmlText) const;
};
#endif // MIMESTORAGESERVICE_H
+165
View File
@@ -0,0 +1,165 @@
/****************************************************************************
** Meta object code from reading C++ file 'accountservice.h'
**
** Created by: The Qt Meta Object Compiler version 69 (Qt 6.10.2)
**
** WARNING! All changes made in this file will be lost!
*****************************************************************************/
#include "accountservice.h"
#include <QtCore/qmetatype.h>
#include <QtCore/qtmochelpers.h>
#include <memory>
#include <QtCore/qxptype_traits.h>
#if !defined(Q_MOC_OUTPUT_REVISION)
#error "The header file 'accountservice.h' doesn't include <QObject>."
#elif Q_MOC_OUTPUT_REVISION != 69
#error "This file was generated using the moc from 6.10.2. It"
#error "cannot be used with the include files from this version of Qt."
#error "(The moc has changed too much.)"
#endif
#ifndef Q_CONSTINIT
#define Q_CONSTINIT
#endif
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
QT_WARNING_DISABLE_GCC("-Wuseless-cast")
namespace {
struct qt_meta_tag_ZN14AccountServiceE_t {};
} // unnamed namespace
template <> constexpr inline auto AccountService::qt_create_metaobjectdata<qt_meta_tag_ZN14AccountServiceE_t>()
{
namespace QMC = QtMocConstants;
QtMocHelpers::StringRefStorage qt_stringData {
"AccountService",
"accountListChanged",
"",
"accountAdded",
"Account",
"account",
"accountRemoved",
"accountId",
"authenticationRequired",
"email",
"authUrl"
};
QtMocHelpers::UintData qt_methods {
// Signal 'accountListChanged'
QtMocHelpers::SignalData<void()>(1, 2, QMC::AccessPublic, QMetaType::Void),
// Signal 'accountAdded'
QtMocHelpers::SignalData<void(const Account &)>(3, 2, QMC::AccessPublic, QMetaType::Void, {{
{ 0x80000000 | 4, 5 },
}}),
// Signal 'accountRemoved'
QtMocHelpers::SignalData<void(int)>(6, 2, QMC::AccessPublic, QMetaType::Void, {{
{ QMetaType::Int, 7 },
}}),
// Signal 'authenticationRequired'
QtMocHelpers::SignalData<void(const QString &, const QString &)>(8, 2, QMC::AccessPublic, QMetaType::Void, {{
{ QMetaType::QString, 9 }, { QMetaType::QString, 10 },
}}),
};
QtMocHelpers::UintData qt_properties {
};
QtMocHelpers::UintData qt_enums {
};
return QtMocHelpers::metaObjectData<AccountService, qt_meta_tag_ZN14AccountServiceE_t>(QMC::MetaObjectFlag{}, qt_stringData,
qt_methods, qt_properties, qt_enums);
}
Q_CONSTINIT const QMetaObject AccountService::staticMetaObject = { {
QMetaObject::SuperData::link<QObject::staticMetaObject>(),
qt_staticMetaObjectStaticContent<qt_meta_tag_ZN14AccountServiceE_t>.stringdata,
qt_staticMetaObjectStaticContent<qt_meta_tag_ZN14AccountServiceE_t>.data,
qt_static_metacall,
nullptr,
qt_staticMetaObjectRelocatingContent<qt_meta_tag_ZN14AccountServiceE_t>.metaTypes,
nullptr
} };
void AccountService::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
auto *_t = static_cast<AccountService *>(_o);
if (_c == QMetaObject::InvokeMetaMethod) {
switch (_id) {
case 0: _t->accountListChanged(); break;
case 1: _t->accountAdded((*reinterpret_cast<std::add_pointer_t<Account>>(_a[1]))); break;
case 2: _t->accountRemoved((*reinterpret_cast<std::add_pointer_t<int>>(_a[1]))); break;
case 3: _t->authenticationRequired((*reinterpret_cast<std::add_pointer_t<QString>>(_a[1])),(*reinterpret_cast<std::add_pointer_t<QString>>(_a[2]))); break;
default: ;
}
}
if (_c == QMetaObject::IndexOfMethod) {
if (QtMocHelpers::indexOfMethod<void (AccountService::*)()>(_a, &AccountService::accountListChanged, 0))
return;
if (QtMocHelpers::indexOfMethod<void (AccountService::*)(const Account & )>(_a, &AccountService::accountAdded, 1))
return;
if (QtMocHelpers::indexOfMethod<void (AccountService::*)(int )>(_a, &AccountService::accountRemoved, 2))
return;
if (QtMocHelpers::indexOfMethod<void (AccountService::*)(const QString & , const QString & )>(_a, &AccountService::authenticationRequired, 3))
return;
}
}
const QMetaObject *AccountService::metaObject() const
{
return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}
void *AccountService::qt_metacast(const char *_clname)
{
if (!_clname) return nullptr;
if (!strcmp(_clname, qt_staticMetaObjectStaticContent<qt_meta_tag_ZN14AccountServiceE_t>.strings))
return static_cast<void*>(this);
return QObject::qt_metacast(_clname);
}
int AccountService::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
_id = QObject::qt_metacall(_c, _id, _a);
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
if (_id < 4)
qt_static_metacall(this, _c, _id, _a);
_id -= 4;
}
if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
if (_id < 4)
*reinterpret_cast<QMetaType *>(_a[0]) = QMetaType();
_id -= 4;
}
return _id;
}
// SIGNAL 0
void AccountService::accountListChanged()
{
QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}
// SIGNAL 1
void AccountService::accountAdded(const Account & _t1)
{
QMetaObject::activate<void>(this, &staticMetaObject, 1, nullptr, _t1);
}
// SIGNAL 2
void AccountService::accountRemoved(int _t1)
{
QMetaObject::activate<void>(this, &staticMetaObject, 2, nullptr, _t1);
}
// SIGNAL 3
void AccountService::authenticationRequired(const QString & _t1, const QString & _t2)
{
QMetaObject::activate<void>(this, &staticMetaObject, 3, nullptr, _t1, _t2);
}
QT_WARNING_POP
+102 -102
View File
@@ -13,6 +13,8 @@
#include <QEventLoop>
#include "../../core/events.h"
#include "../../core/eventbus.h"
#include "db/dao/folderdao.h"
#include "services/mimestorage.h"
OutlookSynchronizer::OutlookSynchronizer(QObject* parent)
: Synchronizer(parent),
@@ -84,7 +86,6 @@ bool OutlookSynchronizer::syncFolder(const Folder& folder)
// En una implementación real, aquí compararíamos con la base de datos local
// y emitiríamos las señales apropiadas para elementos nuevos/actualizados/eliminados
// Por ahora, simulamos que obtenemos algunos elementos
if (!items.isEmpty()) {
for (const MailItem& item : items) {
emit mailItemAdded(item);
@@ -139,129 +140,124 @@ QVector<MailItem> OutlookSynchronizer::fetchMailItems(const QString& folderId,
}
}
qDebug() << "Fetching mail items for folder:" << folderId;
qDebug() << "Fetching complete Outlook messages for folder:" << folderId;
// Construir la URL para Microsoft Graph API
QString endpoint = QString("/me/mailFolders/%1/messages").arg(folderId);
QString url = buildGraphUrl(endpoint);
QString remoteFolderId = folderId;
bool localFolderId = false;
folderId.toInt(&localFolderId);
if (localFolderId) {
const auto folder = FolderDao::findById(folderId.toInt());
if (folder) {
remoteFolderId = folder->parentFolderId();
if (remoteFolderId.isEmpty()) {
const QString name = folder->name().toLower();
if (name == QStringLiteral("inbox")) remoteFolderId = QStringLiteral("inbox");
else if (name == QStringLiteral("sent") || name == QStringLiteral("sent items")) remoteFolderId = QStringLiteral("sentitems");
else if (name == QStringLiteral("drafts")) remoteFolderId = QStringLiteral("drafts");
else if (name == QStringLiteral("trash") || name == QStringLiteral("deleted items")) remoteFolderId = QStringLiteral("deleteditems");
else remoteFolderId = folder->name();
}
}
}
// Parámetros de consulta
QNetworkAccessManager network;
auto get = [&](const QUrl &url, QByteArray &data) {
QNetworkRequest request = createAuthRequest(url.toString());
request.setRawHeader("Accept", "message/rfc822, application/json");
QNetworkReply *reply = network.get(request);
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
const bool ok = reply->error() == QNetworkReply::NoError;
if (ok) data = reply->readAll();
else qWarning() << "Outlook request failed:" << reply->errorString() << url;
reply->deleteLater();
return ok;
};
QVector<QJsonObject> messageRefs;
QUrl nextUrl(buildGraphUrl(QStringLiteral("/me/mailFolders/%1/messages").arg(remoteFolderId)));
QUrlQuery query;
query.addQueryItem("$top", "50"); // Limitar a 50 mensajes por petición
query.addQueryItem("$orderby", "receivedDateTime DESC");
url += "?" + query.toString();
query.addQueryItem(QStringLiteral("$top"), QStringLiteral("100"));
query.addQueryItem(QStringLiteral("$select"), QStringLiteral("id,isRead,importance"));
query.addQueryItem(QStringLiteral("$orderby"), QStringLiteral("receivedDateTime DESC"));
nextUrl.setQuery(query);
while (nextUrl.isValid() && !nextUrl.toString().isEmpty()) {
QByteArray response;
if (!get(nextUrl, response)) return {};
QJsonParseError parseError;
const QJsonDocument document = QJsonDocument::fromJson(response, &parseError);
if (parseError.error != QJsonParseError::NoError || !document.isObject()) return {};
const QJsonObject object = document.object();
for (const QJsonValue &value : object.value(QStringLiteral("value")).toArray())
if (value.isObject()) messageRefs.append(value.toObject());
const QString continuation = object.value(QStringLiteral("@odata.nextLink")).toString();
nextUrl = continuation.isEmpty() ? QUrl() : QUrl(continuation);
}
QNetworkRequest request = createAuthRequest(url);
QNetworkReply* reply = m_networkManager->get(request);
// Nota: En una implementación real, esperaríamos la respuesta asíncronamente
// pero por simplicidad en este stub, simulamos una respuesta
// Simular respuesta para desarrollo
QVector<MailItem> items;
items.append(MailItem(1, folderId.toInt(), "Reunión de proyecto",
"juan.perez@empresa.com", m_account.email(),
QDateTime::currentDateTime().addSecs(-3600),
false, false));
items.append(MailItem(2, folderId.toInt(), "Entrega de documentación",
"ana.gomez@cliente.com", m_account.email(),
QDateTime::currentDateTime().addSecs(-7200),
true, false));
MimeStorageService mimeStorage;
for (const QJsonObject &reference : messageRefs) {
const QString messageId = reference.value(QStringLiteral("id")).toString();
if (messageId.isEmpty()) continue;
const QString encodedId = QString::fromUtf8(QUrl::toPercentEncoding(messageId));
const QUrl rawUrl(buildGraphUrl(QStringLiteral("/me/messages/%1/$value").arg(encodedId)));
QByteArray rawMime;
if (!get(rawUrl, rawMime) || rawMime.isEmpty()) continue;
ParsedMimeMessage parsed;
if (!mimeStorage.parseMessage(rawMime, parsed)) continue;
MailItem item;
item.setFolderId(folderId.toInt());
item.setMessageId(messageId);
item.setRawMime(rawMime);
item.setSubject(parsed.subject.isEmpty() ? QStringLiteral("(No Subject)") : parsed.subject);
item.setSender(parsed.from);
item.setRecipient(parsed.to);
item.setTo(parsed.to);
item.setCc(parsed.cc);
item.setBcc(parsed.bcc);
item.setDate(parsed.date);
item.setBodyHtml(parsed.bodyHtml);
item.setSize(rawMime.size());
item.setRead(reference.value(QStringLiteral("isRead")).toBool(true));
item.setFlagged(reference.value(QStringLiteral("importance")).toString() == QStringLiteral("high"));
QVector<QString> attachmentNames;
for (const ParsedMimeAttachment &attachment : parsed.attachments) attachmentNames.append(attachment.fileName);
item.setAttachments(attachmentNames);
items.append(item);
}
return items;
}
bool OutlookSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
{
if (!m_account.isTokenValid()) {
if (!refreshAccessToken()) {
qWarning() << "Failed to refresh token for appending mail item";
Q_UNUSED(folderId);
Q_UNUSED(item);
qWarning() << "Outlook appendMailItem is not used for sending; MailService sends via Graph API";
return false;
}
}
qDebug() << "Appending mail item to folder:" << folderId;
// En una implementación real, llamaríamos a Microsoft Graph API
// para crear un mensaje en la carpeta especificada
// Por ahora, simulamos éxito
emit mailItemAdded(item);
// Publish MailItemAddedEvent
WinoMail::Events::MailItemAddedEvent mailEvent;
mailEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
mailEvent.timestamp = QDateTime::currentDateTimeUtc();
mailEvent.item = item;
PUBLISH(mailEvent);
return true;
}
bool OutlookSynchronizer::updateMailItemFlags(const QString& folderId,
const QString& itemUid,
bool read, bool flagged)
{
if (!m_account.isTokenValid()) {
if (!refreshAccessToken()) {
qWarning() << "Failed to refresh token for updating mail item flags";
Q_UNUSED(folderId);
Q_UNUSED(itemUid);
Q_UNUSED(read);
Q_UNUSED(flagged);
qWarning() << "Outlook flag update is not available through this synchronizer";
return false;
}
}
qDebug() << "Updating mail item flags:" << itemUid
<< "read:" << read << "flagged:" << flagged;
// En una implementación real, llamaríamos a Microsoft Graph API
// para actualizar las flags del mensaje
// Por ahora, simulamos éxito
MailItem updatedItem;
updatedItem.setId(itemUid.toLongLong());
updatedItem.setFolderId(folderId.toInt());
updatedItem.setRead(read);
updatedItem.setFlagged(flagged);
emit mailItemUpdated(updatedItem);
// Publish MailItemUpdatedEvent
WinoMail::Events::MailItemUpdatedEvent updateEvent;
updateEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
updateEvent.timestamp = QDateTime::currentDateTimeUtc();
updateEvent.item = updatedItem;
updateEvent.changedFields = QStringList() << "read" << "flagged"; // Simplified
PUBLISH(updateEvent);
return true;
}
bool OutlookSynchronizer::deleteMailItem(const QString& folderId,
const QString& itemUid)
{
if (!m_account.isTokenValid()) {
if (!refreshAccessToken()) {
qWarning() << "Failed to refresh token for deleting mail item";
Q_UNUSED(folderId);
Q_UNUSED(itemUid);
qWarning() << "Outlook delete is not available through this synchronizer";
return false;
}
}
qDebug() << "Deleting mail item:" << itemUid << "from folder:" << folderId;
// En una implementación real, llamaríamos a Microsoft Graph API
// para eliminar el mensaje
// Por ahora, simulamos éxito
emit mailItemRemoved(itemUid);
// Publish MailItemRemovedEvent
WinoMail::Events::MailItemRemovedEvent removeEvent;
removeEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
removeEvent.timestamp = QDateTime::currentDateTimeUtc();
removeEvent.itemUid = itemUid;
removeEvent.folderId = folderId.toInt();
PUBLISH(removeEvent);
return true;
}
void OutlookSynchronizer::onGraphReplyFinished(QNetworkReply* reply)
{
@@ -340,9 +336,12 @@ bool OutlookSynchronizer::refreshAccessToken()
return false;
}
// TODO: These should come from secure configuration, not hardcoded
const QString clientId = "YOUR_CLIENT_ID_HERE"; // Replace with actual client ID
const QString clientSecret = "YOUR_CLIENT_SECRET_HERE"; // Replace with actual client secret
const QString clientId = qEnvironmentVariable("WINO_OUTLOOK_CLIENT_ID");
const QString clientSecret = qEnvironmentVariable("WINO_OUTLOOK_CLIENT_SECRET");
if (clientId.isEmpty() || clientSecret.isEmpty()) {
qWarning() << "Outlook token expired and WINO_OUTLOOK_CLIENT_ID/SECRET are not configured";
return false;
}
const QString tokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
QNetworkRequest networkRequest{QUrl(tokenUrl)};
@@ -355,7 +354,8 @@ bool OutlookSynchronizer::refreshAccessToken()
postData.addQueryItem("grant_type", "refresh_token");
postData.addQueryItem("client_secret", clientSecret);
QNetworkReply* reply = m_networkManager->post(networkRequest, postData.toString(QUrl::FullyEncoded).toUtf8());
QNetworkAccessManager network;
QNetworkReply* reply = network.post(networkRequest, postData.toString(QUrl::FullyEncoded).toUtf8());
// Wait for reply synchronously for simplicity in this context
QEventLoop loop;
+318
View File
@@ -0,0 +1,318 @@
#include "pop3synchronizer.h"
#include <QDebug>
#include <QDateTime>
#include <QRegularExpression>
#include <QSqlQuery>
#include <QAbstractSocket>
#include <QMap>
#include <QCryptographicHash>
#include "../../core/mailitem.h"
#include "../../db/dao/mailitemdao.h"
#include "../../db/dao/folderdao.h"
#include "../../core/models/account.h"
#include "../../services/mimestorage.h"
Pop3Synchronizer::Pop3Synchronizer(QObject *parent)
: Synchronizer(parent)
, m_socket(nullptr)
{
}
bool Pop3Synchronizer::initialize(const Account &account)
{
m_account = account;
const auto &settings = account.connectionSettings();
m_host = settings.incomingHost;
m_port = settings.incomingPort;
m_useSsl = settings.incomingSsl;
m_username = settings.username;
m_password = settings.password;
if (m_socket) {
m_socket->deleteLater();
m_socket = nullptr;
}
m_socket = new QSslSocket(this);
qDebug() << "POP3 synchronizer initialized for" << m_username << "at" << m_host;
return true;
}
bool Pop3Synchronizer::syncFolder(const Folder &folder)
{
Q_UNUSED(folder);
return true;
}
QVector<Folder> Pop3Synchronizer::getFolders() const
{
QVector<Folder> folders;
Folder inbox;
inbox.setId(0);
inbox.setAccountId(m_account.id());
inbox.setName(QStringLiteral("Inbox"));
inbox.setParentFolderId(QString());
folders.append(inbox);
return folders;
}
QVector<MailItem> Pop3Synchronizer::fetchMailItems(const QString &folderId, qint64 sinceUid)
{
QVector<MailItem> items;
QSslSocket socket;
if (m_useSsl) socket.connectToHostEncrypted(m_host, m_port);
else socket.connectToHost(m_host, m_port);
if (!socket.waitForConnected(15000)) {
qWarning() << "Failed to connect to POP3 server:" << socket.errorString();
return items;
}
if (m_useSsl && !socket.waitForEncrypted(15000)) {
qWarning() << "TLS handshake failed:" << socket.errorString();
return items;
}
auto readLine = [&](QByteArray &line) {
if (!socket.canReadLine() && !socket.waitForReadyRead(15000)) return false;
line = socket.readLine();
return !line.isEmpty();
};
auto command = [&](const QByteArray &commandText, QByteArray &response, bool multiline) {
socket.write(commandText + QByteArrayLiteral("\r\n"));
socket.flush();
if (!readLine(response) || !response.trimmed().startsWith('+')) return false;
if (multiline) {
while (true) {
QByteArray line;
if (!readLine(line)) return false;
if (line == QByteArrayLiteral(".\r\n") || line == QByteArrayLiteral(".\n")) break;
if (line.startsWith("..")) line.remove(0, 1);
response.append(line);
}
}
return true;
};
QByteArray response;
if (!readLine(response) || !response.trimmed().startsWith('+')) return items;
if (!command(QByteArrayLiteral("USER ") + m_username.toUtf8(), response, false)
|| !command(QByteArrayLiteral("PASS ") + m_password.toUtf8(), response, false)) {
qWarning() << "POP3 authentication failed:" << response;
return items;
}
if (!command(QByteArrayLiteral("UIDL"), response, true)) {
qWarning() << "UIDL failed:" << response;
return items;
}
m_uidToMsgNum.clear();
m_maxUid = 0;
QList<qint64> uidsToFetch;
const QStringList lines = QString::fromUtf8(response).split(QRegularExpression("\\r?\\n"));
for (const QString &line : lines) {
const QStringList parts = line.trimmed().split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
if (parts.size() < 2) continue;
bool okMsgId = false, okUid = false;
const int msgNum = parts.at(0).toInt(&okMsgId);
qint64 uid = parts.at(1).toLongLong(&okUid);
if (!okUid) {
const QByteArray digest = QCryptographicHash::hash(parts.at(1).toUtf8(), QCryptographicHash::Sha256).toHex().left(15);
uid = digest.toLongLong(&okUid, 16);
}
if (!okMsgId || !okUid || uid <= 0) continue;
m_uidToMsgNum[QString::number(uid)] = msgNum;
m_maxUid = qMax(m_maxUid, uid);
if (uid > sinceUid) uidsToFetch.append(uid);
}
MimeStorageService mimeStorage;
int processed = 0;
for (const qint64 uid : uidsToFetch) {
const int msgNum = m_uidToMsgNum.value(QString::number(uid));
QByteArray rawResponse;
if (!command(QByteArrayLiteral("RETR ") + QByteArray::number(msgNum), rawResponse, true)) {
qWarning() << "RETR failed for message" << msgNum;
continue;
}
const int firstLineEnd = rawResponse.indexOf("\r\n");
const QByteArray rawMime = firstLineEnd >= 0 ? rawResponse.mid(firstLineEnd + 2) : QByteArray();
ParsedMimeMessage parsed;
if (rawMime.isEmpty() || !mimeStorage.parseMessage(rawMime, parsed)) {
qWarning() << "Failed to parse POP3 MIME message with UID" << uid;
continue;
}
MailItem item;
item.setFolderId(folderId.toInt());
item.setMessageId(parsed.messageId);
item.setSubject(parsed.subject.isEmpty() ? QStringLiteral("(No Subject)") : parsed.subject);
item.setSender(parsed.from);
item.setRecipient(parsed.to);
item.setTo(parsed.to);
item.setCc(parsed.cc);
item.setBcc(parsed.bcc);
item.setDate(parsed.date);
item.setBodyHtml(parsed.bodyHtml);
item.setSize(rawMime.size());
item.setRawMime(rawMime);
item.setUid(uid);
QVector<QString> attachmentNames;
for (const ParsedMimeAttachment &attachment : parsed.attachments) attachmentNames.append(attachment.fileName);
item.setAttachments(attachmentNames);
items.append(item);
++processed;
emit progressChanged(uidsToFetch.isEmpty() ? 100 : static_cast<int>((100.0 * processed) / uidsToFetch.size()));
emit statusMessage(tr("Fetched %1 of %2 messages").arg(processed).arg(uidsToFetch.size()));
}
command(QByteArrayLiteral("QUIT"), response, false);
socket.disconnectFromHost();
return items;
}
bool Pop3Synchronizer::appendMailItem(const QString &folderId, const MailItem &item)
{
Q_UNUSED(folderId);
Q_UNUSED(item);
qWarning() << "POP3 appendMailItem is not a send transport; MailService owns SMTP sending";
return false;
}
bool Pop3Synchronizer::updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged)
{
QVector<MailItem> items = MailItemDao::findByFolderId(folderId.toInt());
for (MailItem &it : items) {
if (it.uid() == itemUid.toLongLong()) {
it.setRead(read);
it.setFlagged(flagged);
if (MailItemDao::update(it))
return true;
}
}
return false;
}
bool Pop3Synchronizer::deleteMailItem(const QString &folderId, const QString &itemUid)
{
Q_UNUSED(folderId);
if (!m_socket) {
qWarning() << "POP3 socket not initialized";
return false;
}
if (m_socket->state() != QAbstractSocket::UnconnectedState) {
m_socket->disconnectFromHost();
if (!m_socket->waitForDisconnected(3000)) {
qWarning() << "Failed to disconnect from previous connection";
return false;
}
}
if (m_useSsl)
m_socket->connectToHostEncrypted(m_host, m_port);
else
m_socket->connectToHost(m_host, m_port);
if (!m_socket->waitForConnected(5000)) {
qWarning() << "Failed to connect to POP3 server:" << m_socket->errorString();
return false;
}
if (m_useSsl && !m_socket->waitForEncrypted(5000)) {
qWarning() << "TLS handshake failed:" << m_socket->errorString();
m_socket->disconnectFromHost();
return false;
}
QString greeting;
if (!m_socket->waitForReadyRead(5000)) {
qWarning() << "No greeting from POP3 server";
m_socket->disconnectFromHost();
return false;
}
greeting = QString::fromUtf8(m_socket->readLine()).trimmed();
if (!greeting.startsWith(QLatin1Char('+'))) {
qWarning() << "Invalid POP3 greeting:" << greeting;
m_socket->disconnectFromHost();
return false;
}
QString userCmd = QStringLiteral("USER %1\r\n").arg(m_username);
QString passCmd = QStringLiteral("PASS %1\r\n").arg(m_password);
QString resp;
if (!sendCommand(userCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
qWarning() << "USER command failed:" << resp;
m_socket->disconnectFromHost();
return false;
}
if (!sendCommand(passCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
qWarning() << "PASS command failed:" << resp;
m_socket->disconnectFromHost();
return false;
}
// Get UIDL to map uid to message number
QString uidlResp;
if (!sendCommand(QStringLiteral("UIDL\r\n"), uidlResp) || !uidlResp.startsWith(QLatin1Char('+'))) {
qWarning() << "UIDL failed:" << uidlResp;
m_socket->disconnectFromHost();
return false;
}
int msgNum = -1;
QStringList lines = uidlResp.split(QRegularExpression("\\r?\\n"));
for (const QString &line : lines) {
if (line.startsWith(QLatin1Char('.')) || line.isEmpty())
continue;
QString l = line;
if (l.startsWith(QLatin1Char('+')))
l.remove(0, 1);
QStringList parts = l.split(QRegularExpression("\\s+"));
if (parts.size() < 2)
continue;
bool okUid;
qint64 uid = parts[1].toLongLong(&okUid);
if (!okUid)
continue;
if (uid == itemUid.toLongLong()) {
bool okMsgId;
msgNum = parts[0].toInt(&okMsgId);
if (!okMsgId)
msgNum = -1;
break;
}
}
if (msgNum == -1) {
qWarning() << "Could not find message number for UID" << itemUid;
m_socket->disconnectFromHost();
return false;
}
QString deleCmd = QStringLiteral("DELE %1\r\n").arg(msgNum);
if (!sendCommand(deleCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
qWarning() << "DELE failed for msg" << msgNum << ":" << resp;
m_socket->disconnectFromHost();
return false;
}
// Deletion takes effect on QUIT
sendCommand(QStringLiteral("QUIT\r\n"), resp);
m_socket->disconnectFromHost();
return true;
}
QString Pop3Synchronizer::readResponse()
{
if (!m_socket)
return QString();
if (!m_socket->waitForReadyRead(5000))
return QString();
return QString::fromUtf8(m_socket->readLine()).trimmed();
}
bool Pop3Synchronizer::sendCommand(const QString &cmd, QString &response)
{
if (!m_socket)
return false;
m_socket->write(cmd.toUtf8());
if (!m_socket->waitForBytesWritten(5000))
return false;
response = readResponse();
return !response.isEmpty();
}
@@ -0,0 +1,230 @@
#include "pop3synchronizer.h"
#include <QDebug>
#include <QDateTime>
#include <QRegularExpression>
#include "../../core/mailitem.h"
#include "../../db/dao/mailitemdao.h"
#include "../../db/dao/folderdao.h"
#include "../../core/models/account.h"
Pop3Synchronizer::Pop3Synchronizer(QObject *parent)
: Synchronizer(parent)
{
}
bool Pop3Synchronizer::initialize(const Account &account)
{
m_account = account;
const auto &settings = account.connectionSettings();
m_host = settings.incomingHost;
m_port = settings.incomingPort;
m_useSsl = settings.incomingSsl;
m_username = settings.username;
m_password = settings.password;
// For simplicity, we do not actually connect here; connection will be made per operation.
qDebug() << "POP3 synchronizer initialized for" << m_username << "at" << m_host;
return true;
}
bool Pop3Synchronizer::syncFolder(const Folder &folder)
{
Q_UNUSED(folder);
// POP3 does not have folder sync; we just return true.
return true;
}
QVector<Folder> Pop3Synchronizer::getFolders() const
{
QVector<Folder> folders;
Folder inbox;
inbox.setId(0); // Assuming folder ID 0 for Inbox (should be looked up)
inbox.setAccountId(m_account.id());
inbox.setName(QStringLiteral("Inbox"));
inbox.setParentFolderId(QString());
folders.append(inbox);
return folders;
}
QVector<MailItem> Pop3Synchronizer::fetchMailItems(const QString &folderId, qint64 sinceUid)
{
Q_UNUSED(sinceUid);
QVector<MailItem> items;
// Connect to POP3 server
QSslSocket socket;
if (m_useSsl)
socket.connectToHostEncrypted(m_host, m_port);
else
socket.connectToHost(m_host, m_port);
if (!socket.waitForConnected(5000)) {
qWarning() << "Failed to connect to POP3 server:" << socket.errorString();
return items;
}
if (m_useSsl && !socket.waitForEncrypted(5000)) {
qWarning() << "TLS handshake failed:" << socket.errorString();
socket.disconnectFromHost();
return items;
}
// Read greeting
QString greeting;
if (!socket.waitForReadyRead(5000)) {
qWarning() << "No greeting from POP3 server";
socket.disconnectFromHost();
return items;
}
greeting = socket.readLine();
if (!greeting.startsWith(QLatin1Char('+'))) {
qWarning() << "Invalid POP3 greeting:" << greeting;
socket.disconnectFromHost();
return items;
}
// Login
QString userCmd = QStringLiteral("USER %1\r\n").arg(m_username);
QString passCmd = QStringLiteral("PASS %1\r\n").arg(m_password);
QString resp;
if (!sendCommand(userCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
qWarning() << "USER command failed:" << resp;
socket.disconnectFromHost();
return items;
}
if (!sendCommand(passCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
qWarning() << "PASS command failed:" << resp;
socket.disconnectFromHost();
return items;
}
// Get list of messages (STAT and LIST)
QString statResp;
if (!sendCommand(QStringLiteral("STAT\r\n"), statResp) || !statResp.startsWith(QLatin1Char('+'))) {
qWarning() << "STAT failed:" << statResp;
socket.disconnectFromHost();
return items;
}
// Parse STAT: "+<count> <size>"
QStringList statParts = statResp.split(QRegularExpression("\\s+"));
int count = 0;
if (statParts.size() >= 2)
count = statParts[1].toInt();
// For simplicity, we fetch all messages (since POP3 doesn't support UID since easily without UIDL mapping)
// We'll use UIDL to get unique IDs, then RETR for each.
QString uidlResp;
if (!sendCommand(QStringLiteral("UIDL\r\n"), uidlResp) || !uidlResp.startsWith(QLatin1Char('+'))) {
qWarning() << "UIDL failed:" << uidlResp;
socket.disconnectFromHost();
return items;
}
// UIDL response lines: each line: "+<msg-id> <uid>"
QStringList lines = uidlResp.split(QRegularExpression("\\r?\\n"));
for (const QString &line : lines) {
if (line.startsWith(QLatin1Char('.')) || line.isEmpty())
continue;
// Remove leading '+'
QString l = line;
if (l.startsWith(QLatin1Char('+')))
l.remove(0,1);
QStringList parts = l.split(QRegularExpression("\\s+"));
if (parts.size() < 2)
continue;
QString msgIdStr = parts[0];
QString uid = parts[1];
bool ok;
int msgId = msgIdStr.toInt(&ok);
if (!ok)
continue;
// Retrieve message
QString retrCmd = QStringLiteral("RETR %1\r\n").arg(msgIdStr);
QString retrResp;
if (!sendCommand(retrCmd, retrResp) || !retrResp.startsWith(QLatin1Char('+'))) {
qWarning() << "RETR failed for msg" << msgIdStr;
continue;
}
// Read message lines until a line with a single dot.
QByteArray msgData;
while (true) {
if (!socket.waitForReadyRead(5000))
break;
QByteArray line = socket.readLine();
if (line == QByteArray(".\r\n"))
break;
msgData.append(line);
}
// Parse rudimentary headers: we can extract Subject, From, Date.
// For simplicity, we'll set placeholder values.
QString subject = QStringLiteral("(No Subject)");
QString from = QStringLiteral("unknown@example.com");
QDateTime date = QDateTime::currentDateTimeUtc();
bool seen = false; // POP3 doesn't have read flag; we assume unread unless we store locally.
bool flagged = false;
MailItem item;
item.setFolderId(folderId.toInt());
item.setSubject(subject);
item.setSender(from);
item.setDate(date);
item.setRead(seen);
item.setFlagged(flagged);
// We could store the UID in a custom field, but MailItem has uid field (qint64) - we can store the numeric UID if possible.
// For now, we leave uid 0.
// Insert into DB
if (MailItemDao::insert(item)) {
QSqlQuery qry;
qry.exec(QStringLiteral("SELECT last_insert_rowid()"));
qint64 newId = -1;
if (qry.next())
newId = qry.value(0).toLongLong();
item.setId(newId);
items.append(item);
qDebug() << "Fetched and stored msgId" << msgIdStr << "as mail id" << newId;
} else {
qWarning() << "Failed to insert mail item for msgId" << msgIdStr;
}
}
// Quit
sendCommand(QStringLiteral("QUIT\r\n"), resp);
socket.disconnectFromHost();
return items;
}
bool Pop3Synchronizer::appendMailItem(const QString &folderId, const MailItem &item)
{
Q_UNUSED(folderId);
Q_UNUSED(item);
// POP3 does not support uploading; we can implement via SMTP elsewhere.
return false;
}
bool Pop3Synchronizer::updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged)
{
Q_UNUSED(folderId);
Q_UNUSED(itemUid);
Q_UNUSED(read);
Q_UNUSED(flagged);
// POP3 does not support flag updates.
return false;
}
bool Pop3Synchronizer::deleteMailItem(const QString &folderId, const QString &itemUid)
{
Q_UNUSED(folderId);
Q_UNUSED(itemUid);
// POP3 does not support deleting by UID easily without mapping; we could implement DELE by message number.
// For simplicity, we return false.
return false;
}
QString Pop3Synchronizer::readResponse()
{
if (!m_socket)
return QString();
if (!m_socket->waitForReadyRead(5000))
return QString();
return QString::fromUtf8(m_socket->readLine()).trimmed();
}
bool Pop3Synchronizer::sendCommand(const QString &cmd, QString &response)
{
if (!m_socket)
return false;
m_socket->write(cmd.toUtf8());
if (!m_socket->waitForBytesWritten(5000))
return false;
response = readResponse();
return !response.isEmpty();
}
+229
View File
@@ -0,0 +1,229 @@
#include "pop3synchronizer.h"
#include <QDebug>
#include <QDateTime>
#include "../../core/mailitem.h"
#include "../../db/dao/mailitemdao.h"
#include "../../db/dao/folderdao.h"
#include "../../core/models/account.h"
Pop3Synchronizer::Pop3Synchronizer(QObject *parent)
: Synchronizer(parent)
{
}
bool Pop3Synchronizer::initialize(const Account &account)
{
m_account = account;
const auto &settings = account.connectionSettings();
m_host = settings.incomingHost;
m_port = settings.incomingPort;
m_useSsl = settings.incomingSsl;
m_username = settings.username;
m_password = settings.password;
// For simplicity, we do not actually connect here; connection will be made per operation.
qDebug() << "POP3 synchronizer initialized for" << m_username << "at" << m_host;
return true;
}
bool Pop3Synchronizer::syncFolder(const Folder &folder)
{
Q_UNUSED(folder);
// POP3 does not have folder sync; we just return true.
return true;
}
QVector<Folder> Pop3Synchronizer::getFolders() const
{
QVector<Folder> folders;
Folder inbox;
inbox.setId(0); // Assuming folder ID 0 for Inbox (should be looked up)
inbox.setAccountId(m_account.id());
inbox.setName(QStringLiteral("Inbox"));
inbox.setParentId(-1);
folders.append(inbox);
return folders;
}
QVector<MailItem> Pop3Synchronizer::fetchMailItems(const QString &folderId, qint64 sinceUid)
{
Q_UNUSED(sinceUid);
QVector<MailItem> items;
// Connect to POP3 server
QSslSocket socket;
if (m_useSsl)
socket.connectToHostEncrypted(m_host, m_port);
else
socket.connectToHost(m_host, m_port);
if (!socket.waitForConnected(5000)) {
qWarning() << "Failed to connect to POP3 server:" << socket.errorString();
return items;
}
if (m_useSsl && !socket.waitForEncrypted(5000)) {
qWarning() << "TLS handshake failed:" << socket.errorString();
socket.disconnectFromHost();
return items;
}
// Read greeting
QString greeting;
if (!socket.waitForReadyRead(5000)) {
qWarning() << "No greeting from POP3 server";
socket.disconnectFromHost();
return items;
}
greeting = socket.readLine();
if (!greeting.startsWith(QLatin1Char('+'))) {
qWarning() << "Invalid POP3 greeting:" << greeting;
socket.disconnectFromHost();
return items;
}
// Login
QString userCmd = QStringLiteral("USER %1\r\n").arg(m_username);
QString passCmd = QStringLiteral("PASS %1\r\n").arg(m_password);
QString resp;
if (!sendCommand(userCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
qWarning() << "USER command failed:" << resp;
socket.disconnectFromHost();
return items;
}
if (!sendCommand(passCmd, resp) || !resp.startsWith(QLatin1Char('+'))) {
qWarning() << "PASS command failed:" << resp;
socket.disconnectFromHost();
return items;
}
// Get list of messages (STAT and LIST)
QString statResp;
if (!sendCommand(QStringLiteral("STAT\r\n"), statResp) || !statResp.startsWith(QLatin1Char('+'))) {
qWarning() << "STAT failed:" << statResp;
socket.disconnectFromHost();
return items;
}
// Parse STAT: "+<count> <size>"
QStringList statParts = statResp.split(QRegExp("\\s+"));
int count = 0;
if (statParts.size() >= 2)
count = statParts[1].toInt();
// For simplicity, we fetch all messages (since POP3 doesn't support UID since easily without UIDL mapping)
// We'll use UIDL to get unique IDs, then RETR for each.
QString uidlResp;
if (!sendCommand(QStringLiteral("UIDL\r\n"), uidlResp) || !uidlResp.startsWith(QLatin1Char('+'))) {
qWarning() << "UIDL failed:" << uidlResp;
socket.disconnectFromHost();
return items;
}
// UIDL response lines: each line: "+<msg-id> <uid>"
QStringList lines = uidlResp.split(QRegExp("\\r?\\n"));
for (const QString &line : lines) {
if (line.startsWith(QLatin1Char('.')) || line.isEmpty())
continue;
// Remove leading '+'
QString l = line;
if (l.startsWith(QLatin1Char('+')))
l.remove(0,1);
QStringList parts = l.split(QRegExp("\\s+"));
if (parts.size() < 2)
continue;
QString msgIdStr = parts[0];
QString uid = parts[1];
bool ok;
int msgId = msgIdStr.toInt(&ok);
if (!ok)
continue;
// Retrieve message
QString retrCmd = QStringLiteral("RETR %1\r\n").arg(msgIdStr);
QString retrResp;
if (!sendCommand(retrCmd, retrResp) || !retrResp.startsWith(QLatin1Char('+'))) {
qWarning() << "RETR failed for msg" << msgIdStr;
continue;
}
// Read message lines until a line with a single dot.
QByteArray msgData;
while (true) {
if (!socket.waitForReadyRead(5000))
break;
QByteArray line = socket.readLine();
if (line == QByteArray(".\r\n"))
break;
msgData.append(line);
}
// Parse rudimentary headers: we can extract Subject, From, Date.
// For simplicity, we'll set placeholder values.
QString subject = QStringLiteral("(No Subject)");
QString from = QStringLiteral("unknown@example.com");
QDateTime date = QDateTime::currentDateTimeUtc();
bool seen = false; // POP3 doesn't have read flag; we assume unread unless we store locally.
bool flagged = false;
MailItem item;
item.setFolderId(folderId.toInt());
item.setSubject(subject);
item.setFrom(from);
item.setDate(date);
item.setRead(seen);
item.setFlagged(flagged);
// We could store the UID in a custom field, but MailItem has uid field (qint64) - we can store the numeric UID if possible.
// For now, we leave uid 0.
// Insert into DB
if (MailItemDao::insert(item)) {
QSqlQuery qry;
qry.exec(QStringLiteral("SELECT last_insert_rowid()"));
qint64 newId = -1;
if (qry.next())
newId = qry.value(0).toLongLong();
item.setId(newId);
items.append(item);
qDebug() << "Fetched and stored msgId" << msgIdStr << "as mail id" << newId;
} else {
qWarning() << "Failed to insert mail item for msgId" << msgIdStr;
}
}
// Quit
sendCommand(QStringLiteral("QUIT\r\n"), resp);
socket.disconnectFromHost();
return items;
}
bool Pop3Synchronizer::appendMailItem(const QString &folderId, const MailItem &item)
{
Q_UNUSED(folderId);
Q_UNUSED(item);
// POP3 does not support uploading; we can implement via SMTP elsewhere.
return false;
}
bool Pop3Synchronizer::updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged)
{
Q_UNUSED(folderId);
Q_UNUSED(itemUid);
Q_UNUSED(read);
Q_UNUSED(flagged);
// POP3 does not support flag updates.
return false;
}
bool Pop3Synchronizer::deleteMailItem(const QString &folderId, const QString &itemUid)
{
Q_UNUSED(folderId);
Q_UNUSED(itemUid);
// POP3 does not support deleting by UID easily without mapping; we could implement DELE by message number.
// For simplicity, we return false.
return false;
}
QString Pop3Synchronizer::readResponse()
{
if (!m_socket)
return QString();
if (!m_socket->waitForReadyRead(5000))
return QString();
return QString::fromUtf8(m_socket->readLine()).trimmed();
}
bool Pop3Synchronizer::sendCommand(const QString &cmd, QString &response)
{
if (!m_socket)
return false;
m_socket->write(cmd.toUtf8());
if (!m_socket->waitForBytesWritten(5000))
return false;
response = readResponse();
return !response.isEmpty();
}
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#include "../synchronizer.h"
#include <QObject>
#include <QSslSocket>
#include <QStringList>
#include <QMap>
class Pop3Synchronizer : public Synchronizer
{
Q_OBJECT
public:
explicit Pop3Synchronizer(QObject *parent = nullptr);
~Pop3Synchronizer() override = default;
// Synchronizer interface
bool initialize(const Account &account) override;
bool syncFolder(const Folder &folder) override;
QVector<Folder> getFolders() const override;
QVector<MailItem> 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);
void statusMessage(const QString &message);
private:
QString m_host;
quint16 m_port{0};
bool m_useSsl{false};
QString m_username;
QString m_password;
QSslSocket *m_socket{nullptr};
// Mapping from UIDL (as string) to message number for DELE
QMap<QString, int> m_uidToMsgNum;
// Highest UID seen (used for sinceUid)
qint64 m_maxUid{0};
QString readResponse();
bool sendCommand(const QString &cmd, QString &response);
};
+71 -1
View File
@@ -7,6 +7,7 @@
#include <QRegularExpressionValidator>
#include <QIntValidator>
#include <QTimer>
#include "ui/connectionwizard.h"
// ─────────────────────── Constructor ───────────────────────
AccountSetupDialog::AccountSetupDialog(AccountService *accountService, QWidget *parent)
@@ -427,7 +428,8 @@ void AccountSetupDialog::onNextClicked()
if (page == PageProvider) {
if (m_selectedProvider == 2) {
goToPage(PageImap);
// Launch ConnectionWizard for IMAP/POP3 setup
launchConnectionWizard();
} else {
// Update OAuth page subtitle based on provider
goToPage(PageOAuth);
@@ -441,6 +443,20 @@ void AccountSetupDialog::onNextClicked()
}
}
void AccountSetupDialog::launchConnectionWizard()
{
ConnectionWizard wizard(this, m_accountService);
wizard.setWindowTitle("Configurar cuenta IMAP / POP3");
wizard.setWizardStyle(QWizard::ModernStyle);
connect(&wizard, &ConnectionWizard::settingsReady,
this, &AccountSetupDialog::onConnectionWizardFinished);
if (wizard.exec() == QDialog::Accepted) {
// Settings were emitted and handled in onConnectionWizardFinished
}
}
void AccountSetupDialog::onBackClicked()
{
int page = m_stack->currentIndex();
@@ -575,6 +591,60 @@ void AccountSetupDialog::submitImapAccount()
}
}
// ─────────────────── Account Result ───────────────────
void AccountSetupDialog::onConnectionWizardFinished(const Account::ConnectionSettings &settings)
{
QString email = settings.username;
QString name = email.split("@").first();
// Show progress
m_accountCreatedOk = false;
goToPage(PageProgress);
m_progressIcon->setText("");
m_progressText->setText("Conectando al servidor...");
m_progressDetail->setText(
QString("Servidor IMAP: %1:%2\nServidor SMTP: %3:%4\nSSL: %5")
.arg(settings.incomingHost)
.arg(settings.incomingPort)
.arg(settings.outgoingHost)
.arg(settings.outgoingPort)
.arg(settings.incomingSsl ? "" : "No")
);
// Disable UI during connection attempt
m_btnBack->setEnabled(false);
m_btnNext->setEnabled(false);
m_btnCancel->setEnabled(false);
QString errorMsg;
bool success = m_accountService->testConnection(settings, errorMsg);
if (success) {
// Create account with the connection settings
Account account;
account.setEmail(email);
account.setDisplayName(name);
// Determine account type from protocol
if (settings.type == "pop3") {
account.setType(AccountType::POP3);
} else {
account.setType(AccountType::IMAP);
}
account.setConnectionSettings(settings);
if (m_isEditing) {
account.setId(m_editingAccountId);
m_accountService->updateAccount(account);
} else {
m_accountService->addAccount(account);
}
m_btnCancel->setEnabled(true);
} else {
showError(tr("Could not connect to server: %1").arg(errorMsg));
}
}
// ─────────────────── Account Result ───────────────────
void AccountSetupDialog::onAccountAdded(const Account &account)
{
+620
View File
@@ -0,0 +1,620 @@
#include "ui/accountsetupdialog.h"
#include <QMessageBox>
#include <QScrollArea>
#include <QFrame>
#include <QDebug>
#include <QRegularExpression>
#include <QRegularExpressionValidator>
#include <QIntValidator>
#include <QTimer>
// ─────────────────────── Constructor ───────────────────────
AccountSetupDialog::AccountSetupDialog(AccountService *accountService, QWidget *parent)
: QDialog(parent)
, m_accountService(accountService)
, m_selectedProvider(0)
, m_accountCreatedOk(false)
, m_editingAccountId(-1)
, m_isEditing(false)
{
m_authTimeoutTimer = new QTimer(this);
m_authTimeoutTimer->setSingleShot(true);
connect(m_authTimeoutTimer, &QTimer::timeout, this, &AccountSetupDialog::onAuthTimeout);
if (m_accountService) {
connect(m_accountService, &AccountService::accountAdded,
this, &AccountSetupDialog::onAccountAdded);
}
setupUI();
}
// ─────────────────────── Public API ───────────────────────
void AccountSetupDialog::loadAccountForEditing(const Account &account)
{
// Only support editing IMAP accounts for simplicity
if (account.type() != AccountType::IMAP) {
QMessageBox::information(this, tr("Edit Account"),
tr("Editing of OAuth (Gmail/Outlook) accounts is not supported in this version.\n"
"You can add a new account instead."));
m_isEditing = false;
return;
}
m_isEditing = true;
m_editingAccountId = account.id();
// Set window title
setWindowTitle(tr("Edit Email Account Wino Mail"));
// Fill IMAP fields
m_imapEmailEdit->setText(account.email());
m_imapNameEdit->setText(account.displayName());
// Password field left blank for security; user must re-enter
m_imapPasswordEdit->clear();
const Account::ConnectionSettings &settings = account.connectionSettings();
m_imapHostEdit->setText(settings.incomingHost);
m_imapPortEdit->setText(QString::number(settings.incomingPort));
m_smtpHostEdit->setText(settings.outgoingHost);
m_smtpPortEdit->setText(QString::number(settings.outgoingPort));
m_sslCheckbox->setChecked(settings.incomingSsl); // assuming same for SMTP
// Switch to IMAP page
m_selectedProvider = 2; // IMAP
goToPage(PageImap);
}
// ─────────────────────── UI Setup ───────────────────────
void AccountSetupDialog::setupUI()
{
setWindowTitle("Añadir Cuenta de Correo — Wino Mail");
setMinimumSize(600, 500);
resize(660, 520);
// ── Global stylesheet ──
setStyleSheet(
"AccountSetupDialog { background: #f5f5f7; }"
"QLabel { color: #1d1d1f; }"
"QLineEdit {"
" background: #fff; border: 1px solid #d1d1d6; border-radius: 6px;"
" padding: 8px 12px; font-size: 13px; color: #1d1d1f;"
"}"
"QLineEdit:focus { border: 2px solid #0071e3; }"
"QRadioButton { font-size: 14px; padding: 6px 0; }"
"QCheckBox { font-size: 13px; }"
"QPushButton {"
" font-weight: 600; font-size: 13px; padding: 8px 20px; border-radius: 6px;"
"}"
});
QVBoxLayout *root = new QVBoxLayout(this);
root->setContentsMargins(24, 24, 24, 18);
root->setSpacing(16);
// ── Stacked pages ──
m_stack = new QStackedWidget(this);
m_stack->addWidget(createProviderPage()); // 0
m_stack->addWidget(createOAuthPage()); // 1
m_stack->addWidget(createImapPage()); // 2
m_stack->addWidget(createProgressPage()); // 3
root->addWidget(m_stack, 1);
// ── Separator ──
QFrame *sep = new QFrame();
sep->setFrameShape(QFrame::HLine);
sep->setStyleSheet("color: #d1d1d6;");
root->addWidget(sep);
// ── Navigation buttons ──
QHBoxLayout *nav = new QHBoxLayout();
m_btnBack = new QPushButton("← Atrás");
m_btnBack->setStyleSheet(
"QPushButton { background: #fff; color: #0071e3; border: 1px solid #0071e3; }"
"QPushButton:hover { background: #e8f0fe; }"
);
m_btnCancel = new QPushButton("Cancelar");
m_btnCancel->setStyleSheet(
"QPushButton { background: #e5e5ea; color: #1d1d1f; border: none; }"
"QPushButton:hover { background: #d1d1d6; }"
);
m_btnNext = new QPushButton("Siguiente →");
m_btnNext->setStyleSheet(
"QPushButton { background: #0071e3; color: white; border: none; }"
"QPushButton:hover { background: #005bb5; }"
"QPushButton:disabled { background: #a0c4ff; color: #e0e0e0; }"
);
nav->addWidget(m_btnBack);
nav->addStretch();
nav->addWidget(m_btnCancel);
nav->addSpacing(8);
nav->addWidget(m_btnNext);
root->addLayout(nav);
connect(m_btnBack, &QPushButton::clicked, this, &AccountSetupDialog::onBackClicked);
connect(m_btnNext, &QPushButton::clicked, this, &AccountSetupDialog::onNextClicked);
connect(m_btnCancel, &QPushButton::clicked, this, &AccountSetupDialog::onCancelClicked);
goToPage(PageProvider);
}
// ─────────────────── Page 0: Provider ───────────────────
QWidget* AccountSetupDialog::createProviderPage()
{
QWidget *page = new QWidget();
QVBoxLayout *lay = new QVBoxLayout(page);
lay->setContentsMargins(10, 10, 10, 10);
lay->setSpacing(20);
// Title
QLabel *title = new QLabel("Añadir nueva cuenta");
title->setStyleSheet("font-size: 22px; font-weight: 700; color: #1d1d1f;");
title->setAlignment(Qt::AlignCenter);
QLabel *subtitle = new QLabel("Selecciona el tipo de cuenta que deseas configurar:");
subtitle->setStyleSheet("font-size: 14px; color: #8e8e93;");
subtitle->setWordWrap(true);
subtitle->setAlignment(Qt::AlignCenter);
lay->addWidget(title);
lay->addWidget(subtitle);
lay->addSpacing(10);
// Provider cards
m_providerGroup = new QButtonGroup(this);
auto makeRadio = [&](const QString &text, const QString &desc, int id) -> QWidget* {
QWidget *card = new QWidget();
card->setStyleSheet(
"QWidget { background: #fff; border: 1px solid #e0e0e5; border-radius: 10px; }"
"QWidget:hover { border-color: #0071e3; }"
);
QHBoxLayout *h = new QHBoxLayout(card);
h->setContentsMargins(16, 12, 16, 12);
QRadioButton *radio = new QRadioButton(text);
radio->setStyleSheet("font-size: 15px; font-weight: 600;");
m_providerGroup->addButton(radio, id);
QLabel *descLabel = new QLabel(desc);
descLabel->setStyleSheet("font-size: 12px; color: #8e8e93;");
descLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
h->addWidget(radio, 1);
h->addWidget(descLabel);
return card;
};
lay->addWidget(makeRadio("🔴 Google / Gmail", "OAuth2 seguro", 0));
lay->addWidget(makeRadio("🔵 Microsoft / Outlook", "OAuth2 seguro", 1));
lay->addWidget(makeRadio("⚙️ IMAP / SMTP", "Servidor personalizado", 2));
m_providerGroup->button(0)->setChecked(true);
connect(m_providerGroup, QOverload<int>::of(&QButtonGroup::idClicked),
this, &AccountSetupDialog::onProviderSelected);
lay->addStretch();
return page;
}
// ─────────────────── Page 1: OAuth ───────────────────
QWidget* AccountSetupDialog::createOAuthPage()
{
QWidget *page = new QWidget();
QVBoxLayout *lay = new QVBoxLayout(page);
lay->setContentsMargins(10, 10, 10, 10);
lay->setSpacing(14);
QLabel *title = new QLabel("Autenticación OAuth2");
title->setStyleSheet("font-size: 18px; font-weight: 700;");
lay->addWidget(title);
QLabel *info = new QLabel(
"Se abrirá tu navegador web para que inicies sesión de forma segura.\\n"
"Wino Mail no almacena tu contraseña, solo el token de acceso autorizado.\\n\\n"
"Pasos:\\n"
" 1. Introduce tu dirección de correo.\\n"
" 2. Pulsa «Autenticar en Navegador».\\n"
" 3. Completa el inicio de sesión en la ventana del navegador.\\n"
" 4. Al terminar, esta ventana se actualizará automáticamente."
);
info->setWordWrap(true);
info->setStyleSheet("font-size: 13px; color: #555; line-height: 1.5;");
lay->addWidget(info);
lay->addSpacing(6);
QLabel *emailLabel = new QLabel("Correo electrónico:");
emailLabel->setStyleSheet("font-weight: 600; font-size: 13px;");
lay->addWidget(emailLabel);
m_oauthEmailEdit = new QLineEdit();
m_oauthEmailEdit->setPlaceholderText("tu-correo@gmail.com");
QRegularExpression rx(R"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})");
m_oauthEmailEdit->setValidator(new QRegularExpressionValidator(rx, this));
lay->addWidget(m_oauthEmailEdit);
lay->addSpacing(8);
m_btnOAuthStart = new QPushButton("🔑 Autenticar en Navegador");
m_btnOAuthStart->setMinimumHeight(40);
m_btnOAuthStart->setStyleSheet(
"QPushButton { background: #34a853; color: white; border: none; font-size: 14px; font-weight: 700; border-radius: 8px; }"
"QPushButton:hover { background: #2d9249; }"
"QPushButton:disabled { background: #a8d5b8; color: #e0e0e0; }"
);
connect(m_btnOAuthStart, &QPushButton::clicked, this, &AccountSetupDialog::startOAuthAuthentication);
lay->addWidget(m_btnOAuthStart);
m_oauthStatusLabel = new QLabel("Esperando…");
m_oauthStatusLabel->setStyleSheet("font-size: 12px; color: #8e8e93; font-style: italic;");
m_oauthStatusLabel->setAlignment(Qt::AlignCenter);
lay->addWidget(m_oauthStatusLabel);
lay->addStretch();
return page;
}
// ─────────────────── Page 2: IMAP ───────────────────
QWidget* AccountSetupDialog::createImapPage()
{
QWidget *page = new QWidget();
QVBoxLayout *outerLay = new QVBoxLayout(page);
outerLay->setContentsMargins(10, 10, 10, 10);
QLabel *title = new QLabel("Configuración IMAP / SMTP");
title->setStyleSheet("font-size: 18px; font-weight: 700;");
outerLay->addWidget(title);
QLabel *info = new QLabel("Introduce los datos de tu servidor de correo. "
"Consulta a tu proveedor si no conoces los datos del servidor.");
info->setWordWrap(true);
info->setStyleSheet("font-size: 12px; color: #8e8e93; margin-bottom: 8px;");
outerLay->addWidget(info);
// Scrollable form
QScrollArea *scroll = new QScrollArea();
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
QWidget *formWidget = new QWidget();
QFormLayout *form = new QFormLayout(formWidget);
form->setSpacing(10);
form->setContentsMargins(0, 8, 12, 8);
form->setLabelAlignment(Qt::AlignRight);
m_imapEmailEdit = new QLineEdit();
m_imapEmailEdit->setPlaceholderText("usuario@empresa.com");
m_imapNameEdit = new QLineEdit();
m_imapNameEdit->setPlaceholderText("Nombre a mostrar (ej. Javier)");
m_imapPasswordEdit = new QLineEdit();
m_imapPasswordEdit->setEchoMode(QLineEdit::Password);
m_imapPasswordEdit->setPlaceholderText("Contraseña");
m_imapHostEdit = new QLineEdit();
m_imapHostEdit->setPlaceholderText("imap.empresa.com");
m_imapPortEdit = new QLineEdit();
m_imapPortEdit->setPlaceholderText("993");
m_imapPortEdit->setValidator(new QIntValidator(1, 65535, this));
m_imapPortEdit->setMaximumWidth(100);
m_smtpHostEdit = new QLineEdit();
m_smtpHostEdit->setPlaceholderText("smtp.empresa.com");
m_smtpPortEdit = new QLineEdit();
m_smtpPortEdit->setPlaceholderText("587");
m_smtpPortEdit->setValidator(new QIntValidator(1, 65535, this));
m_smtpPortEdit->setMaximumWidth(100);
m_sslCheckbox = new QCheckBox("Usar conexión segura (SSL/TLS)");
m_sslCheckbox->setChecked(true);
form->addRow("Correo:", m_imapEmailEdit);
form->addRow("Nombre:", m_imapNameEdit);
form->addRow("Contraseña:", m_imapPasswordEdit);
// Visual separator
QFrame *line = new QFrame();
line->setFrameShape(QFrame::HLine);
line->setStyleSheet("color: #e0e0e5;");
form->addRow(line);
QLabel *serverHeader = new QLabel("Servidores");
serverHeader->setStyleSheet("font-weight: 700; font-size: 13px; color: #0071e3;");
form->addRow(serverHeader);
form->addRow("Servidor IMAP:", m_imapHostEdit);
form->addRow("Puerto IMAP:", m_imapPortEdit);
form->addRow("Servidor SMTP:", m_smtpHostEdit);
form->addRow("Puerto SMTP:", m_smtpPortEdit);
form->addRow("", m_sslCheckbox);
scroll->setWidget(formWidget);
outerLay->addWidget(scroll, 1);
return page;
}
// ─────────────────── Page 3: Progress ───────────────────
QWidget* AccountSetupDialog::createProgressPage()
{
QWidget *page = new QWidget();
QVBoxLayout *lay = new QVBoxLayout(page);
lay->setContentsMargins(20, 40, 20, 20);
lay->setAlignment(Qt::AlignCenter);
m_progressIcon = new QLabel("⏳");
m_progressIcon->setStyleSheet("font-size: 56px;");
m_progressIcon->setAlignment(Qt::AlignCenter);
m_progressText = new QLabel("Conectando al servidor…");
m_progressText->setStyleSheet("font-size: 16px; font-weight: 600; color: #1d1d1f;");
m_progressText->setAlignment(Qt::AlignCenter);
m_progressDetail = new QLabel("Validando credenciales y configuración del servidor de correo.");
m_progressDetail->setWordWrap(true);
m_progressDetail->setStyleSheet("font-size: 13px; color: #8e8e93;");
m_progressDetail->setAlignment(Qt::AlignCenter);
lay->addWidget(m_progressIcon);
lay->addSpacing(16);
lay->addWidget(m_progressText);
lay->addSpacing(8);
lay->addWidget(m_progressDetail);
lay->addStretch();
return page;
}
// ─────────────────── Navigation ───────────────────
void AccountSetupDialog::goToPage(int page)
{
m_stack->setCurrentIndex(page);
updateNavButtons();
}
void AccountSetupDialog::updateNavButtons()
{
int page = m_stack->currentIndex();
m_btnBack->setVisible(page > 0 && page != PageProgress);
m_btnCancel->setVisible(page != PageProgress || !m_accountCreatedOk);
switch (page) {
case PageProvider:
m_btnNext->setText("Siguiente →");
m_btnNext->setEnabled(true);
m_btnNext->setVisible(true);
break;
case PageOAuth:
m_btnNext->setVisible(false); // OAuth flow is driven by the authenticate button
break;
case PageImap:
m_btnNext->setText(m_isEditing ? "Guardar cambios" : "Conectar");
m_btnNext->setEnabled(true);
m_btnNext->setVisible(true);
break;
case PageProgress:
if (m_accountCreatedOk) {
m_btnNext->setText("Finalizar ✓");
m_btnNext->setEnabled(true);
m_btnNext->setVisible(true);
m_btnBack->setVisible(false);
m_btnCancel->setVisible(false);
} else {
m_btnNext->setVisible(false);
}
break;
}
}
void AccountSetupDialog::onProviderSelected(int id)
{
m_selectedProvider = id;
}
void AccountSetupDialog::onNextClicked()
{
int page = m_stack->currentIndex();
if (page == PageProvider) {
if (m_selectedProvider == 2) {
goToPage(PageImap);
} else {
// Update OAuth page subtitle based on provider
goToPage(PageOAuth);
}
}
else if (page == PageImap) {
submitImapAccount();
}
else if (page == PageProgress) {
accept(); // Finalizar
}
}
void AccountSetupDialog::onBackClicked()
{
int page = m_stack->currentIndex();
if (page == PageOAuth || page == PageImap) {
m_authTimeoutTimer->stop();
goToPage(PageProvider);
}
}
void AccountSetupDialog::onCancelClicked()
{
m_authTimeoutTimer->stop();
reject();
}
// ─────────────────── OAuth Flow ───────────────────
void AccountSetupDialog::startOAuthAuthentication()
{
QString email = m_oauthEmailEdit->text().trimmed();
if (email.isEmpty()) {
QMessageBox::warning(this, "Validación",
"Introduce tu dirección de correo electrónico.");
return;
}
m_btnOAuthStart->setEnabled(false);
m_oauthStatusLabel->setText("Abriendo navegador… Completa el inicio de sesión allí.");
m_oauthStatusLabel->setStyleSheet("font-size: 12px; color: #0071e3; font-weight: 600;");
QString provider = (m_selectedProvider == 0) ? "gmail" : "outlook";
// Start authentication via AccountService (opens browser)
m_accountService->startAuthentication(email, provider);
// Start timeout timer (120 seconds to complete OAuth)
m_authTimeoutTimer->start(120000);
qDebug() << "[AccountSetupDialog] OAuth started for" << email << "provider:" << provider;
}
void AccountSetupDialog::onAuthTimeout()
{
if (m_stack->currentIndex() == PageOAuth) {
m_btnOAuthStart->setEnabled(true);
m_oauthStatusLabel->setText("⚠️ Tiempo de espera agotado. Inténtalo de nuevo.");
m_oauthStatusLabel->setStyleSheet("font-size: 12px; color: #ff3b30; font-weight: 600;");
}
}
// ─────────────────── IMAP Flow ───────────────────
void AccountSetupDialog::submitImapAccount()
{
QString email = m_imapEmailEdit->text().trimmed();
QString name = m_imapNameEdit->text().trimmed();
QString password = m_imapPasswordEdit->text().trimmed();
QString imapHost = m_imapHostEdit->text().trimmed();
QString imapPort = m_imapPortEdit->text().trimmed();
QString smtpHost = m_smtpHostEdit->text().trimmed();
QString smtpPort = m_smtpPortEdit->text().trimmed();
// Validation
if (email.isEmpty()) {
QMessageBox::warning(this, "Campo requerido", "Introduce tu dirección de correo.");
return;
}
if (password.isEmpty()) {
QMessageBox::warning(this, "Campo requerido", "Introduce tu contraseña.");
return;
}
if (imapHost.isEmpty()) {
QMessageBox::warning(this, "Campo requerido", "Introduce el servidor IMAP.");
return;
}
// Use defaults if ports are empty
if (imapPort.isEmpty()) imapPort = m_sslCheckbox->isChecked() ? "993" : "143";
if (smtpPort.isEmpty()) smtpPort = m_sslCheckbox->isChecked() ? "465" : "587";
if (smtpHost.isEmpty()) smtpHost = imapHost.replace("imap.", "smtp.");
if (name.isEmpty()) name = email.split("@").first();
// Create account with connection settings
Account account;
account.setEmail(email);
account.setDisplayName(name);
account.setType(AccountType::IMAP);
Account::ConnectionSettings settings;
settings.type = "imap";
settings.incomingHost = imapHost;
settings.incomingPort = imapPort.toInt();
settings.incomingSsl = m_sslCheckbox->isChecked();
settings.outgoingHost = smtpHost;
settings.outgoingPort = smtpPort.toInt();
settings.outgoingSsl = m_sslCheckbox->isChecked();
settings.username = email;
settings.password = password;
settings.authMethod = "plain";
account.setConnectionSettings(settings);
// Show progress
m_accountCreatedOk = false;
goToPage(PageProgress);
m_progressIcon->setText("⏳");
m_progressText->setText("Conectando al servidor...");
m_progressDetail->setText(
QString("Servidor IMAP: %1:%2\nServidor SMTP: %3:%4\nSSL: %5")
.arg(imapHost, imapPort, smtpHost, smtpPort,
m_sslCheckbox->isChecked() ? "Sí" : "No")
);
// Test connection
QString errorMsg;
if (!m_accountService->testConnection(account.connectionSettings(), errorMsg)) {
showError(tr("Connection test failed: %1").arg(errorMsg));
m_authTimeoutTimer->stop();
return;
}
// Connection OK, proceed to add account
m_progressText->setText(tr("Guardando cuenta..."));
m_progressDetail->setText("");
// Start timeout for safety
m_authTimeoutTimer->start(30000);
// Perform add/update after a short delay to let UI update
QTimer::singleShot(0, this, [this, account]() mutable {
if (m_isEditing) {
account.setId(m_editingAccountId);
m_accountService->updateAccount(account);
} else {
m_accountService->addAccount(account);
}
}
});
// ─────────────────── Account Result ───────────────────
void AccountSetupDialog::onAccountAdded(const Account &account)
{
m_authTimeoutTimer->stop();
m_accountCreatedOk = true;
qDebug() << "[AccountSetupDialog] Account" << (m_isEditing ? "updated" : "created") << "successfully:" << account.email();
// If we were on the OAuth page, move to progress first
if (m_stack->currentIndex() == PageOAuth) {
goToPage(PageProgress);
}
showSuccess(QString(m_isEditing ? "¡Cuenta «%1» actualizada con éxito!\\n\\n"
"Los cambios se aplicarán inmediatamente."
: "¡Cuenta «%1» configurada con éxito!\\n\\n"
"La sincronización de correos comenzará automáticamente en segundo plano.")
.arg(account.email()));
emit accountCreated(account);
}
void AccountSetupDialog::showSuccess(const QString &message)
{
m_progressIcon->setText("✅");
m_progressText->setText("¡Cuenta añadida!");
m_progressDetail->setText(message);
updateNavButtons();
}
void AccountSetupDialog::showError(const QString &message)
{
m_accountCreatedOk = false;
m_progressIcon->setText("❌");
m_progressText->setText("Error de conexión");
m_progressDetail->setText(message);
// Allow going back
m_btnBack->setVisible(true);
m_btnCancel->setVisible(true);
m_btnNext->setVisible(false);
}
#include "accountsetupdialog.moc"
+2
View File
@@ -40,6 +40,7 @@ private slots:
void startOAuthAuthentication();
void onAccountAdded(const Account &account);
void onAuthTimeout();
void onConnectionWizardFinished(const Account::ConnectionSettings &settings);
private:
void setupUI();
@@ -53,6 +54,7 @@ private:
void showError(const QString &message);
void showSuccess(const QString &message);
void submitImapAccount();
void launchConnectionWizard();
// Services
AccountService *m_accountService;
+78
View File
@@ -0,0 +1,78 @@
import sys
filename = '/mnt/c/Users/javie/wino-mail-dtkqt/src/ui/mainmainwindow.cpp'
with open(filename, 'r') as f:
lines = f.readlines()
# 1. Find the line where deleteAction is declared
decl_line = None
for i, line in enumerate(lines):
if 'QAction *deleteAction = m_toolBar->addAction(\"🗑 Delete\");' in line:
decl_line = i
break
if decl_line is None:
print('Could not find deleteAction declaration')
sys.exit(1)
# Insert flag action declaration right after it
flag_decl = ' QAction *flagAction = m_toolBar->addAction(\"🚩 Flag\");\n'
lines = lines[:decl_line+1] + [flag_decl] + lines[decl_line+1:]
# 2. Find the closing brace of the createToolBar function
brace_line = None
brace_count = 0
in_function = False
for i, line in enumerate(lines):
if i < decl_line:
continue
if 'void MainMainWindow::createToolBar() {' in line:
in_function = True
continue
if not in_function:
continue
# Count braces
for ch in line:
if ch == '{':
brace_count += 1
elif ch == '}':
brace_count -= 1
if brace_count == 0:
brace_line = i
break
if brace_line is not None:
break
if brace_line is None:
print('Could not find closing brace of createToolBar')
sys.exit(1)
# 3. Insert flag connection just before the closing brace
flag_conn = ''' connect(flagAction, &QAction::triggered, [this]() {
QModelIndexList indexes = m_mailListView->selectionModel()->selectedIndexes();
if (indexes.isEmpty()) {
statusBar()->showMessage(tr("No email selected"), 2000);
return;
}
int row = indexes.first().row();
QModelIndex idx = m_emailModel->index(row, 0);
qint64 id = idx.data(EmailListModel::IdRole).toLongLong();
// Toggle flagged state
std::optional<MailItem> opt = MailItemDao::findById(id);
if (opt) {
MailItem item = *opt;
item.setFlagged(!item.isFlagged());
if (MailItemDao::update(item)) {
statusBar()->showMessage(tr("Flag toggled"), 2000);
m_emailModel->refresh(); // optional, to update icon if any
} else {
statusBar()->showMessage(tr("Failed to update flag"), 2000);
}
} else {
statusBar()->showMessage(tr("Email not found"), 2000);
}
});\n'''
lines = lines[:brace_line] + [flag_conn] + lines[brace_line:]
with open(filename, 'w') as f:
f.writelines(lines)
print('Flag action added')
+217 -390
View File
@@ -1,277 +1,21 @@
#include "ui/composeview.h"
#include <QFrame>
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
#include <QDebug>
// ===================== ComposeView =====================
#include "composeview.h"
#include <QDialog>
#include <QInputDialog>
#include <QVBoxLayout>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QFileInfo>
#include <QMessageBox>
#include <QLabel>
#include "tags_line_edit.hpp"
#include <QRegularExpression>
#include <QTimer>
#include <QTextList>
#include <QTextTable>
#include <QTextCursor>
#include <QTextBlockFormat>
#include <QTextCharFormat>
// ===================== RichTextEditor =====================
RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) {
setAcceptRichText(true);
setPlaceholderText("Write your message here...");
}
void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
m_toolbar = new QToolBar("Formatting");
m_toolbar->setIconSize(QSize(16, 16));
m_toolbar->setStyleSheet(
"QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }"
"QToolButton { padding: 4px 6px; border-radius: 3px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton:checked { background: #bbdefb; }"
);
// Font family combo
m_fontCombo = new QFontComboBox();
m_fontCombo->setFixedWidth(150);
connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged);
m_toolbar->addWidget(m_fontCombo);
// Font size spin
m_fontSizeSpin = new QSpinBox();
m_fontSizeSpin->setRange(8, 72);
m_fontSizeSpin->setValue(14);
m_fontSizeSpin->setFixedWidth(50);
connect(m_fontSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged);
m_toolbar->addWidget(m_fontSizeSpin);
m_toolbar->addSeparator();
// Bold / Italic / Underline
QAction *boldAct = m_toolbar->addAction("B");
boldAct->setCheckable(true);
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
QAction *italicAct = m_toolbar->addAction("I");
italicAct->setCheckable(true);
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
QAction *underlineAct = m_toolbar->addAction("U");
underlineAct->setCheckable(true);
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
m_toolbar->addSeparator();
// Alignment
QAction *alignLeft = m_toolbar->addAction("L");
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
QAction *alignCenter = m_toolbar->addAction("C");
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
QAction *alignRight = m_toolbar->addAction("R");
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
QAction *alignJustify = m_toolbar->addAction("J");
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
m_toolbar->addSeparator();
// Lists
QAction *bulletAct = m_toolbar->addAction("Bullets");
connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList);
QAction *numAct = m_toolbar->addAction("1. List");
connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList);
m_toolbar->addSeparator();
// Indent / Outdent
QAction *indentAct = m_toolbar->addAction("Indent");
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
QAction *outdentAct = m_toolbar->addAction("Outdent");
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
m_toolbar->addSeparator();
// Insert image
QAction *imgAct = m_toolbar->addAction("Img");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
// Insert table
QAction *tableAct = m_toolbar->addAction("Tbl");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
// Signature button
m_signatureButton = new QToolButton();
m_signatureButton->setText("Signature");
m_signatureButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_signatureButton->setPopupMode(QToolButton::MenuButtonPopup);
m_signatureMenu = new QMenu(m_signatureButton);
m_signatureButton->setMenu(m_signatureMenu);
m_signatureButton->setStyleSheet(
"QToolButton { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 3px; padding: 4px 6px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }"
);
connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked);
m_toolbar->addWidget(m_signatureButton);
QAction *editSigAct = m_signatureMenu->addAction(tr("Editar firmas"));
connect(editSigAct, &QAction::triggered, this, &RichTextEditor::signatureEditRequested);
m_toolbar->addSeparator();
layout->addWidget(m_toolbar);
}
void RichTextEditor::onBold() {
QTextCharFormat fmt;
fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onItalic() {
QTextCharFormat fmt;
fmt.setFontItalic(!textCursor().charFormat().fontItalic());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onUnderline() {
QTextCharFormat fmt;
fmt.setFontUnderline(!textCursor().charFormat().fontUnderline());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onBulletList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDisc) {
// Remove list
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDisc);
cursor.createList(listFormat);
}
}
void RichTextEditor::onNumberedList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDecimal) {
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDecimal);
cursor.createList(listFormat);
}
}
void RichTextEditor::onIndent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setIndent(bfmt.indent() + 1);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onOutdent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
int indent = bfmt.indent();
if (indent > 0) {
bfmt.setIndent(indent - 1);
cursor.setBlockFormat(bfmt);
}
}
void RichTextEditor::onAlignLeft() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignLeft);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignCenter() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignCenter);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignRight() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignRight);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignJustify() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignJustify);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onFontChanged(const QFont &font) {
QTextCharFormat fmt;
fmt.setFontFamilies({font.family()});
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onFontSizeChanged(int size) {
QTextCharFormat fmt;
fmt.setFontPointSize(size);
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onInsertImage() {
QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)");
if (filePath.isEmpty()) return;
QTextCursor cursor = textCursor();
QTextImageFormat imgFmt;
imgFmt.setName(filePath);
// Scale down if too large
QPixmap pm(filePath);
if (pm.width() > 600) {
imgFmt.setWidth(600);
imgFmt.setHeight(pm.height() * 600 / pm.width());
}
cursor.insertImage(imgFmt);
}
void RichTextEditor::onInsertTable() {
bool ok;
int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok);
if (!ok) return;
int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok);
if (!ok) return;
QTextCursor cursor = textCursor();
QTextTableFormat tableFmt;
tableFmt.setBorder(1);
tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
tableFmt.setCellPadding(4);
tableFmt.setCellSpacing(0);
tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100));
cursor.insertTable(rows, cols, tableFmt);
}
#include <QPushButton>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QPixmap>
#include <QStyle>
#include <QApplication>
#include <QFileIconProvider>
ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
setupUI();
// Connect the rich text editor's signature signal to our slot
@@ -282,14 +26,14 @@ ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
ComposeView::~ComposeView() {
// Destructor implementation
}
void ComposeView::onSignatureClicked() {
// This slot is called when the signature button in the rich text editor is clicked
// The actual menu handling is done in RichTextEditor, so we just need to
// handle any ComposeView-specific logic here if needed
}
void ComposeView::onSignatureEditRequested()
{
void ComposeView::onSignatureEditRequested() {
QDialog dialog(this);
dialog.setWindowTitle(tr("Edit Signature"));
dialog.setMinimumSize(600, 400);
@@ -314,14 +58,13 @@ void ComposeView::onSignatureEditRequested()
}
}
}
void ComposeView::setAccountService(AccountService *service)
{
void ComposeView::setAccountService(AccountService *service) {
m_accountService = service;
populateAccountCombo();
}
void ComposeView::populateAccountCombo()
{
void ComposeView::populateAccountCombo() {
if (!m_accountService) {
qWarning() << "AccountService not set";
return;
@@ -342,8 +85,7 @@ void ComposeView::populateAccountCombo()
}
}
void ComposeView::onAccountChanged(int index)
{
void ComposeView::onAccountChanged(int index) {
if (index <= 0) {
// Placeholder item selected or invalid index
m_currentAccountId = -1;
@@ -354,8 +96,7 @@ void ComposeView::onAccountChanged(int index)
loadSignatureForCurrentAccount();
}
void ComposeView::loadSignatureForCurrentAccount()
{
void ComposeView::loadSignatureForCurrentAccount() {
if (!m_accountService || m_currentAccountId <= 0) {
// Clear signature if no account selected
m_bodyEditor->clear();
@@ -367,12 +108,12 @@ void ComposeView::loadSignatureForCurrentAccount()
// For now, we'll just clear the editor since we don't have a signature field in Account yet
// In a real implementation, we would retrieve and display the signature
m_bodyEditor->clear();
delete account;
} else {
m_bodyEditor->clear();
}
}
void ComposeView::setupUI() {
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20, 15, 20, 15);
@@ -383,37 +124,9 @@ void ComposeView::setupUI() {
"QLineEdit:focus, QTextEdit:focus { border-color: #1976D2; }"
);
// Header row: Subject + Detach button
QHBoxLayout *headerLayout = new QHBoxLayout();
m_subjectField = new QLineEdit();
m_subjectField->setPlaceholderText("Subject");
m_subjectField->setFixedHeight(40);
QFont subjectFont = m_subjectField->font();
subjectFont.setPointSize(14);
subjectFont.setBold(true);
m_subjectField->setFont(subjectFont);
headerLayout->addWidget(m_subjectField, 1);
m_detachButton = new QPushButton("\xe2\x87\xa5 Detach");
m_detachButton->setToolTip("Open compose window in a separate window");
m_detachButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 12px; color: #555; font-size: 12px; }"
"QPushButton:hover { background: #f0f0f0; }"
);
connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked);
headerLayout->addWidget(m_detachButton);
mainLayout->addLayout(headerLayout);
// Separator line
QFrame *line1 = new QFrame();
line1->setFrameShape(QFrame::HLine);
line1->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line1);
// From: field + Account selector
// De: field + Account selector (renamed from From:)
QHBoxLayout *fromLayout = new QHBoxLayout();
QLabel *fromLabel = new QLabel("From:");
QLabel *fromLabel = new QLabel(tr("De:"));
fromLabel->setFixedWidth(40);
fromLabel->setStyleSheet("font-weight: bold; color: #555;");
m_accountCombo = new QComboBox();
@@ -429,16 +142,17 @@ void ComposeView::setupUI() {
connect(m_accountCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ComposeView::onAccountChanged);
fromLayout->addWidget(fromLabel);
fromLayout->addWidget(m_accountCombo, 1);
fromLayout->addWidget(m_accountCombo);
fromLayout->addStretch(); // Este resorte empuja el par (label+combo) a la izquierda
mainLayout->addLayout(fromLayout);
// To: field + Cc/Bcc toggle buttons
// Para: field + Cc/Bcc toggle buttons
QHBoxLayout *toLayout = new QHBoxLayout();
QLabel *toLabel = new QLabel("To:");
QLabel *toLabel = new QLabel(tr("Para:"));
toLabel->setFixedWidth(40);
toLabel->setStyleSheet("font-weight: bold; color: #555;");
m_toField = new QLineEdit();
m_toField->setPlaceholderText("Recipients (comma separated)");
m_toField = new everload_tags::TagsLineEdit();
toLayout->addWidget(toLabel);
toLayout->addWidget(m_toField, 1);
@@ -466,19 +180,20 @@ void ComposeView::setupUI() {
m_ccRow = new QWidget();
QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow);
ccLayout->setContentsMargins(0, 0, 0, 0);
QLabel *ccLabel = new QLabel("Cc:");
QLabel *ccLabel = new QLabel(tr("Cc:"));
ccLabel->setFixedWidth(40);
ccLabel->setStyleSheet("color: #555;");
m_ccField = new QLineEdit();
m_ccField->setPlaceholderText("Carbon copy");
m_ccField = new everload_tags::TagsLineEdit();
ccLayout->addWidget(ccLabel);
ccLayout->addWidget(m_ccField, 1);
m_hideCcButton = new QPushButton("x");
m_hideCcButton = new QPushButton("");
m_hideCcButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Linear/Essentional, UI/Close Square.svg")));
m_hideCcButton->setFixedSize(20, 20);
m_hideCcButton->setToolTip("Hide Cc");
m_hideCcButton->setToolTip(tr("Hide Cc"));
m_hideCcButton->setStyleSheet(
"QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }"
"QPushButton { background: transparent; border: none; color: blue; font-weight: bold; }"
"QPushButton:hover { color: #333; }"
);
connect(m_hideCcButton, &QPushButton::clicked, this, &ComposeView::onCcToggle);
@@ -491,19 +206,20 @@ void ComposeView::setupUI() {
m_bccRow = new QWidget();
QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow);
bccLayout->setContentsMargins(0, 0, 0, 0);
QLabel *bccLabel = new QLabel("Bcc:");
QLabel *bccLabel = new QLabel(tr("Bcc:"));
bccLabel->setFixedWidth(40);
bccLabel->setStyleSheet("color: #555;");
m_bccField = new QLineEdit();
m_bccField->setPlaceholderText("Blind carbon copy");
m_bccField = new everload_tags::TagsLineEdit();
bccLayout->addWidget(bccLabel);
bccLayout->addWidget(m_bccField, 1);
m_hideBccButton = new QPushButton("x");
m_hideBccButton = new QPushButton("");
m_hideBccButton->setFixedSize(20, 20);
m_hideBccButton->setToolTip("Hide Bcc");
m_hideBccButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Linear/Essentional, UI/Close Square.svg")));
m_hideBccButton->setToolTip(tr("Hide Bcc"));
m_hideBccButton->setStyleSheet(
"QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }"
"QPushButton { background: transparent; border: none; color: blue; font-weight: bold; }"
"QPushButton:hover { color: #333; }"
);
connect(m_hideBccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle);
@@ -512,11 +228,39 @@ void ComposeView::setupUI() {
m_bccRow->setVisible(false);
mainLayout->addWidget(m_bccRow);
// Separator
QFrame *line2 = new QFrame();
line2->setFrameShape(QFrame::HLine);
line2->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line2);
// Asunto: field
QHBoxLayout *subjectLayout = new QHBoxLayout();
QLabel *subjectLabel = new QLabel(tr("Asunto:"));
subjectLabel->setFixedWidth(60);
subjectLabel->setStyleSheet("font-weight: bold; color: #555;");
m_subjectField = new QLineEdit();
m_subjectField->setPlaceholderText(tr("Subject"));
m_subjectField->setFixedHeight(40);
QFont subjectFont = m_subjectField->font();
subjectFont.setPointSize(14);
subjectFont.setBold(true);
m_subjectField->setFont(subjectFont);
//subjectLayout->addWidget(subjectLabel);
subjectLayout->addWidget(m_subjectField, 1);
// Detach button placed to the right of the subject line
m_detachButton = new QPushButton("");//("↥ Detach");
m_detachButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Linear/Arrows Action/Square Top Down.svg")));
m_detachButton->setToolTip(tr("Open compose window in a separate window"));
m_detachButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 12px; color: #555; font-size: 12px; }"
"QPushButton:hover { background: #f0f0f0; }"
);
connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked);
subjectLayout->addWidget(m_detachButton);
mainLayout->addLayout(subjectLayout);
// Separator line admin@email.com
QFrame *line1 = new QFrame();
line1->setFrameShape(QFrame::HLine);
line1->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line1);
// Rich text editor with toolbar
m_bodyEditor = new RichTextEditor();
@@ -527,7 +271,7 @@ void ComposeView::setupUI() {
m_schedulePanel = new QWidget();
QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel);
scheduleLayout->setContentsMargins(0, 0, 0, 0);
QLabel *scheduleLabel = new QLabel("Send at:");
QLabel *scheduleLabel = new QLabel(tr("Send at:"));
scheduleLabel->setStyleSheet("color: #555; font-weight: bold;");
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600));
m_schedulePicker->setCalendarPopup(true);
@@ -536,79 +280,91 @@ void ComposeView::setupUI() {
scheduleLayout->addWidget(m_schedulePicker);
scheduleLayout->addStretch();
m_scheduleSendButton = new QPushButton("Schedule Send");
m_scheduleSendButton = new QPushButton(tr("Schedule Send"));
m_scheduleSendButton->setFixedWidth(150);
m_scheduleSendButton->setStyleSheet(
"QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 6px 16px; font-weight: bold; }"
"QPushButton:hover { background-color: #1565C0; }"
);
connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked);
scheduleLayout->addWidget(m_scheduleSendButton);
scheduleLayout->addWidget(m_scheduleSendButton, 2);
m_schedulePanel->setVisible(false);
m_schedulePanel->setVisible(false);
mainLayout->addWidget(m_schedulePanel);
// Attachment section
QHBoxLayout *attachmentLayout = new QHBoxLayout();
QLabel *attachmentLabel = new QLabel(tr("Attachments:"));
attachmentLabel->setFixedWidth(80);
attachmentLabel->setStyleSheet("font-weight: bold; color: #555;");
attachmentLayout->addWidget(attachmentLabel);
m_attachButton = new QToolButton();
m_attachButton->setIcon(QIcon(QStringLiteral(":/icons/attachment.svg")));
m_attachButton->setToolTip(tr("Add attachment"));
m_attachButton->setIconSize(QSize(20,20));
m_attachButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }");
bool connected = connect(m_attachButton, &QToolButton::clicked, this, &ComposeView::onAddAttachmentClicked);
qDebug() << "Attach button connected:" << connected;
attachmentLayout->addWidget(m_attachButton);
m_attachmentList = new QListWidget();
m_attachmentList->setViewMode(QListView::IconMode);
m_attachmentList->setResizeMode(QListView::Adjust);
m_attachmentList->setMovement(QListView::Static);
m_attachmentList->setGridSize(QSize(300, 50));
m_attachmentList->setSpacing(10);
m_attachmentList->setLayoutDirection(Qt::LeftToRight);
m_attachmentList->setSelectionMode(QAbstractItemView::SingleSelection);
m_attachmentList->setMaximumHeight(60);
m_attachmentList->setStyleSheet("QListWidget { border: 1px solid #d1d1d6; border-radius: 4px; }");
m_attachmentList->setStyleSheet(
"QListWidget { border: 1px solid #d1d1d6; border-radius: 4px; }"
"QListWidget::item { border: none; padding: 5px; }"
"QListWidget::item:selected { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #1976D2, stop:1 #1565C0); border-radius: 4px; }"
);
m_attachmentList->setVisible(false);
attachmentLayout->addWidget(m_attachmentList, 1);
m_removeAttachmentButton = new QToolButton();
m_removeAttachmentButton->setIcon(QIcon(QStringLiteral(":/icons/trash.svg")));
m_removeAttachmentButton->setToolTip(tr("Remove selected attachment"));
m_removeAttachmentButton->setIconSize(QSize(20,20));
m_removeAttachmentButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }");
connect(m_removeAttachmentButton, &QToolButton::clicked, this, &ComposeView::onRemoveAttachmentClicked);
attachmentLayout->addWidget(m_removeAttachmentButton);
mainLayout->addLayout(attachmentLayout);
// Action buttons row
QHBoxLayout *actionsLayout = new QHBoxLayout();
actionsLayout->addStretch();
m_discardButton = new QPushButton("Discard");
// Attachments and Templates buttons (right-aligned)
QHBoxLayout *buttonRow = new QHBoxLayout();
m_attachButton = new QToolButton();
m_attachButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Outline/Messages, Conversation/Paperclip.svg")));
m_attachButton->setToolTip(tr("Add attachment"));
m_attachButton->setIconSize(QSize(20,20));
m_attachButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }");
connect(m_attachButton, &QToolButton::clicked, this, &ComposeView::onAddAttachmentClicked);
buttonRow->addWidget(m_attachButton);
m_templateButton = new QToolButton();
m_templateButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Outline/Files/File Text.svg"))); // Assuming you have a template icon
m_templateButton->setToolTip(tr("Email templates"));
m_templateButton->setIconSize(QSize(20,20));
m_templateButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }");
connect(m_templateButton, &QToolButton::clicked, this, &ComposeView::onTemplateClicked);
buttonRow->addWidget(m_templateButton);
buttonRow->setSpacing(4);
actionsLayout->addLayout(buttonRow);
actionsLayout->addStretch(); // Este resorte empuja el par (label+combo) a la izquierda
m_discardButton = new QPushButton(tr("Discard"));
m_discardButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px 20px; color: #555; }"
"QPushButton:hover { background: #f5f5f5; }"
);
actionsLayout->addWidget(m_discardButton);
connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested);
actionsLayout->addWidget(m_discardButton);
// Split button for Send / Schedule
m_sendMenu = new QMenu(this);
m_sendNowAction = m_sendMenu->addAction("Send Now");
m_sendNowAction = m_sendMenu->addAction(tr("Send Now"));
connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked);
m_scheduleAction = m_sendMenu->addAction("Schedule for later...");
m_scheduleAction = m_sendMenu->addAction(tr("Schedule for later..."));
connect(m_scheduleAction, &QAction::triggered, [this]() {
m_schedulePanel->setVisible(true);
});
m_sendSplit = new QToolButton();
m_sendSplit->setText("Send");
m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_sendSplit->setText(tr("Send"));
m_sendSplit->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Bold Duotone/Messages, Conversation/Plain.svg")));
m_templateButton->setIconSize(QSize(20,20));
//m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup);
m_sendSplit->setMenu(m_sendMenu);
m_sendSplit->setStyleSheet(
"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 8px 24px; font-weight: bold; }"
"QToolButton:hover { background-color: #1565C0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; width: 40px;}"
"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; width: 20px;}"
"QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }"
);
connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked);
@@ -633,25 +389,27 @@ void ComposeView::onBccToggle() {
void ComposeView::onSendClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_toField->tags2().join(", "),
m_ccVisible ? m_ccField->tags2().join(", ") : QString(),
m_bccVisible ? m_bccField->tags2().join(", ") : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
QDateTime(), // null = send now
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString(),
m_attachmentFiles
);
}
void ComposeView::onScheduleClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_toField->tags2().join(", "),
m_ccVisible ? m_ccField->tags2().join(", ") : QString(),
m_bccVisible ? m_bccField->tags2().join(", ") : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
m_schedulePicker->dateTime(),
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString(),
m_attachmentFiles
);
}
@@ -659,15 +417,19 @@ void ComposeView::onDetachClicked() {
emit detachRequested(this);
}
void ComposeView::setTo(const QString &to) { m_toField->setText(to); }
void ComposeView::setTo(const QString &to) { m_toField->tags(to.split(',', Qt::SkipEmptyParts)); }
void ComposeView::setSubject(const QString &subject) { m_subjectField->setText(subject); }
void ComposeView::setBody(const QString &body) { m_bodyEditor->setHtml(body); }
void ComposeView::initializeComposition() {
m_toField->clear();
m_ccField->clear();
m_bccField->clear();
m_toField->tags(QStringList{});
m_ccField->tags(QStringList{});
m_bccField->tags(QStringList{});
m_subjectField->clear();
m_bodyEditor->clear();
m_attachmentFiles.clear();
m_attachmentList->clear();
m_attachmentList->setVisible(false);
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
@@ -677,26 +439,85 @@ void ComposeView::initializeComposition() {
void ComposeView::startNewEmail(const QString &initialRecipient) {
initializeComposition();
if (!initialRecipient.isEmpty())
m_toField->setText(initialRecipient);
m_toField->tags(initialRecipient.split(',', Qt::SkipEmptyParts));
m_toField->setFocus();
}
void ComposeView::onAddAttachmentClicked()
{
void ComposeView::onAddAttachmentClicked() {
qDebug() << "Attach button clicked";
QStringList files = QFileDialog::getOpenFileNames(this, tr("Select Attachments"), QString(), tr("All Files (*)"));
if (files.isEmpty())
return;
for (const QString &file : files) {
m_attachmentFiles.append(file);
QListWidgetItem *item = new QListWidgetItem(QFileInfo(file).fileName(), m_attachmentList);
item->setToolTip(file);
QListWidgetItem *item = new QListWidgetItem(m_attachmentList);
item->setData(Qt::UserRole, file);
QWidget *widget = new QWidget;
widget->setFixedSize(300, 50);
widget->setStyleSheet(
"background-color: #f8f9fa; "
"border-radius: 8px; "
"border: 1px solid #e9ecef;"
);
QHBoxLayout *layout = new QHBoxLayout(widget);
layout->setContentsMargins(8, 4, 8, 4);
layout->setSpacing(8);
// Icon from system
QFileIconProvider provider;
QIcon icon = provider.icon(QFileInfo(file));
QLabel *iconLabel = new QLabel;
QPixmap pix = icon.pixmap(32, 32);
if (pix.isNull())
pix = QIcon::fromTheme("unknown").pixmap(32,32);
iconLabel->setPixmap(pix);
iconLabel->setFixedSize(32,32);
layout->addWidget(iconLabel);
// Text vertical layout
QVBoxLayout *textLayout = new QVBoxLayout;
textLayout->setSpacing(2);
QString fileName = QFileInfo(file).fileName();
QLabel *nameLabel = new QLabel(fileName);
nameLabel->setStyleSheet("font-weight: bold;");
QLabel *sizeLabel = new QLabel(tr("%1 KB").arg(QFileInfo(file).size()/1024));
sizeLabel->setStyleSheet("color: #666; font-size: 10px;");
textLayout->addWidget(nameLabel);
textLayout->addWidget(sizeLabel);
layout->addLayout(textLayout);
layout->addStretch();
// Delete button
QPushButton *delBtn = new QPushButton;
QIcon delIcon = QIcon::fromTheme("edit-delete");
if (delIcon.isNull())
delIcon = QIcon::fromTheme("list-remove");
if (delIcon.isNull())
delIcon = QIcon::fromTheme("gtk-delete");
delBtn->setIcon(delIcon);
delBtn->setToolTip(tr("Remove attachment"));
delBtn->setFixedSize(24,24);
delBtn->setStyleSheet(
"QPushButton { border: none; background: transparent; }"
"QPushButton:hover { background: #e9ecef; border-radius: 4px; }"
);
layout->addWidget(delBtn);
// Connect delete button
connect(delBtn, &QPushButton::clicked, [this, item]() {
int row = m_attachmentList->row(item);
if (row >= 0) {
QListWidgetItem *it = m_attachmentList->takeItem(row);
if (it) {
m_attachmentFiles.removeAt(row);
delete it;
}
QMessageBox::information(this, tr("Attachments"), tr("Attached %1 file(s).").arg(files.count()));
m_attachmentList->setVisible(!m_attachmentFiles.isEmpty());
}
});
m_attachmentList->addItem(item);
m_attachmentList->setItemWidget(item, widget);
}
m_attachmentList->setVisible(!m_attachmentFiles.isEmpty());
}
void ComposeView::onRemoveAttachmentClicked()
{
void ComposeView::onRemoveAttachmentClicked() {
QListWidgetItem *item = m_attachmentList->currentItem();
if (!item)
return;
@@ -704,6 +525,12 @@ void ComposeView::onRemoveAttachmentClicked()
m_attachmentList->takeItem(row);
m_attachmentFiles.removeAt(row);
delete item;
m_attachmentList->setVisible(!m_attachmentFiles.isEmpty());
}
void ComposeView::onTemplateClicked() {
// Placeholder for template functionality
QMessageBox::information(this, tr("Email Templates"), tr("Email templates feature not yet implemented."));
}
#include "composeview.moc"
+632
View File
@@ -0,0 +1,632 @@
#include "ui/composeview.h"
#include <QFrame>
#include <QFileDialog>
#include <QInputDialog>
#include <QTextList>
#include <QTextTable>
#include <QTextCursor>
#include <QTextBlockFormat>
#include <QTextCharFormat>
// ===================== RichTextEditor =====================
RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) {
setAcceptRichText(true);
setPlaceholderText("Write your message here...");
}
void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
m_toolbar = new QToolBar("Formatting");
m_toolbar->setIconSize(QSize(16, 16));
m_toolbar->setStyleSheet(
"QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }"
"QToolButton { padding: 4px 6px; border-radius: 3px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton:checked { background: #bbdefb; }"
);
// Font family combo
m_fontCombo = new QFontComboBox();
m_fontCombo->setFixedWidth(150);
connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged);
m_toolbar->addWidget(m_fontCombo);
// Font size spin
m_fontSizeSpin = new QSpinBox();
m_fontSizeSpin->setRange(8, 72);
m_fontSizeSpin->setValue(14);
m_fontSizeSpin->setFixedWidth(50);
connect(m_fontSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged);
m_toolbar->addWidget(m_fontSizeSpin);
m_toolbar->addSeparator();
// Bold / Italic / Underline
QAction *boldAct = m_toolbar->addAction("B");
boldAct->setCheckable(true);
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
QAction *italicAct = m_toolbar->addAction("I");
italicAct->setCheckable(true);
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
QAction *underlineAct = m_toolbar->addAction("U");
underlineAct->setCheckable(true);
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
m_toolbar->addSeparator();
// Alignment
QAction *alignLeft = m_toolbar->addAction("L");
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
QAction *alignCenter = m_toolbar->addAction("C");
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
QAction *alignRight = m_toolbar->addAction("R");
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
QAction *alignJustify = m_toolbar->addAction("J");
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
m_toolbar->addSeparator();
// Lists
QAction *bulletAct = m_toolbar->addAction("Bullets");
connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList);
QAction *numAct = m_toolbar->addAction("1. List");
connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList);
m_toolbar->addSeparator();
// Indent / Outdent
QAction *indentAct = m_toolbar->addAction("Indent");
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
QAction *outdentAct = m_toolbar->addAction("Outdent");
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
m_toolbar->addSeparator();
// Insert image
QAction *imgAct = m_toolbar->addAction("Img");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
// Insert table
QAction *tableAct = m_toolbar->addAction("Tbl");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
// Signature button
m_signatureButton = new QToolButton();
m_signatureButton->setText("Signature");
m_signatureButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_signatureButton->setPopupMode(QToolButton::MenuButtonPopup);
m_signatureMenu = new QMenu(m_signatureButton);
m_signatureButton->setMenu(m_signatureMenu);
m_signatureButton->setStyleSheet(
"QToolButton { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 3px; padding: 4px 6px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }"
);
connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked);
m_toolbar->addWidget(m_signatureButton);
m_toolbar->addSeparator();
layout->addWidget(m_toolbar);
}
void RichTextEditor::onBold() {
QTextCharFormat fmt;
fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onItalic() {
QTextCharFormat fmt;
fmt.setFontItalic(!textCursor().charFormat().fontItalic());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onUnderline() {
QTextCharFormat fmt;
fmt.setFontUnderline(!textCursor().charFormat().fontUnderline());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onBulletList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDisc) {
// Remove list
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDisc);
cursor.createList(listFormat);
}
}
void RichTextEditor::onNumberedList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDecimal) {
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDecimal);
cursor.createList(listFormat);
}
}
void RichTextEditor::onIndent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setIndent(bfmt.indent() + 1);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onOutdent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
int indent = bfmt.indent();
if (indent > 0) {
bfmt.setIndent(indent - 1);
cursor.setBlockFormat(bfmt);
}
}
void RichTextEditor::onAlignLeft() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignLeft);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignCenter() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignCenter);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignRight() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignRight);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignJustify() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignJustify);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onFontChanged(const QFont &font) {
QTextCharFormat fmt;
fmt.setFontFamilies({font.family()});
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onFontSizeChanged(int size) {
QTextCharFormat fmt;
fmt.setFontPointSize(size);
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onInsertImage() {
QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)");
if (filePath.isEmpty()) return;
QTextCursor cursor = textCursor();
QTextImageFormat imgFmt;
imgFmt.setName(filePath);
// Scale down if too large
QPixmap pm(filePath);
if (pm.width() > 600) {
imgFmt.setWidth(600);
imgFmt.setHeight(pm.height() * 600 / pm.width());
}
cursor.insertImage(imgFmt);
}
void RichTextEditor::onInsertTable() {
bool ok;
int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok);
if (!ok) return;
int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok);
if (!ok) return;
QTextCursor cursor = textCursor();
QTextTableFormat tableFmt;
tableFmt.setBorder(1);
tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
tableFmt.setCellPadding(4);
tableFmt.setCellSpacing(0);
tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100));
cursor.insertTable(rows, cols, tableFmt);
}
ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
setupUI();
// Connect the rich text editor's signature signal to our slot
connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked);
}
ComposeView::~ComposeView() {
// Destructor implementation
}
void ComposeView::onSignatureClicked() {
// This slot is called when the signature button in the rich text editor is clicked
// The actual menu handling is done in RichTextEditor, so we just need to
// handle any ComposeView-specific logic here if needed
}
void ComposeView::onSignatureEditRequested() {
// This slot will be connected to the "Edit Signature..." action in the menu
// For now, we'll just show a simple input dialog
QString currentSignature = "";
// In a real implementation, we would get the current signature for the selected account
// For now, we'll use an empty string
bool ok;
QString newSignature = QInputDialog::getMultiLineText(this, "Edit Signature",
"Enter your signature:",
currentSignature, &ok);
if (ok && !newSignature.isEmpty()) {
// In a real implementation, we would save this signature for the selected account
// For now, we'll just insert it at the cursor position
QTextCursor cursor = m_bodyEditor->textCursor();
cursor.insertHtml(newSignature);
}
}
void ComposeView::setAccountService(AccountService *service)
{
m_accountService = service;
populateAccountCombo();
}
void ComposeView::populateAccountCombo()
{
if (!m_accountService) {
qWarning() << "AccountService not set";
return;
}
m_accountCombo->clear();
m_accountCombo->addItem(tr("Select Account..."), QVariant());
QVector<Account> accounts = m_accountService->getAllAccounts();
for (const Account &account : accounts) {
m_accountCombo->addItem(account.email(), account.id());
}
// Select the first account by default if there are accounts
if (accounts.size() > 0) {
m_accountCombo->setCurrentIndex(1); // Skip the placeholder item
m_currentAccountId = accounts.first().id();
}
}
void ComposeView::onAccountChanged(int index)
{
if (index <= 0) {
// Placeholder item selected or invalid index
m_currentAccountId = -1;
return;
}
m_currentAccountId = m_accountCombo->itemData(index).toInt();
loadSignatureForCurrentAccount();
}
void ComposeView::loadSignatureForCurrentAccount()
{
if (!m_accountService || m_currentAccountId <= 0) {
// Clear signature if no account selected
m_bodyEditor->clear();
return;
}
Account* account = m_accountService->findAccountById(m_currentAccountId);
if (account) {
// For now, we'll just clear the editor since we don't have a signature field in Account yet
// In a real implementation, we would retrieve and display the signature
m_bodyEditor->clear();
} else {
m_bodyEditor->clear();
}
}
void ComposeView::setupUI() {
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20, 15, 20, 15);
mainLayout->setSpacing(8);
this->setStyleSheet(
"QLineEdit, QTextEdit { border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
"QLineEdit:focus, QTextEdit:focus { border-color: #1976D2; }"
);
// Header row: Subject + Detach button
QHBoxLayout *headerLayout = new QHBoxLayout();
m_subjectField = new QLineEdit();
m_subjectField->setPlaceholderText("Subject");
m_subjectField->setFixedHeight(40);
QFont subjectFont = m_subjectField->font();
subjectFont.setPointSize(14);
subjectFont.setBold(true);
m_subjectField->setFont(subjectFont);
headerLayout->addWidget(m_subjectField, 1);
m_detachButton = new QPushButton("\xe2\x87\xa5 Detach");
m_detachButton->setToolTip("Open compose window in a separate window");
m_detachButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 12px; color: #555; font-size: 12px; }"
"QPushButton:hover { background: #f0f0f0; }"
);
connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked);
headerLayout->addWidget(m_detachButton);
mainLayout->addLayout(headerLayout);
// Separator line
QFrame *line1 = new QFrame();
line1->setFrameShape(QFrame::HLine);
line1->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line1);
// From: field + Account selector
QHBoxLayout *fromLayout = new QHBoxLayout();
QLabel *fromLabel = new QLabel("From:");
fromLabel->setFixedWidth(40);
fromLabel->setStyleSheet("font-weight: bold; color: #555;");
m_accountCombo = new QComboBox();
m_accountCombo->setFixedWidth(180);
m_accountCombo->setStyleSheet(
"QComboBox { border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 8px; "
"background: white; min-height: 20px; }"
"QComboBox:hover { border-color: #bdbdbd; }"
"QComboBox:focus { border-color: #1976D2; }"
"QComboBox::drop-down { border: none; width: 20px; }"
"QComboBox::down-arrow { image: url(:/icons/dropdown.png); width: 10px; height: 10px; }"
);
connect(m_accountCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ComposeView::onAccountChanged);
fromLayout->addWidget(fromLabel);
fromLayout->addWidget(m_accountCombo, 1);
mainLayout->addLayout(fromLayout);
// To: field + Cc/Bcc toggle buttons
QHBoxLayout *toLayout = new QHBoxLayout();
QLabel *toLabel = new QLabel("To:");
toLabel->setFixedWidth(40);
toLabel->setStyleSheet("font-weight: bold; color: #555;");
m_toField = new QLineEdit();
m_toField->setPlaceholderText("Recipients (comma separated)");
toLayout->addWidget(toLabel);
toLayout->addWidget(m_toField, 1);
m_ccButton = new QPushButton("Cc");
m_ccButton->setFixedWidth(32);
m_ccButton->setStyleSheet(
"QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }"
"QPushButton:hover { background: #d0d0d0; }"
);
connect(m_ccButton, &QPushButton::clicked, this, &ComposeView::onCcToggle);
toLayout->addWidget(m_ccButton);
m_bccButton = new QPushButton("Bcc");
m_bccButton->setFixedWidth(36);
m_bccButton->setStyleSheet(
"QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }"
"QPushButton:hover { background: #d0d0d0; }"
);
connect(m_bccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle);
toLayout->addWidget(m_bccButton);
mainLayout->addLayout(toLayout);
// Cc: row (hidden by default)
m_ccRow = new QWidget();
QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow);
ccLayout->setContentsMargins(0, 0, 0, 0);
QLabel *ccLabel = new QLabel("Cc:");
ccLabel->setFixedWidth(40);
ccLabel->setStyleSheet("color: #555;");
m_ccField = new QLineEdit();
m_ccField->setPlaceholderText("Carbon copy");
ccLayout->addWidget(ccLabel);
ccLayout->addWidget(m_ccField, 1);
m_hideCcButton = new QPushButton("x");
m_hideCcButton->setFixedSize(20, 20);
m_hideCcButton->setToolTip("Hide Cc");
m_hideCcButton->setStyleSheet(
"QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }"
"QPushButton:hover { color: #333; }"
);
connect(m_hideCcButton, &QPushButton::clicked, this, &ComposeView::onCcToggle);
ccLayout->addWidget(m_hideCcButton);
m_ccRow->setVisible(false);
mainLayout->addWidget(m_ccRow);
// Bcc: row (hidden by default)
m_bccRow = new QWidget();
QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow);
bccLayout->setContentsMargins(0, 0, 0, 0);
QLabel *bccLabel = new QLabel("Bcc:");
bccLabel->setFixedWidth(40);
bccLabel->setStyleSheet("color: #555;");
m_bccField = new QLineEdit();
m_bccField->setPlaceholderText("Blind carbon copy");
bccLayout->addWidget(bccLabel);
bccLayout->addWidget(m_bccField, 1);
m_hideBccButton = new QPushButton("x");
m_hideBccButton->setFixedSize(20, 20);
m_hideBccButton->setToolTip("Hide Bcc");
m_hideBccButton->setStyleSheet(
"QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }"
"QPushButton:hover { color: #333; }"
);
connect(m_hideBccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle);
bccLayout->addWidget(m_hideBccButton);
m_bccRow->setVisible(false);
mainLayout->addWidget(m_bccRow);
// Separator
QFrame *line2 = new QFrame();
line2->setFrameShape(QFrame::HLine);
line2->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line2);
// Rich text editor with toolbar
m_bodyEditor = new RichTextEditor();
m_bodyEditor->setupToolbar(mainLayout);
mainLayout->addWidget(m_bodyEditor, 1);
// Schedule panel
m_schedulePanel = new QWidget();
QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel);
scheduleLayout->setContentsMargins(0, 0, 0, 0);
QLabel *scheduleLabel = new QLabel("Send at:");
scheduleLabel->setStyleSheet("color: #555; font-weight: bold;");
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600));
m_schedulePicker->setCalendarPopup(true);
m_schedulePicker->setDisplayFormat("dd/MM/yyyy hh:mm AP");
scheduleLayout->addWidget(scheduleLabel);
scheduleLayout->addWidget(m_schedulePicker);
scheduleLayout->addStretch();
m_scheduleSendButton = new QPushButton("Schedule Send");
m_scheduleSendButton->setStyleSheet(
"QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 6px 16px; font-weight: bold; }"
"QPushButton:hover { background-color: #1565C0; }"
);
connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked);
scheduleLayout->addWidget(m_scheduleSendButton);
m_schedulePanel->setVisible(false);
mainLayout->addWidget(m_schedulePanel);
// Action buttons row
QHBoxLayout *actionsLayout = new QHBoxLayout();
actionsLayout->addStretch();
m_discardButton = new QPushButton("Discard");
m_discardButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px 20px; color: #555; }"
"QPushButton:hover { background: #f5f5f5; }"
);
actionsLayout->addWidget(m_discardButton);
connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested);
// Split button for Send / Schedule
m_sendMenu = new QMenu(this);
m_sendNowAction = m_sendMenu->addAction("Send Now");
connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked);
m_scheduleAction = m_sendMenu->addAction("Schedule for later...");
connect(m_scheduleAction, &QAction::triggered, [this]() {
m_schedulePanel->setVisible(true);
});
m_sendSplit = new QToolButton();
m_sendSplit->setText("Send");
m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup);
m_sendSplit->setMenu(m_sendMenu);
m_sendSplit->setStyleSheet(
"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 8px 24px; font-weight: bold; }"
"QToolButton:hover { background-color: #1565C0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; }"
"QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }"
);
connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked);
actionsLayout->addWidget(m_sendSplit);
mainLayout->addLayout(actionsLayout);
}
void ComposeView::onCcToggle() {
m_ccVisible = !m_ccVisible;
m_ccRow->setVisible(m_ccVisible);
m_ccButton->setVisible(!m_ccVisible);
if (m_ccVisible) m_ccField->setFocus();
}
void ComposeView::onBccToggle() {
m_bccVisible = !m_bccVisible;
m_bccRow->setVisible(m_bccVisible);
m_bccButton->setVisible(!m_bccVisible);
if (m_bccVisible) m_bccField->setFocus();
}
void ComposeView::onSendClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
QDateTime(), // null = send now
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
);
}
void ComposeView::onScheduleClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
m_schedulePicker->dateTime(),
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
);
}
void ComposeView::onDetachClicked() {
emit detachRequested(this);
}
void ComposeView::setTo(const QString &to) { m_toField->setText(to); }
void ComposeView::setSubject(const QString &subject) { m_subjectField->setText(subject); }
void ComposeView::setBody(const QString &body) { m_bodyEditor->setHtml(body); }
void ComposeView::initializeComposition() {
m_toField->clear();
m_ccField->clear();
m_bccField->clear();
m_subjectField->clear();
m_bodyEditor->clear();
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
m_toField->setFocus();
}
void ComposeView::startNewEmail(const QString &initialRecipient) {
initializeComposition();
if (!initialRecipient.isEmpty())
m_toField->setText(initialRecipient);
m_toField->setFocus();
}
#include "composeview.moc"
+691
View File
@@ -0,0 +1,691 @@
#include "ui/composeview.h"
#include <QFrame>
#include <QFileDialog>
#include <QInputDialog>
#include <QTextList>
#include <QTextTable>
#include <QTextCursor>
#include <QTextBlockFormat>
#include <QTextCharFormat>
// ===================== RichTextEditor =====================
RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) {
setAcceptRichText(true);
setPlaceholderText("Write your message here...");
}
void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
m_toolbar = new QToolBar("Formatting");
m_toolbar->setIconSize(QSize(16, 16));
m_toolbar->setStyleSheet(
"QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }"
"QToolButton { padding: 4px 6px; border-radius: 3px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton:checked { background: #bbdefb; }"
);
// Font family combo
m_fontCombo = new QFontComboBox();
m_fontCombo->setFixedWidth(150);
connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged);
m_toolbar->addWidget(m_fontCombo);
// Font size spin
m_fontSizeSpin = new QSpinBox();
m_fontSizeSpin->setRange(8, 72);
m_fontSizeSpin->setValue(14);
m_fontSizeSpin->setFixedWidth(50);
connect(m_fontSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged);
m_toolbar->addWidget(m_fontSizeSpin);
m_toolbar->addSeparator();
// Bold / Italic / Underline
QAction *boldAct = m_toolbar->addAction("B");
boldAct->setCheckable(true);
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
QAction *italicAct = m_toolbar->addAction("I");
italicAct->setCheckable(true);
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
QAction *underlineAct = m_toolbar->addAction("U");
underlineAct->setCheckable(true);
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
m_toolbar->addSeparator();
// Alignment
QAction *alignLeft = m_toolbar->addAction("L");
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
QAction *alignCenter = m_toolbar->addAction("C");
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
QAction *alignRight = m_toolbar->addAction("R");
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
QAction *alignJustify = m_toolbar->addAction("J");
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
m_toolbar->addSeparator();
// Lists
QAction *bulletAct = m_toolbar->addAction("Bullets");
connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList);
QAction *numAct = m_toolbar->addAction("1. List");
connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList);
m_toolbar->addSeparator();
// Indent / Outdent
QAction *indentAct = m_toolbar->addAction("Indent");
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
QAction *outdentAct = m_toolbar->addAction("Outdent");
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
m_toolbar->addSeparator();
// Insert image
QAction *imgAct = m_toolbar->addAction("Img");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
// Insert table
QAction *tableAct = m_toolbar->addAction("Tbl");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
// Signature button
m_signatureButton = new QToolButton();
m_signatureButton->setText("Signature");
m_signatureButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_signatureButton->setPopupMode(QToolButton::MenuButtonPopup);
m_signatureMenu = new QMenu(m_signatureButton);
m_signatureButton->setMenu(m_signatureMenu);
m_signatureButton->setStyleSheet(
"QToolButton { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 3px; padding: 4px 6px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }"
);
connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked);
m_toolbar->addWidget(m_signatureButton);
m_toolbar->addSeparator();
layout->addWidget(m_toolbar);
}
void RichTextEditor::onBold() {
QTextCharFormat fmt;
fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onItalic() {
QTextCharFormat fmt;
fmt.setFontItalic(!textCursor().charFormat().fontItalic());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onUnderline() {
QTextCharFormat fmt;
fmt.setFontUnderline(!textCursor().charFormat().fontUnderline());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onBulletList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDisc) {
// Remove list
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDisc);
cursor.createList(listFormat);
}
}
void RichTextEditor::onNumberedList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDecimal) {
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDecimal);
cursor.createList(listFormat);
}
}
void RichTextEditor::onIndent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setIndent(bfmt.indent() + 1);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onOutdent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
int indent = bfmt.indent();
if (indent > 0) {
bfmt.setIndent(indent - 1);
cursor.setBlockFormat(bfmt);
}
}
void RichTextEditor::onAlignLeft() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignLeft);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignCenter() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignCenter);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignRight() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignRight);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignJustify() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignJustify);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onFontChanged(const QFont &font) {
QTextCharFormat fmt;
fmt.setFontFamilies({font.family()});
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onFontSizeChanged(int size) {
QTextCharFormat fmt;
fmt.setFontPointSize(size);
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onInsertImage() {
QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)");
if (filePath.isEmpty()) return;
QTextCursor cursor = textCursor();
QTextImageFormat imgFmt;
imgFmt.setName(filePath);
// Scale down if too large
QPixmap pm(filePath);
if (pm.width() > 600) {
imgFmt.setWidth(600);
imgFmt.setHeight(pm.height() * 600 / pm.width());
}
cursor.insertImage(imgFmt);
}
void RichTextEditor::onInsertTable() {
bool ok;
int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok);
if (!ok) return;
int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok);
if (!ok) return;
QTextCursor cursor = textCursor();
QTextTableFormat tableFmt;
tableFmt.setBorder(1);
tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
tableFmt.setCellPadding(4);
tableFmt.setCellSpacing(0);
tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100));
cursor.insertTable(rows, cols, tableFmt);
}
ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
setupUI();
// Connect the rich text editor's signature signal to our slot
connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked);
}
ComposeView::~ComposeView() {
// Destructor implementation
}
void ComposeView::onSignatureClicked() {
// This slot is called when the signature button in the rich text editor is clicked
// The actual menu handling is done in RichTextEditor, so we just need to
// handle any ComposeView-specific logic here if needed
}
void ComposeView::onSignatureEditRequested() {
// This slot will be connected to the "Edit Signature..." action in the menu
// For now, we'll just show a simple input dialog
QString currentSignature = "";
// In a real implementation, we would get the current signature for the selected account
// For now, we'll use an empty string
bool ok;
QString newSignature = QInputDialog::getMultiLineText(this, "Edit Signature",
"Enter your signature:",
currentSignature, &ok);
if (ok && !newSignature.isEmpty()) {
// In a real implementation, we would save this signature for the selected account
// For now, we'll just insert it at the cursor position
QTextCursor cursor = m_bodyEditor->textCursor();
cursor.insertHtml(newSignature);
}
}
void ComposeView::setAccountService(AccountService *service)
{
m_accountService = service;
populateAccountCombo();
// Update completer models for address widgets with known emails
if (m_accountService) {
QStringList knownEmails;
const QVector<Account> accounts = m_accountService->getAllAccounts();
for (const Account &acc : accounts) {
knownEmails << acc.email();
}
// Set completion model for each widget
QStringListModel *model = new QStringListModel(knownEmails, this);
QCompleter *comp = new QCompleter(model, this);
comp->setCaseSensitivity(Qt::CaseInsensitive);
if (m_toField) m_toField->setCompleter(comp);
// For cc and bcc we need separate completers but can share model
QCompleter *comp2 = new QCompleter(model, this);
comp2->setCaseSensitivity(Qt::CaseInsensitive);
if (m_ccField) m_ccField->setCompleter(comp2);
QCompleter *comp3 = new QCompleter(model, this);
comp3->setCaseSensitivity(Qt::CaseInsensitive);
if (m_bccField) m_bccField->setCompleter(comp3);
}
}
void ComposeView::populateAccountCombo()
{
if (!m_accountService) {
qWarning() << "AccountService not set";
return;
}
m_accountCombo->clear();
m_accountCombo->addItem(tr("Select Account..."), QVariant());
QVector<Account> accounts = m_accountService->getAllAccounts();
for (const Account &account : accounts) {
m_accountCombo->addItem(account.email(), account.id());
}
// Select the first account by default if there are accounts
if (accounts.size() > 0) {
m_accountCombo->setCurrentIndex(1); // Skip the placeholder item
m_currentAccountId = accounts.first().id();
}
}
void ComposeView::onAccountChanged(int index)
{
if (index <= 0) {
// Placeholder item selected or invalid index
m_currentAccountId = -1;
return;
}
m_currentAccountId = m_accountCombo->itemData(index).toInt();
loadSignatureForCurrentAccount();
}
void ComposeView::loadSignatureForCurrentAccount()
{
if (!m_accountService || m_currentAccountId <= 0) {
// Clear signature if no account selected
m_bodyEditor->clear();
return;
}
Account* account = m_accountService->findAccountById(m_currentAccountId);
if (account) {
// For now, we'll just clear the editor since we don't have a signature field in Account yet
// In a real implementation, we would retrieve and display the signature
m_bodyEditor->clear();
} else {
m_bodyEditor->clear();
}
}
void ComposeView::setupUI()
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20,15,20,15);
mainLayout->setSpacing(8);
// Top row: Account selector and detach button
QHBoxLayout *topLayout = new QHBoxLayout();
m_accountLabel = new QLabel(tr("From:"), this);
m_accountLabel->setFixedWidth(40);
m_accountLabel->setStyleSheet(QStringLiteral("font-weight: bold; color: #555;"));
m_accountCombo = new QComboBox(this);
m_accountCombo->setFixedWidth(180);
m_accountCombo->setStyleSheet(
"QComboBox { border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 8px; "
"background: white; min-height: 20px; }"
"QComboBox:hover { border-color: #bdbdbd; }"
"QComboBox:focus { border-color: #1976D2; }"
);
m_detachButton = new QPushButton(this);
m_detachButton->setToolTip(tr("Detach compose window"));
m_detachButton->setText(QStringLiteral("⤢"));
m_detachButton->setFixedSize(28,28);
m_detachButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; "
"padding: 6px; color: #555; font-size: 14px; }"
"QPushButton:hover { background: #f0f0f0; }"
);
connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked);
topLayout->addWidget(m_accountLabel);
topLayout->addWidget(m_accountCombo, 1);
topLayout->addWidget(m_detachButton);
mainLayout->addLayout(topLayout);
// Separator line
QFrame *line1 = new QFrame(this);
line1->setFrameShape(QFrame::HLine);
line1->setStyleSheet(QStringLiteral("color: #e0e0e0;"));
mainLayout->addWidget(line1);
// To row
QHBoxLayout *toLayout = new QHBoxLayout();
QLabel *toLabel = new QLabel(tr("To:"), this);
toLabel->setFixedWidth(40);
toLabel->setStyleSheet(QStringLiteral("font-weight: bold; color: #555;"));
m_toField = new QLineEdit(this);
m_toField->setPlaceholderText(tr("To recipients"));
toLayout->addWidget(toLabel);
toLayout->addWidget(m_toField, 1);
mainLayout->addLayout(toLayout);
// CC/BCC toggle buttons
QHBoxLayout *toggleLayout = new QHBoxLayout();
m_ccButton = new QPushButton(tr("CC"), this);
m_ccButton->setCheckable(true);
m_ccButton->setChecked(false);
connect(m_ccButton, &QPushButton::toggled, this, &ComposeView::onCcToggled);
m_bccButton = new QPushButton(tr("BCC"), this);
m_bccButton->setCheckable(true);
m_bccButton->setChecked(false);
connect(m_bccButton, &QPushButton::toggled, this, &ComposeView::onBccToggled);
toggleLayout->addWidget(m_ccButton);
toggleLayout->addWidget(m_bccButton);
toggleLayout->addStretch();
mainLayout->addLayout(toggleLayout);
// Cc row (hidden by default)
m_ccRow = new QWidget(this);
QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow);
ccLayout->setContentsMargins(0,0,0,0);
QLabel *ccLabel = new QLabel(tr("Cc:"), this);
ccLabel->setFixedWidth(40);
ccLabel->setStyleSheet(QStringLiteral("color: #555;"));
m_ccField = new QLineEdit(this);
m_ccField->setPlaceholderText(tr("Cc recipients"));
ccLayout->addWidget(ccLabel);
ccLayout->addWidget(m_ccField, 1);
m_ccRow->setVisible(false);
mainLayout->addWidget(m_ccRow);
// Bcc row (hidden by default)
m_bccRow = new QWidget(this);
QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow);
bccLayout->setContentsMargins(0,0,0,0);
QLabel *bccLabel = new QLabel(tr("Bcc:"), this);
bccLabel->setFixedWidth(40);
bccLabel->setStyleSheet(QStringLiteral("color: #555;"));
m_bccField = new QLineEdit(this);
m_bccField->setPlaceholderText(tr("Bcc recipients"));
bccLayout->addWidget(bccLabel);
bccLayout->addWidget(m_bccField, 1);
m_bccRow->setVisible(false);
mainLayout->addWidget(m_bccRow);
// Separator
QFrame *line2 = new QFrame(this);
line2->setFrameShape(QFrame::HLine);
line2->setStyleSheet(QStringLiteral("color: #e0e0e0;"));
mainLayout->addWidget(line2);
// Subject field
QLabel *subjectLabel = new QLabel(tr("Subject:"), this);
subjectLabel->setFixedWidth(50);
subjectLabel->setStyleSheet(QStringLiteral("font-weight: bold; color: #555;"));
m_subjectField = new QLineEdit(this);
m_subjectField->setPlaceholderText(tr("Subject"));
m_subjectField->setFixedHeight(36);
QFont subjectFont = m_subjectField->font();
subjectFont.setPointSize(14);
subjectFont.setBold(true);
m_subjectField->setFont(subjectFont);
QHBoxLayout *subjectLayout = new QHBoxLayout();
subjectLayout->addWidget(subjectLabel);
subjectLayout->addWidget(m_subjectField, 1);
mainLayout->addLayout(subjectLayout);
// Body editor with toolbar
m_bodyEditor = new RichTextEditor(this);
m_bodyEditor->setupToolbar(mainLayout);
mainLayout->addWidget(m_bodyEditor, 1);
// Schedule panel (hidden by default)
m_schedulePanel = new QWidget(this);
QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel);
scheduleLayout->setContentsMargins(0,0,0,0);
QLabel *scheduleLabel = new QLabel(tr("Send at:"), this);
scheduleLabel->setStyleSheet(QStringLiteral("color: #555; font-weight: bold;"));
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600), this);
m_schedulePicker->setCalendarPopup(true);
m_schedulePicker->setDisplayFormat(QStringLiteral("dd/MM/yyyy hh:mm AP"));
scheduleLayout->addWidget(scheduleLabel);
scheduleLayout->addWidget(m_schedulePicker);
scheduleLayout->addStretch();
m_scheduleSendButton = new QPushButton(tr("Schedule Send"), this);
m_scheduleSendButton->setStyleSheet(
"QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; "
"padding: 6px 16px; font-weight: bold; }"
"QPushButton:hover { background-color: #1565C0; }"
);
connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked);
scheduleLayout->addWidget(m_scheduleSendButton);
m_schedulePanel->setVisible(false);
mainLayout->addWidget(m_schedulePanel);
// Action buttons row
QHBoxLayout *actionsLayout = new QHBoxLayout();
actionsLayout->addStretch();
m_discardButton = new QPushButton(tr("Discard"), this);
m_discardButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; "
"padding: 8px 20px; color: #555; }"
"QPushButton:hover { background: #f5f5f5; }"
);
actionsLayout->addWidget(m_discardButton);
connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested);
// Split button for Send / Schedule
m_sendMenu = new QMenu(this);
m_sendNowAction = m_sendMenu->addAction(tr("Send Now"));
connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked);
m_scheduleAction = m_sendMenu->addAction(tr("Schedule for later..."));
connect(m_scheduleAction, &QAction::triggered, [this]() {
m_schedulePanel->setVisible(true);
});
m_sendSplit = new QToolButton(this);
m_sendSplit->setText(tr("Send"));
m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup);
m_sendSplit->setMenu(m_sendMenu);
m_sendSplit->setStyleSheet(
"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; "
"padding: 8px 24px; font-weight: bold; }"
"QToolButton:hover { background-color: #1565C0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; }"
"QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }"
);
connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked);
actionsLayout->addWidget(m_sendSplit);
mainLayout->addLayout(actionsLayout);
// Connect rich text editor signature signals
connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked);
connect(m_bodyEditor, &RichTextEditor::signatureEditRequested, this, &ComposeView::onSignatureEditRequested);
}
void ComposeView::onCcToggled(bool checked)
{
m_ccRow->setVisible(checked);
if (checked) {
m_ccField->setFocus();
}
}
void ComposeView::onBccToggled(bool checked)
{
m_bccRow->setVisible(checked);
if (checked) {
m_bccField->setFocus();
}
}
void ComposeView::onSendClicked()
{
QString to = m_toField->text();
QString cc = m_ccButton->isChecked() ? m_ccField->text() : QString();
QString bcc = m_bccButton->isChecked() ? m_bccField->text() : QString();
QString subject = m_subjectField->text();
QString body = m_bodyEditor->toHtml();
QDateTime scheduleTime = m_schedulePanel->isVisible() ? m_schedulePicker->dateTime() : QDateTime();
QString fromAddress;
if (m_currentAccountId > 0 && m_accountCombo->currentIndex() > 0) {
fromAddress = m_accountCombo->currentData().toString();
}
emit sendRequested(to, cc, bcc, subject, body, scheduleTime, fromAddress);
}
void ComposeView::onScheduleClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
m_schedulePicker->dateTime(),
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
);
}
void ComposeView::onDetachClicked() {
emit detachRequested(this);
}
void ComposeView::setTo(const QString &to)
{
if (to.isEmpty()) {
m_toField->clear();
return;
}
// Split by semicolon or comma
QStringList parts = to.split(QRegularExpression("[,;]"), Qt::SkipEmptyParts);
QStringList cleaned;
for (const QString &part : parts) {
QString trimmed = part.trimmed();
if (!trimmed.isEmpty())
cleaned << trimmed;
}
m_toField->setText(cleaned.join("; "));
}
void ComposeView::setSubject(const QString &subject)
{
m_subjectField->setText(subject);
}
void ComposeView::setBody(const QString &body)
{
m_bodyEditor->setHtml(body);
}
void ComposeView::initializeComposition()
{
m_subjectField->clear();
m_bodyEditor->clear();
m_toField->clear();
m_ccField->clear();
m_bccField->clear();
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
m_subjectField->setFocus();
}
void ComposeView::startNewEmail(const QString &initialRecipient)
{
initializeComposition();
if (!initialRecipient.isEmpty()) {
m_toField->setText(initialRecipient);
}
m_toField->setFocus();
}
void ComposeView::setSubject(const QString &subject)
{
m_subjectField->setText(subject);
}
void ComposeView::setBody(const QString &body)
{
m_bodyEditor->setHtml(body);
}
void ComposeView::initializeComposition()
{
m_subjectField->clear();
m_bodyEditor->clear();
m_toField->clear();
m_ccField->clear();
m_bccField->clear();
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
m_subjectField->setFocus();
}
#include "composeview.moc"
+691
View File
@@ -0,0 +1,691 @@
#include "ui/composeview.h"
#include <QFrame>
#include <QFileDialog>
#include <QInputDialog>
#include <QTextList>
#include <QTextTable>
#include <QTextCursor>
#include <QTextBlockFormat>
#include <QTextCharFormat>
// ===================== RichTextEditor =====================
RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) {
setAcceptRichText(true);
setPlaceholderText("Write your message here...");
}
void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
m_toolbar = new QToolBar("Formatting");
m_toolbar->setIconSize(QSize(16, 16));
m_toolbar->setStyleSheet(
"QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }"
"QToolButton { padding: 4px 6px; border-radius: 3px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton:checked { background: #bbdefb; }"
);
// Font family combo
m_fontCombo = new QFontComboBox();
m_fontCombo->setFixedWidth(150);
connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged);
m_toolbar->addWidget(m_fontCombo);
// Font size spin
m_fontSizeSpin = new QSpinBox();
m_fontSizeSpin->setRange(8, 72);
m_fontSizeSpin->setValue(14);
m_fontSizeSpin->setFixedWidth(50);
connect(m_fontSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged);
m_toolbar->addWidget(m_fontSizeSpin);
m_toolbar->addSeparator();
// Bold / Italic / Underline
QAction *boldAct = m_toolbar->addAction("B");
boldAct->setCheckable(true);
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
QAction *italicAct = m_toolbar->addAction("I");
italicAct->setCheckable(true);
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
QAction *underlineAct = m_toolbar->addAction("U");
underlineAct->setCheckable(true);
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
m_toolbar->addSeparator();
// Alignment
QAction *alignLeft = m_toolbar->addAction("L");
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
QAction *alignCenter = m_toolbar->addAction("C");
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
QAction *alignRight = m_toolbar->addAction("R");
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
QAction *alignJustify = m_toolbar->addAction("J");
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
m_toolbar->addSeparator();
// Lists
QAction *bulletAct = m_toolbar->addAction("Bullets");
connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList);
QAction *numAct = m_toolbar->addAction("1. List");
connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList);
m_toolbar->addSeparator();
// Indent / Outdent
QAction *indentAct = m_toolbar->addAction("Indent");
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
QAction *outdentAct = m_toolbar->addAction("Outdent");
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
m_toolbar->addSeparator();
// Insert image
QAction *imgAct = m_toolbar->addAction("Img");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
// Insert table
QAction *tableAct = m_toolbar->addAction("Tbl");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
// Signature button
m_signatureButton = new QToolButton();
m_signatureButton->setText("Signature");
m_signatureButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_signatureButton->setPopupMode(QToolButton::MenuButtonPopup);
m_signatureMenu = new QMenu(m_signatureButton);
m_signatureButton->setMenu(m_signatureMenu);
m_signatureButton->setStyleSheet(
"QToolButton { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 3px; padding: 4px 6px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }"
);
connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked);
m_toolbar->addWidget(m_signatureButton);
m_toolbar->addSeparator();
layout->addWidget(m_toolbar);
}
void RichTextEditor::onBold() {
QTextCharFormat fmt;
fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onItalic() {
QTextCharFormat fmt;
fmt.setFontItalic(!textCursor().charFormat().fontItalic());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onUnderline() {
QTextCharFormat fmt;
fmt.setFontUnderline(!textCursor().charFormat().fontUnderline());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onBulletList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDisc) {
// Remove list
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDisc);
cursor.createList(listFormat);
}
}
void RichTextEditor::onNumberedList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDecimal) {
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDecimal);
cursor.createList(listFormat);
}
}
void RichTextEditor::onIndent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setIndent(bfmt.indent() + 1);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onOutdent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
int indent = bfmt.indent();
if (indent > 0) {
bfmt.setIndent(indent - 1);
cursor.setBlockFormat(bfmt);
}
}
void RichTextEditor::onAlignLeft() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignLeft);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignCenter() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignCenter);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignRight() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignRight);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignJustify() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignJustify);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onFontChanged(const QFont &font) {
QTextCharFormat fmt;
fmt.setFontFamilies({font.family()});
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onFontSizeChanged(int size) {
QTextCharFormat fmt;
fmt.setFontPointSize(size);
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onInsertImage() {
QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)");
if (filePath.isEmpty()) return;
QTextCursor cursor = textCursor();
QTextImageFormat imgFmt;
imgFmt.setName(filePath);
// Scale down if too large
QPixmap pm(filePath);
if (pm.width() > 600) {
imgFmt.setWidth(600);
imgFmt.setHeight(pm.height() * 600 / pm.width());
}
cursor.insertImage(imgFmt);
}
void RichTextEditor::onInsertTable() {
bool ok;
int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok);
if (!ok) return;
int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok);
if (!ok) return;
QTextCursor cursor = textCursor();
QTextTableFormat tableFmt;
tableFmt.setBorder(1);
tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
tableFmt.setCellPadding(4);
tableFmt.setCellSpacing(0);
tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100));
cursor.insertTable(rows, cols, tableFmt);
}
ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
setupUI();
// Connect the rich text editor's signature signal to our slot
connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked);
}
ComposeView::~ComposeView() {
// Destructor implementation
}
void ComposeView::onSignatureClicked() {
// This slot is called when the signature button in the rich text editor is clicked
// The actual menu handling is done in RichTextEditor, so we just need to
// handle any ComposeView-specific logic here if needed
}
void ComposeView::onSignatureEditRequested() {
// This slot will be connected to the "Edit Signature..." action in the menu
// For now, we'll just show a simple input dialog
QString currentSignature = "";
// In a real implementation, we would get the current signature for the selected account
// For now, we'll use an empty string
bool ok;
QString newSignature = QInputDialog::getMultiLineText(this, "Edit Signature",
"Enter your signature:",
currentSignature, &ok);
if (ok && !newSignature.isEmpty()) {
// In a real implementation, we would save this signature for the selected account
// For now, we'll just insert it at the cursor position
QTextCursor cursor = m_bodyEditor->textCursor();
cursor.insertHtml(newSignature);
}
}
void ComposeView::setAccountService(AccountService *service)
{
m_accountService = service;
populateAccountCombo();
// Update completer models for address widgets with known emails
if (m_accountService) {
QStringList knownEmails;
const QVector<Account> accounts = m_accountService->getAllAccounts();
for (const Account &acc : accounts) {
knownEmails << acc.email();
}
// Set completion model for each widget
QStringListModel *model = new QStringListModel(knownEmails, this);
QCompleter *comp = new QCompleter(model, this);
comp->setCaseSensitivity(Qt::CaseInsensitive);
if (m_toField) m_toField->setCompleter(comp);
// For cc and bcc we need separate completers but can share model
QCompleter *comp2 = new QCompleter(model, this);
comp2->setCaseSensitivity(Qt::CaseInsensitive);
if (m_ccField) m_ccField->setCompleter(comp2);
QCompleter *comp3 = new QCompleter(model, this);
comp3->setCaseSensitivity(Qt::CaseInsensitive);
if (m_bccField) m_bccField->setCompleter(comp3);
}
}
void ComposeView::populateAccountCombo()
{
if (!m_accountService) {
qWarning() << "AccountService not set";
return;
}
m_accountCombo->clear();
m_accountCombo->addItem(tr("Select Account..."), QVariant());
QVector<Account> accounts = m_accountService->getAllAccounts();
for (const Account &account : accounts) {
m_accountCombo->addItem(account.email(), account.id());
}
// Select the first account by default if there are accounts
if (accounts.size() > 0) {
m_accountCombo->setCurrentIndex(1); // Skip the placeholder item
m_currentAccountId = accounts.first().id();
}
}
void ComposeView::onAccountChanged(int index)
{
if (index <= 0) {
// Placeholder item selected or invalid index
m_currentAccountId = -1;
return;
}
m_currentAccountId = m_accountCombo->itemData(index).toInt();
loadSignatureForCurrentAccount();
}
void ComposeView::loadSignatureForCurrentAccount()
{
if (!m_accountService || m_currentAccountId <= 0) {
// Clear signature if no account selected
m_bodyEditor->clear();
return;
}
Account* account = m_accountService->findAccountById(m_currentAccountId);
if (account) {
// For now, we'll just clear the editor since we don't have a signature field in Account yet
// In a real implementation, we would retrieve and display the signature
m_bodyEditor->clear();
} else {
m_bodyEditor->clear();
}
}
void ComposeView::setupUI()
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20,15,20,15);
mainLayout->setSpacing(8);
// Top row: Account selector and detach button
QHBoxLayout *topLayout = new QHBoxLayout();
m_accountLabel = new QLabel(tr("From:"), this);
m_accountLabel->setFixedWidth(40);
m_accountLabel->setStyleSheet(QStringLiteral("font-weight: bold; color: #555;"));
m_accountCombo = new QComboBox(this);
m_accountCombo->setFixedWidth(180);
m_accountCombo->setStyleSheet(
"QComboBox { border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 8px; "
"background: white; min-height: 20px; }"
"QComboBox:hover { border-color: #bdbdbd; }"
"QComboBox:focus { border-color: #1976D2; }"
);
m_detachButton = new QPushButton(this);
m_detachButton->setToolTip(tr("Detach compose window"));
m_detachButton->setText(QStringLiteral("⤢"));
m_detachButton->setFixedSize(28,28);
m_detachButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; "
"padding: 6px; color: #555; font-size: 14px; }"
"QPushButton:hover { background: #f0f0f0; }"
);
connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked);
topLayout->addWidget(m_accountLabel);
topLayout->addWidget(m_accountCombo, 1);
topLayout->addWidget(m_detachButton);
mainLayout->addLayout(topLayout);
// Separator line
QFrame *line1 = new QFrame(this);
line1->setFrameShape(QFrame::HLine);
line1->setStyleSheet(QStringLiteral("color: #e0e0e0;"));
mainLayout->addWidget(line1);
// To row
QHBoxLayout *toLayout = new QHBoxLayout();
QLabel *toLabel = new QLabel(tr("To:"), this);
toLabel->setFixedWidth(40);
toLabel->setStyleSheet(QStringLiteral("font-weight: bold; color: #555;"));
m_toField = new QLineEdit(this);
m_toField->setPlaceholderText(tr("To recipients"));
toLayout->addWidget(toLabel);
toLayout->addWidget(m_toField, 1);
mainLayout->addLayout(toLayout);
// CC/BCC toggle buttons
QHBoxLayout *toggleLayout = new QHBoxLayout();
m_ccButton = new QPushButton(tr("CC"), this);
m_ccButton->setCheckable(true);
m_ccButton->setChecked(false);
connect(m_ccButton, &QPushButton::toggled, this, &ComposeView::onCcToggled);
m_bccButton = new QPushButton(tr("BCC"), this);
m_bccButton->setCheckable(true);
m_bccButton->setChecked(false);
connect(m_bccButton, &QPushButton::toggled, this, &ComposeView::onBccToggled);
toggleLayout->addWidget(m_ccButton);
toggleLayout->addWidget(m_bccButton);
toggleLayout->addStretch();
mainLayout->addLayout(toggleLayout);
// Cc row (hidden by default)
m_ccRow = new QWidget(this);
QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow);
ccLayout->setContentsMargins(0,0,0,0);
QLabel *ccLabel = new QLabel(tr("Cc:"), this);
ccLabel->setFixedWidth(40);
ccLabel->setStyleSheet(QStringLiteral("color: #555;"));
m_ccField = new QLineEdit(this);
m_ccField->setPlaceholderText(tr("Cc recipients"));
ccLayout->addWidget(ccLabel);
ccLayout->addWidget(m_ccField, 1);
m_ccRow->setVisible(false);
mainLayout->addWidget(m_ccRow);
// Bcc row (hidden by default)
m_bccRow = new QWidget(this);
QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow);
bccLayout->setContentsMargins(0,0,0,0);
QLabel *bccLabel = new QLabel(tr("Bcc:"), this);
bccLabel->setFixedWidth(40);
bccLabel->setStyleSheet(QStringLiteral("color: #555;"));
m_bccField = new QLineEdit(this);
m_bccField->setPlaceholderText(tr("Bcc recipients"));
bccLayout->addWidget(bccLabel);
bccLayout->addWidget(m_bccField, 1);
m_bccRow->setVisible(false);
mainLayout->addWidget(m_bccRow);
// Separator
QFrame *line2 = new QFrame(this);
line2->setFrameShape(QFrame::HLine);
line2->setStyleSheet(QStringLiteral("color: #e0e0e0;"));
mainLayout->addWidget(line2);
// Subject field
QLabel *subjectLabel = new QLabel(tr("Subject:"), this);
subjectLabel->setFixedWidth(50);
subjectLabel->setStyleSheet(QStringLiteral("font-weight: bold; color: #555;"));
m_subjectField = new QLineEdit(this);
m_subjectField->setPlaceholderText(tr("Subject"));
m_subjectField->setFixedHeight(36);
QFont subjectFont = m_subjectField->font();
subjectFont.setPointSize(14);
subjectFont.setBold(true);
m_subjectField->setFont(subjectFont);
QHBoxLayout *subjectLayout = new QHBoxLayout();
subjectLayout->addWidget(subjectLabel);
subjectLayout->addWidget(m_subjectField, 1);
mainLayout->addLayout(subjectLayout);
// Body editor with toolbar
m_bodyEditor = new RichTextEditor(this);
m_bodyEditor->setupToolbar(mainLayout);
mainLayout->addWidget(m_bodyEditor, 1);
// Schedule panel (hidden by default)
m_schedulePanel = new QWidget(this);
QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel);
scheduleLayout->setContentsMargins(0,0,0,0);
QLabel *scheduleLabel = new QLabel(tr("Send at:"), this);
scheduleLabel->setStyleSheet(QStringLiteral("color: #555; font-weight: bold;"));
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600), this);
m_schedulePicker->setCalendarPopup(true);
m_schedulePicker->setDisplayFormat(QStringLiteral("dd/MM/yyyy hh:mm AP"));
scheduleLayout->addWidget(scheduleLabel);
scheduleLayout->addWidget(m_schedulePicker);
scheduleLayout->addStretch();
m_scheduleSendButton = new QPushButton(tr("Schedule Send"), this);
m_scheduleSendButton->setStyleSheet(
"QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; "
"padding: 6px 16px; font-weight: bold; }"
"QPushButton:hover { background-color: #1565C0; }"
);
connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked);
scheduleLayout->addWidget(m_scheduleSendButton);
m_schedulePanel->setVisible(false);
mainLayout->addWidget(m_schedulePanel);
// Action buttons row
QHBoxLayout *actionsLayout = new QHBoxLayout();
actionsLayout->addStretch();
m_discardButton = new QPushButton(tr("Discard"), this);
m_discardButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; "
"padding: 8px 20px; color: #555; }"
"QPushButton:hover { background: #f5f5f5; }"
);
actionsLayout->addWidget(m_discardButton);
connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested);
// Split button for Send / Schedule
m_sendMenu = new QMenu(this);
m_sendNowAction = m_sendMenu->addAction(tr("Send Now"));
connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked);
m_scheduleAction = m_sendMenu->addAction(tr("Schedule for later..."));
connect(m_scheduleAction, &QAction::triggered, [this]() {
m_schedulePanel->setVisible(true);
});
m_sendSplit = new QToolButton(this);
m_sendSplit->setText(tr("Send"));
m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup);
m_sendSplit->setMenu(m_sendMenu);
m_sendSplit->setStyleSheet(
"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; "
"padding: 8px 24px; font-weight: bold; }"
"QToolButton:hover { background-color: #1565C0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; }"
"QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }"
);
connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked);
actionsLayout->addWidget(m_sendSplit);
mainLayout->addLayout(actionsLayout);
// Connect rich text editor signature signals
connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked);
connect(m_bodyEditor, &RichTextEditor::signatureEditRequested, this, &ComposeView::onSignatureEditRequested);
}
void ComposeView::onCcToggled(bool checked)
{
m_ccRow->setVisible(checked);
if (checked) {
m_ccField->setFocus();
}
}
void ComposeView::onBccToggled(bool checked)
{
m_bccRow->setVisible(checked);
if (checked) {
m_bccField->setFocus();
}
}
void ComposeView::onSendClicked()
{
QString to = m_toField->text();
QString cc = m_ccButton->isChecked() ? m_ccField->text() : QString();
QString bcc = m_bccButton->isChecked() ? m_bccField->text() : QString();
QString subject = m_subjectField->text();
QString body = m_bodyEditor->toHtml();
QDateTime scheduleTime = m_schedulePanel->isVisible() ? m_schedulePicker->dateTime() : QDateTime();
QString fromAddress;
if (m_currentAccountId > 0 && m_accountCombo->currentIndex() > 0) {
fromAddress = m_accountCombo->currentData().toString();
}
emit sendRequested(to, cc, bcc, subject, body, scheduleTime, fromAddress);
}
void ComposeView::onScheduleClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
m_schedulePicker->dateTime(),
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
);
}
void ComposeView::onDetachClicked() {
emit detachRequested(this);
}
void ComposeView::setTo(const QString &to)
{
if (to.isEmpty()) {
m_toField->clear();
return;
}
// Split by semicolon or comma
QStringList parts = to.split(QRegularExpression("[,;]"), Qt::SkipEmptyParts);
QStringList cleaned;
for (const QString &part : parts) {
QString trimmed = part.trimmed();
if (!trimmed.isEmpty())
cleaned << trimmed;
}
m_toField->setText(cleaned.join("; "));
}
void ComposeView::setSubject(const QString &subject)
{
m_subjectField->setText(subject);
}
void ComposeView::setBody(const QString &body)
{
m_bodyEditor->setHtml(body);
}
void ComposeView::initializeComposition()
{
m_subjectField->clear();
m_bodyEditor->clear();
m_toField->clear();
m_ccField->clear();
m_bccField->clear();
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
m_subjectField->setFocus();
}
void ComposeView::startNewEmail(const QString &initialRecipient)
{
initializeComposition();
if (!initialRecipient.isEmpty()) {
m_toField->setText(initialRecipient);
}
m_toField->setFocus();
}
void ComposeView::setSubject(const QString &subject)
{
m_subjectField->setText(subject);
}
void ComposeView::setBody(const QString &body)
{
m_bodyEditor->setHtml(body);
}
void ComposeView::initializeComposition()
{
m_subjectField->clear();
m_bodyEditor->clear();
m_toField->clear();
m_ccField->clear();
m_bccField->clear();
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
m_subjectField->setFocus();
}
#include "composeview.moc"
+698
View File
@@ -0,0 +1,698 @@
#include "ui/composeview.h"
#include <QFrame>
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
#include <QDebug>
#include "tags_line_edit.hpp"
#include <QRegularExpression>
#include <QTimer>
#include <QTextList>
#include <QTextTable>
#include <QTextCursor>
#include <QTextBlockFormat>
#include <QTextCharFormat>
// ===================== RichTextEditor =====================
RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) {
setAcceptRichText(true);
setPlaceholderText("Write your message here...");
}
void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
m_toolbar = new QToolBar("Formatting");
m_toolbar->setIconSize(QSize(16, 16));
m_toolbar->setStyleSheet(
"QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }"
"QToolButton { padding: 4px 6px; border-radius: 3px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton:checked { background: #bbdefb; }"
);
// Font family combo
m_fontCombo = new QFontComboBox();
m_fontCombo->setFixedWidth(150);
connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged);
m_toolbar->addWidget(m_fontCombo);
// Font size spin
m_fontSizeSpin = new QSpinBox();
m_fontSizeSpin->setRange(8, 72);
m_fontSizeSpin->setValue(14);
m_fontSizeSpin->setFixedWidth(50);
connect(m_fontSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged);
m_toolbar->addWidget(m_fontSizeSpin);
m_toolbar->addSeparator();
// Bold / Italic / Underline
QAction *boldAct = m_toolbar->addAction("B");
boldAct->setCheckable(true);
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
QAction *italicAct = m_toolbar->addAction("I");
italicAct->setCheckable(true);
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
QAction *underlineAct = m_toolbar->addAction("U");
underlineAct->setCheckable(true);
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
m_toolbar->addSeparator();
// Alignment
QAction *alignLeft = m_toolbar->addAction("L");
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
QAction *alignCenter = m_toolbar->addAction("C");
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
QAction *alignRight = m_toolbar->addAction("R");
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
QAction *alignJustify = m_toolbar->addAction("J");
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
m_toolbar->addSeparator();
// Lists
QAction *bulletAct = m_toolbar->addAction("Bullets");
connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList);
QAction *numAct = m_toolbar->addAction("1. List");
connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList);
m_toolbar->addSeparator();
// Indent / Outdent
QAction *indentAct = m_toolbar->addAction("Indent");
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
QAction *outdentAct = m_toolbar->addAction("Outdent");
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
m_toolbar->addSeparator();
// Insert image
QAction *imgAct = m_toolbar->addAction("Img");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
// Insert table
QAction *tableAct = m_toolbar->addAction("Tbl");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
// Signature button
m_signatureButton = new QToolButton();
m_signatureButton->setText("Signature");
m_signatureButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_signatureButton->setPopupMode(QToolButton::MenuButtonPopup);
m_signatureMenu = new QMenu(m_signatureButton);
m_signatureButton->setMenu(m_signatureMenu);
m_signatureButton->setStyleSheet(
"QToolButton { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 3px; padding: 4px 6px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }"
);
connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked);
m_toolbar->addWidget(m_signatureButton);
QAction *editSigAct = m_signatureMenu->addAction(tr("Editar firmas"));
connect(editSigAct, &QAction::triggered, this, &RichTextEditor::signatureEditRequested);
m_toolbar->addSeparator();
layout->addWidget(m_toolbar);
}
void RichTextEditor::onBold() {
QTextCharFormat fmt;
fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onItalic() {
QTextCharFormat fmt;
fmt.setFontItalic(!textCursor().charFormat().fontItalic());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onUnderline() {
QTextCharFormat fmt;
fmt.setFontUnderline(!textCursor().charFormat().fontUnderline());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onBulletList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDisc) {
// Remove list
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDisc);
cursor.createList(listFormat);
}
}
void RichTextEditor::onNumberedList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDecimal) {
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDecimal);
cursor.createList(listFormat);
}
}
void RichTextEditor::onIndent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setIndent(bfmt.indent() + 1);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onOutdent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
int indent = bfmt.indent();
if (indent > 0) {
bfmt.setIndent(indent - 1);
cursor.setBlockFormat(bfmt);
}
}
void RichTextEditor::onAlignLeft() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignLeft);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignCenter() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignCenter);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignRight() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignRight);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignJustify() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignJustify);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onFontChanged(const QFont &font) {
QTextCharFormat fmt;
fmt.setFontFamilies({font.family()});
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onFontSizeChanged(int size) {
QTextCharFormat fmt;
fmt.setFontPointSize(size);
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onInsertImage() {
QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)");
if (filePath.isEmpty()) return;
QTextCursor cursor = textCursor();
QTextImageFormat imgFmt;
imgFmt.setName(filePath);
// Scale down if too large
QPixmap pm(filePath);
if (pm.width() > 600) {
imgFmt.setWidth(600);
imgFmt.setHeight(pm.height() * 600 / pm.width());
}
cursor.insertImage(imgFmt);
}
void RichTextEditor::onInsertTable() {
bool ok;
int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok);
if (!ok) return;
int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok);
if (!ok) return;
QTextCursor cursor = textCursor();
QTextTableFormat tableFmt;
tableFmt.setBorder(1);
tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
tableFmt.setCellPadding(4);
tableFmt.setCellSpacing(0);
tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100));
cursor.insertTable(rows, cols, tableFmt);
}
ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
setupUI();
// Connect the rich text editor's signature signal to our slot
connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked);
connect(m_bodyEditor, &RichTextEditor::signatureEditRequested, this, &ComposeView::onSignatureEditRequested);
}
ComposeView::~ComposeView() {
// Destructor implementation
}
void ComposeView::onSignatureClicked() {
// This slot is called when the signature button in the rich text editor is clicked
// The actual menu handling is done in RichTextEditor, so we just need to
// handle any ComposeView-specific logic here if needed
}
void ComposeView::onSignatureEditRequested() {
// This slot will be connected to the "Edit Signature..." action in the menu
// For now, we'll just show a simple input dialog
QString currentSignature = "";
// In a real implementation, we would get the current signature for the selected account
// For now, we'll use an empty string
bool ok;
QString newSignature = QInputDialog::getMultiLineText(this, "Edit Signature",
"Enter your signature:",
currentSignature, &ok);
if (ok && !newSignature.isEmpty()) {
// In a real implementation, we would save this signature for the selected account
// For now, we'll just insert it at the cursor position
QTextCursor cursor = m_bodyEditor->textCursor();
cursor.insertHtml(newSignature);
}
}
void ComposeView::setAccountService(AccountService *service)
{
m_accountService = service;
populateAccountCombo();
}
void ComposeView::populateAccountCombo()
{
if (!m_accountService) {
qWarning() << "AccountService not set";
return;
}
m_accountCombo->clear();
m_accountCombo->addItem(tr("Select Account..."), QVariant());
QVector<Account> accounts = m_accountService->getAllAccounts();
for (const Account &account : accounts) {
m_accountCombo->addItem(account.email(), account.id());
}
// Select the first account by default if there are accounts
if (accounts.size() > 0) {
m_accountCombo->setCurrentIndex(1); // Skip the placeholder item
m_currentAccountId = accounts.first().id();
}
}
void ComposeView::onAccountChanged(int index)
{
if (index <= 0) {
// Placeholder item selected or invalid index
m_currentAccountId = -1;
return;
}
m_currentAccountId = m_accountCombo->itemData(index).toInt();
loadSignatureForCurrentAccount();
}
void ComposeView::loadSignatureForCurrentAccount()
{
if (!m_accountService || m_currentAccountId <= 0) {
// Clear signature if no account selected
m_bodyEditor->clear();
return;
}
Account* account = m_accountService->findAccountById(m_currentAccountId);
if (account) {
// For now, we'll just clear the editor since we don't have a signature field in Account yet
// In a real implementation, we would retrieve and display the signature
m_bodyEditor->clear();
} else {
m_bodyEditor->clear();
}
}
void ComposeView::setupUI() {
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20, 15, 20, 15);
mainLayout->setSpacing(8);
this->setStyleSheet(
"QLineEdit, QTextEdit { border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
"QLineEdit:focus, QTextEdit:focus { border-color: #1976D2; }"
);
// Header row: Subject + Detach button
QHBoxLayout *headerLayout = new QHBoxLayout();
m_subjectField = new QLineEdit();
m_subjectField->setPlaceholderText("Subject");
m_subjectField->setFixedHeight(40);
QFont subjectFont = m_subjectField->font();
subjectFont.setPointSize(14);
subjectFont.setBold(true);
m_subjectField->setFont(subjectFont);
headerLayout->addWidget(m_subjectField, 1);
m_detachButton = new QPushButton("\xe2\x87\xa5 Detach");
m_detachButton->setToolTip("Open compose window in a separate window");
m_detachButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 12px; color: #555; font-size: 12px; }"
"QPushButton:hover { background: #f0f0f0; }"
);
connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked);
headerLayout->addWidget(m_detachButton);
mainLayout->addLayout(headerLayout);
// Separator line
QFrame *line1 = new QFrame();
line1->setFrameShape(QFrame::HLine);
line1->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line1);
// From: field + Account selector
QHBoxLayout *fromLayout = new QHBoxLayout();
QLabel *fromLabel = new QLabel("From:");
fromLabel->setFixedWidth(40);
fromLabel->setStyleSheet("font-weight: bold; color: #555;");
m_accountCombo = new QComboBox();
m_accountCombo->setFixedWidth(180);
m_accountCombo->setStyleSheet(
"QComboBox { border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 8px; "
"background: white; min-height: 20px; }"
"QComboBox:hover { border-color: #bdbdbd; }"
"QComboBox:focus { border-color: #1976D2; }"
"QComboBox::drop-down { border: none; width: 20px; }"
"QComboBox::down-arrow { image: url(:/icons/dropdown.png); width: 10px; height: 10px; }"
);
connect(m_accountCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ComposeView::onAccountChanged);
fromLayout->addWidget(fromLabel);
fromLayout->addWidget(m_accountCombo, 1);
mainLayout->addLayout(fromLayout);
// To: field + Cc/Bcc toggle buttons
QHBoxLayout *toLayout = new QHBoxLayout();
QLabel *toLabel = new QLabel("To:");
toLabel->setFixedWidth(40);
toLabel->setStyleSheet("font-weight: bold; color: #555;");
m_toField = new QLineEdit();
m_toField->setPlaceholderText("Recipients (comma separated)");
toLayout->addWidget(toLabel);
toLayout->addWidget(m_toField, 1);
m_ccButton = new QPushButton("Cc");
m_ccButton->setFixedWidth(32);
m_ccButton->setStyleSheet(
"QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }"
"QPushButton:hover { background: #d0d0d0; }"
);
connect(m_ccButton, &QPushButton::clicked, this, &ComposeView::onCcToggle);
toLayout->addWidget(m_ccButton);
m_bccButton = new QPushButton("Bcc");
m_bccButton->setFixedWidth(36);
m_bccButton->setStyleSheet(
"QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }"
"QPushButton:hover { background: #d0d0d0; }"
);
connect(m_bccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle);
toLayout->addWidget(m_bccButton);
mainLayout->addLayout(toLayout);
// Cc: row (hidden by default)
m_ccRow = new QWidget();
QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow);
ccLayout->setContentsMargins(0, 0, 0, 0);
QLabel *ccLabel = new QLabel("Cc:");
ccLabel->setFixedWidth(40);
ccLabel->setStyleSheet("color: #555;");
m_ccField = new QLineEdit();
m_ccField->setPlaceholderText("Carbon copy");
ccLayout->addWidget(ccLabel);
ccLayout->addWidget(m_ccField, 1);
m_hideCcButton = new QPushButton("x");
m_hideCcButton->setFixedSize(20, 20);
m_hideCcButton->setToolTip("Hide Cc");
m_hideCcButton->setStyleSheet(
"QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }"
"QPushButton:hover { color: #333; }"
);
connect(m_hideCcButton, &QPushButton::clicked, this, &ComposeView::onCcToggle);
ccLayout->addWidget(m_hideCcButton);
m_ccRow->setVisible(false);
mainLayout->addWidget(m_ccRow);
// Bcc: row (hidden by default)
m_bccRow = new QWidget();
QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow);
bccLayout->setContentsMargins(0, 0, 0, 0);
QLabel *bccLabel = new QLabel("Bcc:");
bccLabel->setFixedWidth(40);
bccLabel->setStyleSheet("color: #555;");
m_bccField = new QLineEdit();
m_bccField->setPlaceholderText("Blind carbon copy");
bccLayout->addWidget(bccLabel);
bccLayout->addWidget(m_bccField, 1);
m_hideBccButton = new QPushButton("x");
m_hideBccButton->setFixedSize(20, 20);
m_hideBccButton->setToolTip("Hide Bcc");
m_hideBccButton->setStyleSheet(
"QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }"
"QPushButton:hover { color: #333; }"
);
connect(m_hideBccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle);
bccLayout->addWidget(m_hideBccButton);
m_bccRow->setVisible(false);
mainLayout->addWidget(m_bccRow);
// Separator
QFrame *line2 = new QFrame();
line2->setFrameShape(QFrame::HLine);
line2->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line2);
// Rich text editor with toolbar
m_bodyEditor = new RichTextEditor();
m_bodyEditor->setupToolbar(mainLayout);
mainLayout->addWidget(m_bodyEditor, 1);
// Schedule panel
m_schedulePanel = new QWidget();
QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel);
scheduleLayout->setContentsMargins(0, 0, 0, 0);
QLabel *scheduleLabel = new QLabel("Send at:");
scheduleLabel->setStyleSheet("color: #555; font-weight: bold;");
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600));
m_schedulePicker->setCalendarPopup(true);
m_schedulePicker->setDisplayFormat("dd/MM/yyyy hh:mm AP");
scheduleLayout->addWidget(scheduleLabel);
scheduleLayout->addWidget(m_schedulePicker);
scheduleLayout->addStretch();
m_scheduleSendButton = new QPushButton("Schedule Send");
m_scheduleSendButton->setStyleSheet(
"QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 6px 16px; font-weight: bold; }"
"QPushButton:hover { background-color: #1565C0; }"
);
connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked);
scheduleLayout->addWidget(m_scheduleSendButton);
m_schedulePanel->setVisible(false);
mainLayout->addWidget(m_schedulePanel);
// Attachment section
QHBoxLayout *attachmentLayout = new QHBoxLayout();
QLabel *attachmentLabel = new QLabel(tr("Attachments:"));
attachmentLabel->setFixedWidth(80);
attachmentLabel->setStyleSheet("font-weight: bold; color: #555;");
attachmentLayout->addWidget(attachmentLabel);
m_attachButton = new QToolButton();
m_attachButton->setIcon(QIcon(QStringLiteral(":/icons/attachment.svg")));
m_attachButton->setToolTip(tr("Add attachment"));
m_attachButton->setIconSize(QSize(20,20));
m_attachButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }");
bool connected = connect(m_attachButton, &QToolButton::clicked, this, &ComposeView::onAddAttachmentClicked);
qDebug() << "Attach button connected:" << connected;
attachmentLayout->addWidget(m_attachButton);
m_attachmentList = new QListWidget();
m_attachmentList->setSelectionMode(QAbstractItemView::SingleSelection);
m_attachmentList->setMaximumHeight(60);
m_attachmentList->setStyleSheet("QListWidget { border: 1px solid #d1d1d6; border-radius: 4px; }");
attachmentLayout->addWidget(m_attachmentList, 1);
m_removeAttachmentButton = new QToolButton();
m_removeAttachmentButton->setIcon(QIcon(QStringLiteral(":/icons/trash.svg")));
m_removeAttachmentButton->setToolTip(tr("Remove selected attachment"));
m_removeAttachmentButton->setIconSize(QSize(20,20));
m_removeAttachmentButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }");
connect(m_removeAttachmentButton, &QToolButton::clicked, this, &ComposeView::onRemoveAttachmentClicked);
attachmentLayout->addWidget(m_removeAttachmentButton);
mainLayout->addLayout(attachmentLayout);
// Action buttons row
QHBoxLayout *actionsLayout = new QHBoxLayout();
actionsLayout->addStretch();
m_discardButton = new QPushButton("Discard");
m_discardButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px 20px; color: #555; }"
"QPushButton:hover { background: #f5f5f5; }"
);
actionsLayout->addWidget(m_discardButton);
connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested);
// Split button for Send / Schedule
m_sendMenu = new QMenu(this);
m_sendNowAction = m_sendMenu->addAction("Send Now");
connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked);
m_scheduleAction = m_sendMenu->addAction("Schedule for later...");
connect(m_scheduleAction, &QAction::triggered, [this]() {
m_schedulePanel->setVisible(true);
});
m_sendSplit = new QToolButton();
m_sendSplit->setText("Send");
m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup);
m_sendSplit->setMenu(m_sendMenu);
m_sendSplit->setStyleSheet(
"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 8px 24px; font-weight: bold; }"
"QToolButton:hover { background-color: #1565C0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; width: 40px;}"
"QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }"
);
connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked);
actionsLayout->addWidget(m_sendSplit);
mainLayout->addLayout(actionsLayout);
}
void ComposeView::onCcToggle() {
m_ccVisible = !m_ccVisible;
m_ccRow->setVisible(m_ccVisible);
m_ccButton->setVisible(!m_ccVisible);
if (m_ccVisible) m_ccField->setFocus();
}
void ComposeView::onBccToggle() {
m_bccVisible = !m_bccVisible;
m_bccRow->setVisible(m_bccVisible);
m_bccButton->setVisible(!m_bccVisible);
if (m_bccVisible) m_bccField->setFocus();
}
void ComposeView::onSendClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
QDateTime(), // null = send now
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
);
}
void ComposeView::onScheduleClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
m_schedulePicker->dateTime(),
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
);
}
void ComposeView::onDetachClicked() {
emit detachRequested(this);
}
void ComposeView::setTo(const QString &to) { m_toField->setText(to); }
void ComposeView::setSubject(const QString &subject) { m_subjectField->setText(subject); }
void ComposeView::setBody(const QString &body) { m_bodyEditor->setHtml(body); }
void ComposeView::initializeComposition() {
m_toField->clear();
m_ccField->clear();
m_bccField->clear();
m_subjectField->clear();
m_bodyEditor->clear();
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
m_toField->setFocus();
}
void ComposeView::startNewEmail(const QString &initialRecipient) {
initializeComposition();
if (!initialRecipient.isEmpty())
m_toField->setText(initialRecipient);
m_toField->setFocus();
}
void ComposeView::onAddAttachmentClicked()
{
qDebug() << "Attach button clicked";
QStringList files = QFileDialog::getOpenFileNames(this, tr("Select Attachments"), QString(), tr("All Files (*)"));
if (files.isEmpty())
return;
for (const QString &file : files) {
m_attachmentFiles.append(file);
QListWidgetItem *item = new QListWidgetItem(QFileInfo(file).fileName(), m_attachmentList);
item->setToolTip(file);
}
QMessageBox::information(this, tr("Attachments"), tr("Attached %1 file(s).").arg(files.count()));
}
void ComposeView::onRemoveAttachmentClicked()
{
QListWidgetItem *item = m_attachmentList->currentItem();
if (!item)
return;
int row = m_attachmentList->row(item);
m_attachmentList->takeItem(row);
m_attachmentFiles.removeAt(row);
delete item;
}
#include "composeview.moc"
+632
View File
@@ -0,0 +1,632 @@
#include "ui/composeview.h"
#include <QFrame>
#include <QFileDialog>
#include <QInputDialog>
#include <QTextList>
#include <QTextTable>
#include <QTextCursor>
#include <QTextBlockFormat>
#include <QTextCharFormat>
// ===================== RichTextEditor =====================
RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) {
setAcceptRichText(true);
setPlaceholderText("Write your message here...");
}
void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
m_toolbar = new QToolBar("Formatting");
m_toolbar->setIconSize(QSize(16, 16));
m_toolbar->setStyleSheet(
"QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }"
"QToolButton { padding: 4px 6px; border-radius: 3px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton:checked { background: #bbdefb; }"
);
// Font family combo
m_fontCombo = new QFontComboBox();
m_fontCombo->setFixedWidth(150);
connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged);
m_toolbar->addWidget(m_fontCombo);
// Font size spin
m_fontSizeSpin = new QSpinBox();
m_fontSizeSpin->setRange(8, 72);
m_fontSizeSpin->setValue(14);
m_fontSizeSpin->setFixedWidth(50);
connect(m_fontSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged);
m_toolbar->addWidget(m_fontSizeSpin);
m_toolbar->addSeparator();
// Bold / Italic / Underline
QAction *boldAct = m_toolbar->addAction("B");
boldAct->setCheckable(true);
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
QAction *italicAct = m_toolbar->addAction("I");
italicAct->setCheckable(true);
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
QAction *underlineAct = m_toolbar->addAction("U");
underlineAct->setCheckable(true);
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
m_toolbar->addSeparator();
// Alignment
QAction *alignLeft = m_toolbar->addAction("L");
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
QAction *alignCenter = m_toolbar->addAction("C");
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
QAction *alignRight = m_toolbar->addAction("R");
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
QAction *alignJustify = m_toolbar->addAction("J");
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
m_toolbar->addSeparator();
// Lists
QAction *bulletAct = m_toolbar->addAction("Bullets");
connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList);
QAction *numAct = m_toolbar->addAction("1. List");
connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList);
m_toolbar->addSeparator();
// Indent / Outdent
QAction *indentAct = m_toolbar->addAction("Indent");
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
QAction *outdentAct = m_toolbar->addAction("Outdent");
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
m_toolbar->addSeparator();
// Insert image
QAction *imgAct = m_toolbar->addAction("Img");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
// Insert table
QAction *tableAct = m_toolbar->addAction("Tbl");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
// Signature button
m_signatureButton = new QToolButton();
m_signatureButton->setText("Signature");
m_signatureButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_signatureButton->setPopupMode(QToolButton::MenuButtonPopup);
m_signatureMenu = new QMenu(m_signatureButton);
m_signatureButton->setMenu(m_signatureMenu);
m_signatureButton->setStyleSheet(
"QToolButton { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 3px; padding: 4px 6px; }"
"QToolButton:hover { background: #e0e0e0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }"
);
connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked);
m_toolbar->addWidget(m_signatureButton);
m_toolbar->addSeparator();
layout->addWidget(m_toolbar);
}
void RichTextEditor::onBold() {
QTextCharFormat fmt;
fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onItalic() {
QTextCharFormat fmt;
fmt.setFontItalic(!textCursor().charFormat().fontItalic());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onUnderline() {
QTextCharFormat fmt;
fmt.setFontUnderline(!textCursor().charFormat().fontUnderline());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onBulletList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDisc) {
// Remove list
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDisc);
cursor.createList(listFormat);
}
}
void RichTextEditor::onNumberedList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDecimal) {
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDecimal);
cursor.createList(listFormat);
}
}
void RichTextEditor::onIndent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setIndent(bfmt.indent() + 1);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onOutdent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
int indent = bfmt.indent();
if (indent > 0) {
bfmt.setIndent(indent - 1);
cursor.setBlockFormat(bfmt);
}
}
void RichTextEditor::onAlignLeft() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignLeft);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignCenter() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignCenter);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignRight() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignRight);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignJustify() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignJustify);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onFontChanged(const QFont &font) {
QTextCharFormat fmt;
fmt.setFontFamilies({font.family()});
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onFontSizeChanged(int size) {
QTextCharFormat fmt;
fmt.setFontPointSize(size);
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onInsertImage() {
QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)");
if (filePath.isEmpty()) return;
QTextCursor cursor = textCursor();
QTextImageFormat imgFmt;
imgFmt.setName(filePath);
// Scale down if too large
QPixmap pm(filePath);
if (pm.width() > 600) {
imgFmt.setWidth(600);
imgFmt.setHeight(pm.height() * 600 / pm.width());
}
cursor.insertImage(imgFmt);
}
void RichTextEditor::onInsertTable() {
bool ok;
int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok);
if (!ok) return;
int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok);
if (!ok) return;
QTextCursor cursor = textCursor();
QTextTableFormat tableFmt;
tableFmt.setBorder(1);
tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
tableFmt.setCellPadding(4);
tableFmt.setCellSpacing(0);
tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100));
cursor.insertTable(rows, cols, tableFmt);
}
ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
setupUI();
// Connect the rich text editor's signature signal to our slot
connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked);
}
ComposeView::~ComposeView() {
// Destructor implementation
}
void ComposeView::onSignatureClicked() {
// This slot is called when the signature button in the rich text editor is clicked
// The actual menu handling is done in RichTextEditor, so we just need to
// handle any ComposeView-specific logic here if needed
}
void ComposeView::onSignatureEditRequested() {
// This slot will be connected to the "Edit Signature..." action in the menu
// For now, we'll just show a simple input dialog
QString currentSignature = "";
// In a real implementation, we would get the current signature for the selected account
// For now, we'll use an empty string
bool ok;
QString newSignature = QInputDialog::getMultiLineText(this, "Edit Signature",
"Enter your signature:",
currentSignature, &ok);
if (ok && !newSignature.isEmpty()) {
// In a real implementation, we would save this signature for the selected account
// For now, we'll just insert it at the cursor position
QTextCursor cursor = m_bodyEditor->textCursor();
cursor.insertHtml(newSignature);
}
}
void ComposeView::setAccountService(AccountService *service)
{
m_accountService = service;
populateAccountCombo();
}
void ComposeView::populateAccountCombo()
{
if (!m_accountService) {
qWarning() << "AccountService not set";
return;
}
m_accountCombo->clear();
m_accountCombo->addItem(tr("Select Account..."), QVariant());
QVector<Account> accounts = m_accountService->getAllAccounts();
for (const Account &account : accounts) {
m_accountCombo->addItem(account.email(), account.id());
}
// Select the first account by default if there are accounts
if (accounts.size() > 0) {
m_accountCombo->setCurrentIndex(1); // Skip the placeholder item
m_currentAccountId = accounts.first().id();
}
}
void ComposeView::onAccountChanged(int index)
{
if (index <= 0) {
// Placeholder item selected or invalid index
m_currentAccountId = -1;
return;
}
m_currentAccountId = m_accountCombo->itemData(index).toInt();
loadSignatureForCurrentAccount();
}
void ComposeView::loadSignatureForCurrentAccount()
{
if (!m_accountService || m_currentAccountId <= 0) {
// Clear signature if no account selected
m_bodyEditor->clear();
return;
}
Account* account = m_accountService->findAccountById(m_currentAccountId);
if (account) {
// For now, we'll just clear the editor since we don't have a signature field in Account yet
// In a real implementation, we would retrieve and display the signature
m_bodyEditor->clear();
} else {
m_bodyEditor->clear();
}
}
void ComposeView::setupUI() {
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20, 15, 20, 15);
mainLayout->setSpacing(8);
this->setStyleSheet(
"QLineEdit, QTextEdit { border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
"QLineEdit:focus, QTextEdit:focus { border-color: #1976D2; }"
);
// Header row: Subject + Detach button
QHBoxLayout *headerLayout = new QHBoxLayout();
m_subjectField = new QLineEdit();
m_subjectField->setPlaceholderText("Subject");
m_subjectField->setFixedHeight(40);
QFont subjectFont = m_subjectField->font();
subjectFont.setPointSize(14);
subjectFont.setBold(true);
m_subjectField->setFont(subjectFont);
headerLayout->addWidget(m_subjectField, 1);
m_detachButton = new QPushButton("\xe2\x87\xa5 Detach");
m_detachButton->setToolTip("Open compose window in a separate window");
m_detachButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 12px; color: #555; font-size: 12px; }"
"QPushButton:hover { background: #f0f0f0; }"
);
connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked);
headerLayout->addWidget(m_detachButton);
mainLayout->addLayout(headerLayout);
// Separator line
QFrame *line1 = new QFrame();
line1->setFrameShape(QFrame::HLine);
line1->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line1);
// From: field + Account selector
QHBoxLayout *fromLayout = new QHBoxLayout();
QLabel *fromLabel = new QLabel("From:");
fromLabel->setFixedWidth(40);
fromLabel->setStyleSheet("font-weight: bold; color: #555;");
m_accountCombo = new QComboBox();
m_accountCombo->setFixedWidth(180);
m_accountCombo->setStyleSheet(
"QComboBox { border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 8px; "
"background: white; min-height: 20px; }"
"QComboBox:hover { border-color: #bdbdbd; }"
"QComboBox:focus { border-color: #1976D2; }"
"QComboBox::drop-down { border: none; width: 20px; }"
"QComboBox::down-arrow { image: url(:/icons/dropdown.png); width: 10px; height: 10px; }"
);
connect(m_accountCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ComposeView::onAccountChanged);
fromLayout->addWidget(fromLabel);
fromLayout->addWidget(m_accountCombo, 1);
mainLayout->addLayout(fromLayout);
// To: field + Cc/Bcc toggle buttons
QHBoxLayout *toLayout = new QHBoxLayout();
QLabel *toLabel = new QLabel("To:");
toLabel->setFixedWidth(40);
toLabel->setStyleSheet("font-weight: bold; color: #555;");
m_toField = new QLineEdit();
m_toField->setPlaceholderText("Recipients (comma separated)");
toLayout->addWidget(toLabel);
toLayout->addWidget(m_toField, 1);
m_ccButton = new QPushButton("Cc");
m_ccButton->setFixedWidth(32);
m_ccButton->setStyleSheet(
"QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }"
"QPushButton:hover { background: #d0d0d0; }"
);
connect(m_ccButton, &QPushButton::clicked, this, &ComposeView::onCcToggle);
toLayout->addWidget(m_ccButton);
m_bccButton = new QPushButton("Bcc");
m_bccButton->setFixedWidth(36);
m_bccButton->setStyleSheet(
"QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }"
"QPushButton:hover { background: #d0d0d0; }"
);
connect(m_bccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle);
toLayout->addWidget(m_bccButton);
mainLayout->addLayout(toLayout);
// Cc: row (hidden by default)
m_ccRow = new QWidget();
QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow);
ccLayout->setContentsMargins(0, 0, 0, 0);
QLabel *ccLabel = new QLabel("Cc:");
ccLabel->setFixedWidth(40);
ccLabel->setStyleSheet("color: #555;");
m_ccField = new QLineEdit();
m_ccField->setPlaceholderText("Carbon copy");
ccLayout->addWidget(ccLabel);
ccLayout->addWidget(m_ccField, 1);
m_hideCcButton = new QPushButton("x");
m_hideCcButton->setFixedSize(20, 20);
m_hideCcButton->setToolTip("Hide Cc");
m_hideCcButton->setStyleSheet(
"QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }"
"QPushButton:hover { color: #333; }"
);
connect(m_hideCcButton, &QPushButton::clicked, this, &ComposeView::onCcToggle);
ccLayout->addWidget(m_hideCcButton);
m_ccRow->setVisible(false);
mainLayout->addWidget(m_ccRow);
// Bcc: row (hidden by default)
m_bccRow = new QWidget();
QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow);
bccLayout->setContentsMargins(0, 0, 0, 0);
QLabel *bccLabel = new QLabel("Bcc:");
bccLabel->setFixedWidth(40);
bccLabel->setStyleSheet("color: #555;");
m_bccField = new QLineEdit();
m_bccField->setPlaceholderText("Blind carbon copy");
bccLayout->addWidget(bccLabel);
bccLayout->addWidget(m_bccField, 1);
m_hideBccButton = new QPushButton("x");
m_hideBccButton->setFixedSize(20, 20);
m_hideBccButton->setToolTip("Hide Bcc");
m_hideBccButton->setStyleSheet(
"QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }"
"QPushButton:hover { color: #333; }"
);
connect(m_hideBccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle);
bccLayout->addWidget(m_hideBccButton);
m_bccRow->setVisible(false);
mainLayout->addWidget(m_bccRow);
// Separator
QFrame *line2 = new QFrame();
line2->setFrameShape(QFrame::HLine);
line2->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line2);
// Rich text editor with toolbar
m_bodyEditor = new RichTextEditor();
m_bodyEditor->setupToolbar(mainLayout);
mainLayout->addWidget(m_bodyEditor, 1);
// Schedule panel
m_schedulePanel = new QWidget();
QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel);
scheduleLayout->setContentsMargins(0, 0, 0, 0);
QLabel *scheduleLabel = new QLabel("Send at:");
scheduleLabel->setStyleSheet("color: #555; font-weight: bold;");
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600));
m_schedulePicker->setCalendarPopup(true);
m_schedulePicker->setDisplayFormat("dd/MM/yyyy hh:mm AP");
scheduleLayout->addWidget(scheduleLabel);
scheduleLayout->addWidget(m_schedulePicker);
scheduleLayout->addStretch();
m_scheduleSendButton = new QPushButton("Schedule Send");
m_scheduleSendButton->setStyleSheet(
"QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 6px 16px; font-weight: bold; }"
"QPushButton:hover { background-color: #1565C0; }"
);
connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked);
scheduleLayout->addWidget(m_scheduleSendButton);
m_schedulePanel->setVisible(false);
mainLayout->addWidget(m_schedulePanel);
// Action buttons row
QHBoxLayout *actionsLayout = new QHBoxLayout();
actionsLayout->addStretch();
m_discardButton = new QPushButton("Discard");
m_discardButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px 20px; color: #555; }"
"QPushButton:hover { background: #f5f5f5; }"
);
actionsLayout->addWidget(m_discardButton);
connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested);
// Split button for Send / Schedule
m_sendMenu = new QMenu(this);
m_sendNowAction = m_sendMenu->addAction("Send Now");
connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked);
m_scheduleAction = m_sendMenu->addAction("Schedule for later...");
connect(m_scheduleAction, &QAction::triggered, [this]() {
m_schedulePanel->setVisible(true);
});
m_sendSplit = new QToolButton();
m_sendSplit->setText("Send");
m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup);
m_sendSplit->setMenu(m_sendMenu);
m_sendSplit->setStyleSheet(
"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 8px 24px; font-weight: bold; }"
"QToolButton:hover { background-color: #1565C0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; }"
"QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }"
);
connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked);
actionsLayout->addWidget(m_sendSplit);
mainLayout->addLayout(actionsLayout);
}
void ComposeView::onCcToggle() {
m_ccVisible = !m_ccVisible;
m_ccRow->setVisible(m_ccVisible);
m_ccButton->setVisible(!m_ccVisible);
if (m_ccVisible) m_ccField->setFocus();
}
void ComposeView::onBccToggle() {
m_bccVisible = !m_bccVisible;
m_bccRow->setVisible(m_bccVisible);
m_bccButton->setVisible(!m_bccVisible);
if (m_bccVisible) m_bccField->setFocus();
}
void ComposeView::onSendClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
QDateTime(), // null = send now
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
);
}
void ComposeView::onScheduleClicked() {
emit sendRequested(
m_toField->text(),
m_ccVisible ? m_ccField->text() : QString(),
m_bccVisible ? m_bccField->text() : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
m_schedulePicker->dateTime(),
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString()
);
}
void ComposeView::onDetachClicked() {
emit detachRequested(this);
}
void ComposeView::setTo(const QString &to) { m_toField->setText(to); }
void ComposeView::setSubject(const QString &subject) { m_subjectField->setText(subject); }
void ComposeView::setBody(const QString &body) { m_bodyEditor->setHtml(body); }
void ComposeView::initializeComposition() {
m_toField->clear();
m_ccField->clear();
m_bccField->clear();
m_subjectField->clear();
m_bodyEditor->clear();
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
m_toField->setFocus();
}
void ComposeView::startNewEmail(const QString &initialRecipient) {
initializeComposition();
if (!initialRecipient.isEmpty())
m_toField->setText(initialRecipient);
m_toField->setFocus();
}
#include "composeview.moc"
+11 -40
View File
@@ -17,45 +17,12 @@
#include <QComboBox>
#include <QListWidget>
#include "models/EmailCompositionModel.h"
#include "tags_line_edit.hpp"
#include "../../thirdparty/tags/include/tags_line_edit.hpp"
#include "services/accountservice.h"
class AccountService;
class RichTextEditor : public QTextEdit {
Q_OBJECT
public:
explicit RichTextEditor(QWidget *parent = nullptr);
void setupToolbar(QVBoxLayout *layout);
signals:
void signatureClicked();
void signatureEditRequested();
public slots:
void onBold();
void onItalic();
void onUnderline();
void onBulletList();
void onNumberedList();
void onIndent();
void onOutdent();
void onAlignLeft();
void onAlignCenter();
void onAlignRight();
void onAlignJustify();
void onFontChanged(const QFont &font);
void onFontSizeChanged(int size);
void onInsertImage();
void onInsertTable();
private:
QToolBar *m_toolbar;
QFontComboBox *m_fontCombo;
QSpinBox *m_fontSizeSpin;
QToolButton *m_signatureButton;
QMenu *m_signatureMenu;
};
#include "richtexteditor.h"
class ComposeView : public QWidget {
Q_OBJECT
@@ -71,7 +38,8 @@ signals:
void sendRequested(const QString &to, const QString &cc, const QString &bcc,
const QString &subject, const QString &body,
const QDateTime &scheduleTime,
const QString &fromAddress);
const QString &fromAddress,
const QStringList &attachmentPaths);
void discardRequested();
public slots:
@@ -92,6 +60,7 @@ private slots:
void onSignatureEditRequested();
void onAddAttachmentClicked();
void onRemoveAttachmentClicked();
void onTemplateClicked(); // new slot for templates
private:
void setupUI();
@@ -101,15 +70,17 @@ private:
EmailCompositionModel *m_compositionModel;
AccountService *m_accountService = nullptr;
// UI components
QLineEdit *m_toField = nullptr;
QLabel *m_fromLabel = nullptr;
QComboBox *m_accountCombo = nullptr;
everload_tags::TagsLineEdit *m_toField = nullptr;
everload_tags::TagsLineEdit *m_ccField = nullptr;
everload_tags::TagsLineEdit *m_bccField = nullptr;
QLineEdit *m_subjectField = nullptr;
RichTextEditor *m_bodyEditor = nullptr;
QPushButton *m_detachButton = nullptr;
QWidget *m_ccRow = nullptr;
QWidget *m_bccRow = nullptr;
QWidget *m_schedulePanel = nullptr;
QLineEdit *m_ccField = nullptr;
QLineEdit *m_bccField = nullptr;
QPushButton *m_ccButton = nullptr;
QPushButton *m_bccButton = nullptr;
QPushButton *m_hideCcButton = nullptr;
@@ -121,13 +92,13 @@ private:
QAction *m_sendNowAction = nullptr;
QAction *m_scheduleAction = nullptr;
QToolButton *m_sendSplit = nullptr;
QComboBox *m_accountCombo = nullptr;
bool m_ccVisible = false;
bool m_bccVisible = false;
int m_currentAccountId = -1;
// Attachment UI
QToolButton *m_attachButton = nullptr;
QToolButton *m_templateButton = nullptr;
QListWidget *m_attachmentList = nullptr;
QToolButton *m_removeAttachmentButton = nullptr;
QStringList m_attachmentFiles;
+127
View File
@@ -0,0 +1,127 @@
#ifndef COMPOSE_VIEW_H
#define COMPOSE_VIEW_H
#include <QWidget>
#include <QTextEdit>
#include <QVBoxLayout>
#include <QToolBar>
#include <QPushButton>
#include <QLineEdit>
#include <QLabel>
#include <QFrame>
#include <QMenu>
#include <QAction>
#include <QToolButton>
#include <QDateTimeEdit>
#include <QFontComboBox>
#include <QSpinBox>
#include <QComboBox>
#include "models/EmailCompositionModel.h"
#include "services/accountservice.h"
class AccountService;
class RichTextEditor : public QTextEdit {
Q_OBJECT
public:
explicit RichTextEditor(QWidget *parent = nullptr);
void setupToolbar(QVBoxLayout *layout);
signals:
void signatureClicked();
void signatureEditRequested();
public slots:
void onBold();
void onItalic();
void onUnderline();
void onBulletList();
void onNumberedList();
void onIndent();
void onOutdent();
void onAlignLeft();
void onAlignCenter();
void onAlignRight();
void onAlignJustify();
void onFontChanged(const QFont &font);
void onFontSizeChanged(int size);
void onInsertImage();
void onInsertTable();
private:
QToolBar *m_toolbar;
QFontComboBox *m_fontCombo;
QSpinBox *m_fontSizeSpin;
QToolButton *m_signatureButton;
QMenu *m_signatureMenu;
};
class ComposeView : public QWidget {
Q_OBJECT
public:
explicit ComposeView(QWidget *parent = nullptr);
~ComposeView() override;
void setAccountService(AccountService *service);
signals:
void compositionFinished();
void detachRequested(QWidget *widget);
void sendRequested(const QString &to, const QString &cc, const QString &bcc,
const QString &subject, const QString &body,
const QDateTime &scheduleTime,
const QString &fromAddress);
void discardRequested();
public slots:
void initializeComposition();
void startNewEmail(const QString &initialRecipient);
void setTo(const QString &to);
void setSubject(const QString &subject);
void setBody(const QString &body);
private slots:
void onCcToggle();
void onBccToggle();
void onSendClicked();
void onScheduleClicked();
void onDetachClicked();
void onAccountChanged(int index);
void onSignatureClicked();
void onSignatureEditRequested();
private:
void setupUI();
void populateAccountCombo();
void loadSignatureForCurrentAccount();
EmailCompositionModel *m_compositionModel;
AccountService *m_accountService = nullptr;
// UI components
QLineEdit *m_toField = nullptr;
QLineEdit *m_subjectField = nullptr;
RichTextEditor *m_bodyEditor = nullptr;
QPushButton *m_detachButton = nullptr;
QWidget *m_ccRow = nullptr;
QWidget *m_bccRow = nullptr;
QWidget *m_schedulePanel = nullptr;
QLineEdit *m_ccField = nullptr;
QLineEdit *m_bccField = nullptr;
QPushButton *m_ccButton = nullptr;
QPushButton *m_bccButton = nullptr;
QPushButton *m_hideCcButton = nullptr;
QPushButton *m_hideBccButton = nullptr;
QDateTimeEdit *m_schedulePicker = nullptr;
QPushButton *m_scheduleSendButton = nullptr;
QPushButton *m_discardButton = nullptr;
QMenu *m_sendMenu = nullptr;
QAction *m_sendNowAction = nullptr;
QAction *m_scheduleAction = nullptr;
QToolButton *m_sendSplit = nullptr;
QComboBox *m_accountCombo = nullptr;
bool m_ccVisible = false;
bool m_bccVisible = false;
int m_currentAccountId = -1;
};
#endif // COMPOSE_VIEW_H
+224
View File
@@ -0,0 +1,224 @@
#include "ui/connectionwizard.h"
#include <QMessageBox>
#include <QTimer>
#include <QDebug>
#include <QSslSocket>
ConnectionWizard::ConnectionWizard(QWidget *parent, AccountService *accountService)
: QWizard(parent), m_accountService(accountService)
{
setWindowTitle("Configurar conexión de correo");
setWizardStyle(QWizard::ModernStyle);
setOption(QWizard::HaveHelpButton, false);
setOption(QWizard::HaveFinishButtonOnEarlyPages, true);
setupPageIntro();
setupPageIncoming();
setupPageOutgoing();
setupPageAuth();
setupPageTest();
// Set page order
addPage(m_introPage);
addPage(m_incomingPage);
addPage(m_outgoingPage);
addPage(m_authPage);
addPage(m_testPage);
// Start at intro
startId();
}
void ConnectionWizard::setupPageIntro()
{
m_introPage = new QWizardPage(this);
m_introPage->setTitle("Configuración de conexión");
m_introPage->setSubTitle("Introduce los datos de los servidores entrantes y salientes para configurar tu cuenta de correo.");
QVBoxLayout *lay = new QVBoxLayout(m_introPage);
QLabel *info = new QLabel(
"Este asistente te guiará paso a paso para configurar los servidores IMAP/POP3 y SMTP.\\n"
"Si no conoces los datos, consulta a tu proveedor de correo o busca en sus páginas de soporte."
);
info->setWordWrap(true);
lay->addWidget(info);
lay->addStretch();
m_introPage->setLayout(lay);
}
void ConnectionWizard::setupPageIncoming()
{
m_incomingPage = new QWizardPage(this);
m_incomingPage->setTitle("Servidor entrante (IMAP/POP3)");
m_incomingPage->setSubTitle("Configura el servidor que recibirá tus correos.");
QFormLayout *form = new QFormLayout();
m_incomingTypeCombo = new QComboBox();
m_incomingTypeCombo->addItem("IMAP", QVariant::fromValue(QString("imap")));
m_incomingTypeCombo->addItem("POP3", QVariant::fromValue(QString("pop3")));
form->addRow("Tipo:", m_incomingTypeCombo);
m_incomingHostEdit = new QLineEdit();
m_incomingHostEdit->setPlaceholderText("ej. imap.ejemplo.com");
form->addRow("Servidor:", m_incomingHostEdit);
m_incomingPortEdit = new QLineEdit();
m_incomingPortEdit->setPlaceholderText("993");
m_incomingPortEdit->setValidator(new QIntValidator(1, 65535, this));
form->addRow("Puerto:", m_incomingPortEdit);
m_incomingSslCheck = new QCheckBox("Usar conexión segura (SSL/TLS)");
m_incomingSslCheck->setChecked(true);
form->addRow("", m_incomingSslCheck);
m_incomingPage->setLayout(form);
}
void ConnectionWizard::setupPageOutgoing()
{
m_outgoingPage = new QWizardPage(this);
m_outgoingPage->setTitle("Servidor saliente (SMTP)");
m_outgoingPage->setSubTitle("Configura el servidor que enviará tus correos.");
QFormLayout *form = new QFormLayout();
m_outgoingHostEdit = new QLineEdit();
m_outgoingHostEdit->setPlaceholderText("ej. smtp.ejemplo.com");
form->addRow("Servidor:", m_outgoingHostEdit);
m_outgoingPortEdit = new QLineEdit();
m_outgoingPortEdit->setPlaceholderText("587");
m_outgoingPortEdit->setValidator(new QIntValidator(1, 65535, this));
form->addRow("Puerto:", m_outgoingPortEdit);
m_outgoingSslCheck = new QCheckBox("Usar conexión segura (STARTTLS/SSL)");
m_outgoingSslCheck->setChecked(true);
form->addRow("", m_outgoingSslCheck);
m_outgoingPage->setLayout(form);
}
void ConnectionWizard::setupPageAuth()
{
m_authPage = new QWizardPage(this);
m_authPage->setTitle("Autenticación");
m_authPage->setSubTitle("Introduce tu nombre de usuario y contraseña para acceder a los servidores.");
QFormLayout *form = new QFormLayout();
m_usernameEdit = new QLineEdit();
m_usernameEdit->setPlaceholderText("tu-usuario@dominio.com");
form->addRow("Usuario:", m_usernameEdit);
m_passwordEdit = new QLineEdit();
m_passwordEdit->setEchoMode(QLineEdit::Password);
m_passwordEdit->setPlaceholderText("Contraseña");
form->addRow("Contraseña:", m_passwordEdit);
m_authMethodCombo = new QComboBox();
m_authMethodCombo->addItem("Contraseña normal", QVariant::fromValue(QString("plain")));
m_authMethodCombo->addItem("Autenticación OAuth2", QVariant::fromValue(QString("oauth2")));
form->addRow("Método:", m_authMethodCombo);
m_authPage->setLayout(form);
}
void ConnectionWizard::setupPageTest()
{
m_testPage = new QWizardPage(this);
m_testPage->setTitle("Probar conexión");
m_testPage->setSubTitle("Verifica que los datos ingresados sean correctos.");
QVBoxLayout *lay = new QVBoxLayout(m_testPage);
m_testStatusLabel = new QLabel("Listo para probar la conexión.");
m_testStatusLabel->setAlignment(Qt::AlignCenter);
m_testStatusLabel->setStyleSheet("font-size: 14px; color: #555;");
lay->addWidget(m_testStatusLabel);
m_testDetailLabel = new QLabel("");
m_testDetailLabel->setWordWrap(true);
m_testDetailLabel->setAlignment(Qt::AlignCenter);
m_testDetailLabel->setStyleSheet("font-size: 12px; color: #888;");
lay->addWidget(m_testDetailLabel);
lay->addStretch();
m_testButton = new QPushButton("Probar conexión ahora");
m_testButton->setStyleSheet(
"QPushButton { background-color: #0071e3; color: white; border: none; padding: 8px 16px; border-radius: 4px; }"
"QPushButton:hover { background-color: #005bb5; }"
);
lay->addWidget(m_testButton, 0, Qt::AlignCenter);
connect(m_testButton, &QPushButton::clicked, this, &ConnectionWizard::onTestClicked);
m_testPage->setLayout(lay);
}
void ConnectionWizard::onTestClicked()
{
if (!m_accountService) {
m_testStatusLabel->setText("⚠️ Servicio de cuenta no disponible.");
m_testStatusLabel->setStyleSheet("color: #e60000;");
m_testDetailLabel->setText("El servicio de cuenta no está inicializado.");
return;
}
Account::ConnectionSettings settings = connectionSettings();
// Basic validation
if (settings.incomingHost.isEmpty() || settings.username.isEmpty() || settings.password.isEmpty()) {
m_testStatusLabel->setText("⚠️ Faltan datos obligatorios.");
m_testStatusLabel->setStyleSheet("color: #e60000;");
m_testDetailLabel->setText("Por favor, completa los campos de servidor entrante, usuario y contraseña.");
return;
}
m_testStatusLabel->setText("⏳ Probando conexión...");
m_testStatusLabel->setStyleSheet("color: #0071e3;");
m_testDetailLabel->setText("Conectando al servidor IMAP...");
// Use a small delay to allow UI update before blocking call
QTimer::singleShot(0, this, [this, settings]() {
QString errorMsg;
bool ok = m_accountService->testConnection(settings, errorMsg);
if (ok) {
m_testStatusLabel->setText("✅ Conexión exitosa.");
m_testStatusLabel->setStyleSheet("color: #34c759;");
m_testDetailLabel->setText("Los datos ingresados son correctos. Puedes finalizar la configuración.");
} else {
m_testStatusLabel->setText("❌ Error de conexión.");
m_testStatusLabel->setStyleSheet("color: #ff3b30;");
m_testDetailLabel->setText(errorMsg.isEmpty() ? "Error desconocido." : errorMsg);
}
});
}
Account::ConnectionSettings ConnectionWizard::connectionSettings() const
{
Account::ConnectionSettings s;
// Incoming
s.type = m_incomingTypeCombo->currentData().toString(); // "imap" or "pop3"
s.incomingHost = m_incomingHostEdit->text().trimmed();
s.incomingPort = m_incomingPortEdit->text().toInt();
s.incomingSsl = m_incomingSslCheck->isChecked();
// Outgoing (SMTP)
s.outgoingHost = m_outgoingHostEdit->text().trimmed();
s.outgoingPort = m_outgoingPortEdit->text().toInt();
s.outgoingSsl = m_outgoingSslCheck->isChecked();
// Auth
s.username = m_usernameEdit->text().trimmed();
s.password = m_passwordEdit->text();
s.authMethod = m_authMethodCombo->currentData().toString(); // "plain" or "oauth2"
return s;
}
void ConnectionWizard::accept()
{
// When Finish is pressed, emit the settings and close
Account::ConnectionSettings settings = connectionSettings();
emit settingsReady(settings);
QWizard::accept();
}
#include "connectionwizard.moc"
+73
View File
@@ -0,0 +1,73 @@
#ifndef CONNECTIONWIZARD_H
#define CONNECTIONWIZARD_H
#include <QWizard>
#include <QLineEdit>
#include <QComboBox>
#include <QCheckBox>
#include <QIntValidator>
#include <QLabel>
#include <QVBoxLayout>
#include <QFormLayout>
#include <QPushButton>
#include <QGroupBox>
#include <QRadioButton>
#include "core/models/account.h"
#include "services/accountservice.h"
class ConnectionWizard : public QWizard {
Q_OBJECT
public:
explicit ConnectionWizard(QWidget *parent = nullptr, AccountService *accountService = nullptr);
~ConnectionWizard() override = default;
// Retrieve the filled connection settings
Account::ConnectionSettings connectionSettings() const;
signals:
void settingsReady(const Account::ConnectionSettings &settings);
private slots:
void accept() override;
void onTestClicked();
private:
void setupPageIntro();
void setupPageIncoming();
void setupPageOutgoing();
void setupPageAuth();
void setupPageTest();
// Page pointers (optional)
QWizardPage *m_introPage;
QWizardPage *m_incomingPage;
QWizardPage *m_outgoingPage;
QWizardPage *m_authPage;
QWizardPage *m_testPage;
// Incoming fields
QComboBox *m_incomingTypeCombo; // IMAP, POP3
QLineEdit *m_incomingHostEdit;
QLineEdit *m_incomingPortEdit;
QCheckBox *m_incomingSslCheck;
// Outgoing fields
QLineEdit *m_outgoingHostEdit;
QLineEdit *m_outgoingPortEdit;
QCheckBox *m_outgoingSslCheck;
// Auth fields
QLineEdit *m_usernameEdit;
QLineEdit *m_passwordEdit;
QComboBox *m_authMethodCombo; // Plain, Login, OAuth2 (if supported)
// Test page
QLabel *m_testStatusLabel;
QLabel *m_testDetailLabel;
QPushButton *m_testButton;
AccountService *m_accountService;
};
#endif // CONNECTIONWIZARD_H
+109
View File
@@ -0,0 +1,109 @@
import sys
filename = '/mnt/c/Users/javie/wino-mail-dtkqt/src/ui/mainmainwindow.cpp'
with open(filename, 'r') as f:
lines = f.readlines()
# Find start of delete connect
start = None
for i, line in enumerate(lines):
if 'connect(deleteAction, &QAction::triggered' in line:
start = i
break
if start is None:
print('Could not find delete connect start')
sys.exit(1)
# Find end of that connect (the matching '});' after start)
end = None
brace_count = 0
for i in range(start, len(lines)):
line = lines[i]
for ch in line:
if ch == '{':
brace_count += 1
elif ch == '}':
brace_count -= 1
if brace_count == 0 and '});' in line:
end = i
break
if end is not None:
break
if end is None:
print('Could not find end of delete connect')
sys.exit(1)
# Replacement delete lambda
new_delete = ''' connect(deleteAction, &QAction::triggered, [this]() {
QModelIndexList indexes = m_mailListView->selectionModel()->selectedIndexes();
if (indexes.isEmpty()) {
statusBar()->showMessage(tr("No email selected"), 2000);
return;
}
int row = indexes.first().row();
QModelIndex idx = m_emailModel->index(row, 0);
qint64 id = idx.data(EmailListModel::IdRole).toLongLong();
if (m_mailService->deleteMail(QString::number(id))) {
statusBar()->showMessage(tr("Email deleted"), 2000);
m_emailModel->refresh();
} else {
statusBar()->showMessage(tr("Failed to delete email"), 2000);
}
});\n'''
# Replace lines[start:end+1] with new_delete
lines = lines[:start] + [new_delete] + lines[end+1:]
# Now add flag action declaration after deleteAction declaration
decl_line = None
for i, line in enumerate(lines):
if 'QAction *deleteAction = m_toolBar->addAction(\"🗑 Delete\");' in line:
decl_line = i
break
if decl_line is None:
print('Could not find deleteAction declaration')
sys.exit(1)
flag_decl = ' QAction *flagAction = m_toolBar->addAction(\"🚩 Flag\");\n'
lines = lines[:decl_line+1] + [flag_decl] + lines[decl_line+1:]
# Find where to insert flag connection: after the delete connect we just placed
# Search for the '});' that ends the new delete connect (starting from start)
insert_point = None
for i in range(start, len(lines)):
if '});' in lines[i]:
insert_point = i + 1
break
if insert_point is None:
insert_point = len(lines)
flag_conn = ''' connect(flagAction, &QAction::triggered, [this]() {
QModelIndexList indexes = m_mailListView->selectionModel()->selectedIndexes();
if (indexes.isEmpty()) {
statusBar()->showMessage(tr("No email selected"), 2000);
return;
}
int row = indexes.first().row();
QModelIndex idx = m_emailModel->index(row, 0);
qint64 id = idx.data(EmailListModel::IdRole).toLongLong();
// Toggle flagged state
std::optional<MailItem> opt = MailItemDao::findById(id);
if (opt) {
MailItem item = *opt;
item.setFlagged(!item.isFlagged());
if (MailItemDao::update(item)) {
statusBar()->showMessage(tr("Flag toggled"), 2000);
m_emailModel->refresh();
} else {
statusBar()->showMessage(tr("Failed to update flag"), 2000);
}
} else {
statusBar()->showMessage(tr("Email not found"), 2000);
}
});\n'''
lines = lines[:insert_point] + [flag_conn] + lines[insert_point:]
with open(filename, 'w') as f:
f.writelines(lines)
print('Delete and flag actions updated')
+24
View File
@@ -0,0 +1,24 @@
// Trigger initial mail sync for existing accounts on startup
QVector<Account> accounts = AccountDao::findAll();
for (const Account &account : accounts) {
if (account.type() == AccountType::IMAP) {
QVector<Folder> folders = FolderDao::findByAccountId(account.id());
QString inboxFolderId;
for (const Folder &folder : folders) {
if (folder.isInbox()) {
inboxFolderId = QString::number(folder.id());
break;
}
}
if (inboxFolderId.isEmpty() && !folders.isEmpty()) {
// fallback to first folder
inboxFolderId = QString::number(folders.first().id());
}
if (!inboxFolderId.isEmpty()) {
qDebug() << "[MainMainWindow] Triggering initial mail sync for account" << account.email() << "folder" << inboxFolderId;
m_mailService->fetchMails(QString::number(account.id()), inboxFolderId);
} else {
qWarning() << "[MainMainWindow] No folders found for account" << account.id();
}
}
}
+196 -32
View File
@@ -15,8 +15,11 @@
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
qDebug() << "[MainMainWindow] Constructor start";
setupUI();
qDebug() << "[MainMainWindow] setupUI done";
connectModels();
qDebug() << "[MainMainWindow] connectModels done";
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
// Setup progress bar in status bar
@@ -27,12 +30,20 @@ MainMainWindow::MainMainWindow(QWidget *parent)
// Connect mail service progress/status signals
connect(m_mailService, &MailService::progressChanged, this, &MainMainWindow::onProgressChanged);
connect(m_mailService, &MailService::statusMessage, this, &MainMainWindow::onStatusMessage);
connect(m_mailService, &MailService::mailSent, this, [this](const QString &) {
statusBar()->showMessage(tr("Message sent"), 5000);
});
connect(m_mailService, &MailService::mailSendFailed, this, [this](const QString &, const QString &error) {
statusBar()->showMessage(tr("Send failed: %1").arg(error), 8000);
});
// Provide account service to settings view
m_settingsView->setAccountService(m_accountService);
qDebug() << "[MainMainWindow] Constructor complete";
}
void MainMainWindow::setupUI()
{
qDebug() << "[MainMainWindow::setupUI] Start";
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
@@ -40,8 +51,10 @@ void MainMainWindow::setupUI()
"QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
"QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }"
);
qDebug() << "[MainMainWindow::setupUI] Stylesheet set";
createToolBar();
qDebug() << "[MainMainWindow::setupUI] Toolbar created";
// === Central widget ===
QWidget *central = new QWidget();
@@ -51,6 +64,7 @@ void MainMainWindow::setupUI()
// === Sidebar ===
setupSidebar();
qDebug() << "[MainMainWindow::setupUI] Sidebar created";
centralLayout->addWidget(m_sidebar);
// === Separator line ===
@@ -62,23 +76,34 @@ void MainMainWindow::setupUI()
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
qDebug() << "[MainMainWindow::setupUI] Stack created";
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
qDebug() << "[MainMainWindow::setupUI] Mail page created";
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime, const QString &fromAddr) {
QString msg;
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime, const QString &fromAddr, const QStringList &attachmentPaths) {
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
statusBar()->showMessage(tr("Scheduled sending is not available yet"), 5000);
return;
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
if (fromAddr.isEmpty()) {
statusBar()->showMessage(tr("Select an account before sending"), 5000);
return;
}
MailItem mail;
mail.setTo(to);
mail.setRecipient(to);
mail.setCc(cc);
mail.setBcc(bcc);
mail.setSubject(subject);
mail.setBodyHtml(body);
mail.setDate(QDateTime::currentDateTimeUtc());
m_mailService->sendMail(mail, fromAddr, attachmentPaths);
statusBar()->showMessage(tr("Sending message…"), 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
@@ -120,6 +145,7 @@ void MainMainWindow::setupUI()
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
m_stack->addWidget(m_settingsView);
qDebug() << "[MainMainWindow::setupUI] Settings page created";
// Page 3: Contacts
m_contactsView = new ContactsView();
@@ -131,9 +157,11 @@ void MainMainWindow::setupUI()
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
qDebug() << "[MainMainWindow::setupUI] Central widget set";
// Show mail page by default
switchToPage(PageMail);
qDebug() << "[MainMainWindow::setupUI] Done";
}
void MainMainWindow::setupSidebar()
@@ -161,8 +189,7 @@ void MainMainWindow::setupSidebar()
}
void MainMainWindow::setupMailPage()
{
m_mailPage = new QWidget();
{ m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
@@ -189,7 +216,30 @@ void MainMainWindow::setupMailPage()
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
// Viewer stack: placeholder, reader, compose
m_viewerStack = new QStackedWidget();
// Placeholder widget
m_placeholderWidget = new QWidget();
QVBoxLayout *placeholderLayout = new QVBoxLayout(m_placeholderWidget);
placeholderLayout->setAlignment(Qt::AlignCenter);
QLabel *iconLabel = new QLabel();
QPixmap pixmap = QIcon::fromTheme("mail-unread").pixmap(48, 48);
if (pixmap.isNull()) {
pixmap = QPixmap(48, 48);
pixmap.fill(Qt::gray);
}
iconLabel->setPixmap(pixmap);
iconLabel->setAlignment(Qt::AlignCenter);
QLabel *textLabel = new QLabel(tr("Seleccione un correo para leerlo"));
textLabel->setAlignment(Qt::AlignCenter);
textLabel->setStyleSheet("color: #666; font-size: 14px;");
placeholderLayout->addStretch();
placeholderLayout->addWidget(iconLabel);
placeholderLayout->addWidget(textLabel);
placeholderLayout->addStretch();
m_placeholderWidget->setLayout(placeholderLayout);
// Reader view
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
@@ -198,9 +248,22 @@ void MainMainWindow::setupMailPage()
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
// Embedded compose view
m_embeddedComposeView = new ComposeView();
connect(m_embeddedComposeView, &ComposeView::sendRequested, this, &MainMainWindow::onEmbeddedSendRequested);
connect(m_embeddedComposeView, &ComposeView::discardRequested, this, &MainMainWindow::onEmbeddedDiscardRequested);
connect(m_embeddedComposeView, &ComposeView::detachRequested, this, &MainMainWindow::onEmbeddedDetachRequested);
// Add to stack
m_viewerStack->addWidget(m_placeholderWidget);
m_viewerStack->addWidget(m_emailViewer);
m_viewerStack->addWidget(m_embeddedComposeView);
m_viewerStack->setCurrentIndex(0); // show placeholder by default
m_folderSplitter->addWidget(m_viewerStack);
// Default sizes: folder 240, list 380, viewer flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
@@ -208,12 +271,18 @@ void MainMainWindow::setupMailPage()
void MainMainWindow::connectModels()
{
qDebug() << "[MainMainWindow::connectModels] Start";
m_accountService = new AccountService(this);
qDebug() << "[MainMainWindow::connectModels] AccountService created";
m_mailService = new MailService(m_accountService, this);
qDebug() << "[MainMainWindow::connectModels] MailService created";
m_composeView->setAccountService(m_accountService);
m_embeddedComposeView->setAccountService(m_accountService);
m_folderModel = new FolderListModel(m_accountService, this);
qDebug() << "[MainMainWindow::connectModels] FolderListModel created";
m_emailModel = new EmailListModel(this);
qDebug() << "[MainMainWindow::connectModels] EmailListModel created";
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
@@ -221,6 +290,18 @@ void MainMainWindow::connectModels()
m_folderTree->expandAll();
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
connect(m_mailService, &MailService::mailFetched, this,
[this](const QString &, const QString &folderId, const QVector<MailItem> &) {
if (folderId.toInt() == m_currentFolderId) {
m_emailModel->refresh();
statusBar()->showMessage(tr("Mail synchronized"), 3000);
}
});
connect(m_mailService, &MailService::mailFetchError, this,
[this](const QString &, const QString &, const QString &error) {
statusBar()->showMessage(tr("Synchronization failed: %1").arg(error), 8000);
});
qDebug() << "[MainMainWindow::connectModels] Done";
}
void MainMainWindow::onNavChanged(int index)
@@ -239,8 +320,7 @@ void MainMainWindow::switchToPage(int pageIndex)
}
void MainMainWindow::onFolderSelected(const QModelIndex &index)
{
if (!index.isValid()) return;
{ if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
@@ -257,43 +337,55 @@ void MainMainWindow::onFolderSelected(const QModelIndex &index)
}
}
m_emailModel->refresh();
// Clear selection and show placeholder
m_currentMailId = -1;
m_mailListView->tableView()->clearSelection();
m_viewerStack->setCurrentIndex(0); // placeholder
}
}
void MainMainWindow::onEmailSelected(int mailId)
{
m_currentMailId = mailId;
{ m_currentMailId = mailId;
if (mailId >= 0) {
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
m_viewerStack->setCurrentIndex(0); // placeholder
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
m_viewerStack->setCurrentIndex(1); // reader
} else {
m_emailViewer->setMailItem(nullptr);
m_viewerStack->setCurrentIndex(0); // placeholder
}
}
void MainMainWindow::onComposeRequested()
{
switchToPage(PageCompose);
{ m_embeddedComposeView->initializeComposition();
m_viewerStack->setCurrentIndex(2); // compose
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item)
{
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
{ if (item) {
m_embeddedComposeView->initializeComposition();
m_embeddedComposeView->setTo(item->sender());
m_embeddedComposeView->setSubject(QString("Re: %1").arg(item->subject()));
// Optionally set body with quoted text? We'll leave empty for now.
m_embeddedComposeView->setBody(QString()); // clear
}
switchToPage(PageCompose);
m_viewerStack->setCurrentIndex(2); // compose
}
void MainMainWindow::onNewMessage()
{
switchToPage(PageCompose);
{ // Show compose view in the viewer stack
m_embeddedComposeView->initializeComposition();
m_viewerStack->setCurrentIndex(2); // compose
}
void MainMainWindow::openMailInIndependentWindow(int mailId)
@@ -329,8 +421,11 @@ void MainMainWindow::createToolBar()
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
const auto folder = FolderDao::findById(m_currentFolderId);
if (folder) {
m_mailService->fetchMails(QString::number(folder->accountId()), QString::number(m_currentFolderId));
statusBar()->showMessage(tr("Synchronizing…"), 3000);
}
}
});
connect(openWinAction, &QAction::triggered, [this]() {
@@ -341,7 +436,12 @@ void MainMainWindow::createToolBar()
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
if (m_currentMailId >= 0) {
m_mailService->deleteMail(QString::number(m_currentMailId));
m_currentMailId = -1;
m_emailModel->refresh();
m_viewerStack->setCurrentIndex(0);
}
});
}
@@ -385,6 +485,70 @@ void MainMainWindow::onAccountEditRequested(int accountId)
void MainMainWindow::onAccountDeleteRequested(int accountId)
{
Q_UNUSED(accountId);
statusBar()->showMessage(QString("Delete account %1 requested").arg(accountId), 3000);
m_accountService->removeAccount(accountId);
m_folderModel->refresh();
m_emailModel->setFolderId(-1);
m_emailModel->refresh();
m_currentFolderId = -1;
m_currentMailId = -1;
m_viewerStack->setCurrentIndex(0);
statusBar()->showMessage(tr("Account removed"), 3000);
}
void MainMainWindow::onEmbeddedSendRequested(const QString &to, const QString &cc, const QString &bcc,
const QString &subject, const QString &body,
const QDateTime &scheduleTime,
const QString &fromAddress,
const QStringList &attachmentPaths)
{
if (scheduleTime.isValid()) {
statusBar()->showMessage(tr("Scheduled sending is not available yet"), 5000);
return;
}
if (fromAddress.isEmpty()) {
statusBar()->showMessage(tr("Select an account before sending"), 5000);
return;
}
MailItem mail;
mail.setTo(to);
mail.setRecipient(to);
mail.setCc(cc);
mail.setBcc(bcc);
mail.setSubject(subject);
mail.setBodyHtml(body);
mail.setDate(QDateTime::currentDateTimeUtc());
m_mailService->sendMail(mail, fromAddress, attachmentPaths);
statusBar()->showMessage(tr("Sending message…"), 5000);
m_viewerStack->setCurrentIndex(0); // placeholder
m_embeddedComposeView->initializeComposition();
}
void MainMainWindow::onEmbeddedDiscardRequested()
{
// Go back to placeholder
m_viewerStack->setCurrentIndex(0); // placeholder
m_embeddedComposeView->initializeComposition();
}
void MainMainWindow::onEmbeddedDetachRequested(QWidget *widget)
{
// Detach the compose view to a standalone window (similar to main compose view's detach)
QStackedWidget *stack = qobject_cast<QStackedWidget*>(widget->parentWidget());
if (stack) {
stack->removeWidget(widget);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(tr("Compose - Wino Mail"));
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(widget);
widget->setMinimumSize(800, 600);
widget->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage(tr("Compose view detached to separate window"), 3000);
}
+406
View File
@@ -0,0 +1,406 @@
#include "mainmainwindow.h"
#include "core/models/account.h"
#include "core/mailitem.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
#include <optional>
#include <QMessageBox>
#include "ui/accountsetupdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QDateTime>
#include <QLabel>
#include <QMetaObject>
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
setupUI();
connectModels();
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
}
void MainMainWindow::setupUI() {
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
"QSplitter::handle { background-color: #d1d1d6; width: 1px; }"
"QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
"QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }"
);
createToolBar();
// === Central widget ===
QWidget *central = new QWidget();
QHBoxLayout *centralLayout = new QHBoxLayout(central);
centralLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0);
// === Sidebar ===
setupSidebar();
centralLayout->addWidget(m_sidebar);
// === Separator line ===
QFrame *separator = new QFrame();
separator->setFrameShape(QFrame::VLine);
separator->setStyleSheet("color: #d1d1d6;");
centralLayout->addWidget(separator);
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime) {
QString msg;
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) {
// Detach compose view to a standalone window
QStackedWidget *stack = qobject_cast<QStackedWidget*>(composeView->parentWidget());
if (stack) {
stack->removeWidget(composeView);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle("Compose - Wino Mail");
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(composeView);
composeView->setMinimumSize(800, 600);
composeView->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Compose view detached to separate window", 3000);
});
m_stack->addWidget(m_composeView);
// Page 2: Settings
m_settingsView = new SettingsView();
connect(m_settingsView, &SettingsView::accountAddRequested, [this]() {
AccountSetupDialog dlg(m_accountService, this);
dlg.exec();
});
connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) {
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
m_stack->addWidget(m_settingsView);
// Page 3: Contacts
m_contactsView = new ContactsView();
m_stack->addWidget(m_contactsView);
// Page 4: Calendar
m_calendarView = new CalendarView();
m_stack->addWidget(m_calendarView);
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
// Show mail page by default
switchToPage(PageMail);
}
void MainMainWindow::setupSidebar() {
m_sidebar = new QListWidget();
m_sidebar->setFixedWidth(64);
m_sidebar->setIconSize(QSize(24, 24));
m_sidebar->setSpacing(4);
m_sidebar->setFrameShape(QFrame::NoFrame);
m_sidebar->setStyleSheet(
"QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }"
"QListWidget::item { color: #8e8e93; padding: 12px 0; text-align: center; font-size: 10px; border: none; border-radius: 8px; margin: 2px 8px; }"
"QListWidget::item:selected { background-color: #3a3a3c; color: #ffffff; }"
"QListWidget::item:hover { background-color: #3a3a3c; color: #ffffff; }"
);
m_sidebar->addItem("📧\nMail");
m_sidebar->addItem("✏️\nCompose");
m_sidebar->addItem("⚙️\nSettings");
m_sidebar->addItem("👥\nContacts");
m_sidebar->addItem("📅\nCalendar");
m_sidebar->setCurrentRow(0);
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged);
}
void MainMainWindow::setupMailPage() {
m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
m_folderSplitter = new QSplitter(Qt::Horizontal);
m_folderSplitter->setHandleWidth(1);
// Folder tree
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
m_folderTree->setIndentation(20);
m_folderTree->setMinimumWidth(220);
m_folderTree->setMaximumWidth(350);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderSplitter->addWidget(m_folderTree);
// Mail list (QTableView with sorting)
m_mailListView = new MailListView();
connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected);
connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested);
connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) {
openMailInIndependentWindow(mailId);
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
}
void MainMainWindow::connectModels() {
m_accountService = new AccountService(this);
m_mailService = new MailService(this);
m_folderModel = new FolderListModel(m_accountService, this);
m_emailModel = new EmailListModel(this);
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
m_folderTree->expandAll();
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
}
void MainMainWindow::onNavChanged(int index) {
switchToPage(static_cast<Page>(index));
}
void MainMainWindow::switchToPage(int pageIndex) {
m_stack->setCurrentIndex(pageIndex);
m_sidebar->blockSignals(true);
m_sidebar->setCurrentRow(pageIndex);
m_sidebar->blockSignals(false);
// Show/hide toolbar actions per page
}
void MainMainWindow::onFolderSelected(const QModelIndex &index) {
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
m_emailModel->setFolderId(m_currentFolderId);
// Fetch mails for this folder
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
QString accountId = QString::number(account->id());
QString folderId = QString::number(m_currentFolderId);
// Fetch mails asynchronously to avoid blocking UI
QMetaObject::invokeMethod(m_mailService, "fetchMails", Qt::QueuedConnection,
Q_ARG(QString, accountId), Q_ARG(QString, folderId));
delete account;
}
}
m_emailModel->refresh();
}
}
void MainMainWindow::onEmailSelected(int mailId) {
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested() {
switchToPage(PageCompose);
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item) {
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
}
switchToPage(PageCompose);
}
void MainMainWindow::onNewMessage() {
switchToPage(PageCompose);
}
void MainMainWindow::openMailInIndependentWindow(int mailId) {
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) return;
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject()));
ReaderView *detachedReader = new ReaderView();
detachedReader->setMailItem(&item.value());
detachedWin->setCentralWidget(detachedReader);
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Opened mail in independent window", 3000);
}
void MainMainWindow::onAddAccountRequested() {
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString("Account added: %1").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list to show new account's folders
});
dialog->open();
}
void MainMainWindow::onAccountEditRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage("Account not found", 3000);
return;
}
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->loadAccountForEditing(*account);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString("Account updated: %1").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list
});
dialog->open();
}
void MainMainWindow::onAccountDeleteRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage("Account not found", 3000);
return;
}
if (QMessageBox::warning(this, "Delete Account",
QString("Are you sure you want to delete the account '%1'?").arg(account->email()),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
m_accountService->removeAccount(accountId);
statusBar()->showMessage(QString("Account deleted: %1").arg(account->email()), 3000);
m_folderModel->refresh(); // Refresh folder list to remove deleted account's folders
}
}
void MainMainWindow::onAddAccountRequested() {
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString(\"Account added: %1\").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list to show new account's folders
});
dialog->open();
}
void MainMainWindow::onAccountEditRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage(\"Account not found\", 3000);
return;
}
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->loadAccountForEditing(*account);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString(\"Account updated: %1\").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list
});
dialog->open();
}
void MainMainWindow::onAccountDeleteRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage(\"Account not found\", 3000);
return;
}
if (QMessageBox::warning(this, \"Delete Account\",
QString(\"Are you sure you want to delete the account '%1'?\").arg(account->email()),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
m_accountService->removeAccount(accountId);
statusBar()->showMessage(QString(\"Account deleted: %1\").arg(account->email()), 3000);
m_folderModel->refresh(); // Refresh folder list to remove deleted account's folders
}
}
void MainMainWindow::createToolBar() {
m_toolBar = addToolBar("Main Toolbar");
m_toolBar->setMovable(false);
QAction *newMsgAction = m_toolBar->addAction("✉ New Message");
m_toolBar->addSeparator();
QAction *syncAction = m_toolBar->addAction("⟳ Sync/Refresh");
QAction *openWinAction = m_toolBar->addAction("↗ Abrir en ventana");
QAction *deleteAction = m_toolBar->addAction("🗑 Delete");
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
}
});
connect(openWinAction, &QAction::triggered, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
} else {
statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000);
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
});
}
+390
View File
@@ -0,0 +1,390 @@
#include "mainmainwindow.h"
#include "core/models/account.h"
#include "core/mailitem.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
#include <optional>
#include <QMessageBox>
#include "ui/accountsetupdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QDateTime>
#include <QLabel>
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
setupUI();
connectModels();
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
// Setup progress bar in status bar
m_progressBar = new QProgressBar(this);
m_progressBar->setMaximumWidth(120);
m_progressBar->setVisible(false);
statusBar()->addPermanentWidget(m_progressBar);
// Connect mail service progress/status signals
connect(m_mailService, &MailService::progressChanged, this, &MainMainWindow::onProgressChanged);
connect(m_mailService, &MailService::statusMessage, this, &MainMainWindow::onStatusMessage);
// Provide account service to settings view
m_settingsView->setAccountService(m_accountService);
}
void MainMainWindow::setupUI()
{
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
"QSplitter::handle { background-color: #d1d1d6; width: 1px; }"
"QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
"QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }"
);
createToolBar();
// === Central widget ===
QWidget *central = new QWidget();
QHBoxLayout *centralLayout = new QHBoxLayout(central);
centralLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0);
// === Sidebar ===
setupSidebar();
centralLayout->addWidget(m_sidebar);
// === Separator line ===
QFrame *separator = new QFrame();
separator->setFrameShape(QFrame::VLine);
separator->setStyleSheet("color: #d1d1d6;");
centralLayout->addWidget(separator);
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime, const QString &fromAddr) {
QString msg;
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) {
// Detach compose view to a standalone window
QStackedWidget *stack = qobject_cast<QStackedWidget*>(composeView->parentWidget());
if (stack) {
stack->removeWidget(composeView);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle("Compose - Wino Mail");
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(composeView);
composeView->setMinimumSize(800, 600);
composeView->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Compose view detached to separate window", 3000);
});
m_stack->addWidget(m_composeView);
// Page 2: Settings
m_settingsView = new SettingsView();
connect(m_settingsView, &SettingsView::accountAddRequested, [this]() {
AccountSetupDialog dlg(m_accountService, this);
dlg.exec();
});
connect(m_settingsView, &SettingsView::accountEditRequested, this, &MainMainWindow::onAccountEditRequested);
connect(m_settingsView, &SettingsView::accountDeleteRequested, this, &MainMainWindow::onAccountDeleteRequested);
connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) {
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
m_stack->addWidget(m_settingsView);
// Page 3: Contacts
m_contactsView = new ContactsView();
m_stack->addWidget(m_contactsView);
// Page 4: Calendar
m_calendarView = new CalendarView();
m_stack->addWidget(m_calendarView);
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
// Show mail page by default
switchToPage(PageMail);
}
void MainMainWindow::setupSidebar()
{
m_sidebar = new QListWidget();
m_sidebar->setFixedWidth(100);
m_sidebar->setIconSize(QSize(24, 24));
m_sidebar->setSpacing(4);
m_sidebar->setFrameShape(QFrame::NoFrame);
m_sidebar->setStyleSheet(
"QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }"
"QListWidget::item { color: #8e8e93; padding: 12px 0; text-align: center; font-size: 10px; border: none; border-radius: 8px; margin: 2px 8px; }"
"QListWidget::item:selected { background-color: #3a3a3c; color: #ffffff; }"
"QListWidget::item:hover { background-color: #3a3a3c; color: #ffffff; }"
);
m_sidebar->addItem("📧\nMail");
m_sidebar->addItem("✏️\nCompose");
m_sidebar->addItem("⚙️\nSettings");
m_sidebar->addItem("👥\nContacts");
m_sidebar->addItem("📅\nCalendar");
m_sidebar->setCurrentRow(0);
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged);
}
void MainMainWindow::setupMailPage()
{
m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
m_folderSplitter = new QSplitter(Qt::Horizontal);
m_folderSplitter->setHandleWidth(1);
// Folder tree
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
m_folderTree->setIndentation(20);
m_folderTree->setMinimumWidth(220);
m_folderTree->setMaximumWidth(350);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderSplitter->addWidget(m_folderTree);
// Mail list (QTableView with sorting)
m_mailListView = new MailListView();
connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected);
connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested);
connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) {
openMailInIndependentWindow(mailId);
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
}
void MainMainWindow::connectModels()
{
m_accountService = new AccountService(this);
m_mailService = new MailService(m_accountService, this);
m_composeView->setAccountService(m_accountService);
m_folderModel = new FolderListModel(m_accountService, this);
m_emailModel = new EmailListModel(this);
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
m_folderTree->expandAll();
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
}
void MainMainWindow::onNavChanged(int index)
{
switchToPage(static_cast<Page>(index));
}
void MainMainWindow::switchToPage(int pageIndex)
{
m_stack->setCurrentIndex(pageIndex);
m_sidebar->blockSignals(true);
m_sidebar->setCurrentRow(pageIndex);
m_sidebar->blockSignals(false);
// Show/hide toolbar actions per page
}
void MainMainWindow::onFolderSelected(const QModelIndex &index)
{
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
m_emailModel->setFolderId(m_currentFolderId);
// Fetch mails for this folder
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
m_mailService->fetchMails(QString::number(account->id()), QString::number(m_currentFolderId));
delete account;
}
}
m_emailModel->refresh();
}
}
void MainMainWindow::onEmailSelected(int mailId)
{
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested()
{
switchToPage(PageCompose);
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item)
{
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
}
switchToPage(PageCompose);
}
void MainMainWindow::onNewMessage()
{
switchToPage(PageCompose);
}
void MainMainWindow::openMailInIndependentWindow(int mailId)
{
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) return;
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject()));
ReaderView *detachedReader = new ReaderView();
detachedReader->setMailItem(&item.value());
detachedWin->setCentralWidget(detachedReader);
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Opened mail in independent window", 3000);
}
void MainMainWindow::createToolBar()
{
m_toolBar = addToolBar("Main Toolbar");
m_toolBar->setMovable(false);
QAction *newMsgAction = m_toolBar->addAction("✉ New Message");
m_toolBar->addSeparator();
QAction *syncAction = m_toolBar->addAction("⟳ Sync/Refresh");
QAction *openWinAction = m_toolBar->addAction("↗ Abrir en ventana");
QAction *deleteAction = m_toolBar->addAction("🗑 Delete");
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
}
});
connect(openWinAction, &QAction::triggered, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
} else {
statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000);
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
});
}
void MainMainWindow::onProgressChanged(int percent)
{
if (percent < 0 || percent > 100) {
m_progressBar->setVisible(false);
return;
}
m_progressBar->setValue(percent);
m_progressBar->setVisible(true);
}
void MainMainWindow::onStatusMessage(const QString &msg)
{
// Show temporary message in status bar (timeout 5000 ms)
statusBar()->showMessage(msg, 5000);
}
void MainMainWindow::onAddAccountRequested()
{
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->setWindowFlag(Qt::Window, true); // make it an independent window
dialog->show();
}
void MainMainWindow::onAccountEditRequested(int accountId)
{
QMessageBox::information(this, "Debug", QString("Edit account slot called for ID %1").arg(accountId));
Account* account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage(QString("Error: account %1 not found").arg(accountId), 3000);
return;
}
AccountSetupDialog dlg(m_accountService, this);
dlg.loadAccountForEditing(*account);
dlg.exec();
delete account;
}
void MainMainWindow::onAccountDeleteRequested(int accountId)
{
Q_UNUSED(accountId);
statusBar()->showMessage(QString("Delete account %1 requested").arg(accountId), 3000);
}
+390
View File
@@ -0,0 +1,390 @@
#include "mainmainwindow.h"
#include "core/models/account.h"
#include "core/mailitem.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
#include <optional>
#include <QMessageBox>
#include "ui/accountsetupdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QDateTime>
#include <QLabel>
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
setupUI();
connectModels();
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
// Setup progress bar in status bar
m_progressBar = new QProgressBar(this);
m_progressBar->setMaximumWidth(120);
m_progressBar->setVisible(false);
statusBar()->addPermanentWidget(m_progressBar);
// Connect mail service progress/status signals
connect(m_mailService, &MailService::progressChanged, this, &MainMainWindow::onProgressChanged);
connect(m_mailService, &MailService::statusMessage, this, &MainMainWindow::onStatusMessage);
// Provide account service to settings view
m_settingsView->setAccountService(m_accountService);
}
void MainMainWindow::setupUI()
{
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
"QSplitter::handle { background-color: #d1d1d6; width: 1px; }"
"QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
"QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }"
);
createToolBar();
// === Central widget ===
QWidget *central = new QWidget();
QHBoxLayout *centralLayout = new QHBoxLayout(central);
centralLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0);
// === Sidebar ===
setupSidebar();
centralLayout->addWidget(m_sidebar);
// === Separator line ===
QFrame *separator = new QFrame();
separator->setFrameShape(QFrame::VLine);
separator->setStyleSheet("color: #d1d1d6;");
centralLayout->addWidget(separator);
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime, const QString &fromAddr) {
QString msg;
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) {
// Detach compose view to a standalone window
QStackedWidget *stack = qobject_cast<QStackedWidget*>(composeView->parentWidget());
if (stack) {
stack->removeWidget(composeView);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle("Compose - Wino Mail");
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(composeView);
composeView->setMinimumSize(800, 600);
composeView->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Compose view detached to separate window", 3000);
});
m_stack->addWidget(m_composeView);
// Page 2: Settings
m_settingsView = new SettingsView();
connect(m_settingsView, &SettingsView::accountAddRequested, [this]() {
AccountSetupDialog dlg(m_accountService, this);
dlg.exec();
});
connect(m_settingsView, &SettingsView::accountEditRequested, this, &MainMainWindow::onAccountEditRequested);
connect(m_settingsView, &SettingsView::accountDeleteRequested, this, &MainMainWindow::onAccountDeleteRequested);
connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) {
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
m_stack->addWidget(m_settingsView);
// Page 3: Contacts
m_contactsView = new ContactsView();
m_stack->addWidget(m_contactsView);
// Page 4: Calendar
m_calendarView = new CalendarView();
m_stack->addWidget(m_calendarView);
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
// Show mail page by default
switchToPage(PageMail);
}
void MainMainWindow::setupSidebar()
{
m_sidebar = new QListWidget();
m_sidebar->setFixedWidth(100);
m_sidebar->setIconSize(QSize(24, 24));
m_sidebar->setSpacing(4);
m_sidebar->setFrameShape(QFrame::NoFrame);
m_sidebar->setStyleSheet(
"QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }"
"QListWidget::item { color: #8e8e93; padding: 12px 0; text-align: center; font-size: 10px; border: none; border-radius: 8px; margin: 2px 8px; }"
"QListWidget::item:selected { background-color: #3a3a3c; color: #ffffff; }"
"QListWidget::item:hover { background-color: #3a3a3c; color: #ffffff; }"
);
m_sidebar->addItem("📧\nMail");
m_sidebar->addItem("✏️\nCompose");
m_sidebar->addItem("⚙️\nSettings");
m_sidebar->addItem("👥\nContacts");
m_sidebar->addItem("📅\nCalendar");
m_sidebar->setCurrentRow(0);
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged);
}
void MainMainWindow::setupMailPage()
{
m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
m_folderSplitter = new QSplitter(Qt::Horizontal);
m_folderSplitter->setHandleWidth(1);
// Folder tree
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
m_folderTree->setIndentation(20);
m_folderTree->setMinimumWidth(220);
m_folderTree->setMaximumWidth(350);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderSplitter->addWidget(m_folderTree);
// Mail list (QTableView with sorting)
m_mailListView = new MailListView();
connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected);
connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested);
connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) {
openMailInIndependentWindow(mailId);
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
}
void MainMainWindow::connectModels()
{
m_accountService = new AccountService(this);
m_mailService = new MailService(m_accountService, this);
m_composeView->setAccountService(m_accountService);
m_folderModel = new FolderListModel(m_accountService, this);
m_emailModel = new EmailListModel(this);
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
m_folderTree->expandAll();
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
}
void MainMainWindow::onNavChanged(int index)
{
switchToPage(static_cast<Page>(index));
}
void MainMainWindow::switchToPage(int pageIndex)
{
m_stack->setCurrentIndex(pageIndex);
m_sidebar->blockSignals(true);
m_sidebar->setCurrentRow(pageIndex);
m_sidebar->blockSignals(false);
// Show/hide toolbar actions per page
}
void MainMainWindow::onFolderSelected(const QModelIndex &index)
{
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
m_emailModel->setFolderId(m_currentFolderId);
// Fetch mails for this folder
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
m_mailService->fetchMails(QString::number(account->id()), QString::number(m_currentFolderId));
delete account;
}
}
m_emailModel->refresh();
}
}
void MainMainWindow::onEmailSelected(int mailId)
{
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested()
{
switchToPage(PageCompose);
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item)
{
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
}
switchToPage(PageCompose);
}
void MainMainWindow::onNewMessage()
{
switchToPage(PageCompose);
}
void MainMainWindow::openMailInIndependentWindow(int mailId)
{
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) return;
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject()));
ReaderView *detachedReader = new ReaderView();
detachedReader->setMailItem(&item.value());
detachedWin->setCentralWidget(detachedReader);
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Opened mail in independent window", 3000);
}
void MainMainWindow::createToolBar()
{
m_toolBar = addToolBar("Main Toolbar");
m_toolBar->setMovable(false);
QAction *newMsgAction = m_toolBar->addAction("✉ New Message");
m_toolBar->addSeparator();
QAction *syncAction = m_toolBar->addAction("⟳ Sync/Refresh");
QAction *openWinAction = m_toolBar->addAction("↗ Abrir en ventana");
QAction *deleteAction = m_toolBar->addAction("🗑 Delete");
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
}
});
connect(openWinAction, &QAction::triggered, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
} else {
statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000);
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
});
}
void MainMainWindow::onProgressChanged(int percent)
{
if (percent < 0 || percent > 100) {
m_progressBar->setVisible(false);
return;
}
m_progressBar->setValue(percent);
m_progressBar->setVisible(true);
}
void MainMainWindow::onStatusMessage(const QString &msg)
{
// Show temporary message in status bar (timeout 5000 ms)
statusBar()->showMessage(msg, 5000);
}
void MainMainWindow::onAddAccountRequested()
{
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->setWindowFlag(Qt::Window, true); // make it an independent window
dialog->show();
}
void MainMainWindow::onAccountEditRequested(int accountId)
{
QMessageBox::information(this, "Debug", QString("Edit account slot called for ID %1").arg(accountId));
Account* account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage(QString("Error: account %1 not found").arg(accountId), 3000);
return;
}
AccountSetupDialog dlg(m_accountService, this);
dlg.loadAccountForEditing(*account);
dlg.exec();
delete account;
}
void MainMainWindow::onAccountDeleteRequested(int accountId)
{
Q_UNUSED(accountId);
statusBar()->showMessage(QString("Delete account %1 requested").arg(accountId), 3000);
}
+390
View File
@@ -0,0 +1,390 @@
#include "mainmainwindow.h"
#include "core/models/account.h"
#include "core/mailitem.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
#include <optional>
#include <QMessageBox>
#include "ui/accountsetupdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QDateTime>
#include <QLabel>
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
setupUI();
connectModels();
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
// Setup progress bar in status bar
m_progressBar = new QProgressBar(this);
m_progressBar->setMaximumWidth(120);
m_progressBar->setVisible(false);
statusBar()->addPermanentWidget(m_progressBar);
// Connect mail service progress/status signals
connect(m_mailService, &MailService::progressChanged, this, &MainMainWindow::onProgressChanged);
connect(m_mailService, &MailService::statusMessage, this, &MainMainWindow::onStatusMessage);
// Provide account service to settings view
m_settingsView->setAccountService(m_accountService);
}
void MainMainWindow::setupUI()
{
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
"QSplitter::handle { background-color: #d1d1d6; width: 1px; }"
"QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
"QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }"
);
createToolBar();
// === Central widget ===
QWidget *central = new QWidget();
QHBoxLayout *centralLayout = new QHBoxLayout(central);
centralLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0);
// === Sidebar ===
setupSidebar();
centralLayout->addWidget(m_sidebar);
// === Separator line ===
QFrame *separator = new QFrame();
separator->setFrameShape(QFrame::VLine);
separator->setStyleSheet("color: #d1d1d6;");
centralLayout->addWidget(separator);
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime, const QString &fromAddr) {
QString msg;
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) {
// Detach compose view to a standalone window
QStackedWidget *stack = qobject_cast<QStackedWidget*>(composeView->parentWidget());
if (stack) {
stack->removeWidget(composeView);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle("Compose - Wino Mail");
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(composeView);
composeView->setMinimumSize(800, 600);
composeView->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Compose view detached to separate window", 3000);
});
m_stack->addWidget(m_composeView);
// Page 2: Settings
m_settingsView = new SettingsView();
connect(m_settingsView, &SettingsView::accountAddRequested, [this]() {
AccountSetupDialog dlg(m_accountService, this);
dlg.exec();
});
connect(m_settingsView, &SettingsView::accountEditRequested, this, &MainMainWindow::onAccountEditRequested);
connect(m_settingsView, &SettingsView::accountDeleteRequested, this, &MainMainWindow::onAccountDeleteRequested);
connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) {
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
m_stack->addWidget(m_settingsView);
// Page 3: Contacts
m_contactsView = new ContactsView();
m_stack->addWidget(m_contactsView);
// Page 4: Calendar
m_calendarView = new CalendarView();
m_stack->addWidget(m_calendarView);
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
// Show mail page by default
switchToPage(PageMail);
}
void MainMainWindow::setupSidebar()
{
m_sidebar = new QListWidget();
m_sidebar->setFixedWidth(100);
m_sidebar->setIconSize(QSize(24, 24));
m_sidebar->setSpacing(4);
m_sidebar->setFrameShape(QFrame::NoFrame);
m_sidebar->setStyleSheet(
"QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }"
"QListWidget::item { color: #8e8e93; padding: 12px 0; text-align: center; font-size: 10px; border: none; border-radius: 8px; margin: 2px 8px; }"
"QListWidget::item:selected { background-color: #3a3a3c; color: #ffffff; }"
"QListWidget::item:hover { background-color: #3a3a3c; color: #ffffff; }"
);
m_sidebar->addItem("📧\nMail");
m_sidebar->addItem("✏️\nCompose");
m_sidebar->addItem("⚙️\nSettings");
m_sidebar->addItem("👥\nContacts");
m_sidebar->addItem("📅\nCalendar");
m_sidebar->setCurrentRow(0);
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged);
}
void MainMainWindow::setupMailPage()
{
m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
m_folderSplitter = new QSplitter(Qt::Horizontal);
m_folderSplitter->setHandleWidth(1);
// Folder tree
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
m_folderTree->setIndentation(20);
m_folderTree->setMinimumWidth(220);
m_folderTree->setMaximumWidth(350);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderSplitter->addWidget(m_folderTree);
// Mail list (QTableView with sorting)
m_mailListView = new MailListView();
connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected);
connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested);
connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) {
openMailInIndependentWindow(mailId);
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
}
void MainMainWindow::connectModels()
{
m_accountService = new AccountService(this);
m_mailService = new MailService(m_accountService, this);
m_composeView->setAccountService(m_accountService);
m_folderModel = new FolderListModel(m_accountService, this);
m_emailModel = new EmailListModel(this);
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
m_folderTree->expandAll();
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
}
void MainMainWindow::onNavChanged(int index)
{
switchToPage(static_cast<Page>(index));
}
void MainMainWindow::switchToPage(int pageIndex)
{
m_stack->setCurrentIndex(pageIndex);
m_sidebar->blockSignals(true);
m_sidebar->setCurrentRow(pageIndex);
m_sidebar->blockSignals(false);
// Show/hide toolbar actions per page
}
void MainMainWindow::onFolderSelected(const QModelIndex &index)
{
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
m_emailModel->setFolderId(m_currentFolderId);
// Fetch mails for this folder
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
m_mailService->fetchMails(QString::number(account->id()), QString::number(m_currentFolderId));
delete account;
}
}
m_emailModel->refresh();
}
}
void MainMainWindow::onEmailSelected(int mailId)
{
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested()
{
switchToPage(PageCompose);
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item)
{
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
}
switchToPage(PageCompose);
}
void MainMainWindow::onNewMessage()
{
switchToPage(PageCompose);
}
void MainMainWindow::openMailInIndependentWindow(int mailId)
{
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) return;
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject()));
ReaderView *detachedReader = new ReaderView();
detachedReader->setMailItem(&item.value());
detachedWin->setCentralWidget(detachedReader);
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Opened mail in independent window", 3000);
}
void MainMainWindow::createToolBar()
{
m_toolBar = addToolBar("Main Toolbar");
m_toolBar->setMovable(false);
QAction *newMsgAction = m_toolBar->addAction("✉ New Message");
m_toolBar->addSeparator();
QAction *syncAction = m_toolBar->addAction("⟳ Sync/Refresh");
QAction *openWinAction = m_toolBar->addAction("↗ Abrir en ventana");
QAction *deleteAction = m_toolBar->addAction("🗑 Delete");
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
}
});
connect(openWinAction, &QAction::triggered, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
} else {
statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000);
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
});
}
void MainMainWindow::onProgressChanged(int percent)
{
if (percent < 0 || percent > 100) {
m_progressBar->setVisible(false);
return;
}
m_progressBar->setValue(percent);
m_progressBar->setVisible(true);
}
void MainMainWindow::onStatusMessage(const QString &msg)
{
// Show temporary message in status bar (timeout 5000 ms)
statusBar()->showMessage(msg, 5000);
}
void MainMainWindow::onAddAccountRequested()
{
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->setWindowFlag(Qt::Window, true); // make it an independent window
dialog->show();
}
void MainMainWindow::onAccountEditRequested(int accountId)
{
QMessageBox::information(this, "Debug", QString("Edit account slot called for ID %1").arg(accountId));
Account* account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage(QString("Error: account %1 not found").arg(accountId), 3000);
return;
}
AccountSetupDialog dlg(m_accountService, this);
dlg.loadAccountForEditing(*account);
dlg.exec();
delete account;
}
void MainMainWindow::onAccountDeleteRequested(int accountId)
{
Q_UNUSED(accountId);
statusBar()->showMessage(QString("Delete account %1 requested").arg(accountId), 3000);
}
+366
View File
@@ -0,0 +1,366 @@
#include "mainmainwindow.h"
#include "core/models/account.h"
#include "core/mailitem.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
#include <optional>
#include <QMessageBox>
#include "ui/accountsetupdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QDateTime>
#include <QLabel>
#include <QMetaObject>
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
setupUI();
connectModels();
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
}
void MainMainWindow::setupUI() {
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
"QSplitter::handle { background-color: #d1d1d6; width: 1px; }"
"QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
"QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }"
);
createToolBar();
// === Central widget ===
QWidget *central = new QWidget();
QHBoxLayout *centralLayout = new QHBoxLayout(central);
centralLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0);
// === Sidebar ===
setupSidebar();
centralLayout->addWidget(m_sidebar);
// === Separator line ===
QFrame *separator = new QFrame();
separator->setFrameShape(QFrame::VLine);
separator->setStyleSheet("color: #d1d1d6;");
centralLayout->addWidget(separator);
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime) {
QString msg;
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) {
// Detach compose view to a standalone window
QStackedWidget *stack = qobject_cast<QStackedWidget*>(composeView->parentWidget());
if (stack) {
stack->removeWidget(composeView);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle("Compose - Wino Mail");
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(composeView);
composeView->setMinimumSize(800, 600);
composeView->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Compose view detached to separate window", 3000);
});
m_stack->addWidget(m_composeView);
// Page 2: Settings
m_settingsView = new SettingsView();
connect(m_settingsView, &SettingsView::accountAddRequested, [this]() {
AccountSetupDialog dlg(m_accountService, this);
dlg.exec();
});
connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) {
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
m_stack->addWidget(m_settingsView);
// Page 3: Contacts
m_contactsView = new ContactsView();
m_stack->addWidget(m_contactsView);
// Page 4: Calendar
m_calendarView = new CalendarView();
m_stack->addWidget(m_calendarView);
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
// Show mail page by default
switchToPage(PageMail);
}
void MainMainWindow::setupSidebar() {
m_sidebar = new QListWidget();
m_sidebar->setFixedWidth(64);
m_sidebar->setIconSize(QSize(24, 24));
m_sidebar->setSpacing(4);
m_sidebar->setFrameShape(QFrame::NoFrame);
m_sidebar->setStyleSheet(
"QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }"
"QListWidget::item { color: #8e8e93; padding: 12px 0; text-align: center; font-size: 10px; border: none; border-radius: 8px; margin: 2px 8px; }"
"QListWidget::item:selected { background-color: #3a3a3c; color: #ffffff; }"
"QListWidget::item:hover { background-color: #3a3a3c; color: #ffffff; }"
);
m_sidebar->addItem("📧\nMail");
m_sidebar->addItem("✏️\nCompose");
m_sidebar->addItem("⚙️\nSettings");
m_sidebar->addItem("👥\nContacts");
m_sidebar->addItem("📅\nCalendar");
m_sidebar->setCurrentRow(0);
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged);
}
void MainMainWindow::setupMailPage() {
m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
m_folderSplitter = new QSplitter(Qt::Horizontal);
m_folderSplitter->setHandleWidth(1);
// Folder tree
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
m_folderTree->setIndentation(20);
m_folderTree->setMinimumWidth(220);
m_folderTree->setMaximumWidth(350);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderSplitter->addWidget(m_folderTree);
// Mail list (QTableView with sorting)
m_mailListView = new MailListView();
connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected);
connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested);
connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) {
openMailInIndependentWindow(mailId);
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
}
void MainMainWindow::connectModels() {
m_accountService = new AccountService(this);
m_mailService = new MailService(this);
m_folderModel = new FolderListModel(m_accountService, this);
m_emailModel = new EmailListModel(this);
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
m_folderTree->expandAll();
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
}
void MainMainWindow::onNavChanged(int index) {
switchToPage(static_cast<Page>(index));
}
void MainMainWindow::switchToPage(int pageIndex) {
m_stack->setCurrentIndex(pageIndex);
m_sidebar->blockSignals(true);
m_sidebar->setCurrentRow(pageIndex);
m_sidebar->blockSignals(false);
// Show/hide toolbar actions per page
}
void MainMainWindow::onFolderSelected(const QModelIndex &index) {
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
m_emailModel->setFolderId(m_currentFolderId);
// Fetch mails for this folder
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
QString accountId = QString::number(account->id());
QString folderId = QString::number(m_currentFolderId);
// Fetch mails asynchronously to avoid blocking UI
QMetaObject::invokeMethod(m_mailService, "fetchMails", Qt::QueuedConnection,
Q_ARG(QString, accountId), Q_ARG(QString, folderId));
delete account;
}
}
m_emailModel->refresh();
}
}
void MainMainWindow::onEmailSelected(int mailId) {
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested() {
switchToPage(PageCompose);
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item) {
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
}
switchToPage(PageCompose);
}
void MainMainWindow::onNewMessage() {
switchToPage(PageCompose);
}
void MainMainWindow::openMailInIndependentWindow(int mailId) {
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) return;
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject()));
ReaderView *detachedReader = new ReaderView();
detachedReader->setMailItem(&item.value());
detachedWin->setCentralWidget(detachedReader);
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Opened mail in independent window", 3000);
}
void MainMainWindow::onAddAccountRequested() {
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString("Account added: %1").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list to show new account's folders
});
dialog->open();
}
void MainMainWindow::onAccountEditRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage("Account not found", 3000);
return;
}
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->loadAccountForEditing(*account);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString("Account updated: %1").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list
});
dialog->open();
}
void MainMainWindow::onAccountDeleteRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage("Account not found", 3000);
return;
}
if (QMessageBox::warning(this, "Delete Account",
QString("Are you sure you want to delete the account '%1'?").arg(account->email()),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
m_accountService->removeAccount(accountId);
statusBar()->showMessage(QString("Account deleted: %1").arg(account->email()), 3000);
m_folderModel->refresh(); // Refresh folder list to remove deleted account's folders
}
}
void MainMainWindow::createToolBar() {
m_toolBar = addToolBar("Main Toolbar");
m_toolBar->setMovable(false);
QAction *newMsgAction = m_toolBar->addAction("✉ New Message");
m_toolBar->addSeparator();
QAction *syncAction = m_toolBar->addAction("⟳ Sync/Refresh");
QAction *openWinAction = m_toolBar->addAction("↗ Abrir en ventana");
QAction *deleteAction = m_toolBar->addAction("🗑 Delete");
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
}
});
connect(openWinAction, &QAction::triggered, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
} else {
statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000);
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
});
}
+377
View File
@@ -0,0 +1,377 @@
#include "mainmainwindow.h"
#include "core/models/account.h"
#include "core/mailitem.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
#include <optional>
#include <QMessageBox>
#include "ui/accountsetupdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QDateTime>
#include <QLabel>
#include <QMetaObject>
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
setupUI();
connectModels();
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
}
void MainMainWindow::setupUI() {
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
"QSplitter::handle { background-color: #d1d1d6; width: 1px; }"
"QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
"QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }"
);
createToolBar();
// === Central widget ===
QWidget *central = new QWidget();
QHBoxLayout *centralLayout = new QHBoxLayout(central);
centralLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0);
// === Sidebar ===
setupSidebar();
centralLayout->addWidget(m_sidebar);
// === Separator line ===
QFrame *separator = new QFrame();
separator->setFrameShape(QFrame::VLine);
separator->setStyleSheet("color: #d1d1d6;");
centralLayout->addWidget(separator);
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime) {
QString msg;
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) {
// Detach compose view to a standalone window
QStackedWidget *stack = qobject_cast<QStackedWidget*>(composeView->parentWidget());
if (stack) {
stack->removeWidget(composeView);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle("Compose - Wino Mail");
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(composeView);
composeView->setMinimumSize(800, 600);
composeView->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Compose view detached to separate window", 3000);
});
m_stack->addWidget(m_composeView);
// Page 2: Settings
m_settingsView = new SettingsView();
connect(m_settingsView, &SettingsView::accountAddRequested, [this]() {
AccountSetupDialog dlg(m_accountService, this);
dlg.exec();
});
connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) {
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
m_stack->addWidget(m_settingsView);
// Page 3: Contacts
m_contactsView = new ContactsView();
m_stack->addWidget(m_contactsView);
// Page 4: Calendar
m_calendarView = new CalendarView();
m_stack->addWidget(m_calendarView);
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
// Show mail page by default
switchToPage(PageMail);
}
void MainMainWindow::setupSidebar() {
m_sidebar = new QListWidget();
m_sidebar->setFixedWidth(64);
m_sidebar->setIconSize(QSize(24, 24));
m_sidebar->setSpacing(4);
m_sidebar->setFrameShape(QFrame::NoFrame);
m_sidebar->setStyleSheet(
"QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }"
"QListWidget::item { color: #8e8e93; padding: 12px 0; text-align: center; font-size: 10px; border: none; border-radius: 8px; margin: 2px 8px; }"
"QListWidget::item:selected { background-color: #3a3a3c; color: #ffffff; }"
"QListWidget::item:hover { background-color: #3a3a3c; color: #ffffff; }"
);
m_sidebar->addItem("📧\nMail");
m_sidebar->addItem("✏️\nCompose");
m_sidebar->addItem("⚙️\nSettings");
m_sidebar->addItem("👥\nContacts");
m_sidebar->addItem("📅\nCalendar");
m_sidebar->setCurrentRow(0);
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged);
}
void MainMainWindow::setupMailPage() {
m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
m_folderSplitter = new QSplitter(Qt::Horizontal);
m_folderSplitter->setHandleWidth(1);
// Folder tree
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
m_folderTree->setIndentation(20);
m_folderTree->setMinimumWidth(220);
m_folderTree->setMaximumWidth(350);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderSplitter->addWidget(m_folderTree);
// Mail list (QTableView with sorting)
m_mailListView = new MailListView();
connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected);
connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested);
connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) {
openMailInIndependentWindow(mailId);
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
}
void MainMainWindow::connectModels() {
m_accountService = new AccountService(this);
m_mailService = new MailService(this);
m_folderModel = new FolderListModel(m_accountService, this);
m_emailModel = new EmailListModel(this);
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
m_folderTree->expandAll();
// Auto-select first folder to trigger mail fetch
if (m_folderModel->rowCount() > 0) {
QModelIndex firstIdx = m_folderModel->index(0, 0);
if (firstIdx.isValid()) {
int itemType = firstIdx.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_folderTree->setCurrentIndex(firstIdx);
onFolderSelected(firstIdx);
}
}
}
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
}
void MainMainWindow::onNavChanged(int index) {
switchToPage(static_cast<Page>(index));
}
void MainMainWindow::switchToPage(int pageIndex) {
m_stack->setCurrentIndex(pageIndex);
m_sidebar->blockSignals(true);
m_sidebar->setCurrentRow(pageIndex);
m_sidebar->blockSignals(false);
// Show/hide toolbar actions per page
}
void MainMainWindow::onFolderSelected(const QModelIndex &index) {
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
m_emailModel->setFolderId(m_currentFolderId);
// Fetch mails for this folder
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
QString accountId = QString::number(account->id());
QString folderId = QString::number(m_currentFolderId);
// Fetch mails asynchronously to avoid blocking UI
QMetaObject::invokeMethod(m_mailService, "fetchMails", Qt::QueuedConnection,
Q_ARG(QString, accountId), Q_ARG(QString, folderId));
delete account;
}
}
m_emailModel->refresh();
}
}
void MainMainWindow::onEmailSelected(int mailId) {
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested() {
switchToPage(PageCompose);
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item) {
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
}
switchToPage(PageCompose);
}
void MainMainWindow::onNewMessage() {
switchToPage(PageCompose);
}
void MainMainWindow::openMailInIndependentWindow(int mailId) {
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) return;
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject()));
ReaderView *detachedReader = new ReaderView();
detachedReader->setMailItem(&item.value());
detachedWin->setCentralWidget(detachedReader);
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Opened mail in independent window", 3000);
}
void MainMainWindow::onAddAccountRequested() {
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString("Account added: %1").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list to show new account's folders
});
dialog->open();
}
void MainMainWindow::onAccountEditRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage("Account not found", 3000);
return;
}
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->loadAccountForEditing(*account);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString("Account updated: %1").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list
});
dialog->open();
}
void MainMainWindow::onAccountDeleteRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage("Account not found", 3000);
return;
}
if (QMessageBox::warning(this, "Delete Account",
QString("Are you sure you want to delete the account '%1'?").arg(account->email()),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
m_accountService->removeAccount(accountId);
statusBar()->showMessage(QString("Account deleted: %1").arg(account->email()), 3000);
m_folderModel->refresh(); // Refresh folder list to remove deleted account's folders
}
}
void MainMainWindow::createToolBar() {
m_toolBar = addToolBar("Main Toolbar");
m_toolBar->setMovable(false);
QAction *newMsgAction = m_toolBar->addAction("✉ New Message");
m_toolBar->addSeparator();
QAction *syncAction = m_toolBar->addAction("⟳ Sync/Refresh");
QAction *openWinAction = m_toolBar->addAction("↗ Abrir en ventana");
QAction *deleteAction = m_toolBar->addAction("🗑 Delete");
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
}
});
connect(openWinAction, &QAction::triggered, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
} else {
statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000);
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
});
}
+390
View File
@@ -0,0 +1,390 @@
#include "mainmainwindow.h"
#include "core/models/account.h"
#include "core/mailitem.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
#include <optional>
#include <QMessageBox>
#include "ui/accountsetupdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QDateTime>
#include <QLabel>
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
setupUI();
connectModels();
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
// Setup progress bar in status bar
m_progressBar = new QProgressBar(this);
m_progressBar->setMaximumWidth(120);
m_progressBar->setVisible(false);
statusBar()->addPermanentWidget(m_progressBar);
// Connect mail service progress/status signals
connect(m_mailService, &MailService::progressChanged, this, &MainMainWindow::onProgressChanged);
connect(m_mailService, &MailService::statusMessage, this, &MainMainWindow::onStatusMessage);
// Provide account service to settings view
m_settingsView->setAccountService(m_accountService);
}
void MainMainWindow::setupUI()
{
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
"QSplitter::handle { background-color: #d1d1d6; width: 1px; }"
"QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
"QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }"
);
createToolBar();
// === Central widget ===
QWidget *central = new QWidget();
QHBoxLayout *centralLayout = new QHBoxLayout(central);
centralLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0);
// === Sidebar ===
setupSidebar();
centralLayout->addWidget(m_sidebar);
// === Separator line ===
QFrame *separator = new QFrame();
separator->setFrameShape(QFrame::VLine);
separator->setStyleSheet("color: #d1d1d6;");
centralLayout->addWidget(separator);
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime, const QString &fromAddr) {
QString msg;
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) {
// Detach compose view to a standalone window
QStackedWidget *stack = qobject_cast<QStackedWidget*>(composeView->parentWidget());
if (stack) {
stack->removeWidget(composeView);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle("Compose - Wino Mail");
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(composeView);
composeView->setMinimumSize(800, 600);
composeView->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Compose view detached to separate window", 3000);
});
m_stack->addWidget(m_composeView);
// Page 2: Settings
m_settingsView = new SettingsView();
connect(m_settingsView, &SettingsView::accountAddRequested, [this]() {
AccountSetupDialog dlg(m_accountService, this);
dlg.exec();
});
connect(m_settingsView, &SettingsView::accountEditRequested, this, &MainMainWindow::onAccountEditRequested);
connect(m_settingsView, &SettingsView::accountDeleteRequested, this, &MainMainWindow::onAccountDeleteRequested);
connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) {
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
m_stack->addWidget(m_settingsView);
// Page 3: Contacts
m_contactsView = new ContactsView();
m_stack->addWidget(m_contactsView);
// Page 4: Calendar
m_calendarView = new CalendarView();
m_stack->addWidget(m_calendarView);
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
// Show mail page by default
switchToPage(PageMail);
}
void MainMainWindow::setupSidebar()
{
m_sidebar = new QListWidget();
m_sidebar->setFixedWidth(100);
m_sidebar->setIconSize(QSize(24, 24));
m_sidebar->setSpacing(4);
m_sidebar->setFrameShape(QFrame::NoFrame);
m_sidebar->setStyleSheet(
"QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }"
"QListWidget::item { color: #8e8e93; padding: 12px 0; text-align: center; font-size: 10px; border: none; border-radius: 8px; margin: 2px 8px; }"
"QListWidget::item:selected { background-color: #3a3a3c; color: #ffffff; }"
"QListWidget::item:hover { background-color: #3a3a3c; color: #ffffff; }"
);
m_sidebar->addItem("📧\nMail");
m_sidebar->addItem("✏️\nCompose");
m_sidebar->addItem("⚙️\nSettings");
m_sidebar->addItem("👥\nContacts");
m_sidebar->addItem("📅\nCalendar");
m_sidebar->setCurrentRow(0);
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged);
}
void MainMainWindow::setupMailPage()
{
m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
m_folderSplitter = new QSplitter(Qt::Horizontal);
m_folderSplitter->setHandleWidth(1);
// Folder tree
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
m_folderTree->setIndentation(20);
m_folderTree->setMinimumWidth(220);
m_folderTree->setMaximumWidth(350);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderSplitter->addWidget(m_folderTree);
// Mail list (QTableView with sorting)
m_mailListView = new MailListView();
connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected);
connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested);
connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) {
openMailInIndependentWindow(mailId);
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
}
void MainMainWindow::connectModels()
{
m_accountService = new AccountService(this);
m_mailService = new MailService(m_accountService, this);
m_composeView->setAccountService(m_accountService);
m_folderModel = new FolderListModel(m_accountService, this);
m_emailModel = new EmailListModel(this);
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
m_folderTree->expandAll();
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
}
void MainMainWindow::onNavChanged(int index)
{
switchToPage(static_cast<Page>(index));
}
void MainMainWindow::switchToPage(int pageIndex)
{
m_stack->setCurrentIndex(pageIndex);
m_sidebar->blockSignals(true);
m_sidebar->setCurrentRow(pageIndex);
m_sidebar->blockSignals(false);
// Show/hide toolbar actions per page
}
void MainMainWindow::onFolderSelected(const QModelIndex &index)
{
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
m_emailModel->setFolderId(m_currentFolderId);
// Fetch mails for this folder
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
m_mailService->fetchMails(QString::number(account->id()), QString::number(m_currentFolderId));
delete account;
}
}
m_emailModel->refresh();
}
}
void MainMainWindow::onEmailSelected(int mailId)
{
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested()
{
switchToPage(PageCompose);
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item)
{
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
}
switchToPage(PageCompose);
}
void MainMainWindow::onNewMessage()
{
switchToPage(PageCompose);
}
void MainMainWindow::openMailInIndependentWindow(int mailId)
{
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) return;
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject()));
ReaderView *detachedReader = new ReaderView();
detachedReader->setMailItem(&item.value());
detachedWin->setCentralWidget(detachedReader);
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Opened mail in independent window", 3000);
}
void MainMainWindow::createToolBar()
{
m_toolBar = addToolBar("Main Toolbar");
m_toolBar->setMovable(false);
QAction *newMsgAction = m_toolBar->addAction("✉ New Message");
m_toolBar->addSeparator();
QAction *syncAction = m_toolBar->addAction("⟳ Sync/Refresh");
QAction *openWinAction = m_toolBar->addAction("↗ Abrir en ventana");
QAction *deleteAction = m_toolBar->addAction("🗑 Delete");
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
}
});
connect(openWinAction, &QAction::triggered, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
} else {
statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000);
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
});
}
void MainMainWindow::onProgressChanged(int percent)
{
if (percent < 0 || percent > 100) {
m_progressBar->setVisible(false);
return;
}
m_progressBar->setValue(percent);
m_progressBar->setVisible(true);
}
void MainMainWindow::onStatusMessage(const QString &msg)
{
// Show temporary message in status bar (timeout 5000 ms)
statusBar()->showMessage(msg, 5000);
}
void MainMainWindow::onAddAccountRequested()
{
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->setWindowFlag(Qt::Window, true); // make it an independent window
dialog->show();
}
void MainMainWindow::onAccountEditRequested(int accountId)
{
QMessageBox::information(this, "Debug", QString("Edit account slot called for ID %1").arg(accountId));
Account* account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage(QString("Error: account %1 not found").arg(accountId), 3000);
return;
}
AccountSetupDialog dlg(m_accountService, this);
dlg.loadAccountForEditing(*account);
dlg.exec();
delete account;
}
void MainMainWindow::onAccountDeleteRequested(int accountId)
{
Q_UNUSED(accountId);
statusBar()->showMessage(QString("Delete account %1 requested").arg(accountId), 3000);
}
+373
View File
@@ -0,0 +1,373 @@
#include "mainmainwindow.h"
#include "core/models/account.h"
#include "core/mailitem.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
#include <optional>
#include <QMessageBox>
#include "ui/accountsetupdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QDateTime>
#include <QtConcurrentRun>
#include <QLabel>
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
setupUI();
connectModels();
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
}
void MainMainWindow::setupUI() {
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
"QSplitter::handle { background-color: #d1d1d6; width: 1px; }"
"QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
"QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }"
);
createToolBar();
// === Central widget ===
QWidget *central = new QWidget();
QHBoxLayout *centralLayout = new QHBoxLayout(central);
centralLayout->setContentsMargins(0, 0, 0, 0);
centralLayout->setSpacing(0);
// === Sidebar ===
setupSidebar();
centralLayout->addWidget(m_sidebar);
// === Separator line ===
QFrame *separator = new QFrame();
separator->setFrameShape(QFrame::VLine);
separator->setStyleSheet("color: #d1d1d6;");
centralLayout->addWidget(separator);
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime) {
QString msg;
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) {
// Detach compose view to a standalone window
QStackedWidget *stack = qobject_cast<QStackedWidget*>(composeView->parentWidget());
if (stack) {
stack->removeWidget(composeView);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle("Compose - Wino Mail");
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(composeView);
composeView->setMinimumSize(800, 600);
composeView->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Compose view detached to separate window", 3000);
});
m_stack->addWidget(m_composeView);
// Page 2: Settings
m_settingsView = new SettingsView();
m_settingsView->setAccountService(m_accountService);
connect(m_settingsView, &SettingsView::accountAddRequested, this, &MainMainWindow::onAddAccountRequested);
connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) {
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
connect(m_settingsView, &SettingsView::accountEditRequested, this, &MainMainWindow::onAccountEditRequested);
connect(m_settingsView, &SettingsView::accountDeleteRequested, this, &MainMainWindow::onAccountDeleteRequested);
m_stack->addWidget(m_settingsView);
// Page 3: Contacts
m_contactsView = new ContactsView();
m_stack->addWidget(m_contactsView);
// Page 4: Calendar
m_calendarView = new CalendarView();
m_stack->addWidget(m_calendarView);
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
// Show mail page by default
switchToPage(PageMail);
}
void MainMainWindow::setupSidebar() {
m_sidebar = new QListWidget();
m_sidebar->setFixedWidth(64);
m_sidebar->setIconSize(QSize(24, 24));
m_sidebar->setSpacing(4);
m_sidebar->setFrameShape(QFrame::NoFrame);
m_sidebar->setStyleSheet(
"QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }"
"QListWidget::item { color: #8e8e93; padding: 12px 0; text-align: center; font-size: 10px; border: none; border-radius: 8px; margin: 2px 8px; }"
"QListWidget::item:selected { background-color: #3a3a3c; color: #ffffff; }"
"QListWidget::item:hover { background-color: #3a3a3c; color: #ffffff; }"
);
m_sidebar->addItem("📧\nMail");
m_sidebar->addItem("✏️\nCompose");
m_sidebar->addItem("⚙️\nSettings");
m_sidebar->addItem("👥\nContacts");
m_sidebar->addItem("📅\nCalendar");
m_sidebar->setCurrentRow(0);
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged);
}
void MainMainWindow::setupMailPage() {
m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
m_folderSplitter = new QSplitter(Qt::Horizontal);
m_folderSplitter->setHandleWidth(1);
// Folder tree
m_folderTree = new QTreeView();
m_folderTree->setHeaderHidden(true);
m_folderTree->setIndentation(20);
m_folderTree->setMinimumWidth(220);
m_folderTree->setMaximumWidth(350);
m_folderTree->setFrameShape(QFrame::NoFrame);
m_folderTree->setExpandsOnDoubleClick(true);
m_folderSplitter->addWidget(m_folderTree);
// Mail list (QTableView with sorting)
m_mailListView = new MailListView();
connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected);
connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested);
connect(m_mailListView, &MailListView::emailOpenRequested, this, [this](int mailId) {
openMailInIndependentWindow(mailId);
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
connect(m_emailViewer, &ReaderView::detachRequested, this, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
}
void MainMainWindow::connectModels() {
m_accountService = new AccountService(this);
m_mailService = new MailService(this);
m_folderModel = new FolderListModel(m_accountService, this);
m_emailModel = new EmailListModel(this);
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
m_folderTree->expandAll();
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
connect(m_mailService, &MailService::mailFetched, this, [this](const QString &accountId, const QString &folderId, const QVector<MailItem> &items) {
int fid = folderId.toInt();
if (m_currentFolderId == fid || m_currentFolderId == -1) {
m_emailModel->refresh();
}
});
}
void MainMainWindow::onNavChanged(int index) {
switchToPage(static_cast<Page>(index));
}
void MainMainWindow::switchToPage(int pageIndex) {
m_stack->setCurrentIndex(pageIndex);
m_sidebar->blockSignals(true);
m_sidebar->setCurrentRow(pageIndex);
m_sidebar->blockSignals(false);
// Show/hide toolbar actions per page
}
void MainMainWindow::onFolderSelected(const QModelIndex &index) {
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
m_emailModel->setFolderId(m_currentFolderId);
// Fetch mails for this folder
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
QString accountId = QString::number(account->id());
QString folderId = QString::number(m_currentFolderId);
// Fetch mails asynchronously to avoid blocking UI
QtConcurrent::run([this, accountId, folderId]() {
m_mailService->fetchMails(accountId, folderId);
});
delete account;
}
}
// Removed immediate refresh - will be updated via mailFetched signal
}
}
void MainMainWindow::onEmailSelected(int mailId) {
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested() {
switchToPage(PageCompose);
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item) {
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
}
switchToPage(PageCompose);
}
void MainMainWindow::onNewMessage() {
switchToPage(PageCompose);
}
void MainMainWindow::openMailInIndependentWindow(int mailId) {
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) return;
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(QString("Mail - %1").arg(item->subject()));
ReaderView *detachedReader = new ReaderView();
detachedReader->setMailItem(&item.value());
detachedWin->setCentralWidget(detachedReader);
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage("Opened mail in independent window", 3000);
}
void MainMainWindow::onAddAccountRequested() {
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString("Account added: %1").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list to show new account's folders
});
dialog->open();
}
void MainMainWindow::onAccountEditRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage("Account not found", 3000);
return;
}
AccountSetupDialog *dialog = new AccountSetupDialog(m_accountService, this);
dialog->loadAccountForEditing(*account);
connect(dialog, &AccountSetupDialog::accountCreated, this, [this](const Account &account) {
statusBar()->showMessage(QString("Account updated: %1").arg(account.email()), 3000);
m_folderModel->refresh(); // Refresh folder list
});
dialog->open();
}
void MainMainWindow::onAccountDeleteRequested(int accountId) {
Account *account = m_accountService->findAccountById(accountId);
if (!account) {
statusBar()->showMessage("Account not found", 3000);
return;
}
if (QMessageBox::warning(this, "Delete Account",
QString("Are you sure you want to delete the account '%1'?").arg(account->email()),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) {
m_accountService->removeAccount(accountId);
statusBar()->showMessage(QString("Account deleted: %1").arg(account->email()), 3000);
m_folderModel->refresh(); // Refresh folder list to remove deleted account's folders
}
}
void MainMainWindow::createToolBar() {
m_toolBar = addToolBar("Main Toolbar");
m_toolBar->setMovable(false);
QAction *newMsgAction = m_toolBar->addAction("✉ New Message");
m_toolBar->addSeparator();
QAction *syncAction = m_toolBar->addAction("⟳ Sync/Refresh");
QAction *openWinAction = m_toolBar->addAction("↗ Abrir en ventana");
QAction *deleteAction = m_toolBar->addAction("🗑 Delete");
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
}
});
connect(openWinAction, &QAction::triggered, [this]() {
if (m_currentMailId >= 0) {
openMailInIndependentWindow(m_currentMailId);
} else {
statusBar()->showMessage("Selecciona un correo primero para abrirlo en una ventana", 3000);
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
});
}
+13 -2
View File
@@ -42,6 +42,14 @@ private slots:
void onProgressChanged(int percent);
void onStatusMessage(const QString &msg);
// Slots for embedded compose view
void onEmbeddedSendRequested(const QString &to, const QString &cc, const QString &bcc,
const QString &subject, const QString &body,
const QDateTime &scheduleTime,
const QString &fromAddress,
const QStringList &attachmentPaths);
void onEmbeddedDiscardRequested();
void onEmbeddedDetachRequested(QWidget *widget);
private:
void setupUI();
void setupSidebar();
@@ -68,6 +76,9 @@ private:
QTreeView *m_folderTree;
MailListView *m_mailListView;
ReaderView *m_emailViewer;
QStackedWidget *m_viewerStack;
QWidget *m_placeholderWidget;
ComposeView *m_embeddedComposeView;
// Other pages
ComposeView *m_composeView;
@@ -78,8 +89,8 @@ private:
// Services & Models
FolderListModel *m_folderModel;
EmailListModel *m_emailModel;
AccountService *m_accountService;
MailService *m_mailService;
AccountService *m_accountService = nullptr;
MailService *m_mailService = nullptr;
QToolBar *m_toolBar;
int m_currentFolderId;
View File
View File
+89
View File
@@ -0,0 +1,89 @@
#pragma once
#include <QMainWindow>
#include <QStackedWidget>
#include <QListWidget>
#include <QSplitter>
#include <QTreeView>
#include <QStatusBar>
#include <QProgressBar>
#include <QToolBar>
#include <QAction>
#include "ui/readerview.h"
#include "ui/maillistview.h"
#include "ui/composeview.h"
#include "ui/settingsview.h"
#include "ui/contactsview.h"
#include "ui/calendarview.h"
#include "ui/models/FolderListModel.h"
#include "ui/models/EmailListModel.h"
#include "services/accountservice.h"
#include "services/mailservice.h"
class MainMainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainMainWindow(QWidget *parent = nullptr);
~MainMainWindow() override = default;
private slots:
void onNavChanged(int index);
void onFolderSelected(const QModelIndex &index);
void onEmailSelected(int mailId);
void onComposeRequested();
void onReaderReplyRequested(const MailItem *item);
void onNewMessage();
void openMailInIndependentWindow(int mailId);
void onAddAccountRequested();
void onAccountEditRequested(int accountId);
void onAccountDeleteRequested(int accountId);
void onProgressChanged(int percent);
void onStatusMessage(const QString &msg);
private:
void setupUI();
void setupSidebar();
void setupMailPage();
void connectModels();
void createToolBar();
void switchToPage(int pageIndex);
// Navigation
QListWidget *m_sidebar;
QStackedWidget *m_stack;
enum Page {
PageMail = 0,
PageCompose,
PageSettings,
PageContacts,
PageCalendar
};
// Mail page widgets
QWidget *m_mailPage;
QSplitter *m_folderSplitter;
QTreeView *m_folderTree;
MailListView *m_mailListView;
ReaderView *m_emailViewer;
// Other pages
ComposeView *m_composeView;
SettingsView *m_settingsView;
ContactsView *m_contactsView;
CalendarView *m_calendarView;
// Services & Models
FolderListModel *m_folderModel;
EmailListModel *m_emailModel;
AccountService *m_accountService;
MailService *m_mailService;
QToolBar *m_toolBar;
int m_currentFolderId;
int m_currentMailId;
QProgressBar *m_progressBar;
};
+386
View File
@@ -0,0 +1,386 @@
import sys
import re
with open('composeview.cpp', 'r') as f:
lines = f.readlines()
# 1. Remove AddressInputWidget class definition (lines approx 27-52)
# Find start and end lines.
new_lines = []
i = 0
while i < len(lines):
if lines[i].strip() == '// Address input widget with tokenized entries, delete button, drag-drop reorder, and autocomplete':
# Skip until after the closing brace of the class
# Find the line with '};' after the class
j = i
while j < len(lines) and not lines[j].strip().startswith('};'):
j += 1
# include the '};' line
j += 1
i = j
continue
new_lines.append(lines[i])
i += 1
lines = new_lines
# 2. Replace member variable types
# We'll process line by line, replacing specific patterns.
out_lines = []
for line in lines:
# Replace member declarations
if 'AddressInputWidget *m_toWidget = nullptr;' in line:
line = line.replace('AddressInputWidget *m_toWidget = nullptr;', 'QLineEdit *m_toField = nullptr;')
if 'AddressInputWidget *m_ccWidget = nullptr;' in line:
line = line.replace('AddressInputWidget *m_ccWidget = nullptr;', 'QLineEdit *m_ccField = nullptr;')
if 'AddressInputWidget *m_bccWidget = nullptr;' in line:
line = line.replace('AddressInputWidget *m_bccWidget = nullptr;', 'QLineEdit *m_bccField = nullptr;')
# Remove duplicate m_ccField and m_bccField lines (they are later)
if 'QLineEdit *m_ccField = nullptr; // kept for compatibility? actually not used now' in line:
# skip this line
continue
if 'QLineEdit *m_bccField = nullptr;' in line and '// kept for compatibility' not in line:
# This is the second duplicate; we already have one above; we can keep only one.
# We'll skip this line.
continue
out_lines.append(line)
lines = out_lines
# Now we need to modify setAccountService, setupUI, slots, etc.
# We'll do a more aggressive approach: replace entire sections using markers.
# Let's join back to string and use regex.
text = ''.join(lines)
# Replace setAccountService function
pattern_setAccountService = re.compile(r'void ComposeView::setAccountService\(AccountService \*service\)\s*\{[\s\S]*?\}', re.MULTILINE)
def repl_setAccountService(match):
return '''void ComposeView::setAccountService(AccountService *service)
{
m_accountService = service;
populateAccountCombo();
// Update completer models for address widgets with known emails
if (m_accountService) {
QStringList knownEmails;
const QVector<Account> accounts = m_accountService->getAllAccounts();
for (const Account &acc : accounts) {
knownEmails << acc.email();
}
// Set completion model for each widget
QStringListModel *model = new QStringListModel(knownEmails, this);
QCompleter *comp = new QCompleter(model, this);
comp->setCaseSensitivity(Qt::CaseInsensitive);
if (m_toField) m_toField->setCompleter(comp);
// For cc and bcc we need separate completers but can share model
QCompleter *comp2 = new QCompleter(model, this);
comp2->setCaseSensitivity(Qt::CaseInsensitive);
if (m_ccField) m_ccField->setCompleter(comp2);
QCompleter *comp3 = new QCompleter(model, this);
comp3->setCaseSensitivity(Qt::CaseInsensitive);
if (m_bccField) m_bccField->setCompleter(comp3);
}
}'''
text = re.sub(pattern_setAccountService, repl_setAccountService, text)
# Replace setupCore function: we need to replace the whole function.
# We'll rewrite from scratch using a marker: we will replace the whole function body.
# Let's capture the function and replace.
pattern_setupUI = re.compile(r'void ComposeView::setupUI\(\)\s*\{[\s\S]*?\n\}\s*\n(?=void|\Z)', re.MULTILINE)
def repl_setupUI(match):
return '''void ComposeView::setupUI()
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20,15,20,15);
mainLayout->setSpacing(8);
// Top row: Account selector and detach button
QHBoxLayout *topLayout = new QHBoxLayout();
m_accountLabel = new QLabel(tr(\"From:\"), this);
m_accountLabel->setFixedWidth(40);
m_accountLabel->setStyleSheet(QStringLiteral(\"font-weight: bold; color: #555;\"));
m_accountCombo = new QComboBox(this);
m_accountCombo->setFixedWidth(180);
m_accountCombo->setStyleSheet(
\"QComboBox { border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 8px; \"
\"background: white; min-height: 20px; }\"\n
\"QComboBox:hover { border-color: #bdbdbd; }\"\n
\"QComboBox:focus { border-color: #1976D2; }\"
);
m_detachButton = new QPushButton(this);
m_detachButton->setToolTip(tr(\"Detach compose window\"));
m_detachButton->setText(QStringLiteral(\"\"));
m_detachButton->setFixedSize(28,28);
m_detachButton->setStyleSheet(
\"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; \"
\"padding: 6px; color: #555; font-size: 14px; }\"\n
\"QPushButton:hover { background: #f0f0f0; }\"\n
);
connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked);
topLayout->addWidget(m_accountLabel);
topLayout->addWidget(m_accountCombo, 1);
topLayout->addWidget(m_detachButton);
mainLayout->addLayout(topLayout);
// Separator line
QFrame *line1 = new QFrame(this);
line1->setFrameShape(QFrame::HLine);
line1->setStyleSheet(QStringLiteral(\"color: #e0e0e0;\"));
mainLayout->addWidget(line1);
// To row
QHBoxLayout *toLayout = new QHBoxLayout();
QLabel *toLabel = new QLabel(tr(\"To:\"), this);
toLabel->setFixedWidth(40);
toLabel->setStyleSheet(QStringLiteral(\"font-weight: bold; color: #555;\"));
m_toField = new QLineEdit(this);
m_toField->setPlaceholderText(tr(\"To recipients\"));
toLayout->addWidget(toLabel);
toLayout->addWidget(m_toField, 1);
mainLayout->addLayout(toLayout);
// CC/BCC toggle buttons
QHBoxLayout *toggleLayout = new QHBoxLayout();
m_ccButton = new QPushButton(tr(\"CC\"), this);
m_ccButton->setCheckable(true);
m_ccButton->setChecked(false);
connect(m_ccButton, &QPushButton::toggled, this, &ComposeView::onCcToggled);
m_bccButton = new QPushButton(tr(\"BCC\"), this);
m_bccButton->setCheckable(true);
m_bccButton->setChecked(false);
connect(m_bccButton, &QPushButton::toggled, this, &ComposeView::onBccToggled);
toggleLayout->addWidget(m_ccButton);
toggleLayout->addWidget(m_bccButton);
toggleLayout->addStretch();
mainLayout->addLayout(toggleLayout);
// Cc row (hidden by default)
m_ccRow = new QWidget(this);
QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow);
ccLayout->setContentsMargins(0,0,0,0);
QLabel *ccLabel = new QLabel(tr(\"Cc:\"), this);
ccLabel->setFixedWidth(40);
ccLabel->setStyleSheet(QStringLiteral(\"color: #555;\"));
m_ccField = new QLineEdit(this);
m_ccField->setPlaceholderText(tr(\"Cc recipients\"));
ccLayout->addWidget(ccLabel);
ccLayout->addWidget(m_ccField, 1);
m_ccRow->setVisible(false);
mainLayout->addWidget(m_ccRow);
// Bcc row (hidden by default)
m_bccRow = new QWidget(this);
QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow);
bccLayout->setContentsMargins(0,0,0,0);
QLabel *bccLabel = new QLabel(tr(\"Bcc:\"), this);
bccLabel->setFixedWidth(40);
bccLabel->setStyleSheet(QStringLiteral(\"color: #555;\"));
m_bccField = new QLineEdit(this);
m_bccField->setPlaceholderText(tr(\"Bcc recipients\"));
bccLayout->addWidget(bccLabel);
bccLayout->addWidget(m_bccField, 1);
m_bccRow->setVisible(false);
mainLayout->addWidget(m_bccRow);
// Separator
QFrame *line2 = new QFrame(this);
line2->setFrameShape(QFrame::HLine);
line2->setStyleSheet(QStringLiteral(\"color: #e0e0e0;\"));
mainLayout->addWidget(line2);
// Subject field
QLabel *subjectLabel = new QLabel(tr(\"Subject:\"), this);
subjectLabel->setFixedWidth(50);
subjectLabel->setStyleSheet(QStringLiteral(\"font-weight: bold; color: #555;\"));
m_subjectField = new QLineEdit(this);
m_subjectField->setPlaceholderText(tr(\"Subject\"));
m_subjectField->setFixedHeight(36);
QFont subjectFont = m_subjectField->font();
subjectFont.setPointSize(14);
subjectFont.setBold(true);
m_subjectField->setFont(subjectFont);
QHBoxLayout *subjectLayout = new QHBoxLayout();
subjectLayout->addWidget(subjectLabel);
subjectLayout->addWidget(m_subjectField, 1);
mainLayout->addLayout(subjectLayout);
// Body editor with toolbar
m_bodyEditor = new RichTextEditor(this);
m_bodyEditor->setupToolbar(mainLayout);
mainLayout->addWidget(m_bodyEditor, 1);
// Schedule panel (hidden by default)
m_schedulePanel = new QWidget(this);
QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel);
scheduleLayout->setContentsMargins(0,0,0,0);
QLabel *scheduleLabel = new QLabel(tr(\"Send at:\"), this);
scheduleLabel->setStyleSheet(QStringLiteral(\"color: #555; font-weight: bold;\"));
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600), this);
m_schedulePicker->setCalendarPopup(true);
m_schedulePicker->setDisplayFormat(QStringLiteral(\"dd/MM/yyyy hh:mm AP\"));
scheduleLayout->addWidget(scheduleLabel);
scheduleLayout->addWidget(m_schedulePicker);
scheduleLayout->addStretch();
m_scheduleSendButton = new QPushButton(tr(\"Schedule Send\"), this);
m_scheduleSendButton->setStyleSheet(
\"QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; \"
\"padding: 6px 16px; font-weight: bold; }\"\n
\"QPushButton:hover { background-color: #1565C0; }\"\n
);
connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked);
scheduleLayout->addWidget(m_scheduleSendButton);
m_schedulePanel->setVisible(false);
mainLayout->addWidget(m_schedulePanel);
// Action buttons row
QHBoxLayout *actionsLayout = new QHBoxLayout();
actionsLayout->addStretch();
m_discardButton = new QPushButton(tr(\"Discard\"), this);
m_discardButton->setStyleSheet(
\"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; \"
\"padding: 8px 20px; color: #555; }\"\n
\"QPushButton:hover { background: #f5f5f5; }\"\n
);
actionsLayout->addWidget(m_discardButton);
connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested);
// Split button for Send / Schedule
m_sendMenu = new QMenu(this);
m_sendNowAction = m_sendMenu->addAction(tr(\"Send Now\"));
connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked);
m_scheduleAction = m_sendMenu->addAction(tr(\"Schedule for later...\"));
connect(m_scheduleAction, &QAction::triggered, [this]() {
m_schedulePanel->setVisible(true);
});
m_sendSplit = new QToolButton(this);
m_sendSplit->setText(tr(\"Send\"));
m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup);
m_sendSplit->setMenu(m_sendMenu);
m_sendSplit->setStyleSheet(
\"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; \"
\"padding: 8px 24px; font-weight: bold; }\"\n
\"QToolButton:hover { background-color: #1565C0; }\"\n
\"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; }\"\n
\"QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }\"\n
);
connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked);
actionsLayout->addWidget(m_sendSplit);
mainLayout->addLayout(actionsLayout);
// Connect rich text editor signature signals
connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked);
connect(m_bodyEditor, &RichTextEditor::signatureEditRequested, this, &ComposeView::onSignatureEditRequested);
}
'''
text = re.sub(pattern_setupUI, repl_setupUI, text)
# Replace onCcToggle slot (we renamed to onCcToggled with bool)
pattern_onCcToggle = re.compile(r'void ComposeView::onCcToggle\(\)\s*\{[\s\S]*?\n\}\s*\n', re.MULTILINE)
def repl_onCcToggle(match):
return '''void ComposeView::onCcToggled(bool checked)
{
m_ccRow->setVisible(checked);
if (checked) {
m_ccField->setFocus();
}
}
'''
text = re.sub(pattern_onCcToggle, repl_onCcToggle, text)
# Replace onBccToggle similarly
pattern_onBccToggle = re.compile(r'void ComposeView::onBccToggle\(\)\s*\{[\s\S]*?\n\}\s*\n', re.MULTILINE)
def repl_onBccToggle(match):
return '''void ComposeView::onBccToggled(bool checked)
{
m_bccRow->setVisible(checked);
if (checked) {
m_bccField->setFocus();
}
}
'''
text = re.sub(pattern_onBccToggle, repl_onBccToggle, text)
# Replace onSendClicked
pattern_onSendClicked = re.compile(r'void ComposeView::onSendClicked\(\)\s*\{[\s\S]*?\n\}\s*\n', re.MULTILINE)
def repl_onSendClicked(match):
return '''void ComposeView::onSendClicked()
{
QString to = m_toField->text();
QString cc = m_ccButton->isChecked() ? m_ccField->text() : QString();
QString bcc = m_bccButton->isChecked() ? m_bccField->text() : QString();
QString subject = m_subjectField->text();
QString body = m_bodyEditor->toHtml();
QDateTime scheduleTime = m_schedulePanel->isVisible() ? m_schedulePicker->dateTime() : QDateTime();
QString fromAddress;
if (m_currentAccountId > 0 && m_accountCombo->currentIndex() > 0) {
fromAddress = m_accountCombo->currentData().toString();
}
emit sendRequested(to, cc, bcc, subject, body, scheduleTime, fromAddress);
}
'''
text = re.sub(pattern_onSendClicked, repl_onSendClicked, text)
# Replace initializeComposition
pattern_initComp = re.compile(r'void ComposeView::initializeComposition\(\)\s*\{[\s\S]*?\n\}\s*\n', re.MULTILINE)
def repl_initComp(match):
return '''void ComposeView::initializeComposition()
{
m_subjectField->clear();
m_bodyEditor->clear();
m_toField->clear();
m_ccField->clear();
m_bccField->clear();
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
m_subjectField->setFocus();
}
'''
text = re.sub(pattern_initComp, repl_initComp, text)
# Replace startNewEmail
pattern_startNew = re.compile(r'void ComposeView::startNewEmail\(const QString \&initialRecipient\)\s*\{[\s\S]*?\n\}\s*\n', re.MULTILINE)
def repl_startNew(match):
return '''void ComposeView::startNewEmail(const QString &initialRecipient)
{
initializeComposition();
if (!initialRecipient.isEmpty()) {
m_toField->setText(initialRecipient);
}
m_toField->setFocus();
}
'''
text = re.sub(pattern_startNew, repl_startNew, text)
# Replace setTo
pattern_setTo = re.compile(r'void ComposeView::setTo\(const QString \&to\)\s*\{[\s\S]*?\n\}\s*\n', re.MULTILINE)
def repl_setTo(match):
return '''void ComposeView::setTo(const QString &to)
{
if (to.isEmpty()) {
m_toField->clear();
return;
}
// Split by semicolon or comma
QStringList parts = to.split(QRegularExpression(\"[,;]\"), Qt::SkipEmptyParts);
QStringList cleaned;
for (const QString &part : parts) {
QString trimmed = part.trimmed();
if (!trimmed.isEmpty())
cleaned << trimmed;
}
m_toField->setText(cleaned.join(\"; \"));
}
'''
text = re.sub(pattern_setTo, repl_setTo, text)
# We also need to add a helper function splitAddresses if we used it, but we didn't.
# Ensure we have the forward declaration for the slot in header (already done).
# Now write the file.
with open('composeview.cpp', 'w') as f:
f.write(text)
+38
View File
@@ -0,0 +1,38 @@
import sys
filename = 'mainmainwindow.cpp'
with open(filename, 'r') as f:
lines = f.readlines()
# Find the line after the mailFetchError connect (look for the closing brace and semicolon of that connect)
# We'll look for the pattern: '});' that ends the mailFetchError lambda.
insert_idx = None
for i, line in enumerate(lines):
if 'connect(m_mailService, &MailService::mailFetchError' in line:
# Find the closing brace of this call (could be same line or later)
# We'll just look for the next line that contains '});' after this line.
for j in range(i, len(lines)):
if '});' in lines[j]:
insert_idx = j + 1 # insert after this line
break
break
if insert_idx is None:
print("Could not find insert point")
sys.exit(1)
# The insertion block
insert_lines = [
' connect(m_mailService, &MailService::mailSendFailed, this, [this](const QString &mailItemId, const QString &error) {\n',
' qWarning() << \"[MailSendFailed]\" << error;\n',
' statusBar()->showMessage(tr(\"Error sending mail: %1\").arg(error), 5000);\n',
' });\n'
]
# Insert
lines = lines[:insert_idx] + insert_lines + lines[insert_idx:]
with open(filename, 'w') as f:
f.writelines(lines)
print(f"Inserted mailSendFailed connection at line {insert_idx+1}")
+24
View File
@@ -1,5 +1,8 @@
#include "ui/readerview.h"
#include <QFont>
#include <QDesktopServices>
#include <QUrl>
#include "db/dao/mailitemdao.h"
ReaderView::ReaderView(QWidget *parent) : QWidget(parent) {
setupUI();
@@ -55,8 +58,19 @@ void ReaderView::setupUI() {
m_bodyViewer->setOpenExternalLinks(true);
m_bodyViewer->setFrameStyle(QFrame::NoFrame);
m_attachmentList = new QListWidget();
m_attachmentList->setVisible(false);
m_attachmentList->setMaximumHeight(110);
m_attachmentList->setToolTip(tr("Double-click an attachment to open it"));
connect(m_attachmentList, &QListWidget::itemDoubleClicked, this,
[](QListWidgetItem *listItem) {
const QString path = listItem->data(Qt::UserRole).toString();
if (!path.isEmpty()) QDesktopServices::openUrl(QUrl::fromLocalFile(path));
});
mainLayout->addWidget(headerWidget);
mainLayout->addLayout(actionsLayout);
mainLayout->addWidget(m_attachmentList);
mainLayout->addWidget(m_bodyViewer);
// Connections
@@ -72,6 +86,8 @@ void ReaderView::setMailItem(const MailItem* item) {
m_subjectLabel->setText("No mail selected");
m_fromLabel->setText("From: ");
m_dateLabel->setText("Date: ");
m_attachmentList->clear();
m_attachmentList->setVisible(false);
m_bodyViewer->setHtml("<i>Please select a message to read</i>");
return;
}
@@ -79,5 +95,13 @@ void ReaderView::setMailItem(const MailItem* item) {
m_subjectLabel->setText(item->subject());
m_fromLabel->setText(QString("From: %1").arg(item->sender()));
m_dateLabel->setText(QString("Date: %1").arg(item->date().toString("ddd, d MMM yyyy hh:mm")));
m_attachmentList->clear();
const QVector<StoredAttachmentRecord> attachments = MailItemDao::attachmentsForMail(item->id());
for (const StoredAttachmentRecord &attachment : attachments) {
auto *listItem = new QListWidgetItem(attachment.fileName, m_attachmentList);
listItem->setData(Qt::UserRole, attachment.storedPath);
listItem->setToolTip(attachment.storedPath);
}
m_attachmentList->setVisible(!attachments.isEmpty());
m_bodyViewer->setHtml(item->bodyHtml());
}
+2
View File
@@ -4,6 +4,7 @@
#include <QLabel>
#include <QTextBrowser>
#include <QPushButton>
#include <QListWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include "core/mailitem.h"
@@ -31,6 +32,7 @@ private:
QLabel *m_fromLabel;
QLabel *m_dateLabel;
QTextBrowser *m_bodyViewer;
QListWidget *m_attachmentList;
QPushButton *m_replyButton;
QPushButton *m_forwardButton;
+50
View File
@@ -0,0 +1,50 @@
import sys
filename = '/mnt/c/Users/javie/wino-mail-dtkqt/src/ui/mainmainwindow.cpp'
with open(filename, 'r') as f:
lines = f.readlines()
# Find start line
start = None
for i, line in enumerate(lines):
if line.strip().startswith('connect(deleteAction, &QAction::triggered'):
start = i
break
if start is None:
print('Could not find deleteAction connect')
sys.exit(1)
# Find end line: look for a line that contains '});' after start, assuming it's the end of the lambda
end = None
for i in range(start, len(lines)):
if '});' in lines[i]:
end = i
break
if end is None:
print('Could not find end of lambda')
sys.exit(1)
# Replacement lines
new_lines = [
' connect(deleteAction, &QAction::triggered, [this]() {\n',
' QModelIndexList indexes = m_mailListView->selectionModel()->selectedIndexes();\n',
' if (indexes.isEmpty()) {\n',
' statusBar()->showMessage(tr(\"No email selected\"), 2000);\n',
' return;\n',
' }\n',
' int row = indexes.first().row();\n',
' QModelIndex idx = m_emailModel->index(row, 0);\n',
' qint64 id = idx.data(EmailListModel::IdRole).toLongLong();\n',
' m_mailService->deleteMail(QString::number(id)); // deletes from DB\n',
' m_emailModel->refresh();\n',
' statusBar()->showMessage(tr(\"Email deleted\"), 2000);\n',
' });\n'
]
# Replace
lines = lines[:start] + new_lines + lines[end+1:]
with open(filename, 'w') as f:
f.writelines(lines)
print('Replace done')
+462
View File
@@ -0,0 +1,462 @@
#include "richtexteditor.h"
#include <QFrame>
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
#include <QDebug>
#include <QDialog>
#include <QVBoxLayout>
#include <QDialogButtonBox>
#include <QLabel>
#include "../../thirdparty/tags/include/tags_line_edit.hpp"
#include <QRegularExpression>
#include <QTimer>
#include <QTextList>
#include <QTextTable>
#include <QTextCursor>
#include <QTextBlockFormat>
#include <QTextCharFormat>
#include <QTextImageFormat>
#include <QTextTableFormat>
#include <QTextLength>
#include <QPixmap>
#include <QMouseEvent>
#include <QContextMenuEvent>
#include <QApplication>
#include <QMenu>
#include <QAction>
#include <QWidget>
#include <QLabel>
#include <QResizeEvent>
#include <QTextTableCell>
// ===================== RichTextEditor =====================
RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) {
setAcceptRichText(true);
setPlaceholderText("Write your message here...");
setMouseTracking(true);
}
void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
m_toolbar = new QToolBar("Formatting");
m_toolbar->setIconSize(QSize(16, 16));
m_toolbar->setStyleSheet(
"QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }\n"
"QToolButton { padding: 4px 6px; border-radius: 3px; }\n"
"QToolButton:hover { background: #e0e0e0; }\n"
"QToolButton:checked { background: #bbdefb; }\n"
);
// Font family combo
m_fontCombo = new QFontComboBox();
m_fontCombo->setFixedWidth(150);
connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged);
m_toolbar->addWidget(m_fontCombo);
// Font size spin
m_fontSizeSpin = new QSpinBox();
m_fontSizeSpin->setRange(8, 72);
m_fontSizeSpin->setValue(11);
m_fontSizeSpin->setFixedWidth(70);
m_fontSizeSpin->setFixedHeight(24);
connect(m_fontSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged);
m_toolbar->addWidget(m_fontSizeSpin);
m_toolbar->addSeparator();
// Bold / Italic / Underline
QAction *boldAct = m_toolbar->addAction("B");
boldAct->setCheckable(true);
boldAct->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Bold/Text Formatting/Text Bold.svg")));
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
QAction *italicAct = m_toolbar->addAction("I");
italicAct->setCheckable(true);
italicAct->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Outline/Text Formatting/Text Italic.svg")));
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
QAction *underlineAct = m_toolbar->addAction("U");
underlineAct->setCheckable(true);
underlineAct->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Outline/Text Formatting/Text Underline.svg")));
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
m_toolbar->addSeparator();
// Alignment
QAction *alignLeft = m_toolbar->addAction("L");
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
QAction *alignCenter = m_toolbar->addAction("C");
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
QAction *alignRight = m_toolbar->addAction("R");
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
QAction *alignJustify = m_toolbar->addAction("J");
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
m_toolbar->addSeparator();
// Lists
QAction *bulletAct = m_toolbar->addAction("Bullets");
connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList);
QAction *numAct = m_toolbar->addAction("1. List");
connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList);
m_toolbar->addSeparator();
// Indent / Outdent
QAction *indentAct = m_toolbar->addAction("Indent");
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
QAction *outdentAct = m_toolbar->addAction("Outdent");
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
m_toolbar->addSeparator();
// Insert image
QAction *imgAct = m_toolbar->addAction("Img");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
// Insert table
QAction *tableAct = m_toolbar->addAction("Tbl");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
// Signature button
m_signatureButton = new QToolButton();
m_signatureButton->setText("Signature");
m_signatureButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_signatureButton->setPopupMode(QToolButton::MenuButtonPopup);
m_signatureMenu = new QMenu(m_signatureButton);
m_signatureButton->setMenu(m_signatureMenu);
m_signatureButton->setStyleSheet(
"QToolButton { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 3px; padding: 4px 6px; }\n"
"QToolButton:hover { background: #e0e0e0; }\n"
"QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }\n"
);
connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked);
m_toolbar->addWidget(m_signatureButton);
QAction *editSigAct = m_signatureMenu->addAction(tr("Editar firmas"));
connect(editSigAct, &QAction::triggered, this, &RichTextEditor::signatureEditRequested);
m_toolbar->addSeparator();
layout->addWidget(m_toolbar);
}
void RichTextEditor::onBold() {
QTextCharFormat fmt;
fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onItalic() {
QTextCharFormat fmt;
fmt.setFontItalic(!textCursor().charFormat().fontItalic());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onUnderline() {
QTextCharFormat fmt;
fmt.setFontUnderline(!textCursor().charFormat().fontUnderline());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onBulletList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDisc) {
// Remove list
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDisc);
cursor.createList(listFormat);
}
}
void RichTextEditor::onNumberedList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDecimal) {
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDecimal);
cursor.createList(listFormat);
}
}
void RichTextEditor::onIndent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setIndent(bfmt.indent() + 1);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onOutdent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
int indent = bfmt.indent();
if (indent > 0) {
bfmt.setIndent(indent - 1);
cursor.setBlockFormat(bfmt);
}
}
void RichTextEditor::onAlignLeft() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignLeft);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignCenter() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignCenter);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignRight() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignRight);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignJustify() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignJustify);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onFontChanged(const QFont &font) {
QTextCharFormat fmt;
fmt.setFontFamilies({font.family()});
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onFontSizeChanged(int size) {
QTextCharFormat fmt;
fmt.setFontPointSize(size);
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onInsertImage() {
QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)");
if (filePath.isEmpty()) return;
QTextCursor cursor = textCursor();
QTextImageFormat imgFmt;
imgFmt.setName(filePath);
// Scale down if too large
QPixmap pm(filePath);
if (pm.width() > 600) {
imgFmt.setWidth(600);
imgFmt.setHeight(pm.height() * 600 / pm.width());
}
cursor.insertImage(imgFmt);
}
void RichTextEditor::onInsertTable() {
bool ok;
int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok);
if (!ok) return;
int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok);
if (!ok) return;
QTextCursor cursor = textCursor();
QTextTableFormat tableFmt;
tableFmt.setBorder(1);
tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
tableFmt.setCellPadding(4);
tableFmt.setCellSpacing(0);
tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100));
cursor.insertTable(rows, cols, tableFmt);
}
/* ===================== Mouse events for image/table resizing ===================== */
void RichTextEditor::mousePressEvent(QMouseEvent *event) {
if (event->button() == Qt::LeftButton) {
QPoint pos = event->pos();
QTextCursor cursor = cursorForPosition(pos);
// Image
if (cursor.charFormat().isImageFormat()) {
qDebug() << "Mouse press on image";
m_resizingImage = true;
m_resizeStartPos = pos;
QTextImageFormat imgFormat = cursor.charFormat().toImageFormat();
m_imageStartSize = QSize(imgFormat.width(), imgFormat.height());
if (m_imageStartSize.isEmpty()) {
QPixmap pm(imgFormat.name());
if (!pm.isNull())
m_imageStartSize = pm.size();
}
// Change cursor to indicate resizing
setCursor(Qt::SizeAllCursor);
event->accept();
return;
}
// Table
QTextTable *table = cursor.currentTable();
if (table) {
qDebug() << "Mouse press on table";
m_resizingTable = true;
m_tableStartPos = pos;
m_currentTable = table;
QTextTableFormat fmt = table->format();
m_tableStartSize = QSizeF(fmt.width().rawValue(), fmt.height().rawValue());
setCursor(Qt::SizeAllCursor);
event->accept();
return;
}
}
QTextEdit::mousePressEvent(event);
}
void RichTextEditor::mouseMoveEvent(QMouseEvent *event) {
if (m_resizingImage) {
qDebug() << "Mouse move resizing image";
QPoint delta = event->pos() - m_resizeStartPos;
int newWidth = qMax(20, m_imageStartSize.width() + delta.x());
int newHeight = qMax(20, m_imageStartSize.height() + delta.y());
QTextCursor cursor = cursorForPosition(m_resizeStartPos);
if (cursor.charFormat().isImageFormat()) {
QTextImageFormat fmt = cursor.charFormat().toImageFormat();
fmt.setWidth(newWidth);
fmt.setHeight(newHeight);
cursor.mergeCharFormat(fmt);
setTextCursor(cursor);
}
event->accept();
return;
}
if (m_resizingTable) {
qDebug() << "Mouse move resizing table";
QPoint delta = event->pos() - m_tableStartPos;
qreal newWidth = qMax(10.0, m_tableStartSize.width() + delta.x());
qreal newHeight = qMax(10.0, m_tableStartSize.height() + delta.y());
QTextTableFormat fmt = m_currentTable->format();
fmt.setWidth(QTextLength(QTextLength::FixedLength, newWidth));
fmt.setHeight(QTextLength(QTextLength::FixedLength, newHeight));
m_currentTable->setFormat(fmt);
event->accept();
return;
}
QTextEdit::mouseMoveEvent(event);
}
void RichTextEditor::mouseReleaseEvent(QMouseEvent *event) {
if (event->button() == Qt::LeftButton) {
if (m_resizingImage) {
qDebug() << "Mouse release image resize";
m_resizingImage = false;
setCursor(Qt::ArrowCursor);
event->accept();
return;
}
if (m_resizingTable) {
qDebug() << "Mouse release table resize";
m_resizingTable = false;
setCursor(Qt::ArrowCursor);
event->accept();
return;
}
}
QTextEdit::mouseReleaseEvent(event);
}
void RichTextEditor::contextMenuEvent(QContextMenuEvent *event) {
QPoint pos = event->pos();
QTextCursor cursor = cursorForPosition(pos);
QTextTable *table = cursor.currentTable();
if (table) {
showTableContextMenu(pos);
event->accept();
return;
}
QTextEdit::contextMenuEvent(event);
}
/* ===================== Helper functions ===================== */
void RichTextEditor::showTableContextMenu(const QPoint &pos) {
if (!m_tableContextMenu) {
m_tableContextMenu = new QMenu(this);
m_tableContextMenu->addAction(tr("Insert row above"), this, &RichTextEditor::addTableRow);
m_tableContextMenu->addAction(tr("Insert row below"), this, &RichTextEditor::addTableRow);
m_tableContextMenu->addAction(tr("Insert column left"), this, &RichTextEditor::addTableColumn);
m_tableContextMenu->addAction(tr("Insert column right"), this, &RichTextEditor::addTableColumn);
m_tableContextMenu->addSeparator();
m_tableContextMenu->addAction(tr("Delete selected rows"), this, &RichTextEditor::removeTableRow);
m_tableContextMenu->addAction(tr("Delete selected columns"), this, &RichTextEditor::removeTableColumn);
m_tableContextMenu->addSeparator();
m_tableContextMenu->addAction(tr("Border..."), this, &RichTextEditor::modifyTableBorder);
}
m_tableContextMenu->exec(mapToGlobal(pos));
}
void RichTextEditor::addTableRow() {
if (!m_currentTable) return;
QTextCursor cursor = textCursor();
QTextTable *table = cursor.currentTable();
if (!table) return;
QTextTableCell cell = table->cellAt(cursor.position());
int row = cell.row();
table->insertRows(row, 1);
}
void RichTextEditor::removeTableRow() {
if (!m_currentTable) return;
QTextCursor cursor = textCursor();
QTextTable *table = cursor.currentTable();
if (!table) return;
QTextTableCell cell = table->cellAt(cursor.position());
int row = cell.row();
table->removeRows(row, 1);
}
void RichTextEditor::addTableColumn() {
if (!m_currentTable) return;
QTextCursor cursor = textCursor();
QTextTable *table = cursor.currentTable();
if (!table) return;
QTextTableCell cell = table->cellAt(cursor.position());
int col = cell.column();
table->insertColumns(col, 1);
}
void RichTextEditor::removeTableColumn() {
if (!m_currentTable) return;
QTextCursor cursor = textCursor();
QTextTable *table = cursor.currentTable();
if (!table) return;
QTextTableCell cell = table->cellAt(cursor.position());
int col = cell.column();
table->removeColumns(col, 1);
}
void RichTextEditor::modifyTableBorder() {
if (!m_currentTable) return;
bool ok;
int border = QInputDialog::getInt(this, tr("Table Border"), tr("Border width (px):"),
m_currentTable->format().border(), 0, 20, 1, &ok);
if (!ok) return;
QTextTableFormat fmt = m_currentTable->format();
fmt.setBorder(border);
m_currentTable->setFormat(fmt);
}
+281
View File
@@ -0,0 +1,281 @@
#include "richtexteditor.h"
#include <QFrame>
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
#include <QDebug>
#include <QDialog>
#include <QVBoxLayout>
#include <QDialogButtonBox>
#include <QLabel>
#include "../../thirdparty/tags/include/tags_line_edit.hpp"
#include <QRegularExpression>
#include <QTimer>
#include <QTextList>
#include <QTextTable>
#include <QTextCursor>
#include <QTextBlockFormat>
#include <QTextCharFormat>
#include <QTextImageFormat>
#include <QTextTableFormat>
#include <QTextLength>
#include <QPixmap>
// ===================== RichTextEditor =====================
RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) {
setAcceptRichText(true);
setPlaceholderText(\"Write your message here...\");
}
void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
m_toolbar = new QToolBar(\"Formatting\");
m_toolbar->setIconSize(QSize(16, 16));
m_toolbar->setStyleSheet(
\"QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }\"\n
\"QToolButton { padding: 4px 6px; border-radius: 3px; }\"\n
\"QToolButton:hover { background: #e0e0e0; }\"\n
\"QToolButton:checked { background: #bbdefb; }\"\n
);
// Font family combo
m_fontCombo = new QFontComboBox();
m_fontCombo->setFixedWidth(150);
connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged);
m_toolbar->addWidget(m_fontCombo);
// Font size spin
m_fontSizeSpin = new QSpinBox();
m_fontSizeSpin->setRange(8, 72);
m_fontSizeSpin->setValue(11);
m_fontSizeSpin->setFixedWidth(70);
m_fontSizeSpin->setFixedHeight(24);
connect(m_fontSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged);
m_toolbar->addWidget(m_fontSizeSpin);
m_toolbar->addSeparator();
// Bold / Italic /
QAction *boldAct = m_toolbar->addAction(\"B\");
boldAct->setCheckable(true);
boldAct->setIcon(QIcon(QStringLiteral(\":/icons/resources/icons/SVG/Bold/Text Formatting/Text Bold.svg\")));
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
QAction *italicAct = m_toolbar->addAction(\"I\");
italicAct->setCheckable(true);
italicAct->setIcon(QIcon(QStringLiteral(\":/icons/resources/icons/SVG/Outline/Text Formatting/Text Italic.svg\")));
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
QAction *underlineAct = m_toolbar->addAction(\"U\");
underlineAct->setCheckable(true);
underlineAct->setIcon(QIcon(QStringLiteral(\":/icons/resources/icons/SVG/Outline/Text Formatting/Text Underline.svg\")));
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
m_toolbar->addSeparator();
// Alignment
QAction *alignLeft = m_toolbar->addAction(\"L\");
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
QAction *alignCenter = m_toolbar->addAction(\"C\");
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
QAction *alignRight = m_toolbar->addAction(\"R\");
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
QAction *alignJustify = m_toolbar->addAction(\"J\");
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
m_toolbar->addSeparator();
// Lists
QAction *bulletAct = m_toolbar->addAction(\"Bullets\");
connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList);
QAction *numAct = m_toolbar->addAction(\"1. List\");
connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList);
m_toolbar->addSeparator();
// Indent / Outdent
QAction *indentAct = m_toolbar->addAction(\"Indent\");
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
QAction *outdentAct = m_toolbar->addAction(\"Outdent\");
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
m_toolbar->addSeparator();
// Insert image
QAction *imgAct = m_toolbar->addAction(\"Img\");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
// Insert table
QAction *tableAct = m_toolbar->addAction(\"Tbl\");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
// Signature button
m_signatureButton = new QToolButton();
m_signatureButton->setText(\"Signature\");
m_signatureButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_signatureButton->setPopupMode(QToolButton::MenuButtonPopup);
m_signatureMenu = new QMenu(m_signatureButton);
m_signatureButton->setMenu(m_signatureMenu);
m_signatureButton->setStyleSheet(
\"QToolButton { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 3px; padding: 4px 6px; }\"\n
\"QToolButton:hover { background: #e0e0e0; }\"\n
\"QToolButton::menu-button { border-left: 1px solid rgba(0,0,0,0.1); width: 12px; }\"\n
);
connect(m_signatureButton, &QToolButton::clicked, this, &RichTextEditor::signatureClicked);
m_toolbar->addWidget(m_signatureButton);
QAction *editSigAct = m_signatureMenu->addAction(tr(\"Editar firmas\"));
connect(editSigAct, &QAction::triggered, this, &RichTextEditor::signatureEditRequested);
m_toolbar->addSeparator();
layout->addWidget(m_toolbar);
}
void RichTextEditor::onBold() {
QTextCharFormat fmt;
fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onItalic() {
QTextCharFormat fmt;
fmt.setFontItalic(!textCursor().charFormat().fontItalic());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onUnderline() {
QTextCharFormat fmt;
fmt.setFontUnderline(!textCursor().charFormat().fontUnderline());
mergeCurrentCharFormat(fmt);
setFocus();
}
void RichTextEditor::onBulletList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDisc) {
// Remove list
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDisc);
cursor.createList(listFormat);
}
}
void RichTextEditor::onNumberedList() {
QTextCursor cursor = textCursor();
QTextList *list = cursor.currentList();
if (list && list->format().style() == QTextListFormat::ListDecimal) {
QTextBlockFormat bfmt;
bfmt.setIndent(0);
cursor.setBlockFormat(bfmt);
list->remove(cursor.block());
} else {
QTextListFormat listFormat;
listFormat.setStyle(QTextListFormat::ListDecimal);
cursor.createList(listFormat);
}
}
void RichTextEditor::onIndent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setIndent(bfmt.indent() + 1);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onOutdent() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
int indent = bfmt.indent();
if (indent > 0) {
bfmt.setIndent(indent - 1);
cursor.setBlockFormat(bfmt);
}
}
void RichTextEditor::onAlignLeft() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignLeft);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignCenter() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignCenter);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignRight() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignRight);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onAlignJustify() {
QTextCursor cursor = textCursor();
QTextBlockFormat bfmt = cursor.blockFormat();
bfmt.setAlignment(Qt::AlignJustify);
cursor.setBlockFormat(bfmt);
}
void RichTextEditor::onFontChanged(const QFont &font) {
QTextCharFormat fmt;
fmt.setFontFamilies({font.family()});
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onFontSizeChanged(int size) {
QTextCharFormat fmt;
fmt.setFontPointSize(size);
mergeCurrentCharFormat(fmt);
}
void RichTextEditor::onInsertImage() {
QString filePath = QFileDialog::getOpenFileName(this, \"Insert Image\", QString(), \"Images (*.png *.jpg *.jpeg *.gif *.bmp)\");
if (filePath.isEmpty()) return;
QTextCursor cursor = textCursor();
QTextImageFormat imgFmt;
imgFmt.setName(filePath);
// Scale down if too large
QPixmap pm(filePath);
if (pm.width() > 600) {
imgFmt.setWidth(600);
imgFmt.setHeight(pm.height() * 600 / pm.width());
}
cursor.insertImage(imgFmt);
}
void RichTextEditor::onInsertTable() {
bool ok;
int rows = QInputDialog::getInt(this, \"Table Rows\", \"Rows:\", 3, 1, 50, 1, &ok);
if (!ok) return;
int cols = QInputDialog::getInt(this, \"Table Columns\", \"Columns:\", 3, 1, 20, 1, &ok);
if (!ok) return;
QTextCursor cursor = textCursor();
QTextTableFormat tableFmt;
tableFmt.setBorder(1);
tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
tableFmt.setCellPadding(4);
tableFmt.setCellSpacing(0);
tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100));
cursor.insertTable(rows, cols, tableFmt);
}
+81
View File
@@ -0,0 +1,81 @@
#ifndef RICHTEXTEDITOR_H
#define RICHTEXTEDITOR_H
#include <QTextEdit>
#include <QToolBar>
#include <QFontComboBox>
#include <QSpinBox>
#include <QToolButton>
#include <QMenu>
#include <QVBoxLayout>
#include <QMouseEvent>
#include <QContextMenuEvent>
#include <QPoint>
#include <QSize>
#include <QSizeF>
class QTextTable;
class QTextCursor;
class RichTextEditor : public QTextEdit
{
Q_OBJECT
public:
explicit RichTextEditor(QWidget *parent = nullptr);
void setupToolbar(QVBoxLayout *layout);
signals:
void signatureClicked();
void signatureEditRequested();
public slots:
void onBold();
void onItalic();
void onUnderline();
void onBulletList();
void onNumberedList();
void onIndent();
void onOutdent();
void onAlignLeft();
void onAlignCenter();
void onAlignRight();
void onAlignJustify();
void onFontChanged(const QFont &font);
void onFontSizeChanged(int size);
void onInsertImage();
void onInsertTable();
protected:
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void contextMenuEvent(QContextMenuEvent *event) override;
private:
QToolBar *m_toolbar;
QFontComboBox *m_fontCombo;
QSpinBox *m_fontSizeSpin;
QToolButton *m_signatureButton;
QMenu *m_signatureMenu;
// Image resize handling
bool m_resizingImage = false;
QPoint m_resizeStartPos;
QSize m_imageStartSize;
// Table resize handling
bool m_resizingTable = false;
QPoint m_tableStartPos;
QTextTable *m_currentTable = nullptr;
QSizeF m_tableStartSize;
QMenu *m_tableContextMenu = nullptr;
// Helper functions
void showTableContextMenu(const QPoint &pos);
void addTableRow();
void removeTableRow();
void addTableColumn();
void removeTableColumn();
void modifyTableBorder();
};
#endif // RICHTEXTEDITOR_H
+116
View File
@@ -0,0 +1,116 @@
import sys
filename = '/mnt/c/Users/javie/wino-mail-dtkqt/src/ui/mainmainwindow.cpp'
with open(filename, 'r') as f:
lines = f.readlines()
# Find start of delete connect
start = None
for i, line in enumerate(lines):
if 'connect(deleteAction, &QAction::triggered' in line:
start = i
break
if start is None:
print('Could not find delete connect start')
sys.exit(1)
# Find end of that connect (the matching '});' after start)
end = None
brace_count = 0
for i in range(start, len(lines)):
line = lines[i]
for ch in line:
if ch == '{':
brace_count += 1
elif ch == '}':
brace_count -= 1
if brace_count == 0 and '});' in line:
end = i
break
if end is not None:
break
if end is None:
print('Could not find end of delete connect')
sys.exit(1)
# Replacement delete lambda
new_delete = ''' connect(deleteAction, &QAction::triggered, [this]() {
QModelIndexList indexes = m_mailListView->selectionModel()->selectedIndexes();
if (indexes.isEmpty()) {
statusBar()->showMessage(tr("No email selected"), 2000);
return;
}
int row = indexes.first().row();
QModelIndex idx = m_emailModel->index(row, 0);
qint64 id = idx.data(EmailListModel::IdRole).toLongLong();
if (m_mailService->deleteMail(QString::number(id))) {
statusBar()->showMessage(tr("Email deleted"), 2000);
m_emailModel->refresh();
} else {
statusBar()->showMessage(tr("Failed to delete email"), 2000);
}
});\n'''
# Replace lines[start:end+1] with new_delete
lines = lines[:start] + [new_delete] + lines[end+1:]
# Now add flag action declaration after deleteAction declaration
# Find deleteAction declaration line
decl_line = None
for i, line in enumerate(lines):
if 'QAction *deleteAction = m_toolBar->addAction(\"🗑 Delete\");' in line:
decl_line = i
break
if decl_line is None:
print('Could not find deleteAction declaration')
sys.exit(1)
flag_decl = ' QAction *flagAction = m_toolBar->addAction(\"🚩 Flag\");\n'
lines = lines[:decl_line+1] + [flag_decl] + lines[decl_line+1:]
# Find where to insert flag connection: after the delete connect we just placed (now at start)
# Actually after the delete connect (which now starts at 'start' and ends at start (since we replaced with one line?) Actually new_delete is multiple lines; we inserted as a single string but it contains newlines.
# We'll just insert flag connection after the delete connection block (i.e., after the line where we inserted new_delete).
# We'll search for the end of the new_delete block by looking for the line that contains '});' after start.
# But easier: insert after the deleteAction declaration line? No, we want to be after_decl_line.
# Let's just insert flag connection after the delete connection by adding it after the line that contains the end of delete connect.
# We'll search for the pattern '});' that ends the delete connect (should be in new_delete).
# We'll find the line index of that '});' after start.
for i in range(start, len(lines)):
if '});' in lines[i]:
insert_point = i + 1
break
else:
insert_point is None:
insert_point = len(lines)
flag_conn = ''' connect(flagAction, &QAction::triggered, [this]() {
QModelIndexList indexes = m_mailListView->selectionModel()->selectedIndexes();
if (indexes.isEmpty()) {
statusBar()->showMessage(tr("No email selected"), 2000);
return;
}
int row = indexes.first().row();
QModelIndex idx = m_emailModel->index(row, 0);
qint64 id = idx.data(EmailListModel::IdRole).toLongLong();
// Toggle flagged state
std::optional<MailItem> opt = MailItemDao::findById(id);
if (opt) {
MailItem item = *opt;
item.setFlagged(!item.isFlagged());
if (MailItemDao::update(item)) {
statusBar()->showMessage(tr("Flag toggled"), 2000);
m_emailModel->refresh();
} else {
statusBar()->showMessage(tr("Failed to update flag"), 2000);
}
} else {
statusBar()->showMessage(tr("Email not found"), 2000);
}
});\n'''
lines = lines[:insert_point] + [flag_conn] + lines[insert_point:]
with open(filename, 'w') as f:
f.writelines(lines)
print('Delete and flag actions updated')
+240
View File
@@ -0,0 +1,240 @@
import sys
filename = 'mainmainwindow.cpp'
with open(filename, 'r') as f:
lines = f.readlines()
# 1. Modify onFolderSelected function
# Find the start of onFolderSelected
on_folder_start = None
for i, line in enumerate(lines):
if line.strip().startswith('void MainMainWindow::onFolderSelected(const QModelIndex &index)'):
on_folder_start = i
break
if on_folder_start is None:
print('Could not find onFolderSelected')
sys.exit(1)
# Find the end of the function (look for a line that is just '}' after the function start)
# We'll assume the function ends at the next line that starts with 'void ' or '}' at indentation 0? Safer: find the matching brace.
brace_count = 0
in_function = False
on_folder_end = None
for i in range(on_folder_start, len(lines)):
line = lines[i]
for ch in line:
if ch == '{':
brace_count += 1
in_function = True
elif ch == '}':
brace_count -= 1
if brace_count == 0 and in_function:
on_folder_end = i
break
if on_folder_end is not None:
break
if on_folder_end is None:
print('Could not find end of onFolderSelected')
sys.exit(1)
# Now we need to replace the content between on_folder_start+1 and on_folder_end with new implementation.
# Let's first extract the current function to see what we have.
# We'll replace from line after the opening brace? Actually we want to keep the signature and the opening brace.
# We'll replace lines from on_folder_start+1 to on_folder_end-1 with new body.
# But we need to keep the opening brace line (which is at on_folder_start? Actually the signature line is on_folder_start, the opening brace is on the same line or next?
# Look at the signature line: it ends with '{'? Let's check.
# Let's just replace the whole block from on_folder_start to on_folder_end with new function.
new_on_folder = '''void MainMainWindow::onFolderSelected(const QModelIndex &index)
{
if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
m_emailModel->setFolderId(m_currentFolderId);
// Fetch mails for this folder
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
QString accountId = QString::number(account->id());
QString folderId = QString::number(m_currentFolderId);
// Fetch mails asynchronously to avoid blocking UI
QMetaObject::invokeMethod(m_mailService, "fetchMails", Qt::QueuedConnection,
Q_ARG(QString, accountId), Q_ARG(QString, folderId));
}
}
} else if (itemType == FolderTreeItem::AccountNode) {
// When an account node is selected, select its first folder (if any)
int childCount = m_folderModel->rowCount(index);
if (childCount > 0) {
QModelIndex firstChildIdx = m_folderModel->index(0, 0, index);
m_folderTree->setCurrentIndex(firstChildIdx);
onFolderSelected(firstChildIdx); // recursive call to handle folder selection
} else {
// No folders yet; clear email list
m_currentFolderId = -1;
m_emailModel->setFolderId(m_currentFolderId);
m_emailModel->refresh();
}
}
}
'''
# Replace lines
lines = lines[:on_folder_start] + [new_on_folder] + lines[on_folder_end+1:]
# 2. Modify syncAction slot
# Find the createToolBox function? Actually we need to find the connect(syncAction, ...) line.
# Let's search for 'connect(syncAction, &QAction::triggered'
sync_connect_line = None
for i, line in enumerate(lines):
if 'connect(syncAction, &QAction::triggered' in line:
sync_connect_line = i
break
if sync_connect_line is None:
print('Could not find syncAction connect')
sys.exit(1)
# Find the end of that lambda (the matching '});' after that line)
brace_count = 0
in_lambda = False
sync_end = None
for i in range(sync_connect_line, len(lines)):
line = lines[i]
for ch in line:
if ch == '{':
brace_count += 1
in_lambda = True
elif ch == '}':
brace_count -= 1
if brace_count == 0 and in_lambda:
# Check if the line contains '});' (the end of the lambda)
if '});' in line:
sync_end = i
break
if sync_end is not None:
break
if sync_end is None:
print('Could not find end of syncAction lambda')
sys.exit(1)
# Replace the lambda body with new implementation
new_sync_lambda = ''' connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
statusBar()->showMessage(tr(\"Syncing...\"), 0); // 0 means until cleared
std::optional<Folder> optFolder = FolderDao::findById(m_currentFolderId);
if (optFolder.has_value()) {
Folder folder = optFolder.value();
Account* account = m_accountService->findAccountById(folder.accountId());
if (account) {
QString accountId = QString::number(account->id());
QString folderId = QString::number(m_currentFolderId);
// Disconnect previous connections to avoid multiple slots? We'll just call directly via queued connection.
QMetaObject::invokeMethod(m_mailService, \"fetchMails\", Qt::QueuedConnection,
Q_ARG(QString, accountId), Q_ARG(QString, folderId));
}
}
}
});'''
# Replace lines from sync_connect_line to sync_end inclusive
lines = lines[:sync_connect_line] + [new_sync_lambda] + lines[sync_end+1:]
# 3. Update mailFetched and mailFetchError lambdas in connectModels
# Find the connectModels function
connect_models_start = None
for i, line in enumerate(lines):
if line.strip().startswith('void MainMainWindow::connectModels()'):
connect_models_start = i
break
if connect_models_start is None:
print('Could not find connectModels')
sys.exit(1)
# Find end of connectModels
brace_count = 0
in_func = False
connect_models_end = None
for i in range(connect_models_start, len(lines)):
line = lines[i]
for ch in line:
if ch == '{':
brace_count += 1
in_func = True
elif ch == '}':
brace_count -= 1
if brace_count == 0 and in_func:
connect_models_end = i
break
if connect_models_end is not None:
break
if connect_models_end is None:
print('Could not find end of connectModels')
sys.exit(1)
# Within this function, we need to find the two lambdas: mailFetched and mailFetchError.
# We'll replace the whole function with a new version? That's risky.
# Instead, we'll replace the specific lambda bodies.
# Let's find the line numbers for the mailFetched connect and mailFetchError connect.
mail_fetched_line = None
mail_fetch_error_line = None
for i in range(connect_models_start, connect_models_end+1):
if 'connect(m_mailService, &MailService::mailFetched' in lines[i]:
mail_fetched_line = i
if 'connect(m_mailService, &MailService::mailFetchError' in lines[i]:
mail_fetch_error_line = i
# For each, find the end of the lambda (the '});' line)
def find_lambda_end(start_line):
brace_count = 0
in_lambda = False
for i in range(start_line, len(lines)):
line = lines[i]
for ch in line:
if ch == '{':
brace_count += 1
in_lambda = True
elif ch == '}':
brace_count -= 1
if brace_count == 0 and in_lambda:
# Check if line ends with '});'
if '});' in line:
return i
return None
mfe_end = None
mfer_end = None
if mail_fetched_line is not None:
mfe_end = find_lambda_end(mail_fetched_line)
if mail_fetch_error_line is not None:
mfer_end = find_lambda_end(mail_fetch_error_line)
if mfe_end is None or mfer_end is None:
print('Could not find lambda ends')
sys.exit(1)
# New lambda bodies
new_mfe_lambda = ''' connect(m_mailService, &MailService::mailFetched, this, [this](const QString &accountId, const QString &folderId, const QVector<MailItem> &items) {
int fid = folderId.toInt();
if (m_currentFolderId == fid || m_currentFolderId == -1) {
m_emailModel->refresh();
}
statusBar()->showMessage(tr(\"Synced %1 message(s)\").arg(items.size()), 3000);
});'''
new_mfer_lambda = ''' connect(m_mailService, &MailService::mailFetchError, this, [this](const QString &accountId, const QString &folderId, const QString &error) {
qWarning() << \"[MailFetchError]\" << error;
statusBar()->showMessage(tr(\"Error fetching mail: %1\").arg(error), 5000);
});'''
# Replace the lambdas
lines = lines[:mail_fetched_line] + [new_mfe_lambda] + lines[mfe_end+1:mail_fetch_error_line] + [new_mfer_lambda] + lines[mfer_end+1:]
# Write back
with open(filename, 'w') as f:
f.writelines(lines)
print('Updated mainmainwindow.cpp')