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:
@@ -17,24 +17,26 @@ void AccountSetupDialogLauncher::setupAccountManually(const Account &account)
|
|||||||
{
|
{
|
||||||
qDebug() << "[AccountSetupDialogLauncher] Setting up account:" << account.email();
|
qDebug() << "[AccountSetupDialogLauncher] Setting up account:" << account.email();
|
||||||
|
|
||||||
// Save to database
|
// Save to database and retain the generated id for the synchronizer/event.
|
||||||
bool created = AccountDao::insert(account);
|
const qint64 id = AccountDao::insert(account);
|
||||||
if (!created) {
|
if (id < 0) {
|
||||||
qWarning() << "[AccountSetupDialogLauncher] Failed to save account to DB";
|
qWarning() << "[AccountSetupDialogLauncher] Failed to save account to DB";
|
||||||
emit accountSetupFailed("Failed to save account");
|
emit accountSetupFailed("Failed to save account");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Account storedAccount = account;
|
||||||
|
storedAccount.setId(static_cast<int>(id));
|
||||||
|
|
||||||
// Initialize synchronizer
|
// Initialize synchronizer
|
||||||
initializeSynchronizer(account);
|
initializeSynchronizer(storedAccount);
|
||||||
|
|
||||||
// Publish event
|
// Publish event
|
||||||
WinoMail::Events::AccountAddedEvent event;
|
WinoMail::Events::AccountAddedEvent event;
|
||||||
event.account = account;
|
event.account = storedAccount;
|
||||||
EVENT_BUS.publish(event);
|
EVENT_BUS.publish(event);
|
||||||
|
|
||||||
qDebug() << "[AccountSetupDialogLauncher] Account created successfully:" << account.email();
|
qDebug() << "[AccountSetupDialogLauncher] Account created successfully:" << account.email();
|
||||||
emit accountSetupCompleted(account);
|
emit accountSetupCompleted(storedAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AccountSetupDialogLauncher::initializeSynchronizer(const Account &account)
|
void AccountSetupDialogLauncher::initializeSynchronizer(const Account &account)
|
||||||
|
|||||||
+18
-12
@@ -1,6 +1,9 @@
|
|||||||
#include "emailmanager.h"
|
#include "emailmanager.h"
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include "db/dao/mailitemdao.h"
|
||||||
|
#include "services/mimestorage.h"
|
||||||
|
|
||||||
EmailManager::EmailManager(QObject *parent)
|
EmailManager::EmailManager(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
@@ -9,29 +12,33 @@ EmailManager::EmailManager(QObject *parent)
|
|||||||
|
|
||||||
MailItem EmailManager::getMailItemById(qint64 id) const
|
MailItem EmailManager::getMailItemById(qint64 id) const
|
||||||
{
|
{
|
||||||
Q_UNUSED(id);
|
const auto item = MailItemDao::findById(id);
|
||||||
qDebug() << "[EmailManager] getMailItemById (stub)";
|
return item.value_or(MailItem());
|
||||||
return MailItem();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString EmailManager::getStorageDirectory() const
|
QString EmailManager::getStorageDirectory() const
|
||||||
{
|
{
|
||||||
qDebug() << "[EmailManager] getStorageDirectory (stub)";
|
return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/mails");
|
||||||
return QString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString EmailManager::getEmailHtmlById(qint64 id) const
|
QString EmailManager::getEmailHtmlById(qint64 id) const
|
||||||
{
|
{
|
||||||
Q_UNUSED(id);
|
const auto item = MailItemDao::findById(id);
|
||||||
qDebug() << "[EmailManager] getEmailHtmlById (stub)";
|
if (!item) return QString();
|
||||||
return QString("<html><body><p>GMime not available</p></body></html>");
|
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
|
QString EmailManager::convertEmlToHtml(const QString& emlFilePath) const
|
||||||
{
|
{
|
||||||
Q_UNUSED(emlFilePath);
|
QFile file(emlFilePath);
|
||||||
qDebug() << "[EmailManager] convertEmlToHtml (stub)";
|
if (!file.open(QIODevice::ReadOnly)) return QString();
|
||||||
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)
|
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(to);
|
||||||
Q_UNUSED(subject);
|
Q_UNUSED(subject);
|
||||||
Q_UNUSED(body);
|
Q_UNUSED(body);
|
||||||
qDebug() << "[EmailManager] sendEmail (stub)";
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,7 @@
|
|||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
|
||||||
// Gmail OAuth2 configuration
|
// Configure the OAuth application outside the source tree.
|
||||||
static const char* GMAIL_CLIENT_ID = "your-gmail-client-id.apps.googleusercontent.com";
|
|
||||||
static const char* GMAIL_CLIENT_SECRET = "your-gmail-client-secret";
|
|
||||||
static const char* GMAIL_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
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_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";
|
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)
|
: Authenticator(parent)
|
||||||
, m_callbackServer(new OAuthCallbackServer(8080, this))
|
, m_callbackServer(new OAuthCallbackServer(8080, this))
|
||||||
{
|
{
|
||||||
m_clientId = GMAIL_CLIENT_ID;
|
m_clientId = qEnvironmentVariable("WINO_GMAIL_CLIENT_ID");
|
||||||
m_clientSecret = GMAIL_CLIENT_SECRET;
|
m_clientSecret = qEnvironmentVariable("WINO_GMAIL_CLIENT_SECRET");
|
||||||
m_authEndpoint = GMAIL_AUTH_URL;
|
m_authEndpoint = GMAIL_AUTH_URL;
|
||||||
m_tokenEndpoint = GMAIL_TOKEN_URL;
|
m_tokenEndpoint = GMAIL_TOKEN_URL;
|
||||||
m_scopes = GMAIL_SCOPES;
|
m_scopes = GMAIL_SCOPES;
|
||||||
@@ -31,6 +29,10 @@ GmailAuthenticator::GmailAuthenticator(QObject *parent)
|
|||||||
|
|
||||||
void GmailAuthenticator::authenticate(const QString &email)
|
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_email = email;
|
||||||
m_redirectUri = m_callbackServer->redirectUri();
|
m_redirectUri = m_callbackServer->redirectUri();
|
||||||
qDebug() << "[GmailAuthenticator] Starting authentication for:" << email;
|
qDebug() << "[GmailAuthenticator] Starting authentication for:" << email;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
|
#include <QByteArray>
|
||||||
|
|
||||||
class MailItem
|
class MailItem
|
||||||
{
|
{
|
||||||
@@ -68,6 +69,12 @@ public:
|
|||||||
QString bcc() const { return m_bcc; }
|
QString bcc() const { return m_bcc; }
|
||||||
void setBcc(const QString& bcc) { m_bcc = 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:
|
private:
|
||||||
qint64 m_id{0};
|
qint64 m_id{0};
|
||||||
int m_folderId{0};
|
int m_folderId{0};
|
||||||
@@ -86,4 +93,5 @@ private:
|
|||||||
QString m_to;
|
QString m_to;
|
||||||
QString m_cc;
|
QString m_cc;
|
||||||
QString m_bcc;
|
QString m_bcc;
|
||||||
|
QByteArray m_rawMime;
|
||||||
};
|
};
|
||||||
+68
-17
@@ -1,4 +1,35 @@
|
|||||||
#include "account.h"
|
#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,
|
Account::Account(int id, const QString& email, const QString& displayName,
|
||||||
const QString& signature,
|
const QString& signature,
|
||||||
@@ -18,27 +49,47 @@ Account::Account(int id, const QString& email, const QString& displayName,
|
|||||||
|
|
||||||
QString Account::encryptPassword(const QString& plainText)
|
QString Account::encryptPassword(const QString& plainText)
|
||||||
{
|
{
|
||||||
// Simple XOR encryption with a fixed key, then base64 encode
|
const QByteArray input = plainText.toUtf8();
|
||||||
// Note: This is for obfuscation only, not strong security.
|
#ifdef Q_OS_WIN
|
||||||
static const char keyHex[] = "0123456789ABCDEF"; // 16-byte key
|
DATA_BLOB source{static_cast<DWORD>(input.size()), reinterpret_cast<BYTE*>(const_cast<char*>(input.constData()))};
|
||||||
QByteArray key;
|
DATA_BLOB protectedData{};
|
||||||
bool ok;
|
if (CryptProtectData(&source, L"Wino Mail password", nullptr, nullptr, nullptr,
|
||||||
for (int i = 0; i < 16; ++i) {
|
CRYPTPROTECT_UI_FORBIDDEN, &protectedData)) {
|
||||||
char c = QString::fromLatin1(keyHex + i * 2, 2).toInt(&ok, 16);
|
const QByteArray output(reinterpret_cast<const char*>(protectedData.pbData), protectedData.cbData);
|
||||||
if (!ok) c = 0;
|
LocalFree(protectedData.pbData);
|
||||||
key.append(c);
|
return QStringLiteral("dpapi:") + QString::fromLatin1(output.toBase64());
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
QByteArray input = plainText.toUtf8();
|
const QByteArray key = localPasswordKey();
|
||||||
QByteArray result(input.size(), 0);
|
QByteArray result(input.size(), 0);
|
||||||
for (int i = 0; i < input.size(); ++i) {
|
for (int i = 0; i < input.size(); ++i) result[i] = input[i] ^ key.at(i % key.size());
|
||||||
result[i] = input[i] ^ key[i % key.size()];
|
return QStringLiteral("local:") + QString::fromLatin1(result.toBase64());
|
||||||
}
|
|
||||||
return result.toBase64();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Account::decryptPassword(const QString& encrypted)
|
QString Account::decryptPassword(const QString& encrypted)
|
||||||
{
|
{
|
||||||
// Same as encryption since XOR is symmetric
|
if (encrypted.startsWith(QStringLiteral("dpapi:"))) {
|
||||||
return encryptPassword(encrypted);
|
#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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ public:
|
|||||||
const ConnectionSettings& connectionSettings() const { return m_connectionSettings; }
|
const ConnectionSettings& connectionSettings() const { return m_connectionSettings; }
|
||||||
void setConnectionSettings(const ConnectionSettings& settings) { m_connectionSettings = settings; }
|
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 encryptPassword(const QString& plainText);
|
||||||
static QString decryptPassword(const QString& encrypted);
|
static QString decryptPassword(const QString& encrypted);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -4,9 +4,7 @@
|
|||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
|
||||||
// Microsoft Graph OAuth2 configuration
|
// Configure the OAuth application outside the source tree.
|
||||||
static const char* OUTLOOK_CLIENT_ID = "your-outlook-client-id";
|
|
||||||
static const char* OUTLOOK_CLIENT_SECRET = "your-outlook-client-secret";
|
|
||||||
static const char* OUTLOOK_AUTH_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
|
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_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
|
||||||
static const char* OUTLOOK_SCOPES = "offline_access Mail.ReadWrite Mail.Send User.Read";
|
static const char* OUTLOOK_SCOPES = "offline_access Mail.ReadWrite Mail.Send User.Read";
|
||||||
@@ -15,8 +13,8 @@ OutlookAuthenticator::OutlookAuthenticator(QObject *parent)
|
|||||||
: Authenticator(parent)
|
: Authenticator(parent)
|
||||||
, m_callbackServer(new OAuthCallbackServer(8080, this))
|
, m_callbackServer(new OAuthCallbackServer(8080, this))
|
||||||
{
|
{
|
||||||
m_clientId = OUTLOOK_CLIENT_ID;
|
m_clientId = qEnvironmentVariable("WINO_OUTLOOK_CLIENT_ID");
|
||||||
m_clientSecret = OUTLOOK_CLIENT_SECRET;
|
m_clientSecret = qEnvironmentVariable("WINO_OUTLOOK_CLIENT_SECRET");
|
||||||
m_authEndpoint = OUTLOOK_AUTH_URL;
|
m_authEndpoint = OUTLOOK_AUTH_URL;
|
||||||
m_tokenEndpoint = OUTLOOK_TOKEN_URL;
|
m_tokenEndpoint = OUTLOOK_TOKEN_URL;
|
||||||
m_scopes = OUTLOOK_SCOPES;
|
m_scopes = OUTLOOK_SCOPES;
|
||||||
@@ -31,6 +29,10 @@ OutlookAuthenticator::OutlookAuthenticator(QObject *parent)
|
|||||||
|
|
||||||
void OutlookAuthenticator::authenticate(const QString &email)
|
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_email = email;
|
||||||
m_redirectUri = m_callbackServer->redirectUri();
|
m_redirectUri = m_callbackServer->redirectUri();
|
||||||
qDebug() << "[OutlookAuthenticator] Starting authentication for:" << email;
|
qDebug() << "[OutlookAuthenticator] Starting authentication for:" << email;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
qint64 AccountDao::insert(const Account& account)
|
qint64 AccountDao::insert(const Account& account)
|
||||||
{
|
{
|
||||||
QSqlDatabase& db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
query.prepare(
|
query.prepare(
|
||||||
"INSERT INTO Account (email, displayName, signature, type, accessToken, refreshToken, tokenExpires, "
|
"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)
|
bool AccountDao::update(const Account& account)
|
||||||
{
|
{
|
||||||
QSqlDatabase& db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
query.prepare(
|
query.prepare(
|
||||||
"UPDATE Account SET "
|
"UPDATE Account SET "
|
||||||
"email = :email, "
|
"email = :email, "
|
||||||
"displayName = :displayName, "
|
"displayName = :displayName, "
|
||||||
|
"signature = :signature, "
|
||||||
"type = :type, "
|
"type = :type, "
|
||||||
"accessToken = :accessToken, "
|
"accessToken = :accessToken, "
|
||||||
"refreshToken = :refreshToken, "
|
"refreshToken = :refreshToken, "
|
||||||
@@ -66,6 +67,7 @@ bool AccountDao::update(const Account& account)
|
|||||||
query.bindValue(":type", static_cast<int>(account.type()));
|
query.bindValue(":type", static_cast<int>(account.type()));
|
||||||
query.bindValue(":accessToken", account.accessToken());
|
query.bindValue(":accessToken", account.accessToken());
|
||||||
query.bindValue(":refreshToken", account.refreshToken());
|
query.bindValue(":refreshToken", account.refreshToken());
|
||||||
|
query.bindValue(":tokenExpires", account.tokenExpires());
|
||||||
const auto& settings = account.connectionSettings();
|
const auto& settings = account.connectionSettings();
|
||||||
query.bindValue(":incomingHost", settings.incomingHost);
|
query.bindValue(":incomingHost", settings.incomingHost);
|
||||||
query.bindValue(":incomingPort", settings.incomingPort);
|
query.bindValue(":incomingPort", settings.incomingPort);
|
||||||
@@ -86,7 +88,7 @@ bool AccountDao::update(const Account& account)
|
|||||||
|
|
||||||
bool AccountDao::remove(int id)
|
bool AccountDao::remove(int id)
|
||||||
{
|
{
|
||||||
QSqlDatabase& db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
query.prepare("DELETE FROM Account WHERE id = :id");
|
query.prepare("DELETE FROM Account WHERE id = :id");
|
||||||
query.bindValue(":id", id);
|
query.bindValue(":id", id);
|
||||||
@@ -100,10 +102,10 @@ bool AccountDao::remove(int id)
|
|||||||
|
|
||||||
Account* AccountDao::findById(int id)
|
Account* AccountDao::findById(int id)
|
||||||
{
|
{
|
||||||
QSqlDatabase& db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
query.prepare(
|
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, "
|
"incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, "
|
||||||
"connUsername, connPassword, authMethod "
|
"connUsername, connPassword, authMethod "
|
||||||
"FROM Account WHERE id = :id");
|
"FROM Account WHERE id = :id");
|
||||||
@@ -144,10 +146,10 @@ Account* AccountDao::findById(int id)
|
|||||||
QVector<Account> AccountDao::findAll()
|
QVector<Account> AccountDao::findAll()
|
||||||
{
|
{
|
||||||
QVector<Account> accounts;
|
QVector<Account> accounts;
|
||||||
QSqlDatabase& db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
if (!query.exec(
|
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, "
|
"incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, "
|
||||||
"connUsername, connPassword, authMethod "
|
"connUsername, connPassword, authMethod "
|
||||||
"FROM Account")) {
|
"FROM Account")) {
|
||||||
@@ -160,6 +162,7 @@ QVector<Account> AccountDao::findAll()
|
|||||||
acc.setId(query.value("id").toInt());
|
acc.setId(query.value("id").toInt());
|
||||||
acc.setEmail(query.value("email").toString());
|
acc.setEmail(query.value("email").toString());
|
||||||
acc.setDisplayName(query.value("displayName").toString());
|
acc.setDisplayName(query.value("displayName").toString());
|
||||||
|
acc.setSignature(query.value("signature").toString());
|
||||||
acc.setType(static_cast<AccountType>(query.value("type").toInt()));
|
acc.setType(static_cast<AccountType>(query.value("type").toInt()));
|
||||||
acc.setAccessToken(query.value("accessToken").toString());
|
acc.setAccessToken(query.value("accessToken").toString());
|
||||||
acc.setRefreshToken(query.value("refreshToken").toString());
|
acc.setRefreshToken(query.value("refreshToken").toString());
|
||||||
@@ -182,10 +185,10 @@ QVector<Account> AccountDao::findAll()
|
|||||||
|
|
||||||
Account* AccountDao::findByEmail(const QString& email)
|
Account* AccountDao::findByEmail(const QString& email)
|
||||||
{
|
{
|
||||||
QSqlDatabase& db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
query.prepare(
|
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, "
|
"incomingHost, incomingPort, incomingSsl, outgoingHost, outgoingPort, outgoingSsl, "
|
||||||
"connUsername, connPassword, authMethod "
|
"connUsername, connPassword, authMethod "
|
||||||
"FROM Account WHERE email = :email");
|
"FROM Account WHERE email = :email");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
@@ -4,34 +4,9 @@
|
|||||||
#include <QSqlError>
|
#include <QSqlError>
|
||||||
#include <optional>
|
#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)
|
bool FolderDao::update(const Folder& folder)
|
||||||
{
|
{
|
||||||
QSqlDatabase& db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
query.prepare(
|
query.prepare(
|
||||||
"UPDATE Folder SET "
|
"UPDATE Folder SET "
|
||||||
@@ -66,7 +41,7 @@ bool FolderDao::update(const Folder& folder)
|
|||||||
|
|
||||||
bool FolderDao::remove(int id)
|
bool FolderDao::remove(int id)
|
||||||
{
|
{
|
||||||
QSqlDatabase& db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
query.prepare("DELETE FROM Folder WHERE id = :id");
|
query.prepare("DELETE FROM Folder WHERE id = :id");
|
||||||
query.bindValue(":id", id);
|
query.bindValue(":id", id);
|
||||||
@@ -80,7 +55,7 @@ bool FolderDao::remove(int id)
|
|||||||
|
|
||||||
std::optional<Folder> FolderDao::findById(int id)
|
std::optional<Folder> FolderDao::findById(int id)
|
||||||
{
|
{
|
||||||
QSqlDatabase& db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
query.prepare("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder WHERE id = :id");
|
query.prepare("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder WHERE id = :id");
|
||||||
query.bindValue(":id", id);
|
query.bindValue(":id", id);
|
||||||
@@ -110,7 +85,7 @@ std::optional<Folder> FolderDao::findById(int id)
|
|||||||
QVector<Folder> FolderDao::findAll()
|
QVector<Folder> FolderDao::findAll()
|
||||||
{
|
{
|
||||||
QVector<Folder> folders;
|
QVector<Folder> folders;
|
||||||
QSqlDatabase& db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
if (!query.exec("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder")) {
|
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();
|
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> FolderDao::findByAccountId(int accountId)
|
||||||
{
|
{
|
||||||
QVector<Folder> folders;
|
QVector<Folder> folders;
|
||||||
QSqlDatabase& db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
query.prepare("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder WHERE accountId = :accountId");
|
query.prepare("SELECT id, accountId, name, parentFolderId, isInbox, isSent, isDrafts, isTrash, unreadCount, lastSynced FROM Folder WHERE accountId = :accountId");
|
||||||
query.bindValue(":accountId", accountId);
|
query.bindValue(":accountId", accountId);
|
||||||
@@ -166,7 +141,7 @@ QVector<Folder> FolderDao::findByAccountId(int accountId)
|
|||||||
|
|
||||||
bool FolderDao::removeByAccountId(int accountId)
|
bool FolderDao::removeByAccountId(int accountId)
|
||||||
{
|
{
|
||||||
QSqlDatabase& db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
query.prepare("DELETE FROM Folder WHERE accountId = :accountId");
|
query.prepare("DELETE FROM Folder WHERE accountId = :accountId");
|
||||||
query.bindValue(":accountId", accountId);
|
query.bindValue(":accountId", accountId);
|
||||||
@@ -177,3 +152,28 @@ bool FolderDao::removeByAccountId(int accountId)
|
|||||||
}
|
}
|
||||||
return true;
|
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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
class FolderDao
|
class FolderDao
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static bool insert(const Folder& folder);
|
static qint64 insert(const Folder& folder);
|
||||||
static bool update(const Folder& folder);
|
static bool update(const Folder& folder);
|
||||||
static bool remove(int id);
|
static bool remove(int id);
|
||||||
static bool removeByAccountId(int accountId);
|
static bool removeByAccountId(int accountId);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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
@@ -1,18 +1,38 @@
|
|||||||
#include <QSqlQuery>
|
#include <QSqlQuery>
|
||||||
#include <QSqlError>
|
#include <QSqlError>
|
||||||
#include "mailitemdao.h"
|
#include "mailitemdao.h"
|
||||||
|
#include <QDebug>
|
||||||
#include <optional>
|
#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);
|
QSqlQuery query(db);
|
||||||
query.prepare(
|
query.prepare(
|
||||||
"INSERT INTO MailCopy (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)"
|
"VALUES (:folderId, :messageId, :subject, :sender, :recipient, :date, :read, :flagged, :hasAttachment, :size, :fileId, :uid, :bodyHtml, :toAddr, :ccAddr, :bccAddr)"
|
||||||
);
|
);
|
||||||
query.bindValue(":folderId", item.folderId());
|
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(":subject", item.subject());
|
||||||
query.bindValue(":sender", item.sender());
|
query.bindValue(":sender", item.sender());
|
||||||
query.bindValue(":recipient", item.recipient());
|
query.bindValue(":recipient", item.recipient());
|
||||||
@@ -23,17 +43,27 @@ bool MailItemDao::insert(const MailItem& item)
|
|||||||
query.bindValue(":size", item.size());
|
query.bindValue(":size", item.size());
|
||||||
query.bindValue(":fileId", item.fileId());
|
query.bindValue(":fileId", item.fileId());
|
||||||
query.bindValue(":uid", item.uid());
|
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()) {
|
if (!query.exec()) {
|
||||||
qWarning() << "Failed to insert mail item:" << query.lastError().text();
|
qWarning() << "Failed to insert mail item:" << query.lastError().text();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
item.setId(query.lastInsertId().toLongLong());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MailItemDao::upsert(MailItem& item)
|
||||||
|
{
|
||||||
|
return insert(item);
|
||||||
|
}
|
||||||
|
|
||||||
bool MailItemDao::update(const MailItem& item)
|
bool MailItemDao::update(const MailItem& item)
|
||||||
{
|
{
|
||||||
QSqlDatabase& db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
query.prepare(
|
query.prepare(
|
||||||
"UPDATE MailCopy SET "
|
"UPDATE MailCopy SET "
|
||||||
@@ -48,12 +78,16 @@ bool MailItemDao::update(const MailItem& item)
|
|||||||
"hasAttachment = :hasAttachment, "
|
"hasAttachment = :hasAttachment, "
|
||||||
"size = :size, "
|
"size = :size, "
|
||||||
"fileId = :fileId, "
|
"fileId = :fileId, "
|
||||||
"uid = :uid "
|
"uid = :uid, "
|
||||||
|
"bodyHtml = :bodyHtml, "
|
||||||
|
"toAddr = :toAddr, "
|
||||||
|
"ccAddr = :ccAddr, "
|
||||||
|
"bccAddr = :bccAddr "
|
||||||
"WHERE id = :id"
|
"WHERE id = :id"
|
||||||
);
|
);
|
||||||
query.bindValue(":id", item.id());
|
query.bindValue(":id", item.id());
|
||||||
query.bindValue(":folderId", item.folderId());
|
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(":subject", item.subject());
|
||||||
query.bindValue(":sender", item.sender());
|
query.bindValue(":sender", item.sender());
|
||||||
query.bindValue(":recipient", item.recipient());
|
query.bindValue(":recipient", item.recipient());
|
||||||
@@ -64,6 +98,10 @@ bool MailItemDao::update(const MailItem& item)
|
|||||||
query.bindValue(":size", item.size());
|
query.bindValue(":size", item.size());
|
||||||
query.bindValue(":fileId", item.fileId());
|
query.bindValue(":fileId", item.fileId());
|
||||||
query.bindValue(":uid", item.uid());
|
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()) {
|
if (!query.exec()) {
|
||||||
qWarning() << "Failed to update mail item:" << query.lastError().text();
|
qWarning() << "Failed to update mail item:" << query.lastError().text();
|
||||||
@@ -74,7 +112,7 @@ bool MailItemDao::update(const MailItem& item)
|
|||||||
|
|
||||||
bool MailItemDao::remove(qint64 id)
|
bool MailItemDao::remove(qint64 id)
|
||||||
{
|
{
|
||||||
QSqlDatabase& db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
query.prepare("DELETE FROM MailCopy WHERE id = :id");
|
query.prepare("DELETE FROM MailCopy WHERE id = :id");
|
||||||
query.bindValue(":id", id);
|
query.bindValue(":id", id);
|
||||||
@@ -88,9 +126,9 @@ bool MailItemDao::remove(qint64 id)
|
|||||||
|
|
||||||
std::optional<MailItem> MailItemDao::findById(qint64 id)
|
std::optional<MailItem> MailItemDao::findById(qint64 id)
|
||||||
{
|
{
|
||||||
QSqlDatabase& db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
QSqlQuery query(db);
|
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);
|
query.bindValue(":id", id);
|
||||||
|
|
||||||
if (!query.exec()) {
|
if (!query.exec()) {
|
||||||
@@ -112,6 +150,13 @@ std::optional<MailItem> MailItemDao::findById(qint64 id)
|
|||||||
item.setSize(query.value(10).toLongLong());
|
item.setSize(query.value(10).toLongLong());
|
||||||
item.setFileId(query.value(11).toString());
|
item.setFileId(query.value(11).toString());
|
||||||
item.setUid(query.value(12).toLongLong());
|
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 item;
|
||||||
}
|
}
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
@@ -120,9 +165,9 @@ std::optional<MailItem> MailItemDao::findById(qint64 id)
|
|||||||
QVector<MailItem> MailItemDao::findAll()
|
QVector<MailItem> MailItemDao::findAll()
|
||||||
{
|
{
|
||||||
QVector<MailItem> items;
|
QVector<MailItem> items;
|
||||||
QSqlDatabase& db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
QSqlQuery query(db);
|
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();
|
qWarning() << "Failed to fetch all mail items:" << query.lastError().text();
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -141,6 +186,13 @@ QVector<MailItem> MailItemDao::findAll()
|
|||||||
item.setSize(query.value(10).toLongLong());
|
item.setSize(query.value(10).toLongLong());
|
||||||
item.setFileId(query.value(11).toString());
|
item.setFileId(query.value(11).toString());
|
||||||
item.setUid(query.value(12).toLongLong());
|
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);
|
items.append(item);
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
@@ -149,9 +201,9 @@ QVector<MailItem> MailItemDao::findAll()
|
|||||||
QVector<MailItem> MailItemDao::findByFolderId(int folderId)
|
QVector<MailItem> MailItemDao::findByFolderId(int folderId)
|
||||||
{
|
{
|
||||||
QVector<MailItem> items;
|
QVector<MailItem> items;
|
||||||
QSqlDatabase& db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
QSqlQuery query(db);
|
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);
|
query.bindValue(":folderId", folderId);
|
||||||
|
|
||||||
if (!query.exec()) {
|
if (!query.exec()) {
|
||||||
@@ -173,6 +225,13 @@ QVector<MailItem> MailItemDao::findByFolderId(int folderId)
|
|||||||
item.setSize(query.value(10).toLongLong());
|
item.setSize(query.value(10).toLongLong());
|
||||||
item.setFileId(query.value(11).toString());
|
item.setFileId(query.value(11).toString());
|
||||||
item.setUid(query.value(12).toLongLong());
|
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);
|
items.append(item);
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
@@ -181,9 +240,9 @@ QVector<MailItem> MailItemDao::findByFolderId(int folderId)
|
|||||||
QVector<MailItem> MailItemDao::findByFolderIdSinceUid(int folderId, qint64 sinceUid)
|
QVector<MailItem> MailItemDao::findByFolderIdSinceUid(int folderId, qint64 sinceUid)
|
||||||
{
|
{
|
||||||
QVector<MailItem> items;
|
QVector<MailItem> items;
|
||||||
QSqlDatabase& db = DatabaseManager::instance().database();
|
QSqlDatabase db = DatabaseManager::instance().database();
|
||||||
QSqlQuery query(db);
|
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(":folderId", folderId);
|
||||||
query.bindValue(":sinceUid", sinceUid);
|
query.bindValue(":sinceUid", sinceUid);
|
||||||
|
|
||||||
@@ -206,7 +265,132 @@ QVector<MailItem> MailItemDao::findByFolderIdSinceUid(int folderId, qint64 since
|
|||||||
item.setSize(query.value(10).toLongLong());
|
item.setSize(query.value(10).toLongLong());
|
||||||
item.setFileId(query.value(11).toString());
|
item.setFileId(query.value(11).toString());
|
||||||
item.setUid(query.value(12).toLongLong());
|
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);
|
items.append(item);
|
||||||
}
|
}
|
||||||
return items;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -5,14 +5,31 @@
|
|||||||
#include <QVector>
|
#include <QVector>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
|
struct StoredAttachmentRecord
|
||||||
|
{
|
||||||
|
QString fileName;
|
||||||
|
QString mimeType;
|
||||||
|
QString contentId;
|
||||||
|
qint64 size{0};
|
||||||
|
QString storedPath;
|
||||||
|
};
|
||||||
|
|
||||||
class MailItemDao
|
class MailItemDao
|
||||||
{
|
{
|
||||||
public:
|
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 update(const MailItem& item);
|
||||||
static bool remove(qint64 id);
|
static bool remove(qint64 id);
|
||||||
static std::optional<MailItem> findById(qint64 id);
|
static std::optional<MailItem> findById(qint64 id);
|
||||||
static QVector<MailItem> findAll();
|
static QVector<MailItem> findAll();
|
||||||
static QVector<MailItem> findByFolderId(int folderId);
|
static QVector<MailItem> findByFolderId(int folderId);
|
||||||
static QVector<MailItem> findByFolderIdSinceUid(int folderId, qint64 sinceUid);
|
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);
|
||||||
};
|
};
|
||||||
@@ -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)
|
||||||
@@ -37,6 +37,11 @@ bool DatabaseManager::initialize(const QString& databasePath)
|
|||||||
dbPath = dir.filePath("wino-mail.sqlite");
|
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 = QSqlDatabase::addDatabase("QSQLITE", "wino_mail_connection");
|
||||||
m_db.setDatabaseName(dbPath);
|
m_db.setDatabaseName(dbPath);
|
||||||
|
|
||||||
@@ -45,18 +50,22 @@ bool DatabaseManager::initialize(const QString& databasePath)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
qDebug() << "[DatabaseManager] Database opened successfully";
|
||||||
|
|
||||||
QSqlQuery query(m_db);
|
QSqlQuery query(m_db);
|
||||||
// Enable foreign keys
|
// Enable foreign keys
|
||||||
|
qDebug() << "[DatabaseManager] Enabling foreign keys";
|
||||||
query.exec("PRAGMA foreign_keys = ON;");
|
query.exec("PRAGMA foreign_keys = ON;");
|
||||||
|
|
||||||
// Create tables if they don't exist
|
// Create tables if they don't exist
|
||||||
|
qDebug() << "[DatabaseManager] Creating Account table";
|
||||||
|
|
||||||
// Account table
|
// Account table
|
||||||
if (!query.exec(
|
if (!query.exec(
|
||||||
"CREATE TABLE IF NOT EXISTS Account ("
|
"CREATE TABLE IF NOT EXISTS Account ("
|
||||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||||
"email TEXT NOT NULL, "
|
"email TEXT NOT NULL, "
|
||||||
"displayName TEXT, ""signature TEXT, "
|
"displayName TEXT, signature TEXT, "
|
||||||
"type INTEGER NOT NULL, " // 0=Outlook,1=Gmail,2=IMAP
|
"type INTEGER NOT NULL, " // 0=Outlook,1=Gmail,2=IMAP
|
||||||
"accessToken TEXT, "
|
"accessToken TEXT, "
|
||||||
"refreshToken TEXT, "
|
"refreshToken TEXT, "
|
||||||
@@ -74,6 +83,7 @@ bool DatabaseManager::initialize(const QString& databasePath)
|
|||||||
qWarning() << "Failed to create Account table:" << query.lastError().text();
|
qWarning() << "Failed to create Account table:" << query.lastError().text();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
qDebug() << "[DatabaseManager] Account table created successfully";
|
||||||
|
|
||||||
// Folder table
|
// Folder table
|
||||||
if (!query.exec(
|
if (!query.exec(
|
||||||
@@ -110,6 +120,10 @@ bool DatabaseManager::initialize(const QString& databasePath)
|
|||||||
"size INTEGER, "
|
"size INTEGER, "
|
||||||
"uid INTEGER, "
|
"uid INTEGER, "
|
||||||
"fileId TEXT, " // references the .eml file in storage
|
"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"
|
"FOREIGN KEY(folderId) REFERENCES Folder(id) ON DELETE CASCADE"
|
||||||
");")) {
|
");")) {
|
||||||
qWarning() << "Failed to create MailCopy table:" << query.lastError().text();
|
qWarning() << "Failed to create MailCopy table:" << query.lastError().text();
|
||||||
@@ -125,18 +139,52 @@ bool DatabaseManager::initialize(const QString& databasePath)
|
|||||||
"mimeType TEXT, "
|
"mimeType TEXT, "
|
||||||
"size INTEGER, "
|
"size INTEGER, "
|
||||||
"contentId TEXT, "
|
"contentId TEXT, "
|
||||||
|
"storedPath TEXT, "
|
||||||
"FOREIGN KEY(mailCopyId) REFERENCES MailCopy(id) ON DELETE CASCADE"
|
"FOREIGN KEY(mailCopyId) REFERENCES MailCopy(id) ON DELETE CASCADE"
|
||||||
");")) {
|
");")) {
|
||||||
qWarning() << "Failed to create Attachment table:" << query.lastError().text();
|
qWarning() << "Failed to create Attachment table:" << query.lastError().text();
|
||||||
return false;
|
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;
|
m_initialized = true;
|
||||||
qDebug() << "Database initialized at:" << dbPath;
|
qDebug() << "Database initialized at:" << dbPath;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
QSqlDatabase& DatabaseManager::database()
|
QSqlDatabase DatabaseManager::database()
|
||||||
{
|
{
|
||||||
|
if (QThread::currentThreadId() == m_ownerThread)
|
||||||
return m_db;
|
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;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <QSqlDatabase>
|
#include <QSqlDatabase>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
|
#include <QThread>
|
||||||
|
|
||||||
class DatabaseManager
|
class DatabaseManager
|
||||||
{
|
{
|
||||||
@@ -10,8 +11,10 @@ public:
|
|||||||
static DatabaseManager& instance();
|
static DatabaseManager& instance();
|
||||||
~DatabaseManager();
|
~DatabaseManager();
|
||||||
|
|
||||||
bool initialize(const QString& databasePath = QStringLiteral("wino-mail.sqlite"));
|
bool initialize(const QString& databasePath = QString());
|
||||||
QSqlDatabase& database();
|
// Returns a connection owned by the calling thread. SQLite/Qt database
|
||||||
|
// connections must never be shared across threads.
|
||||||
|
QSqlDatabase database();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
DatabaseManager();
|
DatabaseManager();
|
||||||
@@ -19,5 +22,7 @@ private:
|
|||||||
|
|
||||||
QSqlDatabase m_db;
|
QSqlDatabase m_db;
|
||||||
QMutex m_mutex;
|
QMutex m_mutex;
|
||||||
|
QString m_databasePath;
|
||||||
|
Qt::HANDLE m_ownerThread{nullptr};
|
||||||
bool m_initialized{false};
|
bool m_initialized{false};
|
||||||
};
|
};
|
||||||
@@ -123,7 +123,8 @@ void DbChangeProcessor::processBatch()
|
|||||||
if (!m_mailItemAddedQueue.isEmpty()) {
|
if (!m_mailItemAddedQueue.isEmpty()) {
|
||||||
qDebug() << "DbChangeProcessor: Processing" << m_mailItemAddedQueue.size() << "MailItemAdded events";
|
qDebug() << "DbChangeProcessor: Processing" << m_mailItemAddedQueue.size() << "MailItemAdded events";
|
||||||
for (const auto& event : m_mailItemAddedQueue) {
|
for (const auto& event : m_mailItemAddedQueue) {
|
||||||
MailItemDao::insert(event.item);
|
MailItem item = event.item;
|
||||||
|
MailItemDao::insert(item);
|
||||||
}
|
}
|
||||||
m_mailItemAddedQueue.clear();
|
m_mailItemAddedQueue.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -19,19 +19,34 @@ int main(int argc, char *argv[])
|
|||||||
app.setApplicationVersion("1.0.0");
|
app.setApplicationVersion("1.0.0");
|
||||||
app.setOrganizationName("Wino");
|
app.setOrganizationName("Wino");
|
||||||
|
|
||||||
|
qDebug() << "[main] Starting Wino Mail";
|
||||||
|
|
||||||
// Initialize subsystems
|
// 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();
|
EventBus &bus = EventBus::instance();
|
||||||
|
qDebug() << "[main] EventBus created";
|
||||||
NotificationManager notificationManager(&bus);
|
NotificationManager notificationManager(&bus);
|
||||||
|
qDebug() << "[main] NotificationManager created";
|
||||||
|
|
||||||
// Start background sync
|
// Start background sync
|
||||||
|
qDebug() << "[main] Creating SyncScheduler";
|
||||||
SyncScheduler syncScheduler(&bus);
|
SyncScheduler syncScheduler(&bus);
|
||||||
syncScheduler.start();
|
qDebug() << "[main] Starting SyncScheduler";
|
||||||
|
// syncScheduler.start(); // Temporarily disabled for debugging
|
||||||
|
qDebug() << "[main] SyncScheduler created (not started)";
|
||||||
|
|
||||||
// Create main window
|
// Create main window
|
||||||
|
qDebug() << "[main] Creating MainMainWindow";
|
||||||
MainMainWindow window;
|
MainMainWindow window;
|
||||||
|
qDebug() << "[main] MainMainWindow created, showing window";
|
||||||
window.show();
|
window.show();
|
||||||
|
qDebug() << "[main] Window shown, entering event loop";
|
||||||
|
|
||||||
qDebug() << "Wino Mail started successfully";
|
qDebug() << "Wino Mail started successfully";
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include <QSslSocket>
|
#include <QSslSocket>
|
||||||
#include "db/dao/accountdao.h"
|
#include "db/dao/accountdao.h"
|
||||||
#include "db/dao/folderdao.h"
|
#include "db/dao/folderdao.h"
|
||||||
|
#include "db/dao/mailitemdao.h"
|
||||||
|
#include "services/mimestorage.h"
|
||||||
#include "core/authenticator.h"
|
#include "core/authenticator.h"
|
||||||
#include "core/gmailauthenticator.h"
|
#include "core/gmailauthenticator.h"
|
||||||
#include "core/outlookauthenticator.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 == "Sent") folder.setSent(true);
|
||||||
else if (folderName == "Drafts") folder.setDrafts(true);
|
else if (folderName == "Drafts") folder.setDrafts(true);
|
||||||
else if (folderName == "Trash") folder.setTrash(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()
|
qWarning() << "[AccountService] Failed to insert folder" << folder.name()
|
||||||
<< "for account ID" << accToAdd.id();
|
<< "for account ID" << accToAdd.id();
|
||||||
}
|
}
|
||||||
@@ -100,6 +102,12 @@ void AccountService::removeAccount(int accountId)
|
|||||||
{
|
{
|
||||||
Account *account = AccountDao::findById(accountId);
|
Account *account = AccountDao::findById(accountId);
|
||||||
if (account) {
|
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));
|
SynchronizerProvider::instance().unregisterSynchronizer(QString::number(accountId));
|
||||||
AccountDao::remove(accountId);
|
AccountDao::remove(accountId);
|
||||||
publishAccountEvent(*account, false);
|
publishAccountEvent(*account, false);
|
||||||
@@ -124,10 +132,7 @@ void AccountService::startAuthentication(const QString &email, const QString &pr
|
|||||||
Authenticator *auth = createAuthenticator(providerType);
|
Authenticator *auth = createAuthenticator(providerType);
|
||||||
if (auth) {
|
if (auth) {
|
||||||
connect(auth, &Authenticator::authenticationCompleted, this, [this](const Account &account) {
|
connect(auth, &Authenticator::authenticationCompleted, this, [this](const Account &account) {
|
||||||
addAccount(account.email(),
|
addAccount(account);
|
||||||
account.type() == AccountType::Gmail ? "gmail"
|
|
||||||
: account.type() == AccountType::Outlook ? "outlook" : "imap",
|
|
||||||
account.accessToken(), account.refreshToken());
|
|
||||||
});
|
});
|
||||||
connect(auth, &Authenticator::authenticationFailed, this, [](const QString &error) {
|
connect(auth, &Authenticator::authenticationFailed, this, [](const QString &error) {
|
||||||
qWarning() << "[AccountService] Authentication failed:" << error;
|
qWarning() << "[AccountService] Authentication failed:" << error;
|
||||||
@@ -239,7 +244,7 @@ void AccountService::syncFoldersForAccount(const Account &account)
|
|||||||
folder.setAccountId(account.id());
|
folder.setAccountId(account.id());
|
||||||
// Ensure the folder has an invalid ID (0) so DB assigns new one
|
// Ensure the folder has an invalid ID (0) so DB assigns new one
|
||||||
folder.setId(0);
|
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;
|
qWarning() << QStringLiteral("[AccountService] Failed to insert folder") << folder.name() << QStringLiteral("for account") << accountIdStr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 re‑initialized 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"
|
||||||
@@ -11,8 +11,11 @@
|
|||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QEventLoop>
|
#include <QEventLoop>
|
||||||
|
#include <QNetworkRequest>
|
||||||
#include "../../core/events.h"
|
#include "../../core/events.h"
|
||||||
#include "../../core/eventbus.h"
|
#include "../../core/eventbus.h"
|
||||||
|
#include "db/dao/folderdao.h"
|
||||||
|
#include "services/mimestorage.h"
|
||||||
|
|
||||||
GmailSynchronizer::GmailSynchronizer(QObject* parent)
|
GmailSynchronizer::GmailSynchronizer(QObject* parent)
|
||||||
: Synchronizer(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
|
// 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
|
// y emitiríamos las señales apropiadas para elementos nuevos/actualizados/eliminados
|
||||||
|
|
||||||
// Por ahora, simulamos que obtenemos algunos elementos
|
|
||||||
if (!items.isEmpty()) {
|
if (!items.isEmpty()) {
|
||||||
for (const MailItem& item : items) {
|
for (const MailItem& item : items) {
|
||||||
emit mailItemAdded(item);
|
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
|
QString labelId = folderId;
|
||||||
// Nota: En Gmail, las carpetas se identifican por su nombre de label (ej: INBOX, DRAFTS, etc.)
|
bool localFolderId = false;
|
||||||
QString endpoint = QString("/me/mailFolders/%1/messages").arg(folderId);
|
folderId.toInt(&localFolderId);
|
||||||
QString url = buildGmailUrl(endpoint);
|
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;
|
QUrlQuery query;
|
||||||
query.addQueryItem("maxResults", "50"); // Limitar a 50 mensajes por petición
|
query.addQueryItem(QStringLiteral("labelIds"), labelId);
|
||||||
query.addQueryItem("q", "in:" + folderId); // Filtrar por label
|
query.addQueryItem(QStringLiteral("maxResults"), QStringLiteral("100"));
|
||||||
url += "?" + query.toString();
|
if (!pageToken.isEmpty()) query.addQueryItem(QStringLiteral("pageToken"), pageToken);
|
||||||
|
url.setQuery(query);
|
||||||
|
|
||||||
QNetworkRequest request = createAuthRequest(url);
|
QByteArray response;
|
||||||
QNetworkReply* reply = m_networkManager->get(request);
|
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;
|
QVector<MailItem> items;
|
||||||
items.append(MailItem(1, folderId.toInt(), "Actualización de proyecto",
|
MimeStorageService mimeStorage;
|
||||||
"carlos.rodriguez@proveedor.com", m_account.email(),
|
for (const QJsonObject &reference : messageRefs) {
|
||||||
QDateTime::currentDateTime().addSecs(-1800),
|
const QString gmailId = reference.value(QStringLiteral("id")).toString();
|
||||||
false, false));
|
if (gmailId.isEmpty()) continue;
|
||||||
items.append(MailItem(2, folderId.toInt(), "Factura adjunta",
|
QUrl rawUrl(buildGmailUrl(QStringLiteral("messages/%1").arg(gmailId)));
|
||||||
"facturacion@servicios.com", m_account.email(),
|
QUrlQuery rawQuery;
|
||||||
QDateTime::currentDateTime().addSecs(-5400),
|
rawQuery.addQueryItem(QStringLiteral("format"), QStringLiteral("raw"));
|
||||||
true, true)); // Marcada como importante y leída
|
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;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GmailSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
|
bool GmailSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
|
||||||
{
|
{
|
||||||
if (!m_account.isTokenValid()) {
|
Q_UNUSED(folderId);
|
||||||
if (!refreshAccessToken()) {
|
Q_UNUSED(item);
|
||||||
qWarning() << "Failed to refresh token for appending mail item";
|
qWarning() << "Gmail appendMailItem is not used for sending; MailService sends via Gmail API";
|
||||||
return false;
|
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,
|
bool GmailSynchronizer::updateMailItemFlags(const QString& folderId,
|
||||||
const QString& itemUid,
|
const QString& itemUid,
|
||||||
bool read, bool flagged)
|
bool read, bool flagged)
|
||||||
{
|
{
|
||||||
if (!m_account.isTokenValid()) {
|
Q_UNUSED(folderId);
|
||||||
if (!refreshAccessToken()) {
|
Q_UNUSED(itemUid);
|
||||||
qWarning() << "Failed to refresh token for updating mail item flags";
|
Q_UNUSED(read);
|
||||||
|
Q_UNUSED(flagged);
|
||||||
|
qWarning() << "Gmail flag update is not available through this synchronizer";
|
||||||
return false;
|
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,
|
bool GmailSynchronizer::deleteMailItem(const QString& folderId,
|
||||||
const QString& itemUid)
|
const QString& itemUid)
|
||||||
{
|
{
|
||||||
if (!m_account.isTokenValid()) {
|
Q_UNUSED(folderId);
|
||||||
if (!refreshAccessToken()) {
|
Q_UNUSED(itemUid);
|
||||||
qWarning() << "Failed to refresh token for deleting mail item";
|
qWarning() << "Gmail delete is not available through this synchronizer";
|
||||||
return false;
|
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)
|
void GmailSynchronizer::onGmailReplyFinished(QNetworkReply* reply)
|
||||||
{
|
{
|
||||||
@@ -321,9 +340,11 @@ void GmailSynchronizer::onHistoryTimer()
|
|||||||
|
|
||||||
QString GmailSynchronizer::buildGmailUrl(const QString& endpoint) const
|
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")
|
return QStringLiteral("https://gmail.googleapis.com/gmail/v1/users/%1/%2")
|
||||||
.arg(m_account.email())
|
.arg(QString::fromUtf8(QUrl::toPercentEncoding(m_account.email())))
|
||||||
.arg(endpoint);
|
.arg(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkRequest GmailSynchronizer::createAuthRequest(const QString& url) const
|
QNetworkRequest GmailSynchronizer::createAuthRequest(const QString& url) const
|
||||||
@@ -338,12 +359,41 @@ QNetworkRequest GmailSynchronizer::createAuthRequest(const QString& url) const
|
|||||||
|
|
||||||
bool GmailSynchronizer::refreshAccessToken()
|
bool GmailSynchronizer::refreshAccessToken()
|
||||||
{
|
{
|
||||||
// En una implementación real, aquí usaríamos el refresh token
|
if (m_refreshToken.isEmpty()) return false;
|
||||||
// para obtener un nuevo access token desde Google OAuth2
|
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";
|
QNetworkAccessManager network;
|
||||||
// Simulamos éxito por ahora
|
QNetworkRequest request(QUrl(QStringLiteral("https://oauth2.googleapis.com/token")));
|
||||||
return !m_refreshToken.isEmpty();
|
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
|
qint64 GmailSynchronizer::getHistoryId(const QString& folderId) const
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
#include "../../core/models/folder.h"
|
#include "../../core/models/folder.h"
|
||||||
#include "../../db/dao/folderdao.h"
|
#include "../../db/dao/folderdao.h"
|
||||||
#include "../../db/dao/mailitemdao.h"
|
#include "../../db/dao/mailitemdao.h"
|
||||||
|
#include "../../services/mimestorage.h"
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
ImapSynchronizer::ImapSynchronizer(QObject* parent)
|
ImapSynchronizer::ImapSynchronizer(QObject* parent)
|
||||||
: Synchronizer(parent)
|
: Synchronizer(parent)
|
||||||
@@ -16,6 +18,7 @@ ImapSynchronizer::ImapSynchronizer(QObject* parent)
|
|||||||
|
|
||||||
bool ImapSynchronizer::initialize(const Account& account)
|
bool ImapSynchronizer::initialize(const Account& account)
|
||||||
{
|
{
|
||||||
|
m_account = account;
|
||||||
const Account::ConnectionSettings& settings = account.connectionSettings();
|
const Account::ConnectionSettings& settings = account.connectionSettings();
|
||||||
m_host = settings.incomingHost;
|
m_host = settings.incomingHost;
|
||||||
m_port = settings.incomingPort;
|
m_port = settings.incomingPort;
|
||||||
@@ -37,9 +40,13 @@ bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
|
|||||||
QEventLoop connectLoop;
|
QEventLoop connectLoop;
|
||||||
QTimer connectTimer;
|
QTimer connectTimer;
|
||||||
bool connectTimeout = false;
|
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){
|
connect(&conn, &ImapConnection::errorOccurred, this, [&](const QString& err){
|
||||||
qWarning() << "IMAP connection error:" << err;
|
qWarning() << "IMAP connection error:" << err;
|
||||||
|
connected = false;
|
||||||
connectLoop.quit();
|
connectLoop.quit();
|
||||||
});
|
});
|
||||||
connectTimer.setSingleShot(true);
|
connectTimer.setSingleShot(true);
|
||||||
@@ -54,7 +61,7 @@ bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
|
|||||||
qWarning() << "Connection timeout to" << m_host;
|
qWarning() << "Connection timeout to" << m_host;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
connected = true;
|
if (!connected) return false;
|
||||||
|
|
||||||
// Wait for server greeting (first untagged response)
|
// Wait for server greeting (first untagged response)
|
||||||
QEventLoop greetLoop;
|
QEventLoop greetLoop;
|
||||||
@@ -114,99 +121,173 @@ bool ImapSynchronizer::connectAndLogin(ImapConnection& conn) const
|
|||||||
return true;
|
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;
|
QVector<qint64> uids;
|
||||||
QStringList lines = searchResp.split('\n');
|
QStringList lines = response.split(QStringLiteral("\n"));
|
||||||
for (const QString& line : lines) {
|
for (const QString& line : lines) {
|
||||||
if (line.startsWith("* SEARCH")) {
|
if (line.startsWith(QStringLiteral("* SEARCH"))) {
|
||||||
QStringList parts = line.split(QRegularExpression("\\s+"));
|
QStringList parts = line.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
|
||||||
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
|
for (int i = 2; i < parts.size(); ++i) { // skip "* SEARCH"
|
||||||
bool ok;
|
bool ok;
|
||||||
qint64 uid = parts[i].toLongLong(&ok);
|
qint64 uid = parts[i].toLongLong(&ok);
|
||||||
if (ok && uid > 0) uids.append(uid);
|
if (ok && uid > 0)
|
||||||
|
uids.append(uid);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return uids;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
bool ImapSynchronizer::syncFolder(const Folder& folder)
|
||||||
// Get flags for all UIDs in one command
|
{
|
||||||
QStringList uidStrs;
|
// First, get all UIDs from server to detect deletions
|
||||||
for (qint64 uid : uids) uidStrs.append(QString::number(uid));
|
QVector<qint64> allUids = fetchAllUids(QString::number(folder.id()));
|
||||||
QString fetchCmd = "UID FETCH " + uidStrs.join(',') + " (FLAGS)";
|
if (allUids.isEmpty()) {
|
||||||
QString fetchResp;
|
qWarning() << "Failed to fetch UIDs for folder" << folder.id();
|
||||||
if (!conn.sendCommandWait(fetchCmd, fetchResp, 30000) || !fetchResp.contains(" OK ")) {
|
|
||||||
qWarning() << "FETCH FLAGS failed";
|
|
||||||
conn.disconnect();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse flags and update local DB
|
// Get local UIDs
|
||||||
QMap<qint64, QPair<bool,bool>> flagsMap; // uid -> (seen, flagged)
|
QVector<qint64> localUids = MailItemDao::getUidsForFolder(folder.id());
|
||||||
QRegularExpression flagsRx(R"(UID (\d+).*FLAGS \(([^)]*)\))");
|
|
||||||
auto it = flagsRx.globalMatch(fetchResp);
|
// Find deleted UIDs (exist locally but not on server)
|
||||||
while (it.hasNext()) {
|
QSet<qint64> serverUidSet(allUids.begin(), allUids.end());
|
||||||
auto m = it.next();
|
QVector<qint64> deletedUids;
|
||||||
qint64 uid = m.captured(1).toLongLong();
|
for (qint64 uid : localUids) {
|
||||||
QString flags = m.captured(2);
|
if (!serverUidSet.contains(uid)) {
|
||||||
bool seen = flags.contains("\\Seen");
|
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");
|
bool flagged = flags.contains("\\Flagged");
|
||||||
flagsMap[uid] = qMakePair(seen, flagged);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update local items
|
// Update local DB if different
|
||||||
auto localItems = MailItemDao::findByFolderId(folder.id());
|
auto itemOpt = MailItemDao::findByUid(folderId, uid);
|
||||||
for (MailItem& item : localItems) {
|
if (itemOpt.has_value()) {
|
||||||
if (flagsMap.contains(item.uid())) {
|
MailItem item = itemOpt.value();
|
||||||
bool seen = flagsMap[item.uid()].first;
|
if (item.isRead() != read || item.isFlagged() != flagged) {
|
||||||
bool flagged = flagsMap[item.uid()].second;
|
item.setRead(read);
|
||||||
if (item.isRead() != seen || item.isFlagged() != flagged) {
|
|
||||||
item.setRead(seen);
|
|
||||||
item.setFlagged(flagged);
|
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)
|
QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint64 sinceUid)
|
||||||
@@ -242,7 +323,7 @@ QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint
|
|||||||
// SEARCH
|
// SEARCH
|
||||||
QString searchCommand;
|
QString searchCommand;
|
||||||
if (sinceUid > 0) {
|
if (sinceUid > 0) {
|
||||||
searchCommand = QString("UID SEARCH %1:*").arg(sinceUid);
|
searchCommand = QString("UID SEARCH UID %1:*").arg(sinceUid + 1);
|
||||||
} else {
|
} else {
|
||||||
searchCommand = "UID SEARCH ALL";
|
searchCommand = "UID SEARCH ALL";
|
||||||
}
|
}
|
||||||
@@ -283,24 +364,38 @@ QVector<MailItem> ImapSynchronizer::fetchMailItems(const QString& folderId, qint
|
|||||||
}
|
}
|
||||||
batchList.chop(1); // remove trailing comma
|
batchList.chop(1); // remove trailing comma
|
||||||
|
|
||||||
// Fetch headers and flags (efficient)
|
// Fetch the complete RFC822 message. Headers alone are not enough to
|
||||||
QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[HEADER.FIELDS (SUBJECT FROM TO DATE)] FLAGS INTERNALDATE)")
|
// reconstruct HTML, inline resources or attachments.
|
||||||
|
QString fetchCommand = QString("UID FETCH %1 (BODY.PEEK[] FLAGS INTERNALDATE)")
|
||||||
.arg(batchList);
|
.arg(batchList);
|
||||||
QString fetchResponse;
|
QByteArray fetchResponse;
|
||||||
if (!conn.sendCommandWait(fetchCommand, fetchResponse, 30000)) {
|
if (!conn.sendCommandWaitBytes(fetchCommand, fetchResponse, 30000)) {
|
||||||
qWarning() << "FETCH failed for batch" << i;
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse and persist
|
// Parse and persist
|
||||||
QVector<MailItem> batchItems = parseFetchResponse(fetchResponse.split('\n'));
|
QVector<MailItem> batchItems = parseFetchResponseBytes(fetchResponse);
|
||||||
for (MailItem& item : batchItems) {
|
for (MailItem& item : batchItems) {
|
||||||
item.setFolderId(fid);
|
item.setFolderId(fid);
|
||||||
if (MailItemDao::insert(item)) {
|
|
||||||
items.append(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;
|
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)
|
bool ImapSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
|
||||||
{
|
{
|
||||||
// Get folder name
|
// Get folder name
|
||||||
@@ -562,19 +783,20 @@ QVector<MailItem> ImapSynchronizer::parseFetchResponse(const QStringList& lines)
|
|||||||
int flagsEnd = line.indexOf(')', flagsPos + 7);
|
int flagsEnd = line.indexOf(')', flagsPos + 7);
|
||||||
if (flagsEnd != -1) {
|
if (flagsEnd != -1) {
|
||||||
QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7);
|
QString flags = line.mid(flagsPos + 7, flagsEnd - flagsPos - 7);
|
||||||
item.setRead(flags.contains(QStringLiteral("\\\\Seen")));
|
item.setRead(flags.contains(QStringLiteral("\\Seen")));
|
||||||
item.setFlagged(flags.contains(QStringLiteral("\\\\Flagged")));
|
item.setFlagged(flags.contains(QStringLiteral("\\Flagged")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract INTERNALDATE
|
// Extract INTERNALDATE
|
||||||
int datePos = line.indexOf(QStringLiteral("INTERNALDATE \"\""));
|
int datePos = line.indexOf(QStringLiteral("INTERNALDATE \""));
|
||||||
if (datePos != -1) {
|
if (datePos != -1) {
|
||||||
int dateStart = datePos + 16; // length of "INTERNALDATE \""
|
int dateStart = datePos + 15; // length of "INTERNALDATE \""
|
||||||
int dateEnd = line.indexOf('\"', dateStart);
|
int dateEnd = line.indexOf('\"', dateStart);
|
||||||
if (dateEnd != -1) {
|
if (dateEnd != -1) {
|
||||||
QString dateStr = line.mid(dateStart, dateEnd - dateStart);
|
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())
|
if (dt.isValid())
|
||||||
item.setDate(dt);
|
item.setDate(dt);
|
||||||
}
|
}
|
||||||
@@ -582,7 +804,7 @@ QVector<MailItem> ImapSynchronizer::parseFetchResponse(const QStringList& lines)
|
|||||||
|
|
||||||
// Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines
|
// Fetch BODY[HEADER.FIELDS ...] literal, may span multiple lines
|
||||||
QString bodyData;
|
QString bodyData;
|
||||||
int bodyPos = line.indexOf(QStringLiteral("BODY["));
|
int bodyPos = line.indexOf(QStringLiteral("BODY[]"));
|
||||||
if (bodyPos != -1) {
|
if (bodyPos != -1) {
|
||||||
int bracePos = line.indexOf('{', bodyPos);
|
int bracePos = line.indexOf('{', bodyPos);
|
||||||
if (bracePos != -1) {
|
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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
#include "../synchronizer.h"
|
#include "../synchronizer.h"
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QSslSocket>
|
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
|
#include "imapconnection.h"
|
||||||
|
|
||||||
class ImapSynchronizer : public Synchronizer
|
class ImapSynchronizer : public Synchronizer
|
||||||
{
|
{
|
||||||
@@ -30,20 +30,28 @@ signals:
|
|||||||
void statusMessage(const QString& message) const;
|
void statusMessage(const QString& message) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Connection details
|
QVector<qint64> parseUidSearchResponse(const QString& response) const;
|
||||||
|
// Connection details (set by initialize)
|
||||||
mutable QString m_host;
|
mutable QString m_host;
|
||||||
mutable quint16 m_port{0};
|
mutable quint16 m_port{0};
|
||||||
mutable bool m_useSsl{false};
|
mutable bool m_useSsl{false};
|
||||||
mutable QString m_username;
|
mutable QString m_username;
|
||||||
mutable QString m_password;
|
mutable QString m_password;
|
||||||
mutable QString m_authMethod; // "plain" or "oauth2"
|
mutable QString m_authMethod; // "plain" or "oauth2"
|
||||||
mutable QByteArray m_capabilities;
|
|
||||||
|
|
||||||
// Helper para generar IDs únicos de eventos
|
// Helper para generar IDs únicos de eventos
|
||||||
QString generateEventId() const;
|
QString generateEventId() const;
|
||||||
|
|
||||||
// IMAP command helpers
|
// Helper: conecta y loguea al servidor (reutilizable)
|
||||||
bool sendCommand(QSslSocket& socket, const QString& command, QString& response) const;
|
bool connectAndLogin(ImapConnection &conn) const;
|
||||||
bool waitForResponse(QSslSocket& socket, const QString& expectedTag, QString& response) const;
|
|
||||||
QVector<Folder> parseListResponse(const QString& response) 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
@@ -3,6 +3,302 @@
|
|||||||
#include <QFutureWatcher>
|
#include <QFutureWatcher>
|
||||||
#include <QtConcurrent/QtConcurrent>
|
#include <QtConcurrent/QtConcurrent>
|
||||||
#include <QDebug>
|
#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)
|
MailService::MailService(AccountService *accountService, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
@@ -18,13 +314,43 @@ QVector<MailItem> MailService::getMails(const QString &folderId)
|
|||||||
|
|
||||||
void MailService::sendMail(const MailItem &mail, const QString &accountId)
|
void MailService::sendMail(const MailItem &mail, const QString &accountId)
|
||||||
{
|
{
|
||||||
// For now we just simulate sending by using EmailComposerBridge
|
sendMail(mail, accountId, {});
|
||||||
// In a real implementation, this would use SMTP settings from the account.
|
}
|
||||||
Q_UNUSED(mail);
|
|
||||||
Q_UNUSED(accountId);
|
void MailService::sendMail(const MailItem &mail, const QString &accountId,
|
||||||
// Emit success immediately; actual sending would be asynchronous.
|
const QStringList &attachmentPaths)
|
||||||
QMetaObject::invokeMethod(this, "mailSent", Qt::QueuedConnection,
|
{
|
||||||
Q_ARG(QString, QString()));
|
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)
|
void MailService::fetchMails(const QString &accountId, const QString &folderId)
|
||||||
@@ -35,6 +361,10 @@ void MailService::fetchMails(const QString &accountId, const QString &folderId)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Account *acc = m_accountService->findAccountById(accountId.toLongLong());
|
Account *acc = m_accountService->findAccountById(accountId.toLongLong());
|
||||||
|
if (!acc) {
|
||||||
|
emit mailFetchError(accountId, folderId, tr("Account not found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
QString providerType = QStringLiteral("imap"); // fallback
|
QString providerType = QStringLiteral("imap"); // fallback
|
||||||
if (acc) {
|
if (acc) {
|
||||||
switch (acc->type()) {
|
switch (acc->type()) {
|
||||||
@@ -65,10 +395,12 @@ void MailService::fetchMails(const QString &accountId, const QString &folderId)
|
|||||||
|
|
||||||
// Initialize synchronizer with account details before using it
|
// Initialize synchronizer with account details before using it
|
||||||
if (!sync->initialize(*acc)) {
|
if (!sync->initialize(*acc)) {
|
||||||
|
delete acc;
|
||||||
emit mailFetchError(accountId, folderId, tr("Failed to initialize synchronizer"));
|
emit mailFetchError(accountId, folderId, tr("Failed to initialize synchronizer"));
|
||||||
sync->deleteLater();
|
SynchronizerProvider::instance().unregisterSynchronizer(accountId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
delete acc;
|
||||||
|
|
||||||
// Connect progress signals (if they exist) using string-based syntax for compatibility
|
// 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(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(progressChanged(int)), this, SIGNAL(progressChanged(int)));
|
||||||
QObject::disconnect(sync, SIGNAL(statusMessage(const QString&)), this, SIGNAL(statusMessage(const QString&)));
|
QObject::disconnect(sync, SIGNAL(statusMessage(const QString&)), this, SIGNAL(statusMessage(const QString&)));
|
||||||
QVector<MailItem> items = watcher->result();
|
QVector<MailItem> items = watcher->result();
|
||||||
|
QVector<MailItem> persisted;
|
||||||
|
for (MailItem &item : items) {
|
||||||
|
if (persistFetchedItem(item, accountId, folderId)) persisted.append(item);
|
||||||
|
}
|
||||||
watcher->deleteLater();
|
watcher->deleteLater();
|
||||||
emit mailFetched(accountId, folderId, items);
|
emit mailFetched(accountId, folderId, persisted);
|
||||||
});
|
});
|
||||||
watcher->setFuture(future);
|
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)
|
void MailService::moveMail(const QString &mailItemId, const QString &targetFolderId)
|
||||||
{
|
{
|
||||||
bool ok;
|
bool ok;
|
||||||
@@ -109,9 +481,15 @@ void MailService::deleteMail(const QString &mailItemId)
|
|||||||
bool ok;
|
bool ok;
|
||||||
qint64 id = mailItemId.toLongLong(&ok);
|
qint64 id = mailItemId.toLongLong(&ok);
|
||||||
if (!ok) return;
|
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);
|
emit mailDeleted(mailItemId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MailService::markAsRead(const QString &mailItemId, bool read)
|
void MailService::markAsRead(const QString &mailItemId, bool read)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -22,6 +22,8 @@ public:
|
|||||||
|
|
||||||
QVector<MailItem> getMails(const QString &folderId);
|
QVector<MailItem> getMails(const QString &folderId);
|
||||||
void sendMail(const MailItem &mail, const QString &accountId);
|
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 fetchMails(const QString &accountId, const QString &folderId);
|
||||||
void moveMail(const QString &mailItemId, const QString &targetFolderId);
|
void moveMail(const QString &mailItemId, const QString &targetFolderId);
|
||||||
void deleteMail(const QString &mailItemId);
|
void deleteMail(const QString &mailItemId);
|
||||||
@@ -40,6 +42,8 @@ signals:
|
|||||||
void statusMessage(const QString &message);
|
void statusMessage(const QString &message);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
bool persistFetchedItem(MailItem &item, const QString &accountId,
|
||||||
|
const QString &folderId);
|
||||||
EmailComposerBridge *m_composer;
|
EmailComposerBridge *m_composer;
|
||||||
AccountService *m_accountService;
|
AccountService *m_accountService;
|
||||||
};
|
};
|
||||||
|
|||||||
+296
-53
@@ -2,6 +2,14 @@
|
|||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QTextStream>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QSaveFile>
|
||||||
|
#include <QMap>
|
||||||
|
|
||||||
MimeStorageService::MimeStorageService(QObject *parent)
|
MimeStorageService::MimeStorageService(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
@@ -23,53 +31,99 @@ QString MimeStorageService::ensureDirectory(const QString &path) const
|
|||||||
return dir.absolutePath();
|
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 MimeStorageService::saveEmlFile(const QString &accountId, const QString &folderId, const MailItem &mail)
|
||||||
{
|
{
|
||||||
QString dir = storagePath() + "/" + accountId + "/" + folderId;
|
QByteArray raw;
|
||||||
ensureDirectory(dir);
|
raw.append("From: ").append(mail.sender().toUtf8()).append("\r\n");
|
||||||
|
raw.append("To: ").append(mail.recipient().toUtf8()).append("\r\n");
|
||||||
QString fileName = mail.messageId();
|
if (!mail.cc().isEmpty()) raw.append("Cc: ").append(mail.cc().toUtf8()).append("\r\n");
|
||||||
if (fileName.isEmpty()) {
|
if (!mail.bcc().isEmpty()) raw.append("Bcc: ").append(mail.bcc().toUtf8()).append("\r\n");
|
||||||
fileName = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
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
|
QString MimeStorageService::saveRawEmlFile(const QString &accountId, const QString &folderId,
|
||||||
QFile file(filePath);
|
const QByteArray &rawMime, const QString &stableId)
|
||||||
if (file.open(QIODevice::WriteOnly)) {
|
{
|
||||||
QTextStream stream(&file);
|
if (rawMime.isEmpty()) return QString();
|
||||||
stream << "From: " << mail.sender() << "\r\n";
|
|
||||||
stream << "To: " << mail.recipient() << "\r\n";
|
const QString accountPart = safeComponent(accountId);
|
||||||
stream << "Subject: " << mail.subject() << "\r\n";
|
const QString folderPart = safeComponent(folderId);
|
||||||
stream << "Date: " << mail.date().toString(Qt::RFC2822Date) << "\r\n";
|
const QByteArray digest = QCryptographicHash::hash(rawMime, QCryptographicHash::Sha256).toHex();
|
||||||
stream << "MIME-Version: 1.0\r\n";
|
const QString fileName = safeComponent(stableId.isEmpty()
|
||||||
stream << "Content-Type: text/html; charset=UTF-8\r\n";
|
? QString::fromLatin1(digest)
|
||||||
stream << "\r\n";
|
: stableId) + QStringLiteral("_") + QString::fromLatin1(digest.left(12)) + QStringLiteral(".eml");
|
||||||
stream << mail.bodyHtml() << "\r\n";
|
const QString relative = accountPart + "/" + folderPart + "/" + fileName;
|
||||||
file.close();
|
const QString path = QDir(storagePath()).filePath(relative);
|
||||||
qDebug() << "[MimeStorage] Saved .eml:" << filePath;
|
ensureDirectory(QFileInfo(path).absolutePath());
|
||||||
return fileName;
|
|
||||||
}
|
QSaveFile file(path);
|
||||||
qWarning() << "[MimeStorage] Failed to save .eml:" << filePath;
|
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 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
|
QString MimeStorageService::getEmlFilePath(const QString &fileId) const
|
||||||
{
|
{
|
||||||
// Search in all account/folder directories (simplified)
|
QString relative = fileId;
|
||||||
QDir baseDir(storagePath());
|
if (relative.endsWith(QStringLiteral(".eml"))) relative.chop(4);
|
||||||
QStringList filters;
|
const QString base = QDir::cleanPath(storagePath());
|
||||||
filters << fileId + ".eml";
|
const QString path = QDir::cleanPath(QDir(base).filePath(relative + QStringLiteral(".eml")));
|
||||||
|
if (!path.startsWith(base + QDir::separator()) || !QFileInfo::exists(path)) return QString();
|
||||||
QFileInfoList files = baseDir.entryInfoList(filters, QDir::Files, QDir::Name);
|
return path;
|
||||||
for (const auto &info : files) {
|
|
||||||
QStringList parts = info.absoluteFilePath().split('/');
|
|
||||||
if (parts.size() >= 2) {
|
|
||||||
return info.absoluteFilePath();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return QString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray MimeStorageService::readEmlFile(const QString &fileId) const
|
QByteArray MimeStorageService::readEmlFile(const QString &fileId) const
|
||||||
@@ -88,29 +142,218 @@ bool MimeStorageService::deleteEmlFile(const QString &fileId)
|
|||||||
{
|
{
|
||||||
QString path = getEmlFilePath(fileId);
|
QString path = getEmlFilePath(fileId);
|
||||||
if (path.isEmpty()) return false;
|
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
|
QStringList MimeStorageService::listAttachments(const QString &mailItemId) const
|
||||||
{
|
{
|
||||||
Q_UNUSED(mailItemId);
|
const QString emlPath = getEmlFilePath(mailItemId);
|
||||||
// TODO: Parse .eml to get attachments when GMime is available
|
if (emlPath.isEmpty()) return {};
|
||||||
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);
|
const QString emlPath = getEmlFilePath(fileId);
|
||||||
QString dir = storagePath() + "/attachments";
|
if (emlPath.isEmpty()) return QString();
|
||||||
ensureDirectory(dir);
|
const QString dir = ensureDirectory(QFileInfo(emlPath).absolutePath() + QStringLiteral("/attachments"));
|
||||||
|
const QString originalName = QFileInfo(fileName).fileName();
|
||||||
QFile file(dir + "/" + fileName);
|
const QString safeName = safeComponent(originalName.isEmpty()
|
||||||
if (file.open(QIODevice::WriteOnly)) {
|
? QStringLiteral("attachment") : originalName);
|
||||||
file.write(data);
|
QString path = QDir(dir).filePath(safeName);
|
||||||
file.close();
|
if (QFileInfo::exists(path)) {
|
||||||
return true;
|
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"
|
#include "mimestorage.moc"
|
||||||
|
|||||||
@@ -5,8 +5,31 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QVector>
|
||||||
#include "core/mailitem.h"
|
#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
|
class MimeStorageService : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -15,16 +38,28 @@ public:
|
|||||||
~MimeStorageService() override = default;
|
~MimeStorageService() override = default;
|
||||||
|
|
||||||
QString saveEmlFile(const QString &accountId, const QString &folderId, const MailItem &mail);
|
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;
|
QString getEmlFilePath(const QString &fileId) const;
|
||||||
QByteArray readEmlFile(const QString &fileId) const;
|
QByteArray readEmlFile(const QString &fileId) const;
|
||||||
bool deleteEmlFile(const QString &fileId);
|
bool deleteEmlFile(const QString &fileId);
|
||||||
|
|
||||||
QStringList listAttachments(const QString &mailItemId) const;
|
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:
|
private:
|
||||||
QString storagePath() const;
|
QString storagePath() const;
|
||||||
QString ensureDirectory(const QString &path) 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
|
#endif // MIMESTORAGESERVICE_H
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
#include <QEventLoop>
|
#include <QEventLoop>
|
||||||
#include "../../core/events.h"
|
#include "../../core/events.h"
|
||||||
#include "../../core/eventbus.h"
|
#include "../../core/eventbus.h"
|
||||||
|
#include "db/dao/folderdao.h"
|
||||||
|
#include "services/mimestorage.h"
|
||||||
|
|
||||||
OutlookSynchronizer::OutlookSynchronizer(QObject* parent)
|
OutlookSynchronizer::OutlookSynchronizer(QObject* parent)
|
||||||
: Synchronizer(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
|
// 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
|
// y emitiríamos las señales apropiadas para elementos nuevos/actualizados/eliminados
|
||||||
|
|
||||||
// Por ahora, simulamos que obtenemos algunos elementos
|
|
||||||
if (!items.isEmpty()) {
|
if (!items.isEmpty()) {
|
||||||
for (const MailItem& item : items) {
|
for (const MailItem& item : items) {
|
||||||
emit mailItemAdded(item);
|
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 remoteFolderId = folderId;
|
||||||
QString endpoint = QString("/me/mailFolders/%1/messages").arg(folderId);
|
bool localFolderId = false;
|
||||||
QString url = buildGraphUrl(endpoint);
|
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;
|
QUrlQuery query;
|
||||||
query.addQueryItem("$top", "50"); // Limitar a 50 mensajes por petición
|
query.addQueryItem(QStringLiteral("$top"), QStringLiteral("100"));
|
||||||
query.addQueryItem("$orderby", "receivedDateTime DESC");
|
query.addQueryItem(QStringLiteral("$select"), QStringLiteral("id,isRead,importance"));
|
||||||
url += "?" + query.toString();
|
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;
|
QVector<MailItem> items;
|
||||||
items.append(MailItem(1, folderId.toInt(), "Reunión de proyecto",
|
MimeStorageService mimeStorage;
|
||||||
"juan.perez@empresa.com", m_account.email(),
|
for (const QJsonObject &reference : messageRefs) {
|
||||||
QDateTime::currentDateTime().addSecs(-3600),
|
const QString messageId = reference.value(QStringLiteral("id")).toString();
|
||||||
false, false));
|
if (messageId.isEmpty()) continue;
|
||||||
items.append(MailItem(2, folderId.toInt(), "Entrega de documentación",
|
const QString encodedId = QString::fromUtf8(QUrl::toPercentEncoding(messageId));
|
||||||
"ana.gomez@cliente.com", m_account.email(),
|
const QUrl rawUrl(buildGraphUrl(QStringLiteral("/me/messages/%1/$value").arg(encodedId)));
|
||||||
QDateTime::currentDateTime().addSecs(-7200),
|
QByteArray rawMime;
|
||||||
true, false));
|
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;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OutlookSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
|
bool OutlookSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
|
||||||
{
|
{
|
||||||
if (!m_account.isTokenValid()) {
|
Q_UNUSED(folderId);
|
||||||
if (!refreshAccessToken()) {
|
Q_UNUSED(item);
|
||||||
qWarning() << "Failed to refresh token for appending mail item";
|
qWarning() << "Outlook appendMailItem is not used for sending; MailService sends via Graph API";
|
||||||
return false;
|
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,
|
bool OutlookSynchronizer::updateMailItemFlags(const QString& folderId,
|
||||||
const QString& itemUid,
|
const QString& itemUid,
|
||||||
bool read, bool flagged)
|
bool read, bool flagged)
|
||||||
{
|
{
|
||||||
if (!m_account.isTokenValid()) {
|
Q_UNUSED(folderId);
|
||||||
if (!refreshAccessToken()) {
|
Q_UNUSED(itemUid);
|
||||||
qWarning() << "Failed to refresh token for updating mail item flags";
|
Q_UNUSED(read);
|
||||||
|
Q_UNUSED(flagged);
|
||||||
|
qWarning() << "Outlook flag update is not available through this synchronizer";
|
||||||
return false;
|
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,
|
bool OutlookSynchronizer::deleteMailItem(const QString& folderId,
|
||||||
const QString& itemUid)
|
const QString& itemUid)
|
||||||
{
|
{
|
||||||
if (!m_account.isTokenValid()) {
|
Q_UNUSED(folderId);
|
||||||
if (!refreshAccessToken()) {
|
Q_UNUSED(itemUid);
|
||||||
qWarning() << "Failed to refresh token for deleting mail item";
|
qWarning() << "Outlook delete is not available through this synchronizer";
|
||||||
return false;
|
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)
|
void OutlookSynchronizer::onGraphReplyFinished(QNetworkReply* reply)
|
||||||
{
|
{
|
||||||
@@ -340,9 +336,12 @@ bool OutlookSynchronizer::refreshAccessToken()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: These should come from secure configuration, not hardcoded
|
const QString clientId = qEnvironmentVariable("WINO_OUTLOOK_CLIENT_ID");
|
||||||
const QString clientId = "YOUR_CLIENT_ID_HERE"; // Replace with actual client ID
|
const QString clientSecret = qEnvironmentVariable("WINO_OUTLOOK_CLIENT_SECRET");
|
||||||
const QString clientSecret = "YOUR_CLIENT_SECRET_HERE"; // Replace with actual 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";
|
const QString tokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
|
||||||
|
|
||||||
QNetworkRequest networkRequest{QUrl(tokenUrl)};
|
QNetworkRequest networkRequest{QUrl(tokenUrl)};
|
||||||
@@ -355,7 +354,8 @@ bool OutlookSynchronizer::refreshAccessToken()
|
|||||||
postData.addQueryItem("grant_type", "refresh_token");
|
postData.addQueryItem("grant_type", "refresh_token");
|
||||||
postData.addQueryItem("client_secret", clientSecret);
|
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
|
// Wait for reply synchronously for simplicity in this context
|
||||||
QEventLoop loop;
|
QEventLoop loop;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <QRegularExpressionValidator>
|
#include <QRegularExpressionValidator>
|
||||||
#include <QIntValidator>
|
#include <QIntValidator>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include "ui/connectionwizard.h"
|
||||||
|
|
||||||
// ─────────────────────── Constructor ───────────────────────
|
// ─────────────────────── Constructor ───────────────────────
|
||||||
AccountSetupDialog::AccountSetupDialog(AccountService *accountService, QWidget *parent)
|
AccountSetupDialog::AccountSetupDialog(AccountService *accountService, QWidget *parent)
|
||||||
@@ -427,7 +428,8 @@ void AccountSetupDialog::onNextClicked()
|
|||||||
|
|
||||||
if (page == PageProvider) {
|
if (page == PageProvider) {
|
||||||
if (m_selectedProvider == 2) {
|
if (m_selectedProvider == 2) {
|
||||||
goToPage(PageImap);
|
// Launch ConnectionWizard for IMAP/POP3 setup
|
||||||
|
launchConnectionWizard();
|
||||||
} else {
|
} else {
|
||||||
// Update OAuth page subtitle based on provider
|
// Update OAuth page subtitle based on provider
|
||||||
goToPage(PageOAuth);
|
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()
|
void AccountSetupDialog::onBackClicked()
|
||||||
{
|
{
|
||||||
int page = m_stack->currentIndex();
|
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 ? "Sí" : "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 ───────────────────
|
// ─────────────────── Account Result ───────────────────
|
||||||
void AccountSetupDialog::onAccountAdded(const Account &account)
|
void AccountSetupDialog::onAccountAdded(const Account &account)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -40,6 +40,7 @@ private slots:
|
|||||||
void startOAuthAuthentication();
|
void startOAuthAuthentication();
|
||||||
void onAccountAdded(const Account &account);
|
void onAccountAdded(const Account &account);
|
||||||
void onAuthTimeout();
|
void onAuthTimeout();
|
||||||
|
void onConnectionWizardFinished(const Account::ConnectionSettings &settings);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setupUI();
|
void setupUI();
|
||||||
@@ -53,6 +54,7 @@ private:
|
|||||||
void showError(const QString &message);
|
void showError(const QString &message);
|
||||||
void showSuccess(const QString &message);
|
void showSuccess(const QString &message);
|
||||||
void submitImapAccount();
|
void submitImapAccount();
|
||||||
|
void launchConnectionWizard();
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
AccountService *m_accountService;
|
AccountService *m_accountService;
|
||||||
|
|||||||
@@ -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
@@ -1,277 +1,21 @@
|
|||||||
#include "ui/composeview.h"
|
// ===================== ComposeView =====================
|
||||||
#include <QFrame>
|
|
||||||
#include <QFileDialog>
|
#include "composeview.h"
|
||||||
#include <QInputDialog>
|
|
||||||
#include <QMessageBox>
|
|
||||||
#include <QDebug>
|
|
||||||
#include <QDialog>
|
#include <QDialog>
|
||||||
|
#include <QInputDialog>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include "tags_line_edit.hpp"
|
#include <QPushButton>
|
||||||
#include <QRegularExpression>
|
#include <QHBoxLayout>
|
||||||
#include <QTimer>
|
#include <QVBoxLayout>
|
||||||
#include <QTextList>
|
#include <QPixmap>
|
||||||
#include <QTextTable>
|
#include <QStyle>
|
||||||
#include <QTextCursor>
|
#include <QApplication>
|
||||||
#include <QTextBlockFormat>
|
#include <QFileIconProvider>
|
||||||
#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) {
|
ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
|
||||||
setupUI();
|
setupUI();
|
||||||
// Connect the rich text editor's signature signal to our slot
|
// Connect the rich text editor's signature signal to our slot
|
||||||
@@ -282,14 +26,14 @@ ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
|
|||||||
ComposeView::~ComposeView() {
|
ComposeView::~ComposeView() {
|
||||||
// Destructor implementation
|
// Destructor implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
void ComposeView::onSignatureClicked() {
|
void ComposeView::onSignatureClicked() {
|
||||||
// This slot is called when the signature button in the rich text editor is clicked
|
// 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
|
// The actual menu handling is done in RichTextEditor, so we just need to
|
||||||
// handle any ComposeView-specific logic here if needed
|
// handle any ComposeView-specific logic here if needed
|
||||||
}
|
}
|
||||||
|
|
||||||
void ComposeView::onSignatureEditRequested()
|
void ComposeView::onSignatureEditRequested() {
|
||||||
{
|
|
||||||
QDialog dialog(this);
|
QDialog dialog(this);
|
||||||
dialog.setWindowTitle(tr("Edit Signature"));
|
dialog.setWindowTitle(tr("Edit Signature"));
|
||||||
dialog.setMinimumSize(600, 400);
|
dialog.setMinimumSize(600, 400);
|
||||||
@@ -314,14 +58,13 @@ void ComposeView::onSignatureEditRequested()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void ComposeView::setAccountService(AccountService *service)
|
|
||||||
{
|
void ComposeView::setAccountService(AccountService *service) {
|
||||||
m_accountService = service;
|
m_accountService = service;
|
||||||
populateAccountCombo();
|
populateAccountCombo();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ComposeView::populateAccountCombo()
|
void ComposeView::populateAccountCombo() {
|
||||||
{
|
|
||||||
if (!m_accountService) {
|
if (!m_accountService) {
|
||||||
qWarning() << "AccountService not set";
|
qWarning() << "AccountService not set";
|
||||||
return;
|
return;
|
||||||
@@ -342,8 +85,7 @@ void ComposeView::populateAccountCombo()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ComposeView::onAccountChanged(int index)
|
void ComposeView::onAccountChanged(int index) {
|
||||||
{
|
|
||||||
if (index <= 0) {
|
if (index <= 0) {
|
||||||
// Placeholder item selected or invalid index
|
// Placeholder item selected or invalid index
|
||||||
m_currentAccountId = -1;
|
m_currentAccountId = -1;
|
||||||
@@ -354,8 +96,7 @@ void ComposeView::onAccountChanged(int index)
|
|||||||
loadSignatureForCurrentAccount();
|
loadSignatureForCurrentAccount();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ComposeView::loadSignatureForCurrentAccount()
|
void ComposeView::loadSignatureForCurrentAccount() {
|
||||||
{
|
|
||||||
if (!m_accountService || m_currentAccountId <= 0) {
|
if (!m_accountService || m_currentAccountId <= 0) {
|
||||||
// Clear signature if no account selected
|
// Clear signature if no account selected
|
||||||
m_bodyEditor->clear();
|
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
|
// 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
|
// In a real implementation, we would retrieve and display the signature
|
||||||
m_bodyEditor->clear();
|
m_bodyEditor->clear();
|
||||||
|
delete account;
|
||||||
} else {
|
} else {
|
||||||
m_bodyEditor->clear();
|
m_bodyEditor->clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void ComposeView::setupUI() {
|
void ComposeView::setupUI() {
|
||||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||||
mainLayout->setContentsMargins(20, 15, 20, 15);
|
mainLayout->setContentsMargins(20, 15, 20, 15);
|
||||||
@@ -383,37 +124,9 @@ void ComposeView::setupUI() {
|
|||||||
"QLineEdit:focus, QTextEdit:focus { border-color: #1976D2; }"
|
"QLineEdit:focus, QTextEdit:focus { border-color: #1976D2; }"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Header row: Subject + Detach button
|
// De: field + Account selector (renamed from From:)
|
||||||
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();
|
QHBoxLayout *fromLayout = new QHBoxLayout();
|
||||||
QLabel *fromLabel = new QLabel("From:");
|
QLabel *fromLabel = new QLabel(tr("De:"));
|
||||||
fromLabel->setFixedWidth(40);
|
fromLabel->setFixedWidth(40);
|
||||||
fromLabel->setStyleSheet("font-weight: bold; color: #555;");
|
fromLabel->setStyleSheet("font-weight: bold; color: #555;");
|
||||||
m_accountCombo = new QComboBox();
|
m_accountCombo = new QComboBox();
|
||||||
@@ -429,16 +142,17 @@ void ComposeView::setupUI() {
|
|||||||
connect(m_accountCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
connect(m_accountCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||||
this, &ComposeView::onAccountChanged);
|
this, &ComposeView::onAccountChanged);
|
||||||
fromLayout->addWidget(fromLabel);
|
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);
|
mainLayout->addLayout(fromLayout);
|
||||||
|
|
||||||
// To: field + Cc/Bcc toggle buttons
|
// Para: field + Cc/Bcc toggle buttons
|
||||||
QHBoxLayout *toLayout = new QHBoxLayout();
|
QHBoxLayout *toLayout = new QHBoxLayout();
|
||||||
QLabel *toLabel = new QLabel("To:");
|
QLabel *toLabel = new QLabel(tr("Para:"));
|
||||||
toLabel->setFixedWidth(40);
|
toLabel->setFixedWidth(40);
|
||||||
toLabel->setStyleSheet("font-weight: bold; color: #555;");
|
toLabel->setStyleSheet("font-weight: bold; color: #555;");
|
||||||
m_toField = new QLineEdit();
|
m_toField = new everload_tags::TagsLineEdit();
|
||||||
m_toField->setPlaceholderText("Recipients (comma separated)");
|
|
||||||
toLayout->addWidget(toLabel);
|
toLayout->addWidget(toLabel);
|
||||||
toLayout->addWidget(m_toField, 1);
|
toLayout->addWidget(m_toField, 1);
|
||||||
|
|
||||||
@@ -466,19 +180,20 @@ void ComposeView::setupUI() {
|
|||||||
m_ccRow = new QWidget();
|
m_ccRow = new QWidget();
|
||||||
QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow);
|
QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow);
|
||||||
ccLayout->setContentsMargins(0, 0, 0, 0);
|
ccLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
QLabel *ccLabel = new QLabel("Cc:");
|
QLabel *ccLabel = new QLabel(tr("Cc:"));
|
||||||
ccLabel->setFixedWidth(40);
|
ccLabel->setFixedWidth(40);
|
||||||
ccLabel->setStyleSheet("color: #555;");
|
ccLabel->setStyleSheet("color: #555;");
|
||||||
m_ccField = new QLineEdit();
|
m_ccField = new everload_tags::TagsLineEdit();
|
||||||
m_ccField->setPlaceholderText("Carbon copy");
|
|
||||||
ccLayout->addWidget(ccLabel);
|
ccLayout->addWidget(ccLabel);
|
||||||
ccLayout->addWidget(m_ccField, 1);
|
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->setFixedSize(20, 20);
|
||||||
m_hideCcButton->setToolTip("Hide Cc");
|
m_hideCcButton->setToolTip(tr("Hide Cc"));
|
||||||
m_hideCcButton->setStyleSheet(
|
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; }"
|
"QPushButton:hover { color: #333; }"
|
||||||
);
|
);
|
||||||
connect(m_hideCcButton, &QPushButton::clicked, this, &ComposeView::onCcToggle);
|
connect(m_hideCcButton, &QPushButton::clicked, this, &ComposeView::onCcToggle);
|
||||||
@@ -491,19 +206,20 @@ void ComposeView::setupUI() {
|
|||||||
m_bccRow = new QWidget();
|
m_bccRow = new QWidget();
|
||||||
QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow);
|
QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow);
|
||||||
bccLayout->setContentsMargins(0, 0, 0, 0);
|
bccLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
QLabel *bccLabel = new QLabel("Bcc:");
|
QLabel *bccLabel = new QLabel(tr("Bcc:"));
|
||||||
bccLabel->setFixedWidth(40);
|
bccLabel->setFixedWidth(40);
|
||||||
bccLabel->setStyleSheet("color: #555;");
|
bccLabel->setStyleSheet("color: #555;");
|
||||||
m_bccField = new QLineEdit();
|
m_bccField = new everload_tags::TagsLineEdit();
|
||||||
m_bccField->setPlaceholderText("Blind carbon copy");
|
|
||||||
bccLayout->addWidget(bccLabel);
|
bccLayout->addWidget(bccLabel);
|
||||||
bccLayout->addWidget(m_bccField, 1);
|
bccLayout->addWidget(m_bccField, 1);
|
||||||
|
|
||||||
m_hideBccButton = new QPushButton("x");
|
m_hideBccButton = new QPushButton("");
|
||||||
m_hideBccButton->setFixedSize(20, 20);
|
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(
|
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; }"
|
"QPushButton:hover { color: #333; }"
|
||||||
);
|
);
|
||||||
connect(m_hideBccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle);
|
connect(m_hideBccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle);
|
||||||
@@ -512,11 +228,39 @@ void ComposeView::setupUI() {
|
|||||||
m_bccRow->setVisible(false);
|
m_bccRow->setVisible(false);
|
||||||
mainLayout->addWidget(m_bccRow);
|
mainLayout->addWidget(m_bccRow);
|
||||||
|
|
||||||
// Separator
|
// Asunto: field
|
||||||
QFrame *line2 = new QFrame();
|
QHBoxLayout *subjectLayout = new QHBoxLayout();
|
||||||
line2->setFrameShape(QFrame::HLine);
|
QLabel *subjectLabel = new QLabel(tr("Asunto:"));
|
||||||
line2->setStyleSheet("color: #e0e0e0;");
|
subjectLabel->setFixedWidth(60);
|
||||||
mainLayout->addWidget(line2);
|
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
|
// Rich text editor with toolbar
|
||||||
m_bodyEditor = new RichTextEditor();
|
m_bodyEditor = new RichTextEditor();
|
||||||
@@ -527,7 +271,7 @@ void ComposeView::setupUI() {
|
|||||||
m_schedulePanel = new QWidget();
|
m_schedulePanel = new QWidget();
|
||||||
QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel);
|
QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel);
|
||||||
scheduleLayout->setContentsMargins(0, 0, 0, 0);
|
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;");
|
scheduleLabel->setStyleSheet("color: #555; font-weight: bold;");
|
||||||
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600));
|
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600));
|
||||||
m_schedulePicker->setCalendarPopup(true);
|
m_schedulePicker->setCalendarPopup(true);
|
||||||
@@ -536,79 +280,91 @@ void ComposeView::setupUI() {
|
|||||||
scheduleLayout->addWidget(m_schedulePicker);
|
scheduleLayout->addWidget(m_schedulePicker);
|
||||||
scheduleLayout->addStretch();
|
scheduleLayout->addStretch();
|
||||||
|
|
||||||
m_scheduleSendButton = new QPushButton("Schedule Send");
|
m_scheduleSendButton = new QPushButton(tr("Schedule Send"));
|
||||||
|
m_scheduleSendButton->setFixedWidth(150);
|
||||||
m_scheduleSendButton->setStyleSheet(
|
m_scheduleSendButton->setStyleSheet(
|
||||||
"QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 6px 16px; font-weight: bold; }"
|
"QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 6px 16px; font-weight: bold; }"
|
||||||
"QPushButton:hover { background-color: #1565C0; }"
|
"QPushButton:hover { background-color: #1565C0; }"
|
||||||
);
|
);
|
||||||
connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked);
|
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);
|
m_schedulePanel->setVisible(false);
|
||||||
mainLayout->addWidget(m_schedulePanel);
|
mainLayout->addWidget(m_schedulePanel);
|
||||||
|
|
||||||
// Attachment section
|
// Attachment section
|
||||||
QHBoxLayout *attachmentLayout = new QHBoxLayout();
|
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 = 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->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||||
m_attachmentList->setMaximumHeight(60);
|
m_attachmentList->setStyleSheet(
|
||||||
m_attachmentList->setStyleSheet("QListWidget { border: 1px solid #d1d1d6; border-radius: 4px; }");
|
"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);
|
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);
|
mainLayout->addLayout(attachmentLayout);
|
||||||
|
|
||||||
|
|
||||||
// Action buttons row
|
// Action buttons row
|
||||||
QHBoxLayout *actionsLayout = new QHBoxLayout();
|
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(
|
m_discardButton->setStyleSheet(
|
||||||
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px 20px; color: #555; }"
|
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px 20px; color: #555; }"
|
||||||
"QPushButton:hover { background: #f5f5f5; }"
|
"QPushButton:hover { background: #f5f5f5; }"
|
||||||
);
|
);
|
||||||
actionsLayout->addWidget(m_discardButton);
|
|
||||||
connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested);
|
connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested);
|
||||||
|
actionsLayout->addWidget(m_discardButton);
|
||||||
|
|
||||||
// Split button for Send / Schedule
|
// Split button for Send / Schedule
|
||||||
m_sendMenu = new QMenu(this);
|
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);
|
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]() {
|
connect(m_scheduleAction, &QAction::triggered, [this]() {
|
||||||
m_schedulePanel->setVisible(true);
|
m_schedulePanel->setVisible(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
m_sendSplit = new QToolButton();
|
m_sendSplit = new QToolButton();
|
||||||
m_sendSplit->setText("Send");
|
m_sendSplit->setText(tr("Send"));
|
||||||
m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
|
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->setPopupMode(QToolButton::MenuButtonPopup);
|
||||||
m_sendSplit->setMenu(m_sendMenu);
|
m_sendSplit->setMenu(m_sendMenu);
|
||||||
m_sendSplit->setStyleSheet(
|
m_sendSplit->setStyleSheet(
|
||||||
"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 8px 24px; font-weight: bold; }"
|
"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 8px 24px; font-weight: bold; }"
|
||||||
"QToolButton:hover { background-color: #1565C0; }"
|
"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; }"
|
"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);
|
connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked);
|
||||||
@@ -633,25 +389,27 @@ void ComposeView::onBccToggle() {
|
|||||||
|
|
||||||
void ComposeView::onSendClicked() {
|
void ComposeView::onSendClicked() {
|
||||||
emit sendRequested(
|
emit sendRequested(
|
||||||
m_toField->text(),
|
m_toField->tags2().join(", "),
|
||||||
m_ccVisible ? m_ccField->text() : QString(),
|
m_ccVisible ? m_ccField->tags2().join(", ") : QString(),
|
||||||
m_bccVisible ? m_bccField->text() : QString(),
|
m_bccVisible ? m_bccField->tags2().join(", ") : QString(),
|
||||||
m_subjectField->text(),
|
m_subjectField->text(),
|
||||||
m_bodyEditor->toHtml(),
|
m_bodyEditor->toHtml(),
|
||||||
QDateTime(), // null = send now
|
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() {
|
void ComposeView::onScheduleClicked() {
|
||||||
emit sendRequested(
|
emit sendRequested(
|
||||||
m_toField->text(),
|
m_toField->tags2().join(", "),
|
||||||
m_ccVisible ? m_ccField->text() : QString(),
|
m_ccVisible ? m_ccField->tags2().join(", ") : QString(),
|
||||||
m_bccVisible ? m_bccField->text() : QString(),
|
m_bccVisible ? m_bccField->tags2().join(", ") : QString(),
|
||||||
m_subjectField->text(),
|
m_subjectField->text(),
|
||||||
m_bodyEditor->toHtml(),
|
m_bodyEditor->toHtml(),
|
||||||
m_schedulePicker->dateTime(),
|
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);
|
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::setSubject(const QString &subject) { m_subjectField->setText(subject); }
|
||||||
void ComposeView::setBody(const QString &body) { m_bodyEditor->setHtml(body); }
|
void ComposeView::setBody(const QString &body) { m_bodyEditor->setHtml(body); }
|
||||||
|
|
||||||
void ComposeView::initializeComposition() {
|
void ComposeView::initializeComposition() {
|
||||||
m_toField->clear();
|
m_toField->tags(QStringList{});
|
||||||
m_ccField->clear();
|
m_ccField->tags(QStringList{});
|
||||||
m_bccField->clear();
|
m_bccField->tags(QStringList{});
|
||||||
m_subjectField->clear();
|
m_subjectField->clear();
|
||||||
m_bodyEditor->clear();
|
m_bodyEditor->clear();
|
||||||
|
m_attachmentFiles.clear();
|
||||||
|
m_attachmentList->clear();
|
||||||
|
m_attachmentList->setVisible(false);
|
||||||
m_ccRow->setVisible(false);
|
m_ccRow->setVisible(false);
|
||||||
m_bccRow->setVisible(false);
|
m_bccRow->setVisible(false);
|
||||||
m_schedulePanel->setVisible(false);
|
m_schedulePanel->setVisible(false);
|
||||||
@@ -677,26 +439,85 @@ void ComposeView::initializeComposition() {
|
|||||||
void ComposeView::startNewEmail(const QString &initialRecipient) {
|
void ComposeView::startNewEmail(const QString &initialRecipient) {
|
||||||
initializeComposition();
|
initializeComposition();
|
||||||
if (!initialRecipient.isEmpty())
|
if (!initialRecipient.isEmpty())
|
||||||
m_toField->setText(initialRecipient);
|
m_toField->tags(initialRecipient.split(',', Qt::SkipEmptyParts));
|
||||||
m_toField->setFocus();
|
m_toField->setFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ComposeView::onAddAttachmentClicked()
|
void ComposeView::onAddAttachmentClicked() {
|
||||||
{
|
|
||||||
qDebug() << "Attach button clicked";
|
qDebug() << "Attach button clicked";
|
||||||
QStringList files = QFileDialog::getOpenFileNames(this, tr("Select Attachments"), QString(), tr("All Files (*)"));
|
QStringList files = QFileDialog::getOpenFileNames(this, tr("Select Attachments"), QString(), tr("All Files (*)"));
|
||||||
if (files.isEmpty())
|
if (files.isEmpty())
|
||||||
return;
|
return;
|
||||||
for (const QString &file : files) {
|
for (const QString &file : files) {
|
||||||
m_attachmentFiles.append(file);
|
m_attachmentFiles.append(file);
|
||||||
QListWidgetItem *item = new QListWidgetItem(QFileInfo(file).fileName(), m_attachmentList);
|
QListWidgetItem *item = new QListWidgetItem(m_attachmentList);
|
||||||
item->setToolTip(file);
|
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();
|
QListWidgetItem *item = m_attachmentList->currentItem();
|
||||||
if (!item)
|
if (!item)
|
||||||
return;
|
return;
|
||||||
@@ -704,6 +525,12 @@ void ComposeView::onRemoveAttachmentClicked()
|
|||||||
m_attachmentList->takeItem(row);
|
m_attachmentList->takeItem(row);
|
||||||
m_attachmentFiles.removeAt(row);
|
m_attachmentFiles.removeAt(row);
|
||||||
delete item;
|
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"
|
#include "composeview.moc"
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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
@@ -17,45 +17,12 @@
|
|||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QListWidget>
|
#include <QListWidget>
|
||||||
#include "models/EmailCompositionModel.h"
|
#include "models/EmailCompositionModel.h"
|
||||||
#include "tags_line_edit.hpp"
|
#include "../../thirdparty/tags/include/tags_line_edit.hpp"
|
||||||
#include "services/accountservice.h"
|
#include "services/accountservice.h"
|
||||||
|
|
||||||
class AccountService;
|
class AccountService;
|
||||||
|
|
||||||
class RichTextEditor : public QTextEdit {
|
#include "richtexteditor.h"
|
||||||
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 {
|
class ComposeView : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -71,7 +38,8 @@ signals:
|
|||||||
void sendRequested(const QString &to, const QString &cc, const QString &bcc,
|
void sendRequested(const QString &to, const QString &cc, const QString &bcc,
|
||||||
const QString &subject, const QString &body,
|
const QString &subject, const QString &body,
|
||||||
const QDateTime &scheduleTime,
|
const QDateTime &scheduleTime,
|
||||||
const QString &fromAddress);
|
const QString &fromAddress,
|
||||||
|
const QStringList &attachmentPaths);
|
||||||
void discardRequested();
|
void discardRequested();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
@@ -92,6 +60,7 @@ private slots:
|
|||||||
void onSignatureEditRequested();
|
void onSignatureEditRequested();
|
||||||
void onAddAttachmentClicked();
|
void onAddAttachmentClicked();
|
||||||
void onRemoveAttachmentClicked();
|
void onRemoveAttachmentClicked();
|
||||||
|
void onTemplateClicked(); // new slot for templates
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setupUI();
|
void setupUI();
|
||||||
@@ -101,15 +70,17 @@ private:
|
|||||||
EmailCompositionModel *m_compositionModel;
|
EmailCompositionModel *m_compositionModel;
|
||||||
AccountService *m_accountService = nullptr;
|
AccountService *m_accountService = nullptr;
|
||||||
// UI components
|
// 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;
|
QLineEdit *m_subjectField = nullptr;
|
||||||
RichTextEditor *m_bodyEditor = nullptr;
|
RichTextEditor *m_bodyEditor = nullptr;
|
||||||
QPushButton *m_detachButton = nullptr;
|
QPushButton *m_detachButton = nullptr;
|
||||||
QWidget *m_ccRow = nullptr;
|
QWidget *m_ccRow = nullptr;
|
||||||
QWidget *m_bccRow = nullptr;
|
QWidget *m_bccRow = nullptr;
|
||||||
QWidget *m_schedulePanel = nullptr;
|
QWidget *m_schedulePanel = nullptr;
|
||||||
QLineEdit *m_ccField = nullptr;
|
|
||||||
QLineEdit *m_bccField = nullptr;
|
|
||||||
QPushButton *m_ccButton = nullptr;
|
QPushButton *m_ccButton = nullptr;
|
||||||
QPushButton *m_bccButton = nullptr;
|
QPushButton *m_bccButton = nullptr;
|
||||||
QPushButton *m_hideCcButton = nullptr;
|
QPushButton *m_hideCcButton = nullptr;
|
||||||
@@ -121,13 +92,13 @@ private:
|
|||||||
QAction *m_sendNowAction = nullptr;
|
QAction *m_sendNowAction = nullptr;
|
||||||
QAction *m_scheduleAction = nullptr;
|
QAction *m_scheduleAction = nullptr;
|
||||||
QToolButton *m_sendSplit = nullptr;
|
QToolButton *m_sendSplit = nullptr;
|
||||||
QComboBox *m_accountCombo = nullptr;
|
|
||||||
bool m_ccVisible = false;
|
bool m_ccVisible = false;
|
||||||
bool m_bccVisible = false;
|
bool m_bccVisible = false;
|
||||||
int m_currentAccountId = -1;
|
int m_currentAccountId = -1;
|
||||||
|
|
||||||
// Attachment UI
|
// Attachment UI
|
||||||
QToolButton *m_attachButton = nullptr;
|
QToolButton *m_attachButton = nullptr;
|
||||||
|
QToolButton *m_templateButton = nullptr;
|
||||||
QListWidget *m_attachmentList = nullptr;
|
QListWidget *m_attachmentList = nullptr;
|
||||||
QToolButton *m_removeAttachmentButton = nullptr;
|
QToolButton *m_removeAttachmentButton = nullptr;
|
||||||
QStringList m_attachmentFiles;
|
QStringList m_attachmentFiles;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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
@@ -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')
|
||||||
@@ -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
@@ -15,8 +15,11 @@
|
|||||||
MainMainWindow::MainMainWindow(QWidget *parent)
|
MainMainWindow::MainMainWindow(QWidget *parent)
|
||||||
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
|
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
|
||||||
{
|
{
|
||||||
|
qDebug() << "[MainMainWindow] Constructor start";
|
||||||
setupUI();
|
setupUI();
|
||||||
|
qDebug() << "[MainMainWindow] setupUI done";
|
||||||
connectModels();
|
connectModels();
|
||||||
|
qDebug() << "[MainMainWindow] connectModels done";
|
||||||
setWindowTitle("Wino Mail DTK");
|
setWindowTitle("Wino Mail DTK");
|
||||||
resize(1280, 820);
|
resize(1280, 820);
|
||||||
// Setup progress bar in status bar
|
// Setup progress bar in status bar
|
||||||
@@ -27,12 +30,20 @@ MainMainWindow::MainMainWindow(QWidget *parent)
|
|||||||
// Connect mail service progress/status signals
|
// Connect mail service progress/status signals
|
||||||
connect(m_mailService, &MailService::progressChanged, this, &MainMainWindow::onProgressChanged);
|
connect(m_mailService, &MailService::progressChanged, this, &MainMainWindow::onProgressChanged);
|
||||||
connect(m_mailService, &MailService::statusMessage, this, &MainMainWindow::onStatusMessage);
|
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
|
// Provide account service to settings view
|
||||||
m_settingsView->setAccountService(m_accountService);
|
m_settingsView->setAccountService(m_accountService);
|
||||||
|
qDebug() << "[MainMainWindow] Constructor complete";
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainMainWindow::setupUI()
|
void MainMainWindow::setupUI()
|
||||||
{
|
{
|
||||||
|
qDebug() << "[MainMainWindow::setupUI] Start";
|
||||||
// === Global stylesheet ===
|
// === Global stylesheet ===
|
||||||
this->setStyleSheet(
|
this->setStyleSheet(
|
||||||
"QMainWindow { background-color: #f5f5f7; }"
|
"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; }"
|
"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; }"
|
"QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }"
|
||||||
);
|
);
|
||||||
|
qDebug() << "[MainMainWindow::setupUI] Stylesheet set";
|
||||||
|
|
||||||
createToolBar();
|
createToolBar();
|
||||||
|
qDebug() << "[MainMainWindow::setupUI] Toolbar created";
|
||||||
|
|
||||||
// === Central widget ===
|
// === Central widget ===
|
||||||
QWidget *central = new QWidget();
|
QWidget *central = new QWidget();
|
||||||
@@ -51,6 +64,7 @@ void MainMainWindow::setupUI()
|
|||||||
|
|
||||||
// === Sidebar ===
|
// === Sidebar ===
|
||||||
setupSidebar();
|
setupSidebar();
|
||||||
|
qDebug() << "[MainMainWindow::setupUI] Sidebar created";
|
||||||
centralLayout->addWidget(m_sidebar);
|
centralLayout->addWidget(m_sidebar);
|
||||||
|
|
||||||
// === Separator line ===
|
// === Separator line ===
|
||||||
@@ -62,23 +76,34 @@ void MainMainWindow::setupUI()
|
|||||||
// === Stacked pages ===
|
// === Stacked pages ===
|
||||||
m_stack = new QStackedWidget();
|
m_stack = new QStackedWidget();
|
||||||
m_stack->setStyleSheet("background-color: #f5f5f7;");
|
m_stack->setStyleSheet("background-color: #f5f5f7;");
|
||||||
|
qDebug() << "[MainMainWindow::setupUI] Stack created";
|
||||||
|
|
||||||
// Page 0: Mail (folder tree + mail list + reader)
|
// Page 0: Mail (folder tree + mail list + reader)
|
||||||
setupMailPage();
|
setupMailPage();
|
||||||
|
qDebug() << "[MainMainWindow::setupUI] Mail page created";
|
||||||
m_stack->addWidget(m_mailPage);
|
m_stack->addWidget(m_mailPage);
|
||||||
|
|
||||||
// Page 1: Compose
|
// Page 1: Compose
|
||||||
m_composeView = new ComposeView();
|
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) {
|
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) {
|
||||||
QString msg;
|
|
||||||
if (scheduleTime.isValid()) {
|
if (scheduleTime.isValid()) {
|
||||||
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
|
statusBar()->showMessage(tr("Scheduled sending is not available yet"), 5000);
|
||||||
} else {
|
return;
|
||||||
msg = "Message sent (simulated)";
|
|
||||||
}
|
}
|
||||||
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
|
if (fromAddr.isEmpty()) {
|
||||||
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
|
statusBar()->showMessage(tr("Select an account before sending"), 5000);
|
||||||
statusBar()->showMessage(msg, 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);
|
switchToPage(PageMail);
|
||||||
});
|
});
|
||||||
connect(m_composeView, &ComposeView::discardRequested, [this]() {
|
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);
|
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
|
||||||
});
|
});
|
||||||
m_stack->addWidget(m_settingsView);
|
m_stack->addWidget(m_settingsView);
|
||||||
|
qDebug() << "[MainMainWindow::setupUI] Settings page created";
|
||||||
|
|
||||||
// Page 3: Contacts
|
// Page 3: Contacts
|
||||||
m_contactsView = new ContactsView();
|
m_contactsView = new ContactsView();
|
||||||
@@ -131,9 +157,11 @@ void MainMainWindow::setupUI()
|
|||||||
|
|
||||||
centralLayout->addWidget(m_stack, 1);
|
centralLayout->addWidget(m_stack, 1);
|
||||||
setCentralWidget(central);
|
setCentralWidget(central);
|
||||||
|
qDebug() << "[MainMainWindow::setupUI] Central widget set";
|
||||||
|
|
||||||
// Show mail page by default
|
// Show mail page by default
|
||||||
switchToPage(PageMail);
|
switchToPage(PageMail);
|
||||||
|
qDebug() << "[MainMainWindow::setupUI] Done";
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainMainWindow::setupSidebar()
|
void MainMainWindow::setupSidebar()
|
||||||
@@ -161,8 +189,7 @@ void MainMainWindow::setupSidebar()
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MainMainWindow::setupMailPage()
|
void MainMainWindow::setupMailPage()
|
||||||
{
|
{ m_mailPage = new QWidget();
|
||||||
m_mailPage = new QWidget();
|
|
||||||
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
|
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
|
||||||
mailLayout->setContentsMargins(0, 0, 0, 0);
|
mailLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
mailLayout->setSpacing(0);
|
mailLayout->setSpacing(0);
|
||||||
@@ -189,7 +216,30 @@ void MainMainWindow::setupMailPage()
|
|||||||
});
|
});
|
||||||
m_folderSplitter->addWidget(m_mailListView);
|
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 = new ReaderView();
|
||||||
m_emailViewer->setMinimumWidth(350);
|
m_emailViewer->setMinimumWidth(350);
|
||||||
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
|
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
|
||||||
@@ -198,9 +248,22 @@ void MainMainWindow::setupMailPage()
|
|||||||
openMailInIndependentWindow(m_currentMailId);
|
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});
|
m_folderSplitter->setSizes({240, 380, 600});
|
||||||
|
|
||||||
mailLayout->addWidget(m_folderSplitter);
|
mailLayout->addWidget(m_folderSplitter);
|
||||||
@@ -208,12 +271,18 @@ void MainMainWindow::setupMailPage()
|
|||||||
|
|
||||||
void MainMainWindow::connectModels()
|
void MainMainWindow::connectModels()
|
||||||
{
|
{
|
||||||
|
qDebug() << "[MainMainWindow::connectModels] Start";
|
||||||
m_accountService = new AccountService(this);
|
m_accountService = new AccountService(this);
|
||||||
|
qDebug() << "[MainMainWindow::connectModels] AccountService created";
|
||||||
m_mailService = new MailService(m_accountService, this);
|
m_mailService = new MailService(m_accountService, this);
|
||||||
|
qDebug() << "[MainMainWindow::connectModels] MailService created";
|
||||||
m_composeView->setAccountService(m_accountService);
|
m_composeView->setAccountService(m_accountService);
|
||||||
|
m_embeddedComposeView->setAccountService(m_accountService);
|
||||||
|
|
||||||
m_folderModel = new FolderListModel(m_accountService, this);
|
m_folderModel = new FolderListModel(m_accountService, this);
|
||||||
|
qDebug() << "[MainMainWindow::connectModels] FolderListModel created";
|
||||||
m_emailModel = new EmailListModel(this);
|
m_emailModel = new EmailListModel(this);
|
||||||
|
qDebug() << "[MainMainWindow::connectModels] EmailListModel created";
|
||||||
|
|
||||||
m_folderTree->setModel(m_folderModel);
|
m_folderTree->setModel(m_folderModel);
|
||||||
m_mailListView->setModel(m_emailModel);
|
m_mailListView->setModel(m_emailModel);
|
||||||
@@ -221,6 +290,18 @@ void MainMainWindow::connectModels()
|
|||||||
m_folderTree->expandAll();
|
m_folderTree->expandAll();
|
||||||
|
|
||||||
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
|
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)
|
void MainMainWindow::onNavChanged(int index)
|
||||||
@@ -239,8 +320,7 @@ void MainMainWindow::switchToPage(int pageIndex)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MainMainWindow::onFolderSelected(const QModelIndex &index)
|
void MainMainWindow::onFolderSelected(const QModelIndex &index)
|
||||||
{
|
{ if (!index.isValid()) return;
|
||||||
if (!index.isValid()) return;
|
|
||||||
|
|
||||||
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
|
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
|
||||||
if (itemType == FolderTreeItem::FolderNode) {
|
if (itemType == FolderTreeItem::FolderNode) {
|
||||||
@@ -257,43 +337,55 @@ void MainMainWindow::onFolderSelected(const QModelIndex &index)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_emailModel->refresh();
|
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)
|
void MainMainWindow::onEmailSelected(int mailId)
|
||||||
{
|
{ m_currentMailId = mailId;
|
||||||
m_currentMailId = mailId;
|
if (mailId >= 0) {
|
||||||
std::optional<MailItem> item = MailItemDao::findById(mailId);
|
std::optional<MailItem> item = MailItemDao::findById(mailId);
|
||||||
if (!item.has_value()) {
|
if (!item.has_value()) {
|
||||||
m_emailViewer->setMailItem(nullptr);
|
m_emailViewer->setMailItem(nullptr);
|
||||||
|
m_viewerStack->setCurrentIndex(0); // placeholder
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
MailItem &mail = item.value();
|
MailItem &mail = item.value();
|
||||||
if (!mail.isRead()) {
|
if (!mail.isRead()) {
|
||||||
mail.setRead(true);
|
mail.setRead(true);
|
||||||
MailItemDao::update(mail);
|
MailItemDao::update(mail);
|
||||||
}
|
}
|
||||||
m_emailViewer->setMailItem(&mail);
|
m_emailViewer->setMailItem(&mail);
|
||||||
|
m_viewerStack->setCurrentIndex(1); // reader
|
||||||
|
} else {
|
||||||
|
m_emailViewer->setMailItem(nullptr);
|
||||||
|
m_viewerStack->setCurrentIndex(0); // placeholder
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainMainWindow::onComposeRequested()
|
void MainMainWindow::onComposeRequested()
|
||||||
{
|
{ m_embeddedComposeView->initializeComposition();
|
||||||
switchToPage(PageCompose);
|
m_viewerStack->setCurrentIndex(2); // compose
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainMainWindow::onReaderReplyRequested(const MailItem *item)
|
void MainMainWindow::onReaderReplyRequested(const MailItem *item)
|
||||||
{
|
{ if (item) {
|
||||||
if (item) {
|
m_embeddedComposeView->initializeComposition();
|
||||||
m_composeView->setTo(item->sender());
|
m_embeddedComposeView->setTo(item->sender());
|
||||||
m_composeView->setSubject("Re: " + item->subject());
|
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()
|
void MainMainWindow::onNewMessage()
|
||||||
{
|
{ // Show compose view in the viewer stack
|
||||||
switchToPage(PageCompose);
|
m_embeddedComposeView->initializeComposition();
|
||||||
|
m_viewerStack->setCurrentIndex(2); // compose
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainMainWindow::openMailInIndependentWindow(int mailId)
|
void MainMainWindow::openMailInIndependentWindow(int mailId)
|
||||||
@@ -329,8 +421,11 @@ void MainMainWindow::createToolBar()
|
|||||||
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
|
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
|
||||||
connect(syncAction, &QAction::triggered, [this]() {
|
connect(syncAction, &QAction::triggered, [this]() {
|
||||||
if (m_currentFolderId >= 0) {
|
if (m_currentFolderId >= 0) {
|
||||||
m_emailModel->refresh();
|
const auto folder = FolderDao::findById(m_currentFolderId);
|
||||||
statusBar()->showMessage("Refreshed", 2000);
|
if (folder) {
|
||||||
|
m_mailService->fetchMails(QString::number(folder->accountId()), QString::number(m_currentFolderId));
|
||||||
|
statusBar()->showMessage(tr("Synchronizing…"), 3000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
connect(openWinAction, &QAction::triggered, [this]() {
|
connect(openWinAction, &QAction::triggered, [this]() {
|
||||||
@@ -341,7 +436,12 @@ void MainMainWindow::createToolBar()
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
connect(deleteAction, &QAction::triggered, [this]() {
|
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)
|
void MainMainWindow::onAccountDeleteRequested(int accountId)
|
||||||
{
|
{
|
||||||
Q_UNUSED(accountId);
|
m_accountService->removeAccount(accountId);
|
||||||
statusBar()->showMessage(QString("Delete account %1 requested").arg(accountId), 3000);
|
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);
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
@@ -42,6 +42,14 @@ private slots:
|
|||||||
void onProgressChanged(int percent);
|
void onProgressChanged(int percent);
|
||||||
void onStatusMessage(const QString &msg);
|
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:
|
private:
|
||||||
void setupUI();
|
void setupUI();
|
||||||
void setupSidebar();
|
void setupSidebar();
|
||||||
@@ -68,6 +76,9 @@ private:
|
|||||||
QTreeView *m_folderTree;
|
QTreeView *m_folderTree;
|
||||||
MailListView *m_mailListView;
|
MailListView *m_mailListView;
|
||||||
ReaderView *m_emailViewer;
|
ReaderView *m_emailViewer;
|
||||||
|
QStackedWidget *m_viewerStack;
|
||||||
|
QWidget *m_placeholderWidget;
|
||||||
|
ComposeView *m_embeddedComposeView;
|
||||||
|
|
||||||
// Other pages
|
// Other pages
|
||||||
ComposeView *m_composeView;
|
ComposeView *m_composeView;
|
||||||
@@ -78,8 +89,8 @@ private:
|
|||||||
// Services & Models
|
// Services & Models
|
||||||
FolderListModel *m_folderModel;
|
FolderListModel *m_folderModel;
|
||||||
EmailListModel *m_emailModel;
|
EmailListModel *m_emailModel;
|
||||||
AccountService *m_accountService;
|
AccountService *m_accountService = nullptr;
|
||||||
MailService *m_mailService;
|
MailService *m_mailService = nullptr;
|
||||||
|
|
||||||
QToolBar *m_toolBar;
|
QToolBar *m_toolBar;
|
||||||
int m_currentFolderId;
|
int m_currentFolderId;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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)
|
||||||
@@ -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}")
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
#include "ui/readerview.h"
|
#include "ui/readerview.h"
|
||||||
#include <QFont>
|
#include <QFont>
|
||||||
|
#include <QDesktopServices>
|
||||||
|
#include <QUrl>
|
||||||
|
#include "db/dao/mailitemdao.h"
|
||||||
|
|
||||||
ReaderView::ReaderView(QWidget *parent) : QWidget(parent) {
|
ReaderView::ReaderView(QWidget *parent) : QWidget(parent) {
|
||||||
setupUI();
|
setupUI();
|
||||||
@@ -55,8 +58,19 @@ void ReaderView::setupUI() {
|
|||||||
m_bodyViewer->setOpenExternalLinks(true);
|
m_bodyViewer->setOpenExternalLinks(true);
|
||||||
m_bodyViewer->setFrameStyle(QFrame::NoFrame);
|
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->addWidget(headerWidget);
|
||||||
mainLayout->addLayout(actionsLayout);
|
mainLayout->addLayout(actionsLayout);
|
||||||
|
mainLayout->addWidget(m_attachmentList);
|
||||||
mainLayout->addWidget(m_bodyViewer);
|
mainLayout->addWidget(m_bodyViewer);
|
||||||
|
|
||||||
// Connections
|
// Connections
|
||||||
@@ -72,6 +86,8 @@ void ReaderView::setMailItem(const MailItem* item) {
|
|||||||
m_subjectLabel->setText("No mail selected");
|
m_subjectLabel->setText("No mail selected");
|
||||||
m_fromLabel->setText("From: ");
|
m_fromLabel->setText("From: ");
|
||||||
m_dateLabel->setText("Date: ");
|
m_dateLabel->setText("Date: ");
|
||||||
|
m_attachmentList->clear();
|
||||||
|
m_attachmentList->setVisible(false);
|
||||||
m_bodyViewer->setHtml("<i>Please select a message to read</i>");
|
m_bodyViewer->setHtml("<i>Please select a message to read</i>");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -79,5 +95,13 @@ void ReaderView::setMailItem(const MailItem* item) {
|
|||||||
m_subjectLabel->setText(item->subject());
|
m_subjectLabel->setText(item->subject());
|
||||||
m_fromLabel->setText(QString("From: %1").arg(item->sender()));
|
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_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());
|
m_bodyViewer->setHtml(item->bodyHtml());
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QTextBrowser>
|
#include <QTextBrowser>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QListWidget>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include "core/mailitem.h"
|
#include "core/mailitem.h"
|
||||||
@@ -31,6 +32,7 @@ private:
|
|||||||
QLabel *m_fromLabel;
|
QLabel *m_fromLabel;
|
||||||
QLabel *m_dateLabel;
|
QLabel *m_dateLabel;
|
||||||
QTextBrowser *m_bodyViewer;
|
QTextBrowser *m_bodyViewer;
|
||||||
|
QListWidget *m_attachmentList;
|
||||||
|
|
||||||
QPushButton *m_replyButton;
|
QPushButton *m_replyButton;
|
||||||
QPushButton *m_forwardButton;
|
QPushButton *m_forwardButton;
|
||||||
|
|||||||
@@ -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')
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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')
|
||||||
@@ -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')
|
||||||
Reference in New Issue
Block a user