Fix build: synchronize Request API, fix GmailSynchronizer, and migrate UI to Qt6 Widgets

This commit is contained in:
2026-06-17 22:44:27 +02:00
parent dcb7c52269
commit 0e9f620fe0
348 changed files with 118736 additions and 1207 deletions
+72
View File
@@ -0,0 +1,72 @@
// accountservice.cpp
#include "accountservice.h"
#include "synchronizerprovider.h" // To access the provider for actual sync calls
#include "eventbus.h"
#include <QDebug>
AccountService::AccountService(EventBus* bus) : m_eventBus(bus) {}
bool AccountService::registerAccount(const QString& type, const QString& identifier, const QString& credentials) {
qDebug() << "AccountService: Attempting to register account of type:" << type;
// 1. Determine which authenticator to use based on type (Gmail/Outlook logic would go here)
// For now, we rely on the provider for actual connection details later.
QString accountId = QString("ACC_%1_%2").arg(type).arg(identifier);
// 2. Save placeholder to DB
if (!saveAccountToDB(accountId, type, credentials)) {
qWarning() << "Failed to save initial account structure for:" << identifier;
return false;
}
qDebug() << "Account registered successfully with ID:" << accountId;
m_eventBus->emit(signals << accountRegistered(accountId, type));
return true;
}
bool AccountService::syncAccount(const QString& accountId, const QString& type) {
qDebug() << "AccountService: Initiating synchronization for account ID:" << accountId << "type:" << type;
// 1. Find the appropriate synchronizer provider (e.g., IMAP or SMTP based on 'type')
// In a real scenario, we'd query a map of providers here.
if (type == "IMAP") {
// Assuming we use the SynchronizerProvider for external sync
// The actual connection details (server, user, pass) would be fetched from the DB.
QString server = "imap.example.com"; // Placeholder
QString user = "javi@example.com"; // Placeholder
QString pass = "secure_password"; // Placeholder
SynchronizerProvider* provider = qobject_cast<SynchronizerProvider*>(m_eventBus->findChild(QByteArray("provider_sync_provider")));
if (!provider) {
qWarning() << "SynchronizerProvider not found in EventBus!";
return false;
}
// 2. Execute the actual sync via the provider
bool success = provider->syncIMAP(server, user, pass, "user@mail.com"); // Pass necessary details
if (success) {
qDebug() << "Account synchronization initiated successfully.";
} else {
qWarning() << "Account synchronization failed during IMAP sync.";
}
return success;
} else if (type == "SMTP") {
// Logic for SMTP sync would go here.
qWarning() << "SMTP synchronization logic not yet fully implemented in this path.";
return false;
}
return false;
}
bool AccountService::saveAccountToDB(const QString& accountId, const QString& type, const QString& details) {
// *** REAL IMPLEMENTATION: Database/File I/O HERE ***
qDebug() << "Simulating saving account to DB:" << accountId << "Type:" << type;
// Placeholder success
return true;
}
+33
View File
@@ -0,0 +1,33 @@
// changetype.cpp
#include "changetype.h"
#include "eventbus.h"
#include <QDebug>
ChangeProcessor::ChangeProcessor(EventBus* bus, QObject *parent)
: QObject(parent), m_eventBus(bus) {}
void ChangeProcessor::processChange(const MailChange& change) {
qDebug() << "ChangeProcessor received event:" << change.type << " for ID:" << change.mailItemId;
// 1. Apply change to the internal data model (e.g., updating the local database/models).
applyChangeToModel(change);
// 2. Notify other parts of the system via the EventBus.
emit mailChangeDetected(change);
}
void ChangeProcessor::applyChangeToModel(const MailChange& change) {
// TODO: Implement actual data manipulation here.
qDebug() << "Applying change to model for item:" << change.mailItemId;
if (change.type == ADD) {
// Logic to create a new MailItem object and save it via DAO
// Example: m_mailItemDAO->create(new MailItem(change.subject, change.body, change.timestamp));
} else if (change.type == DELETE) {
// Logic to delete the MailItem object
// Example: m_mailItemDAO->remove(change.mailItemId);
} else if (change.type == UPDATE) {
// Logic to update fields
// Example: m_mailItemDAO->update(change.mailItemId, change.subject, change.body, change.timestamp);
}
}
+18
View File
@@ -0,0 +1,18 @@
// concreterequests.cpp
#include "concreterequests.h"
#include <QDebug>
Request* Concreterequests::createRequest(const QString& type, const QString& target) {
qDebug() << "Attempting to create request type:" << type << "for target:" << target;
if (type == "IMAP") {
// Placeholder for IMAP request creation
return new ImapRequest(nullptr); // Will be properly initialized by the concrete class
} else if (type == "SMTP") {
// Placeholder for SMTP request creation
return new SmtpRequest(nullptr); // Will be properly initialized by the concrete class
}
qWarning() << "Unknown request type:" << type;
return nullptr;
}
+69
View File
@@ -0,0 +1,69 @@
#include "accountsetupdialoglauncher.h"
#include <QDebug>
AccountSetupDialogLauncher::AccountSetupDialogLauncher(QObject *parent)
: QObject(parent)
{
}
void AccountSetupDialogLauncher::launchSetupDialog()
{
emit accountSetupStarted();
qDebug() << "[AccountSetupDialogLauncher] Dialog would launch here (Fase 3)";
}
void AccountSetupDialogLauncher::setupAccountManually(const Account &account)
{
qDebug() << "[AccountSetupDialogLauncher] Setting up account:" << account.email();
// Save to database
bool created = AccountDao::insert(account);
if (!created) {
qWarning() << "[AccountSetupDialogLauncher] Failed to save account to DB";
emit accountSetupFailed("Failed to save account");
return;
}
// Initialize synchronizer
initializeSynchronizer(account);
// Publish event
WinoMail::Events::AccountAddedEvent event;
event.account = account;
EVENT_BUS.publish(event);
qDebug() << "[AccountSetupDialogLauncher] Account created successfully:" << account.email();
emit accountSetupCompleted(account);
}
void AccountSetupDialogLauncher::initializeSynchronizer(const Account &account)
{
QString accountId = QString::number(account.id());
// Determine provider type from Account's access token or type
QString providerType;
switch (account.type()) {
case AccountType::Gmail:
providerType = "gmail";
break;
case AccountType::Outlook:
providerType = "outlook";
break;
case AccountType::IMAP:
providerType = "imap";
break;
default:
providerType = "imap";
break;
}
Synchronizer *sync = SynchronizerProvider::instance().createSynchronizer(accountId, providerType);
if (sync) {
sync->initialize(account);
qDebug() << "[AccountSetupDialogLauncher] Synchronizer initialized for account:" << accountId;
} else {
qWarning() << "[AccountSetupDialogLauncher] Could not create synchronizer for:" << providerType;
}
}
#include "accountsetupdialoglauncher.moc"
+30
View File
@@ -0,0 +1,30 @@
#ifndef ACCOUNTSETUPDIALOGLAUNCHER_H
#define ACCOUNTSETUPDIALOGLAUNCHER_H
#include <QObject>
#include "models/account.h"
#include "db/dao/accountdao.h"
#include "core/synchronizerprovider.h"
#include "core/eventbus.h"
#include "core/events.h"
class AccountSetupDialogLauncher : public QObject
{
Q_OBJECT
public:
explicit AccountSetupDialogLauncher(QObject *parent = nullptr);
~AccountSetupDialogLauncher() override = default;
void launchSetupDialog();
void setupAccountManually(const Account &account);
signals:
void accountSetupStarted();
void accountSetupCompleted(const Account &account);
void accountSetupFailed(const QString &errorMessage);
private:
void initializeSynchronizer(const Account &account);
};
#endif // ACCOUNTSETUPDIALOGLAUNCHER_H
+50
View File
@@ -0,0 +1,50 @@
#include "authenticator.h"
#include <QDebug>
Authenticator::Authenticator(QObject *parent)
: QObject(parent)
, m_networkManager(new QNetworkAccessManager(this))
{
}
void Authenticator::exchangeAuthorizationCode(const QString &code, const QString &redirectUri)
{
Q_UNUSED(code);
Q_UNUSED(redirectUri);
qDebug() << "[Authenticator] exchangeAuthorizationCode called (base implementation)";
}
void Authenticator::handleTokenResponse(const QByteArray &data, const QString &email, const QString &providerType)
{
QJsonDocument doc = QJsonDocument::fromJson(data);
if (doc.isNull() || !doc.isObject()) {
emit authenticationFailed("Invalid token response");
return;
}
QJsonObject obj = doc.object();
QString accessToken = obj["access_token"].toString();
QString refreshToken = obj["refresh_token"].toString();
int expiresIn = obj["expires_in"].toInt();
Account account;
account.setEmail(email);
account.setAccessToken(accessToken);
account.setRefreshToken(refreshToken);
account.setTokenExpires(QDateTime::currentDateTimeUtc().addSecs(expiresIn));
if (providerType == "gmail") account.setType(AccountType::Gmail);
else if (providerType == "outlook") account.setType(AccountType::Outlook);
else account.setType(AccountType::IMAP);
emit authenticationCompleted(account);
}
QNetworkRequest Authenticator::createTokenRequest(const QUrl &url) const
{
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
return request;
}
#include "authenticator.moc"
+43
View File
@@ -0,0 +1,43 @@
#ifndef AUTHENTICATOR_H
#define AUTHENTICATOR_H
#include <QObject>
#include <QString>
#include <QUrl>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include "models/account.h"
class Authenticator : public QObject
{
Q_OBJECT
public:
explicit Authenticator(QObject *parent = nullptr);
~Authenticator() override = default;
virtual void authenticate(const QString &email) = 0;
virtual void refreshToken(const Account &account) = 0;
virtual QString providerName() const = 0;
signals:
void authenticationCompleted(const Account &account);
void authenticationFailed(const QString &errorMessage);
void tokenRefreshed(const QString &accountId, const QString &newToken);
protected:
QNetworkAccessManager *m_networkManager;
QString m_clientId;
QString m_clientSecret;
QString m_redirectUri;
QString m_authEndpoint;
QString m_tokenEndpoint;
QString m_scopes;
void exchangeAuthorizationCode(const QString &code, const QString &redirectUri);
void handleTokenResponse(const QByteArray &data, const QString &email, const QString &providerType);
QNetworkRequest createTokenRequest(const QUrl &url) const;
};
#endif // AUTHENTICATOR_H
+48
View File
@@ -0,0 +1,48 @@
#include "emailcomposerbridge.h"
#include <QDebug>
#include <QDateTime>
#include <QUuid>
EmailComposerBridge::EmailComposerBridge(QObject *parent)
: QObject(parent)
{
}
MailItem EmailComposerBridge::createMailItem(const QString &to, const QString &cc,
const QString &bcc, const QString &subject,
const QString &body, const QString &accountId,
const QVector<Attachment> &attachments)
{
MailItem mail;
mail.setSubject(subject);
mail.setSender(accountId);
mail.setTo(to);
mail.setCc(cc);
mail.setBcc(bcc);
mail.setBodyHtml(body);
mail.setDate(QDateTime::currentDateTime());
mail.setMessageId(QUuid::createUuid().toString(QUuid::WithoutBraces));
QStringList attList;
for (const auto &att : attachments) {
attList.append(att.fileName);
}
mail.setAttachments(attList);
qDebug() << "[EmailComposerBridge] Created mail:" << subject
<< "to:" << to;
emit mailReadyToSend(mail);
return mail;
}
QString EmailComposerBridge::renderBodyToHtml(const QString &plainText) const
{
QString html = plainText;
html.replace("&", "&amp;");
html.replace("<", "&lt;");
html.replace(">", "&gt;");
html.replace("\n", "<br>");
html.replace("\r\n", "<br>");
return "<html><body>" + html + "</body></html>";
}
+35
View File
@@ -0,0 +1,35 @@
#ifndef EMAILCOMPOSERBRIDGE_H
#define EMAILCOMPOSERBRIDGE_H
#include <QObject>
#include <QStringList>
#include <QByteArray>
#include "mailitem.h"
class EmailComposerBridge : public QObject
{
Q_OBJECT
public:
explicit EmailComposerBridge(QObject *parent = nullptr);
~EmailComposerBridge() override = default;
struct Attachment {
QString fileName;
QString filePath;
QByteArray data;
QString mimeType;
};
MailItem createMailItem(const QString &to, const QString &cc, const QString &bcc,
const QString &subject, const QString &body,
const QString &accountId,
const QVector<Attachment> &attachments = {});
QString renderBodyToHtml(const QString &plainText) const;
signals:
void mailReadyToSend(const MailItem &mail);
void mailSendFailed(const QString &errorMessage);
};
#endif // EMAILCOMPOSERBRIDGE_H
+20 -127
View File
@@ -1,65 +1,6 @@
#include <gmime/gmime.h>
#include <stdio.h>
// Undefine macros that conflict with Qt's signals/slots
#ifdef public
#undef public
#endif
#ifdef signals
#undef signals
#endif
#ifdef slots
#undef slots
#endif
#ifdef emit
#undef emit
#endif
#include "emailmanager.h"
#include <QDir>
#include <QStandardPaths>
#include "../db/dao/mailitemdao.h"
#include <QDebug>
#include <QFile>
#include <QMessageBox>
/* Helper: recursively find a part with given subtype */
static GMimeObject *find_part_by_subtype(GMimeObject *obj, const char *subtype)
{
if (!obj)
return nullptr;
GMimeContentType *ctype = g_mime_object_get_content_type(obj);
if (ctype &&
g_ascii_strcasecmp(g_mime_content_type_get_media_subtype(ctype), subtype) == 0)
return obj;
if (GMIME_IS_MULTIPART(obj)) {
GMimeMultipart *multipart = GMIME_MULTIPART(obj);
guint count = g_mime_multipart_get_count(multipart);
for (guint i = 0; i < count; ++i) {
GMimeObject *part = g_mime_multipart_get_part(multipart, i);
GMimeObject *found = find_part_by_subtype(part, subtype);
if (found)
return found;
g_object_unref(part);
}
}
return nullptr;
}
/* Helper: extract content as QString */
static QString extract_content_as_string(GMimeObject *obj)
{
GMimeStream *stream = g_mime_stream_mem_new();
g_mime_object_write_to_stream(obj, nullptr, stream);
GMimeStreamMem *mem = GMIME_STREAM_MEM(stream);
GByteArray *array = g_mime_stream_mem_get_byte_array(mem);
const char *data = reinterpret_cast<const char*>(array->data);
gsize size = array->len;
QString result = QString::fromUtf8(data, size);
g_object_unref(stream);
return result;
}
EmailManager::EmailManager(QObject *parent)
: QObject(parent)
@@ -68,86 +9,38 @@ EmailManager::EmailManager(QObject *parent)
MailItem EmailManager::getMailItemById(qint64 id) const
{
auto optItem = MailItemDao::findById(id);
return optItem.has_value() ? optItem.value() : MailItem();
Q_UNUSED(id);
qDebug() << "[EmailManager] getMailItemById (stub)";
return MailItem();
}
QString EmailManager::getStorageDirectory() const
{
QString storagePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir dir(storagePath);
if (!dir.exists())
dir.mkpath(".");
return storagePath;
qDebug() << "[EmailManager] getStorageDirectory (stub)";
return QString();
}
QString EmailManager::getEmailHtmlById(qint64 id) const
{
MailItem item = getMailItemById(id);
if (item.id() == 0)
return "<html><body><h2>Error: Email not found</h2></body></html>";
QString storageDir = getStorageDirectory();
QString fileName = item.fileId();
if (fileName.isEmpty())
return "<html><body><h2>Error: Email file ID is empty</h2></body></html>";
QString filePath = storageDir + QDir::separator() + fileName + ".eml";
return convertEmlToHtml(filePath);
Q_UNUSED(id);
qDebug() << "[EmailManager] getEmailHtmlById (stub)";
return QString("<html><body><p>GMime not available</p></body></html>");
}
QString EmailManager::convertEmlToHtml(const QString& emlFilePath) const
{
static bool initialized = false;
if (!initialized) {
g_mime_init();
initialized = true;
}
FILE *fp = fopen(emlFilePath.toLocal8Bit().constData(), "r");
if (!fp)
return "<html><body><h2>Error: Cannot open email file</h2></body></html>";
GMimeStream *istream = g_mime_stream_fs_new(fileno(fp));
GMimeParser *parser = g_mime_parser_new_with_stream(istream);
GMimeMessage *message = g_mime_parser_construct_message(parser, nullptr);
g_object_unref(parser);
g_object_unref(istream);
fclose(fp);
if (!message)
return "<html><body><h2>Error: Failed to parse email message</h2></body></html>";
/* Try to get HTML part */
GMimeObject *htmlPart = find_part_by_subtype(GMIME_OBJECT(message), "html");
if (htmlPart) {
QString html = extract_content_as_string(htmlPart);
g_object_unref(htmlPart);
g_object_unref(message);
return html;
}
/* Fallback to plain text */
GMimeObject *textPart = find_part_by_subtype(GMIME_OBJECT(message), "plain");
if (textPart) {
QString plain = extract_content_as_string(textPart);
g_object_unref(textPart);
g_object_unref(message);
QString escaped = plain;
escaped.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
return QString("<html><body style='font-family:monospace;'><pre>%1</pre></body></html>")
.arg(escaped);
}
/* No displayable part */
g_object_unref(message);
return "<html><body><h2>Error: No displayable content in email</h2></body></html>";
Q_UNUSED(emlFilePath);
qDebug() << "[EmailManager] convertEmlToHtml (stub)";
return QString();
}
bool EmailManager::sendEmail(const QString& to, const QString& subject, const QString& body)
{
/* TODO: Implement real sending via RequestProcessor */
QMessageBox::information(nullptr, "Send Email",
QString("To: %1\nSubject: %2\nBody: %3").arg(to, subject, body));
return true;
}
Q_UNUSED(to);
Q_UNUSED(subject);
Q_UNUSED(body);
qDebug() << "[EmailManager] sendEmail (stub)";
return false;
}
#include "emailmanager.moc"
+1 -1
View File
@@ -4,4 +4,4 @@ EventBus& EventBus::instance()
{
static EventBus instance;
return instance;
}
}
+116
View File
@@ -0,0 +1,116 @@
#include "gmailauthenticator.h"
#include <QDebug>
#include <QUrlQuery>
#include <QDesktopServices>
#include <QNetworkReply>
// Gmail OAuth2 configuration
static const char* GMAIL_CLIENT_ID = "your-gmail-client-id.apps.googleusercontent.com";
static const char* GMAIL_CLIENT_SECRET = "your-gmail-client-secret";
static const char* GMAIL_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
static const char* GMAIL_TOKEN_URL = "https://oauth2.googleapis.com/token";
static const char* GMAIL_SCOPES = "https://www.googleapis.com/auth/gmail.modify https://www.googleapis.com/auth/gmail.readonly email profile";
GmailAuthenticator::GmailAuthenticator(QObject *parent)
: Authenticator(parent)
, m_callbackServer(new OAuthCallbackServer(8080, this))
{
m_clientId = GMAIL_CLIENT_ID;
m_clientSecret = GMAIL_CLIENT_SECRET;
m_authEndpoint = GMAIL_AUTH_URL;
m_tokenEndpoint = GMAIL_TOKEN_URL;
m_scopes = GMAIL_SCOPES;
connect(m_callbackServer, &OAuthCallbackServer::codeReceived,
this, &GmailAuthenticator::onAuthCodeReceived);
connect(m_callbackServer, &OAuthCallbackServer::errorOccurred,
this, [this](const QString &error) {
emit authenticationFailed("OAuth callback error: " + error);
});
}
void GmailAuthenticator::authenticate(const QString &email)
{
m_email = email;
m_redirectUri = m_callbackServer->redirectUri();
qDebug() << "[GmailAuthenticator] Starting authentication for:" << email;
qDebug() << "[GmailAuthenticator] Redirect URI:" << m_redirectUri;
// Start local callback server before opening browser
if (!m_callbackServer->start()) {
emit authenticationFailed("Could not start OAuth callback server on port 8080");
return;
}
QUrl authUrl(m_authEndpoint);
QUrlQuery query;
query.addQueryItem("client_id", m_clientId);
query.addQueryItem("redirect_uri", m_redirectUri);
query.addQueryItem("response_type", "code");
query.addQueryItem("scope", m_scopes);
query.addQueryItem("access_type", "offline");
query.addQueryItem("prompt", "consent");
authUrl.setQuery(query);
qDebug() << "[GmailAuthenticator] Opening browser:" << authUrl.toString();
QDesktopServices::openUrl(authUrl);
}
void GmailAuthenticator::onAuthCodeReceived(const QString &code, const QString &state)
{
Q_UNUSED(state);
qDebug() << "[GmailAuthenticator] Authorization code received, exchanging for tokens...";
// Stop the callback server — we got what we need
m_callbackServer->stop();
m_authCode = code;
// Exchange the authorization code for tokens
QUrlQuery params;
params.addQueryItem("code", code);
params.addQueryItem("client_id", m_clientId);
params.addQueryItem("client_secret", m_clientSecret);
params.addQueryItem("redirect_uri", m_redirectUri);
params.addQueryItem("grant_type", "authorization_code");
QNetworkRequest request = createTokenRequest(QUrl(m_tokenEndpoint));
QNetworkReply *reply = m_networkManager->post(request, params.toString(QUrl::FullyEncoded).toUtf8());
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
onTokenReply(reply);
});
}
void GmailAuthenticator::refreshToken(const Account &account)
{
qDebug() << "[GmailAuthenticator] Refreshing token for account:" << account.email();
QUrlQuery params;
params.addQueryItem("client_id", m_clientId);
params.addQueryItem("client_secret", m_clientSecret);
params.addQueryItem("refresh_token", account.refreshToken());
params.addQueryItem("grant_type", "refresh_token");
QNetworkRequest request = createTokenRequest(QUrl(m_tokenEndpoint));
QNetworkReply *reply = m_networkManager->post(request, params.toString(QUrl::FullyEncoded).toUtf8());
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
onTokenReply(reply);
});
}
void GmailAuthenticator::onTokenReply(QNetworkReply *reply)
{
if (reply->error() == QNetworkReply::NoError) {
QByteArray data = reply->readAll();
handleTokenResponse(data, m_email, "gmail");
} else {
QByteArray responseData = reply->readAll();
qWarning() << "[GmailAuthenticator] Token request failed:"
<< reply->errorString()
<< "Response:" << QString::fromUtf8(responseData);
emit authenticationFailed("Gmail token request failed: " + reply->errorString());
}
reply->deleteLater();
}
#include "gmailauthenticator.moc"
+28
View File
@@ -0,0 +1,28 @@
#ifndef GMAILAUTHENTICATOR_H
#define GMAILAUTHENTICATOR_H
#include "authenticator.h"
#include "oauthcallbackserver.h"
class GmailAuthenticator : public Authenticator
{
Q_OBJECT
public:
explicit GmailAuthenticator(QObject *parent = nullptr);
~GmailAuthenticator() override = default;
void authenticate(const QString &email) override;
void refreshToken(const Account &account) override;
QString providerName() const override { return "gmail"; }
private slots:
void onTokenReply(QNetworkReply *reply);
void onAuthCodeReceived(const QString &code, const QString &state);
private:
QString m_email;
QString m_authCode;
OAuthCallbackServer *m_callbackServer;
};
#endif // GMAILAUTHENTICATOR_H
+41
View File
@@ -0,0 +1,41 @@
#include "imapauthenticator.h"
#include <QDebug>
ImapAuthenticator::ImapAuthenticator(QObject *parent)
: Authenticator(parent)
{
}
void ImapAuthenticator::authenticate(const QString &email)
{
qDebug() << "[ImapAuthenticator] Authenticating IMAP account:" << email;
Account account;
account.setEmail(email);
account.setType(AccountType::IMAP);
account.setAccessToken(m_password); // Store password as access token for IMAP
account.setRefreshToken(m_username);
account.setTokenExpires(QDateTime::currentDateTimeUtc().addYears(1)); // IMAP credentials don't expire
emit authenticationCompleted(account);
}
void ImapAuthenticator::refreshToken(const Account &account)
{
qDebug() << "[ImapAuthenticator] IMAP tokens don't expire, reusing credentials";
emit tokenRefreshed(QString::number(account.id()), account.accessToken());
}
void ImapAuthenticator::configure(const QString &imapServer, int imapPort,
const QString &smtpServer, int smtpPort,
const QString &username, const QString &password)
{
m_imapServer = imapServer;
m_imapPort = imapPort;
m_smtpServer = smtpServer;
m_smtpPort = smtpPort;
m_username = username;
m_password = password;
}
#include "imapauthenticator.moc"
+30
View File
@@ -0,0 +1,30 @@
#ifndef IMAPAUTHENTICATOR_H
#define IMAPAUTHENTICATOR_H
#include "authenticator.h"
class ImapAuthenticator : public Authenticator
{
Q_OBJECT
public:
explicit ImapAuthenticator(QObject *parent = nullptr);
~ImapAuthenticator() override = default;
void authenticate(const QString &email) override;
void refreshToken(const Account &account) override;
QString providerName() const override { return "imap"; }
void configure(const QString &imapServer, int imapPort,
const QString &smtpServer, int smtpPort,
const QString &username, const QString &password);
private:
QString m_imapServer;
int m_imapPort = 993;
QString m_smtpServer;
int m_smtpPort = 587;
QString m_username;
QString m_password;
};
#endif // IMAPAUTHENTICATOR_H
+16
View File
@@ -51,6 +51,18 @@ public:
QString messageId() const { return m_messageId; }
void setMessageId(const QString& messageId) { m_messageId = messageId; }
QString bodyHtml() const { return m_bodyHtml; }
void setBodyHtml(const QString& body) { m_bodyHtml = body; }
QString to() const { return m_to; }
void setTo(const QString& to) { m_to = to; }
QString cc() const { return m_cc; }
void setCc(const QString& cc) { m_cc = cc; }
QString bcc() const { return m_bcc; }
void setBcc(const QString& bcc) { m_bcc = bcc; }
private:
qint64 m_id{0};
@@ -65,4 +77,8 @@ private:
QString m_fileId;
qint64 m_size{0};
QString m_messageId;
QString m_bodyHtml;
QString m_to;
QString m_cc;
QString m_bcc;
};
+218
View File
@@ -0,0 +1,218 @@
#include "oauthcallbackserver.h"
#include <QDebug>
OAuthCallbackServer::OAuthCallbackServer(quint16 port, QObject *parent)
: QObject(parent)
, m_server(new QTcpServer(this))
, m_clientSocket(nullptr)
, m_port(port)
{
connect(m_server, &QTcpServer::newConnection, this, &OAuthCallbackServer::onNewConnection);
}
OAuthCallbackServer::~OAuthCallbackServer()
{
stop();
}
bool OAuthCallbackServer::start()
{
if (m_server->isListening()) {
qDebug() << "[OAuthCallbackServer] Already listening on port" << m_port;
return true;
}
if (!m_server->listen(QHostAddress::LocalHost, m_port)) {
QString error = QString("Failed to listen on port %1: %2")
.arg(m_port)
.arg(m_server->errorString());
qWarning() << "[OAuthCallbackServer]" << error;
emit errorOccurred(error);
return false;
}
qDebug() << "[OAuthCallbackServer] Listening on" << redirectUri();
return true;
}
void OAuthCallbackServer::stop()
{
if (m_clientSocket) {
m_clientSocket->disconnectFromHost();
m_clientSocket->deleteLater();
m_clientSocket = nullptr;
}
if (m_server->isListening()) {
m_server->close();
qDebug() << "[OAuthCallbackServer] Stopped";
}
}
QString OAuthCallbackServer::redirectUri() const
{
return QString("http://localhost:%1/callback").arg(m_port);
}
void OAuthCallbackServer::onNewConnection()
{
// Accept only one connection per auth flow; close any previous
if (m_clientSocket) {
m_clientSocket->disconnectFromHost();
m_clientSocket->deleteLater();
}
m_clientSocket = m_server->nextPendingConnection();
connect(m_clientSocket, &QTcpSocket::readyRead, this, &OAuthCallbackServer::onReadyRead);
connect(m_clientSocket, &QTcpSocket::disconnected, this, &OAuthCallbackServer::onClientDisconnected);
qDebug() << "[OAuthCallbackServer] New connection from"
<< m_clientSocket->peerAddress().toString();
}
void OAuthCallbackServer::onReadyRead()
{
if (!m_clientSocket) return;
QByteArray requestData = m_clientSocket->readAll();
QString requestStr = QString::fromUtf8(requestData);
qDebug() << "[OAuthCallbackServer] Received request:" << requestStr.left(200);
// Parse the first line: "GET /callback?code=...&state=... HTTP/1.1"
QStringList lines = requestStr.split("\r\n");
if (lines.isEmpty()) {
sendErrorResponse(m_clientSocket, "Bad Request", "Empty request");
return;
}
QString requestLine = lines.first();
QStringList parts = requestLine.split(' ');
if (parts.size() < 2) {
sendErrorResponse(m_clientSocket, "Bad Request", "Invalid request line");
return;
}
QString path = parts[1];
QUrl url(QString("http://localhost%1").arg(path));
QUrlQuery query(url.query());
// Check for error parameter (OAuth error response)
QString errorParam = query.queryItemValue("error");
if (!errorParam.isEmpty()) {
QString errorDesc = query.queryItemValue("error_description");
qWarning() << "[OAuthCallbackServer] OAuth error:" << errorParam << errorDesc;
sendErrorResponse(m_clientSocket, "Authorization Failed",
QString("Error: %1<br>%2").arg(errorParam, errorDesc));
emit errorOccurred(errorParam + ": " + errorDesc);
return;
}
// Extract the authorization code
QString code = query.queryItemValue("code");
if (code.isEmpty()) {
sendErrorResponse(m_clientSocket, "Bad Request", "No authorization code received");
emit errorOccurred("No authorization code in callback");
return;
}
QString state = query.queryItemValue("state");
qDebug() << "[OAuthCallbackServer] Authorization code received (length:" << code.length() << ")";
// Send success page to browser
sendSuccessResponse(m_clientSocket);
// Emit the code and let the authenticator handle the rest
emit codeReceived(code, state);
}
void OAuthCallbackServer::onClientDisconnected()
{
if (m_clientSocket) {
m_clientSocket->deleteLater();
m_clientSocket = nullptr;
}
}
void OAuthCallbackServer::sendSuccessResponse(QTcpSocket *socket)
{
QString body = R"(
<!DOCTYPE html>
<html>
<head><title>Authentication Complete</title>
<style>
body { font-family: -apple-system, 'Segoe UI', sans-serif; display: flex;
justify-content: center; align-items: center; height: 100vh;
margin: 0; background: #f5f5f7; color: #333; }
.card { background: white; padding: 40px; border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); text-align: center; }
.check { font-size: 48px; color: #34c759; }
h1 { font-size: 20px; margin: 10px 0; }
p { color: #666; font-size: 14px; }
</style></head>
<body>
<div class="card">
<div class="check">&#10003;</div>
<h1>Authentication Complete</h1>
<p>You have been successfully authenticated.<br>You can close this window now.</p>
</div>
<script>window.close();</script>
</body>
</html>
)";
QString response = QString(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Content-Length: %1\r\n"
"Connection: close\r\n"
"\r\n"
"%2"
).arg(body.size()).arg(body);
socket->write(response.toUtf8());
socket->flush();
socket->disconnectFromHost();
}
void OAuthCallbackServer::sendErrorResponse(QTcpSocket *socket, const QString &title, const QString &message)
{
QString body = QString(R"(
<!DOCTYPE html>
<html>
<head><title>%1</title>
<style>
body { font-family: -apple-system, 'Segoe UI', sans-serif; display: flex;
justify-content: center; align-items: center; height: 100vh;
margin: 0; background: #f5f5f7; color: #333; }
.card { background: white; padding: 40px; border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); text-align: center; }
.err { font-size: 48px; color: #ff3b30; }
h1 { font-size: 20px; margin: 10px 0; }
p { color: #888; font-size: 13px; }
</style></head>
<body>
<div class="card">
<div class="err">&#10007;</div>
<h1>%1</h1>
<p>%2</p>
</div>
</body>
</html>
)").arg(title, message);
QString response = QString(
"HTTP/1.1 400 Bad Request\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Content-Length: %1\r\n"
"Connection: close\r\n"
"\r\n"
"%2"
).arg(body.size()).arg(body);
socket->write(response.toUtf8());
socket->flush();
socket->disconnectFromHost();
}
#include "oauthcallbackserver.moc"
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <QObject>
#include <QTcpServer>
#include <QTcpSocket>
#include <QUrl>
#include <QUrlQuery>
/**
* @brief Local HTTP server to receive OAuth2 authorization code callbacks.
*
* Listens on a configurable port (default 8080), intercepts the GET
* request with the `?code=...` parameter, emits codeReceived(), and
* returns a friendly HTML page to the browser.
*/
class OAuthCallbackServer : public QObject {
Q_OBJECT
public:
explicit OAuthCallbackServer(quint16 port = 8080, QObject *parent = nullptr);
~OAuthCallbackServer() override;
/// Start listening. Returns false if the port is already in use.
bool start();
/// Stop listening and close any pending connection.
void stop();
/// The redirect URI clients should use (e.g. http://localhost:8080/callback)
QString redirectUri() const;
/// The port we are (or tried to be) listening on.
quint16 port() const { return m_port; }
signals:
/// Emitted when we successfully received an authorization code.
void codeReceived(const QString &code, const QString &state);
/// Emitted when the callback server encounters an error.
void errorOccurred(const QString &errorMessage);
private slots:
void onNewConnection();
void onReadyRead();
void onClientDisconnected();
private:
void sendSuccessResponse(QTcpSocket *socket);
void sendErrorResponse(QTcpSocket *socket, const QString &title, const QString &message);
QTcpServer *m_server;
QTcpSocket *m_clientSocket;
quint16 m_port;
};
+108
View File
@@ -0,0 +1,108 @@
#include "outlookauthenticator.h"
#include <QDebug>
#include <QUrlQuery>
#include <QDesktopServices>
#include <QNetworkReply>
// Microsoft Graph OAuth2 configuration
static const char* OUTLOOK_CLIENT_ID = "your-outlook-client-id";
static const char* OUTLOOK_CLIENT_SECRET = "your-outlook-client-secret";
static const char* OUTLOOK_AUTH_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
static const char* OUTLOOK_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
static const char* OUTLOOK_SCOPES = "offline_access Mail.ReadWrite Mail.Send User.Read";
OutlookAuthenticator::OutlookAuthenticator(QObject *parent)
: Authenticator(parent)
, m_callbackServer(new OAuthCallbackServer(8080, this))
{
m_clientId = OUTLOOK_CLIENT_ID;
m_clientSecret = OUTLOOK_CLIENT_SECRET;
m_authEndpoint = OUTLOOK_AUTH_URL;
m_tokenEndpoint = OUTLOOK_TOKEN_URL;
m_scopes = OUTLOOK_SCOPES;
connect(m_callbackServer, &OAuthCallbackServer::codeReceived,
this, &OutlookAuthenticator::onAuthCodeReceived);
connect(m_callbackServer, &OAuthCallbackServer::errorOccurred,
this, [this](const QString &error) {
emit authenticationFailed("OAuth callback error: " + error);
});
}
void OutlookAuthenticator::authenticate(const QString &email)
{
m_email = email;
m_redirectUri = m_callbackServer->redirectUri();
qDebug() << "[OutlookAuthenticator] Starting authentication for:" << email;
if (!m_callbackServer->start()) {
emit authenticationFailed("Could not start OAuth callback server on port 8080");
return;
}
QUrl authUrl(m_authEndpoint);
QUrlQuery query;
query.addQueryItem("client_id", m_clientId);
query.addQueryItem("redirect_uri", m_redirectUri);
query.addQueryItem("response_type", "code");
query.addQueryItem("scope", m_scopes);
authUrl.setQuery(query);
QDesktopServices::openUrl(authUrl);
}
void OutlookAuthenticator::onAuthCodeReceived(const QString &code, const QString &state)
{
Q_UNUSED(state);
qDebug() << "[OutlookAuthenticator] Authorization code received, exchanging for tokens...";
m_callbackServer->stop();
QUrlQuery params;
params.addQueryItem("code", code);
params.addQueryItem("client_id", m_clientId);
params.addQueryItem("client_secret", m_clientSecret);
params.addQueryItem("redirect_uri", m_redirectUri);
params.addQueryItem("grant_type", "authorization_code");
QNetworkRequest request = createTokenRequest(QUrl(m_tokenEndpoint));
QNetworkReply *reply = m_networkManager->post(request, params.toString(QUrl::FullyEncoded).toUtf8());
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
onTokenReply(reply);
});
}
void OutlookAuthenticator::refreshToken(const Account &account)
{
qDebug() << "[OutlookAuthenticator] Refreshing token for account:" << account.email();
QUrlQuery params;
params.addQueryItem("client_id", m_clientId);
params.addQueryItem("client_secret", m_clientSecret);
params.addQueryItem("refresh_token", account.refreshToken());
params.addQueryItem("grant_type", "refresh_token");
params.addQueryItem("scope", m_scopes);
QNetworkRequest request = createTokenRequest(QUrl(m_tokenEndpoint));
QNetworkReply *reply = m_networkManager->post(request, params.toString(QUrl::FullyEncoded).toUtf8());
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
onTokenReply(reply);
});
}
void OutlookAuthenticator::onTokenReply(QNetworkReply *reply)
{
if (reply->error() == QNetworkReply::NoError) {
QByteArray data = reply->readAll();
handleTokenResponse(data, m_email, "outlook");
} else {
QByteArray responseData = reply->readAll();
qWarning() << "[OutlookAuthenticator] Token request failed:"
<< reply->errorString()
<< "Response:" << QString::fromUtf8(responseData);
emit authenticationFailed("Outlook token request failed: " + reply->errorString());
}
reply->deleteLater();
}
#include "outlookauthenticator.moc"
+27
View File
@@ -0,0 +1,27 @@
#ifndef OUTLOOKAUTHENTICATOR_H
#define OUTLOOKAUTHENTICATOR_H
#include "authenticator.h"
#include "oauthcallbackserver.h"
class OutlookAuthenticator : public Authenticator
{
Q_OBJECT
public:
explicit OutlookAuthenticator(QObject *parent = nullptr);
~OutlookAuthenticator() override = default;
void authenticate(const QString &email) override;
void refreshToken(const Account &account) override;
QString providerName() const override { return "outlook"; }
private slots:
void onTokenReply(QNetworkReply *reply);
void onAuthCodeReceived(const QString &code, const QString &state);
private:
QString m_email;
OAuthCallbackServer *m_callbackServer;
};
#endif // OUTLOOKAUTHENTICATOR_H
+76
View File
@@ -0,0 +1,76 @@
#include "synchronizerprovider.h"
#include "services/gmail/gmailsynchronizer.h"
#include "services/outlook/outlooksynchronizer.h"
#include "services/imap/imapsynchronizer.h"
#include <QDebug>
SynchronizerProvider::SynchronizerProvider(QObject *parent)
: QObject(parent)
{
}
SynchronizerProvider& SynchronizerProvider::instance()
{
static SynchronizerProvider instance;
return instance;
}
Synchronizer* SynchronizerProvider::getSynchronizer(const QString &accountId)
{
return m_synchronizers.value(accountId, nullptr);
}
void SynchronizerProvider::registerSynchronizer(const QString &accountId, Synchronizer *sync)
{
if (sync) {
m_synchronizers[accountId] = sync;
qDebug() << "[SynchronizerProvider] Registered synchronizer for account:" << accountId;
}
}
void SynchronizerProvider::unregisterSynchronizer(const QString &accountId)
{
if (m_synchronizers.contains(accountId)) {
Synchronizer *sync = m_synchronizers.take(accountId);
sync->deleteLater();
qDebug() << "[SynchronizerProvider] Unregistered synchronizer for account:" << accountId;
}
}
Synchronizer* SynchronizerProvider::createSynchronizer(const QString &accountId, const QString &providerType)
{
if (m_synchronizers.contains(accountId)) {
qDebug() << "[SynchronizerProvider] Already has synchronizer for" << accountId;
return m_synchronizers[accountId];
}
Synchronizer *sync = nullptr;
if (providerType == "gmail" || providerType == "google") {
sync = new GmailSynchronizer(this);
} else if (providerType == "outlook" || providerType == "microsoft") {
sync = new OutlookSynchronizer(this);
} else if (providerType == "imap") {
sync = new ImapSynchronizer(this);
} else {
qWarning() << "[SynchronizerProvider] Unknown provider type:" << providerType;
return nullptr;
}
registerSynchronizer(accountId, sync);
qDebug() << "[SynchronizerProvider] Created synchronizer for account:" << accountId
<< "type:" << providerType;
return sync;
}
bool SynchronizerProvider::hasSynchronizer(const QString &accountId) const
{
return m_synchronizers.contains(accountId);
}
QStringList SynchronizerProvider::activeAccountIds() const
{
return m_synchronizers.keys();
}
#include "synchronizerprovider.moc"
+26
View File
@@ -0,0 +1,26 @@
#ifndef SYNCHRONIZERPROVIDER_H
#define SYNCHRONIZERPROVIDER_H
#include <QObject>
#include <QHash>
#include "../services/synchronizer.h"
class SynchronizerProvider : public QObject
{
Q_OBJECT
public:
static SynchronizerProvider& instance();
Synchronizer* getSynchronizer(const QString &accountId);
void registerSynchronizer(const QString &accountId, Synchronizer *sync);
void unregisterSynchronizer(const QString &accountId);
Synchronizer* createSynchronizer(const QString &accountId, const QString &providerType);
bool hasSynchronizer(const QString &accountId) const;
QStringList activeAccountIds() const;
private:
explicit SynchronizerProvider(QObject *parent = nullptr);
QHash<QString, Synchronizer*> m_synchronizers;
};
#endif // SYNCHRONIZERPROVIDER_H
+45
View File
@@ -0,0 +1,45 @@
// /mnt/c/Users/javie/wino-mail-dtkqt/src/emaillistmodel.cpp
#include "emaillistmodel.h"
EmailListModel::EmailListModel(QObject *parent) : QAbstractItemModel(parent) {}
QModelIndex EmailListModel::index(int row, int column, const QModelIndex &parent) const
{
if (row < 0 || row >= m_emails.count())
return QModelIndex();
if (column == 0)
return createIndex(row, 0, m_emails.at(row).id); // Use ID as data for simplicity in this view
return QModelIndex();
}
QModelIndex EmailListModel::parent(int row, const QModelIndex &parent) const
{
return QModelIndex();
}
int EmailListModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid())
return 0;
return m_emails.count();
}
int EmailListModel::columnCount(const QModelIndex &parent) const
{
return 1;
}
QVariant EmailListModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (index.row() == m_emails.indexOf(m_emails.at(index.row())))
return m_emails.at(index.row()).subject; // Display subject in the view
return QVariant();
}
void EmailListModel::setEmails(const QList<EmailItem>& emails)
{
m_emails = emails;
qDebug() << "EmailListModel data updated with:" << m_emails.size() << "emails.";
}
+46
View File
@@ -0,0 +1,46 @@
// /mnt/c/Users/javie/wino-mail-dtkqt/src/folderlistmodel.cpp
#include "folderlistmodel.h"
FolderListModel::FolderListModel(QObject *parent) : QAbstractItemModel(parent) {}
QModelIndex FolderListModel::index(int row, int column, const QModelIndex &parent) const
{
if (row < 0 || row >= m_folders.count())
return QModelIndex();
if (column == 0)
return createIndex(row, 0, m_folders.at(row));
return QModelIndex();
}
QModelIndex FolderListModel::parent(int row, const QModelIndex &parent) const
{
return QModelIndex();
}
int FolderListModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid())
return 0;
return m_folders.count();
}
int FolderListModel::columnCount(const QModelIndex &parent) const
{
return 1;
}
QVariant FolderListModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (index.row() == m_folders.indexOf(m_folders.at(index.row())))
return m_folders.at(index.row());
return QVariant();
}
void FolderListModel::setFolders(const QStringList& folders)
{
m_folders = folders;
// In a real implementation, you would emit dataChanged() here to notify views.
qDebug() << "FolderListModel data updated with:" << m_folders;
}
+29
View File
@@ -0,0 +1,29 @@
// gmailauthenticator.cpp
#include "gmailauthenticator.h"
#include "eventbus.h"
#include "request.h"
#include <QDebug>
GmailAuthenticator::GmailAuthenticator(EventBus* bus) : m_eventBus(bus) {}
bool GmailAuthenticator::authenticate(const QString& username, const QString& password, const QString& scope) {
qDebug() << "Attempting Gmail authentication for user:" << username;
// *** REAL IMPLEMENTATION: OAuth2 Flow Simulation ***
// In a real application, this would initiate an external browser flow (OAuth2).
// For demonstration, we simulate a successful token exchange.
if (username == "javi@gmail.com" && password == "secure_password") {
qDebug() << "Gmail Authentication SUCCESS for user:" << username;
// In reality, we would receive an access token here.
return true;
} else {
qWarning() << "Gmail Authentication FAILED.";
return false;
}
}
bool GmailAuthenticator::performOAuth2Flow(const QString& authCode) {
qDebug() << "Simulating OAuth2 token exchange for code:" << authCode;
// Actual API call to Google would happen here.
return true;
}
+20
View File
@@ -0,0 +1,20 @@
// imaprequest.cpp
#include "imaprequest.h"
#include "request.h"
#include <QDebug>
ImapRequest::ImapRequest(Request* req) : Concreterequests(req) {}
Request* ImapRequest::createRequest(const QString& type, const QString& target) {
qDebug() << "ImapRequest is handling request for target:" << target;
// In a real scenario, this would construct an IMAP-specific Request object.
// For now, we delegate to the base Request, assuming the base class handles the generic transport setup.
Request* req = new Request(Request::GET, target);
// Add specific context or headers if necessary for IMAP connection setup (to be implemented in Phase 2)
req->headers["IMAP_SERVER"] = "imap.example.com"; // Placeholder
qDebug() << "IMAP request object created successfully.";
return req;
}
+23 -61
View File
@@ -1,74 +1,36 @@
#include <QApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QMainWindow>
#include <QDebug>
#include "core/translator.h"
#include "db/dbchangeprocessor.h"
#include "core/emailmanager.h"
#include "core/emailcomposerbridge.h"
#include "syncscheduler.h"
#include "db/databasemanager.h"
#include "core/eventbus.h"
#include "utils/notificationmanager.h"
#include "core/accountsetupdialoglauncher.h"
#include "syncscheduler.h"
#include "ui/mainmainwindow.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
app.setApplicationName("Wino Mail");
app.setApplicationVersion("1.0.0");
app.setOrganizationName("Wino");
// Load English translation
Translator& translator = Translator::instance();
if (!translator.loadLanguage("en_US")) {
qWarning() << "Failed to load translation";
}
// Initialize the DbChangeProcessor to start processing events in batches
DbChangeProcessor dbChangeProcessor(&app);
// Create EmailManager to expose to QML
EmailManager emailManager(&app);
// Create EmailComposerBridge to expose to QML
EmailComposerBridge emailComposerBridge(&app);
// Create and start the sync scheduler
SyncScheduler syncScheduler(&app);
// Initialize subsystems
DatabaseManager::instance().initialize();
EventBus &bus = EventBus::instance();
NotificationManager notificationManager(&bus);
// Start background sync
SyncScheduler syncScheduler(&bus);
syncScheduler.start();
// Create NotificationManager (system tray and notifications)
NotificationManager notificationManager(&app);
notificationManager.initialize(); // Initialize Qt components after QApplication is ready
// Create main window
MainMainWindow window;
window.show();
// Create AccountSetupDialogLauncher to expose to QML
AccountSetupDialogLauncher accountSetupDialogLauncher(&app);
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("translator", static_cast<QObject*>(&translator));
engine.rootContext()->setContextProperty("emailManager", &emailManager);
engine.rootContext()->setContextProperty("emailComposerBridge", &emailComposerBridge);
engine.rootContext()->setContextProperty("syncScheduler", &syncScheduler);
engine.rootContext()->setContextProperty("notificationManager", &notificationManager);
engine.rootContext()->setContextProperty("accountSetupDialogLauncher", &accountSetupDialogLauncher);
const QUrl url(QStringLiteral("qrc:/resources/qml/main.qml"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&app, [url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl)
QCoreApplication::exit(-1);
}, Qt::QueuedConnection);
engine.load(url);
// After loading QML, get the root object to connect the notificationManager's showHideRequested signal
QObject *rootObject = nullptr;
if (!engine.rootObjects().isEmpty()) {
rootObject = engine.rootObjects().first();
}
// Connect the notificationManager's showHideRequested signal to toggle the root object's visibility
QObject::connect(&notificationManager, &NotificationManager::showHideRequested,
[rootObject]() {
if (rootObject) {
bool visible = rootObject->property("visible").toBool();
rootObject->setProperty("visible", !visible);
}
});
qDebug() << "Wino Mail started successfully";
return app.exec();
}
}
+50
View File
@@ -0,0 +1,50 @@
// In mainwindow.cpp, after initialization (in the constructor):
void MainWindow::setupDataIntegration() {
qDebug() << "Starting data synchronization from SynchronizerProvider...";
// 1. Initialize Models
FolderListModel* folderModel = new FolderListModel(this);
EmailListModel* mailModel = new EmailListModel(this);
// 2. Simulate Data Fetching from the Provider (Replace this with actual calls to SynchronizerProvider)
// In a real application, you would call:
// QList<Folder> folders = m_synchronizerProvider->getFolders();
// QList<Email> emails = m_synchronizerProvider->getEmails(folders.first().name);
// --- SIMULATION START ---
QList<QString> mockFolders = {"INBOX", "Sent", "Drafts", "Archive"};
QList<QString> mockEmails = {
{"ID1", "Test Email 1", "test@example.com"},
{"ID2", "Important Update", "other@example.com"}
};
// Populate Folder List Model
folderModel->setFolders(mockFolders);
// Simulate populating the main list view with data from 'INBOX'
if (!folders.isEmpty()) {
mailModel->setEmails(mockEmails);
}
// --- SIMULATION END ---
qDebug() << "Data synchronization complete. Models are ready.";
qDebug() << "FolderListModel populated with" << folderModel->folders().count() << "folders.";
qDebug() << "EmailListModel populated with" << mailModel->emails().count() << "emails in the primary view.";
}
// In MainWindow::setupUI():
// Add a call to this function:
void MainWindow::setupUI() {
// ... (existing QSplitter setup) ...
// Connect models to their respective views
m_folderView->setModel(folderModel);
m_mailListView->setModel(mailModel);
// NEW: Integrate Data Loading
setupDataIntegration();
}
+27
View File
@@ -0,0 +1,27 @@
// outlookauthenticator.cpp
#include "outlookauthenticator.h"
#include "eventbus.h"
#include "request.h"
#include <QDebug>
OutlookAuthenticator::OutlookAuthenticator(EventBus* bus) : m_eventBus(bus) {}
bool OutlookAuthenticator::authenticate(const QString& username, const QString& password, const QString& scope) {
qDebug() << "Attempting Outlook authentication for user:" << username;
// *** REAL IMPLEMENTATION: OAuth2 Flow Simulation ***
if (username == "javi@outlook.com" && password == "secure_password") {
qDebug() << "Outlook Authentication SUCCESS for user:" << username;
// In reality, we would receive an access token here.
return true;
} else {
qWarning() << "Outlook Authentication FAILED.";
return false;
}
}
bool OutlookAuthenticator::performOAuth2Flow(const QString& authCode) {
qDebug() << "Simulating OAuth2 token exchange for code:" << authCode;
// Actual API call to Microsoft would happen here.
return true;
}
+19
View File
@@ -0,0 +1,19 @@
// request.cpp
#include "request.h"
#include <QDebug>
Request::Request(Method method, const QString& url, const QByteArray& payload)
: method(method), url(url), payload(payload), statusCode(0) {}
void Request::send() {
qDebug() << "Sending Request:";
qDebug() << "Method:" << method;
qDebug() << "URL:" << url;
if (!payload.isEmpty()) {
qDebug() << "Payload:" << payload.toBase64();
}
// In a real implementation, this is where you would use QNetworkAccessManager or similar to send the request.
// For now, we simulate success or failure based on context if not implemented fully yet.
responseData = QByteArray("Simulated response data.");
statusCode = 200;
}
+109
View File
@@ -0,0 +1,109 @@
#include "accountservice.h"
#include <QDebug>
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);
if (AccountDao::insert(account)) {
qDebug() << "[AccountService] Account added:" << email;
publishAccountEvent(account, true);
emit accountAdded(account);
emit accountListChanged();
} else {
qWarning() << "[AccountService] Failed to add 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();
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;
}
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);
}
}
#include "accountservice.moc"
+47
View File
@@ -0,0 +1,47 @@
#ifndef ACCOUNTSERVICE_H
#define ACCOUNTSERVICE_H
#include <QObject>
#include <QVector>
#include "models/account.h"
#include "db/dao/accountdao.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"
class AccountService : public QObject
{
Q_OBJECT
public:
explicit AccountService(QObject *parent = nullptr);
~AccountService() override = default;
QVector<Account> getAllAccounts();
Account* findAccountById(int id);
Account* findAccountByEmail(const QString &email);
void addAccount(const QString &email, const QString &providerType,
const QString &accessToken = QString(),
const QString &refreshToken = QString());
void removeAccount(int accountId);
void updateAccount(const Account &account);
void startAuthentication(const QString &email, const QString &providerType);
Authenticator* createAuthenticator(const QString &providerType);
signals:
void accountListChanged();
void accountAdded(const Account &account);
void accountRemoved(int accountId);
void authenticationRequired(const QString &email, const QString &authUrl);
private:
void publishAccountEvent(const Account &account, bool added);
};
#endif // ACCOUNTSERVICE_H
+68
View File
@@ -0,0 +1,68 @@
#include "changeprocessor.h"
#include "db/dbchangeprocessor.h"
#include "core/eventbus.h"
#include "core/events.h"
#include <QDebug>
ChangeProcessor::ChangeProcessor(QObject *parent)
: QObject(parent)
{
}
void ChangeProcessor::processMailItemAdded(const QString &accountId, const MailItem &item)
{
using namespace WinoMail::Events;
MailItemAddedEvent event;
event.item = item;
EVENT_BUS.publish(event);
emit changeProcessed(ChangeType::MailItemAdded, accountId, QVariant::fromValue(item));
emit mailItemsChanged(accountId, QString::number(item.folderId()));
}
void ChangeProcessor::processMailItemUpdated(const QString &accountId, const MailItem &item)
{
using namespace WinoMail::Events;
MailItemUpdatedEvent event;
event.item = item;
EVENT_BUS.publish(event);
emit changeProcessed(ChangeType::MailItemUpdated, accountId, QVariant::fromValue(item));
}
void ChangeProcessor::processMailItemDeleted(const QString &accountId, const QString &mailItemId)
{
using namespace WinoMail::Events;
MailItemRemovedEvent event;
event.itemUid = mailItemId;
event.folderId = accountId.toInt();
EVENT_BUS.publish(event);
emit changeProcessed(ChangeType::MailItemDeleted, accountId, mailItemId);
}
void ChangeProcessor::processFolderAdded(const QString &accountId, const Folder &folder)
{
Q_UNUSED(accountId);
qDebug() << "[ChangeProcessor] Folder added:" << folder.name();
emit changeProcessed(ChangeType::FolderAdded, accountId, QVariant::fromValue(folder));
}
void ChangeProcessor::processFolderDeleted(const QString &accountId, const QString &folderId)
{
Q_UNUSED(accountId);
qDebug() << "[ChangeProcessor] Folder deleted:" << folderId;
emit changeProcessed(ChangeType::FolderDeleted, accountId, folderId);
}
void ChangeProcessor::processAccountAdded(const Account &account)
{
qDebug() << "[ChangeProcessor] Account added:" << account.email();
emit changeProcessed(ChangeType::AccountAdded, QString::number(account.id()), QVariant::fromValue(account));
}
void ChangeProcessor::processAccountRemoved(const QString &accountId)
{
qDebug() << "[ChangeProcessor] Account removed:" << accountId;
emit changeProcessed(ChangeType::AccountDeleted, accountId, accountId);
}
+31
View File
@@ -0,0 +1,31 @@
#ifndef CHANGEPROCESSOR_H
#define CHANGEPROCESSOR_H
#include <QObject>
#include <QVector>
#include "changetype.h"
#include "core/mailitem.h"
#include "core/models/folder.h"
#include "core/models/account.h"
class ChangeProcessor : public QObject
{
Q_OBJECT
public:
explicit ChangeProcessor(QObject *parent = nullptr);
~ChangeProcessor() override = default;
void processMailItemAdded(const QString &accountId, const MailItem &item);
void processMailItemUpdated(const QString &accountId, const MailItem &item);
void processMailItemDeleted(const QString &accountId, const QString &mailItemId);
void processFolderAdded(const QString &accountId, const Folder &folder);
void processFolderDeleted(const QString &accountId, const QString &folderId);
void processAccountAdded(const Account &account);
void processAccountRemoved(const QString &accountId);
signals:
void changeProcessed(ChangeType type, const QString &accountId, const QVariant &data);
void mailItemsChanged(const QString &accountId, const QString &folderId);
};
#endif // CHANGEPROCESSOR_H
+36
View File
@@ -0,0 +1,36 @@
#ifndef CHANGETYPE_H
#define CHANGETYPE_H
#include <QString>
enum class ChangeType {
MailItemAdded,
MailItemUpdated,
MailItemDeleted,
MailItemFlagsChanged,
FolderAdded,
FolderUpdated,
FolderDeleted,
AccountAdded,
AccountUpdated,
AccountDeleted,
Unknown
};
inline QString changeTypeToString(ChangeType type) {
switch (type) {
case ChangeType::MailItemAdded: return "MailItemAdded";
case ChangeType::MailItemUpdated: return "MailItemUpdated";
case ChangeType::MailItemDeleted: return "MailItemDeleted";
case ChangeType::MailItemFlagsChanged: return "MailItemFlagsChanged";
case ChangeType::FolderAdded: return "FolderAdded";
case ChangeType::FolderUpdated: return "FolderUpdated";
case ChangeType::FolderDeleted: return "FolderDeleted";
case ChangeType::AccountAdded: return "AccountAdded";
case ChangeType::AccountUpdated: return "AccountUpdated";
case ChangeType::AccountDeleted: return "AccountDeleted";
default: return "Unknown";
}
}
#endif // CHANGETYPE_H
+62
View File
@@ -0,0 +1,62 @@
#include "concreterequests.h"
// Gmail
GmailListMessagesRequest::GmailListMessagesRequest(const QString &accountId, const QString &folderId,
const QString &pageToken)
: Request()
{
setAccountId(accountId);
setType(RequestType::GmailApi);
setMethod(Request::Method::GET);
// Build Gmail API URL
// folderId for Gmail is typically "INBOX", "SENT", etc.
QString path = QString("https://gmail.googleapis.com/gmail/v1/users/me/messages");
QString query = QString("q=in:%1").arg(folderId.toLower());
if (!pageToken.isEmpty()) {
query += "&pageToken=" + pageToken;
}
QUrl url(path);
url.setQuery(query);
setUrl(url);
}
GmailGetMessageRequest::GmailGetMessageRequest(const QString &accountId, const QString &messageId)
: Request()
{
setAccountId(accountId);
setType(RequestType::GmailApi);
setMethod(Request::Method::GET);
QUrl url(QString("https://gmail.googleapis.com/gmail/v1/users/me/messages/%1").arg(messageId));
setUrl(url);
}
// Graph API
GraphListMessagesRequest::GraphListMessagesRequest(const QString &accountId, const QString &folderId,
const QString &deltaLink)
: Request()
{
setAccountId(accountId);
setType(RequestType::GraphApi);
setMethod(Request::Method::GET);
// Use deltaLink if available (for incremental sync)
if (!deltaLink.isEmpty()) {
setUrl(QUrl(deltaLink));
} else {
QUrl url(QString("https://graph.microsoft.com/v1.0/me/mailFolders/%1/messages").arg(folderId));
setUrl(url);
}
}
GraphGetMessageRequest::GraphGetMessageRequest(const QString &accountId, const QString &messageId)
: Request()
{
setAccountId(accountId);
setType(RequestType::GraphApi);
setMethod(Request::Method::GET);
QUrl url(QString("https://graph.microsoft.com/v1.0/me/messages/%1").arg(messageId));
setUrl(url);
}
+34
View File
@@ -0,0 +1,34 @@
#ifndef CONCRETEREQUESTS_H
#define CONCRETEREQUESTS_H
#include "request.h"
// Gmail API Requests
class GmailListMessagesRequest : public Request
{
public:
explicit GmailListMessagesRequest(const QString &accountId, const QString &folderId,
const QString &pageToken = QString());
};
class GmailGetMessageRequest : public Request
{
public:
explicit GmailGetMessageRequest(const QString &accountId, const QString &messageId);
};
// Outlook / Graph API Requests
class GraphListMessagesRequest : public Request
{
public:
explicit GraphListMessagesRequest(const QString &accountId, const QString &folderId,
const QString &deltaLink = QString());
};
class GraphGetMessageRequest : public Request
{
public:
explicit GraphGetMessageRequest(const QString &accountId, const QString &messageId);
};
#endif // CONCRETEREQUESTS_H
+33
View File
@@ -0,0 +1,33 @@
#include "folderservice.h"
#include "core/synchronizerprovider.h"
#include <QDebug>
FolderService::FolderService(QObject *parent)
: QObject(parent)
{
}
QVector<Folder> FolderService::getFoldersForAccount(const QString &accountId)
{
return FolderDao::findByAccountId(accountId.toInt());
}
void FolderService::refreshFolders(const QString &accountId)
{
Synchronizer *sync = SynchronizerProvider::instance().getSynchronizer(accountId);
if (sync) {
QVector<Folder> folders = sync->getFolders();
for (const auto &f : folders) {
FolderDao::insert(f);
}
}
emit foldersChanged(accountId);
}
Folder FolderService::getFolderById(const QString &folderId)
{
std::optional<Folder> f = FolderDao::findById(folderId.toInt());
return f.value_or(Folder());
}
#include "folderservice.moc"
+24
View File
@@ -0,0 +1,24 @@
#ifndef FOLDERSERVICE_H
#define FOLDERSERVICE_H
#include <QObject>
#include <QVector>
#include "models/folder.h"
#include "db/dao/folderdao.h"
class FolderService : public QObject
{
Q_OBJECT
public:
explicit FolderService(QObject *parent = nullptr);
~FolderService() override = default;
QVector<Folder> getFoldersForAccount(const QString &accountId);
void refreshFolders(const QString &accountId);
Folder getFolderById(const QString &folderId);
signals:
void foldersChanged(const QString &accountId);
};
#endif // FOLDERSERVICE_H
+26 -5
View File
@@ -358,11 +358,32 @@ void GmailSynchronizer::setHistoryId(const QString& folderId, qint64 historyId)
MailItem GmailSynchronizer::parseMailItemFromGmail(const QJsonObject& mailJson) const
{
// Esta función convertiría un objeto JSON de Gmail API
// a nuestro objeto MailItem
MailItem item;
// Por ahora, devolvemos un objeto vacío como stub
return MailItem();
// ID de Gmail es la clave primaria
item.setId(mailJson["id"].toString().toLongLong());
// El snippet sirve como cuerpo preliminar
item.setBodyHtml(mailJson["snippet"].toString());
// Parsear headers
QJsonArray headers = mailJson["payload"].toObject()["headers"].toArray();
QString from = extractHeaderValue(headers, "From").value(0);
QString subject = extractHeaderValue(headers, "Subject").value(0);
QString dateStr = extractHeaderValue(headers, "Date").value(0);
item.setSender(from.isEmpty() ? "Unknown" : from);
item.setSubject(subject.isEmpty() ? "(No Subject)" : subject);
// Parseo básico de fecha (RFC 2822)
QDateTime date = QDateTime::fromString(dateStr, Qt::ISODate);
if (!date.isValid()) {
date = QDateTime::currentDateTime();
}
item.setDate(date);
return item;
}
Folder GmailSynchronizer::parseFolderFromGmail(const QJsonObject& labelJson) const
@@ -386,4 +407,4 @@ QStringList GmailSynchronizer::extractHeaderValue(const QJsonArray& headers, con
}
}
return values;
}
}
+1 -1
View File
@@ -145,4 +145,4 @@ QString ImapSynchronizer::generateEventId() const
// Simple implementation using timestamp and random component
return QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" +
QString::number(std::rand());
}
}
+72
View File
@@ -0,0 +1,72 @@
// src/services/imapsynchronizer.cpp
#include "imapsynchronizer.h"
#include <QDebug>
#include <Q例外>
// NOTE: Actual implementation requires an IMAP3 library (e.g., libimap or Qt's built-in network classes).
// For this demonstration, we mock the connection and data fetching based on the requirement.
ImapSynchronizer::ImapSynchronizer(QObject *parent) : QObject(parent) {}
bool ImapSynchronizer::connectImap(const QString &host, int port, const QString &user, const QString &password) {
qDebug() << "[IMAP] Attempting connection to" << host << ":" << port;
// In a real implementation, this would involve setting up the IMAP3 client connection.
if (host.isEmpty() || port == 0) {
qWarning() << "[IMAP Error] Host or Port is invalid.";
return false;
}
// Mock successful connection for demonstration purposes.
qDebug() << "[IMAP Success] Connected to server" << host;
return true;
}
QVector<MailItem> ImapSynchronizer::fetchFromImap3(const QString &folderId, int offset) {
if (!connectImap("imap.example.com", 993, "user", "password")) { // Mock credentials
qWarning() << "[IMAP Error] Failed to connect.";
return QVector<MailItem>();
}
qDebug() << "[IMAP] Fetching items for folder:" << folderId << " starting at offset:" << offset;
// Mock data retrieval. In a real scenario, this reads from the IMAP stream.
QVector<MailItem> mails;
if (folderId == "inbox") {
mails.append(MailItem{ "mail_id_123", "Subject 1", "Body 1", true });
mails.append(MailItem{ "mail_id_456", "Subject 2", "Body 2", false });
} else {
// Simulate no mail found for other folders
}
return mails;
}
bool ImapSynchronizer::sendMail(const MailItem &mail, const QString &accountId) {
qDebug() << "[IMAP] Attempting to send mail:" << mail.subject() << "to account:" << accountId;
// In a real scenario, this would construct the IMAP command sequence to send the message.
return true; // Mock success
}
void ImapSynchronizer::fetchMails(const QString &accountId, const QString &folderId) {
QVector<MailItem> items = fetchFromImap3(folderId, 0);
if (!items.isEmpty()) {
qDebug() << "[IMAP] Successfully fetched" << items.size() << " mail items.";
} else {
qDebug() << "[IMAP] No mail items found in folder:" << folderId;
}
}
void ImapSynchronizer::moveMail(const QString &mailItemId, const QString &targetFolderId) {
qDebug() << "[IMAP] Moving mail:" << mailItemId << "to" << targetFolderId;
// Real implementation would involve IMAP MOVE command.
}
void ImapSynchronizer::deleteMail(const QString &mailItemId) {
qDebug() << "[IMAP] Deleting mail:" << mailItemId;
// Real implementation would involve IMAP DELE command.
}
void ImapSynchronizer::markAsRead(const QString &mailItemId, bool read) {
qDebug() << "[IMAP] Marking mail" << mailItemId << "as read:" << (read ? "True" : "False");
// Real implementation would involve IMAP MARK_READ command.
}
+35
View File
@@ -0,0 +1,35 @@
// src/services/imapsynchronizer.h
#ifndef IMAP_SYNCHRONIZER_H
#define IMAP_SYNCHRONIZER_H
#include "synchronizerprovider.h"
#include <QString>
#include <QVector>
class ImapSynchronizer : public SynchronizerProvider
{
public:
ImapSynchronizer(QObject *parent = nullptr);
~ImapSynchronizer() override = default;
bool isImapSupported() const { return true; } // Indicate that this implementation supports IMAP
// Implement the required methods from SynchronizerProvider
QVector<MailItem> fetchMailItems(const QString &folderId, int offset) override;
bool sendMail(const MailItem &mail, const QString &accountId) override;
void fetchMails(const QString &accountId, const QString &folderId) override;
void moveMail(const QString &mailItemId, const QString &targetFolderId) override;
void deleteMail(const QString &mailItemId) override;
void markAsRead(const QString &mailItemId, bool read) override;
private:
// Private methods for actual IMAP3 logic (mocked here)
bool connectImap(const QString &host, int port, const QString &user, const QString &password);
QVector<MailItem> fetchFromImap3(const QString &folderId, int offset);
bool sendViaImap3(const MailItem &mail, const QString &accountId);
void moveViaImap3(const QString &mailItemId, const QString &targetFolderId);
void deleteViaImap3(const QString &mailItemId);
void markReadViaImap3(const QString &mailItemId, bool read);
};
#endif // IMAP_SYNCHRONIZER_H
+86
View File
@@ -0,0 +1,86 @@
#include "mailservice.h"
#include <QDebug>
#include <QUuid>
#include <QSslSocket>
#include <QUrl>
#include "db/dao/accountdao.h"
static void sendViaSmtp(const MailItem &mail, const Account &account);
MailService::MailService(QObject *parent)
: QObject(parent)
, m_composer(new EmailComposerBridge(this))
{
}
QVector<MailItem> MailService::getMails(const QString &folderId)
{
return MailItemDao::findByFolderId(folderId.toInt());
}
void MailService::sendMail(const MailItem &mail, const QString &accountId)
{
qDebug() << "[MailService] Sending mail:" << mail.subject() << "from account:" << accountId;
Synchronizer *sync = SynchronizerProvider::instance().getSynchronizer(accountId);
if (!sync) {
emit mailSendFailed(mail.messageId(), "Account not synchronized");
return;
}
Account *account = AccountDao::findById(accountId.toInt());
if (account && account->type() == AccountType::IMAP) {
sendViaSmtp(mail, *account);
delete account;
emit mailSent(mail.messageId());
return;
}
if (account) delete account;
MailItem copy = mail;
copy.setFolderId(5);
MailItemDao::insert(copy);
emit mailSent(mail.messageId());
}
void MailService::fetchMails(const QString &accountId, const QString &folderId)
{
Synchronizer *sync = SynchronizerProvider::instance().getSynchronizer(accountId);
if (!sync) { qWarning() << "[MailService] No synchronizer"; return; }
QVector<MailItem> items = sync->fetchMailItems(folderId);
emit mailFetched(accountId, folderId, items);
}
void MailService::moveMail(const QString &mailItemId, const QString &targetFolderId)
{
qDebug() << "[MailService] Moving mail:" << mailItemId;
std::optional<MailItem> item = MailItemDao::findById(mailItemId.toLongLong());
if (item.has_value()) {
MailItem m = item.value();
m.setFolderId(targetFolderId.toInt());
MailItemDao::update(m);
}
emit mailMoved(mailItemId);
}
void MailService::deleteMail(const QString &mailItemId)
{
MailItemDao::remove(mailItemId.toLongLong());
emit mailDeleted(mailItemId);
}
void MailService::markAsRead(const QString &mailItemId, bool read)
{
std::optional<MailItem> item = MailItemDao::findById(mailItemId.toLongLong());
if (item.has_value()) {
MailItem m = item.value();
m.setRead(read);
MailItemDao::update(m);
}
emit mailReadStateChanged(mailItemId, read);
}
static void sendViaSmtp(const MailItem &mail, const Account &account)
{
Q_UNUSED(mail); Q_UNUSED(account);
qDebug() << "[MailService] SMTP not implemented (requires GMime)";
}
#include "mailservice.moc"
+41
View File
@@ -0,0 +1,41 @@
#ifndef MAILSERVICE_H
#define MAILSERVICE_H
#include <QObject>
#include <QVector>
#include <QSslSocket>
#include <QString>
#include "core/mailitem.h"
#include "core/emailcomposerbridge.h"
#include "core/synchronizerprovider.h"
#include "models/account.h"
#include "db/dao/mailitemdao.h"
#include "db/dao/folderdao.h"
class MailService : public QObject
{
Q_OBJECT
public:
explicit MailService(QObject *parent = nullptr);
~MailService() override = default;
QVector<MailItem> getMails(const QString &folderId);
void sendMail(const MailItem &mail, const QString &accountId);
void fetchMails(const QString &accountId, const QString &folderId);
void moveMail(const QString &mailItemId, const QString &targetFolderId);
void deleteMail(const QString &mailItemId);
void markAsRead(const QString &mailItemId, bool read);
signals:
void mailSent(const QString &mailItemId);
void mailSendFailed(const QString &mailItemId, const QString &error);
void mailFetched(const QString &accountId, const QString &folderId, const QVector<MailItem> &items);
void mailMoved(const QString &mailItemId);
void mailDeleted(const QString &mailItemId);
void mailReadStateChanged(const QString &mailItemId, bool read);
private:
EmailComposerBridge *m_composer;
};
#endif // MAILSERVICE_H
+116
View File
@@ -0,0 +1,116 @@
#include "mimestorage.h"
#include <QStandardPaths>
#include <QDebug>
#include <QUuid>
MimeStorageService::MimeStorageService(QObject *parent)
: QObject(parent)
{
}
QString MimeStorageService::storagePath() const
{
QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/mails";
return ensureDirectory(path);
}
QString MimeStorageService::ensureDirectory(const QString &path) const
{
QDir dir(path);
if (!dir.exists()) {
dir.mkpath(".");
}
return dir.absolutePath();
}
QString MimeStorageService::saveEmlFile(const QString &accountId, const QString &folderId, const MailItem &mail)
{
QString dir = storagePath() + "/" + accountId + "/" + folderId;
ensureDirectory(dir);
QString fileName = mail.messageId();
if (fileName.isEmpty()) {
fileName = QUuid::createUuid().toString(QUuid::WithoutBraces);
}
fileName.replace('/', '_').replace('\\', '_');
QString filePath = dir + "/" + fileName + ".eml";
// Write a basic .eml structure
QFile file(filePath);
if (file.open(QIODevice::WriteOnly)) {
QTextStream stream(&file);
stream << "From: " << mail.sender() << "\r\n";
stream << "To: " << mail.recipient() << "\r\n";
stream << "Subject: " << mail.subject() << "\r\n";
stream << "Date: " << mail.date().toString(Qt::RFC2822Date) << "\r\n";
stream << "MIME-Version: 1.0\r\n";
stream << "Content-Type: text/html; charset=UTF-8\r\n";
stream << "\r\n";
stream << mail.bodyHtml() << "\r\n";
file.close();
qDebug() << "[MimeStorage] Saved .eml:" << filePath;
return fileName;
}
qWarning() << "[MimeStorage] Failed to save .eml:" << filePath;
return QString();
}
QString MimeStorageService::getEmlFilePath(const QString &fileId) const
{
// Search in all account/folder directories (simplified)
QDir baseDir(storagePath());
QStringList filters;
filters << fileId + ".eml";
QFileInfoList files = baseDir.entryInfoList(filters, QDir::Files, QDir::Name);
for (const auto &info : files) {
QStringList parts = info.absoluteFilePath().split('/');
if (parts.size() >= 2) {
return info.absoluteFilePath();
}
}
return QString();
}
QByteArray MimeStorageService::readEmlFile(const QString &fileId) const
{
QString path = getEmlFilePath(fileId);
if (path.isEmpty()) return {};
QFile file(path);
if (file.open(QIODevice::ReadOnly)) {
return file.readAll();
}
return {};
}
bool MimeStorageService::deleteEmlFile(const QString &fileId)
{
QString path = getEmlFilePath(fileId);
if (path.isEmpty()) return false;
return QFile::remove(path);
}
QStringList MimeStorageService::listAttachments(const QString &mailItemId) const
{
Q_UNUSED(mailItemId);
// TODO: Parse .eml to get attachments when GMime is available
return {};
}
bool MimeStorageService::saveAttachment(const QString &mailItemId, const QString &fileName, const QByteArray &data)
{
Q_UNUSED(mailItemId);
QString dir = storagePath() + "/attachments";
ensureDirectory(dir);
QFile file(dir + "/" + fileName);
if (file.open(QIODevice::WriteOnly)) {
file.write(data);
file.close();
return true;
}
return false;
}
#include "mimestorage.moc"
+30
View File
@@ -0,0 +1,30 @@
#ifndef MIMESTORAGESERVICE_H
#define MIMESTORAGESERVICE_H
#include <QObject>
#include <QString>
#include <QDir>
#include <QFile>
#include "core/mailitem.h"
class MimeStorageService : public QObject
{
Q_OBJECT
public:
explicit MimeStorageService(QObject *parent = nullptr);
~MimeStorageService() override = default;
QString saveEmlFile(const QString &accountId, const QString &folderId, const MailItem &mail);
QString getEmlFilePath(const QString &fileId) const;
QByteArray readEmlFile(const QString &fileId) const;
bool deleteEmlFile(const QString &fileId);
QStringList listAttachments(const QString &mailItemId) const;
bool saveAttachment(const QString &mailItemId, const QString &fileName, const QByteArray &data);
private:
QString storagePath() const;
QString ensureDirectory(const QString &path) const;
};
#endif // MIMESTORAGESERVICE_H
+1 -1
View File
@@ -423,4 +423,4 @@ Folder OutlookSynchronizer::parseFolderFromGraph(const QJsonObject& folderJson)
// Por ahora, devolvemos un objeto vacío como stub
return Folder();
}
}
+67
View File
@@ -0,0 +1,67 @@
// src/services/pop/popsynchronizer.cpp
#include "popsynchronizer.h"
#include "../imaprequest.h"
#include "../core/mailitem.h"
#include "../core/models/account.h"
#include "../core/models/folder.h"
#include "../db/dao/mailitemdao.h"
#include <QDebug>
#include <QProcess>
PopSynchronizer::PopSynchronizer(QObject *parent) : Synchronizer(parent) {}
bool PopSynchronizer::initialize(const Account &account) {
qDebug() << "[POP Synchronizer] Initializing for account:" << account.id();
// In a real implementation, this would involve establishing the POP3 connection and authentication.
m_isConnected = true; // Simulate successful connection for now
return true;
}
bool PopSynchronizer::syncFolder(const Folder &folder) {
qDebug() << "[POP Synchronizer] Syncing folder:" << folder.id();
// In a full implementation, this would handle checking the folder state on the server.
return true;
}
QVector<MailItem> PopSynchronizer::fetchMailItems(const QString &folderId, qint64 sinceUid) {
if (!m_isConnected) {
qWarning() << "[POP Synchronizer] Not connected.";
return {};
}
qDebug() << "[POP Synchronizer] Fetching mails for folder:" << folderId << "since UID:" << sinceUid;
// *** Placeholder Implementation ***
// In a real scenario, this calls PopRequest::createRequest(type, target) to fetch emails.
QVector<MailItem> fetchedItems;
// Simulate fetching some data based on the folder ID
if (folderId == 1) {
fetchedItems.append(MailItem("12345", "Folder A", 100));
fetchedItems.append(MailItem("12346", "Folder B", 101));
} else {
fetchedItems.append(MailItem("99999", "Folder C", 102));
}
return fetchedItems;
}
bool PopSynchronizer::appendMailItem(const QString &folderId, const MailItem &item) {
qDebug() << "[POP Synchronizer] Appending mail to folder:" << folderId << "ID:" << item.messageId();
// In a full implementation, this would use the IMAP request to move or copy the message.
MailItemDao::insert(item);
return true;
}
bool PopSynchronizer::updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged) {
qDebug() << "[POP Synchronizer] Updating flags for item:" << itemUid;
// In a full implementation, this would use IMAP commands (e.g., UIDVALIDATE, SETUIDLATER, etc.) to update the server state.
return true;
}
bool PopSynchronizer::deleteMailItem(const QString &folderId, const QString &itemUid) {
qDebug() << "[POP Synchronizer] Deleting mail item:" << itemUid;
// In a full implementation, this would execute the IMAP DELETE command.
MailItemDao::remove(itemUid.toLongLong());
return true;
}
+28
View File
@@ -0,0 +1,28 @@
// src/services/pop/popsynchronizer.h
#ifndef POPSYNCHRONIZER_H
#define POPSYNCHRONIZER_H
#include "synchronizer.h"
#include "../imaprequest.h" // Include necessary request structures
#include "../core/models/account.h"
#include "../core/models/folder.h"
#include "../core/mailitem.h"
class PopSynchronizer : public Synchronizer {
public:
explicit PopSynchronizer(QObject *parent = nullptr);
// Implement the core synchronization methods
bool initialize(const Account &account) override;
bool syncFolder(const Folder &folder) 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;
private:
// Internal state related to POP synchronization (e.g., connection status)
bool m_isConnected = false;
};
#endif // POPSYNCHRONIZER_H
+21
View File
@@ -0,0 +1,21 @@
// poprequest.cpp
#include "poprequest.h"
#include "request.h"
#include <QUrl>
#include <QDebug>
PopRequest::PopRequest(Request* req) : Concreterequests() {}
Request* PopRequest::createRequest(const QString& type, const QString& target) {
qDebug() << "PopRequest is handling request for target: " << target;
// Construct a request object for POP3 operations.
Request* req = new Request(Request::Method::GET, QUrl(target));
// Add POP3 specific context
req->setHeader("POP3_SERVER", "pop.example.com"); // Placeholder
req->setHeader("PROTOCOL", "POP3");
qDebug() << "POP3 request object created successfully.";
return req;
}
+14
View File
@@ -0,0 +1,14 @@
// poprequest.h
#ifndef POPREQUEST_H
#define POPREQUEST_H
#include "../../include/concreterequests.h"
#include "../../include/request.h"
class PopRequest : public Concreterequests {
public:
PopRequest(Request* req);
Request* createRequest(const QString& type, const QString& target) override;
};
#endif // POPREQUEST_H
+74
View File
@@ -0,0 +1,74 @@
// src/services/popsynchronizer.cpp
#include "popsynchronizer.h"
#include <QDebug>
#include <Q例外>
#include <QFile>
// NOTE: Actual implementation requires a POP3 library (e.g., libpop3 or Qt's built-in network classes).
// For this demonstration, we mock the connection and data fetching based on the requirement.
PopSynchronizer::PopSynchronizer(QObject *parent) : QObject(parent) {}
bool PopSynchronizer::connectPop3(const QString &host, int port, const QString &user, const QString &password) {
qDebug() << "[POP3] Attempting connection to" << host << ":" << port;
// In a real implementation, this would involve setting up the POP3 client connection.
if (host.isEmpty() || port == 0) {
qWarning() << "[POP3 Error] Host or Port is invalid.";
return false;
}
// Mock successful connection for demonstration purposes.
qDebug() << "[POP3 Success] Connected to server " << host;
return true;
}
QVector<MailItem> PopSynchronizer::fetchFromPop3(const QString &folderId, int offset) {
if (!connectPop3("pop3.example.com", 110, "user", "password")) { // Mock credentials
qWarning() << "[POP3 Error] Failed to connect.";
return QVector<MailItem>();
}
qDebug() << "[POP3] Fetching items for folder:" << folderId << " starting at offset:" << offset;
// Mock data retrieval. In a real scenario, this reads from the POP3 stream.
QVector<MailItem> mails;
if (folderId == "inbox") {
mails.append(MailItem{ "mail_id_123", "Subject 1", "Body 1", true });
mails.append(MailItem{ "mail_id_456", "Subject 2", "Body 2", false });
} else {
// Simulate no mail found for other folders
}
return mails;
}
bool PopSynchronizer::sendMail(const MailItem &mail, const QString &accountId) {
qDebug() << "[POP3] Attempting to send mail:" << mail.subject() << "to account:" << accountId;
// In a real scenario, this would construct the POP3 command sequence to send the message.
return true; // Mock success
}
void PopSynchronizer::fetchMails(const QString &accountId, const QString &folderId) {
QVector<MailItem> items = fetchFromPop3(folderId, 0);
if (!items.isEmpty()) {
qDebug() << "[POP3] Successfully fetched" << items.size() << " mail items.";
// The caller (MailService) will handle emitting the result.
} else {
qDebug() << "[POP3] No mail items found in folder:" << folderId;
}
}
void PopSynchronizer::moveMail(const QString &mailItemId, const QString &targetFolderId) {
qDebug() << "[POP3] Moving mail:" << mailItemId << "to" << targetFolderId;
// Real implementation would involve POP3 MOVE command.
}
void PopSynchronizer::deleteMail(const QString &mailItemId) {
qDebug() << "[POP3] Deleting mail:" << mailItemId;
// Real implementation would involve POP3 DELE command.
}
void PopSynchronizer::markAsRead(const QString &mailItemId, bool read) {
qDebug() << "[POP3] Marking mail" << mailItemId << "as read:" << (read ? "True" : "False");
// Real implementation would involve POP3 MARK_READ command.
}
+35
View File
@@ -0,0 +1,35 @@
// src/services/popsynchronizer.h
#ifndef POP_SYNCHRONIZER_H
#define POP_SYNCHRONIZER_H
#include "synchronizerprovider.h"
#include <QString>
#include <QVector>
class PopSynchronizer : public SynchronizerProvider
{
public:
PopSynchronizer(QObject *parent = nullptr);
~PopSynchronizer() override = default;
bool isPop3Supported() const { return true; } // Indicate that this implementation supports POP3
// Implement the required methods from SynchronizerProvider
QVector<MailItem> fetchMailItems(const QString &folderId, int offset) override;
bool sendMail(const MailItem &mail, const QString &accountId) override;
void fetchMails(const QString &accountId, const QString &folderId) override;
void moveMail(const QString &mailItemId, const QString &targetFolderId) override;
void deleteMail(const QString &mailItemId) override;
void markAsRead(const QString &mailItemId, bool read) override;
private:
// Private methods for actual POP3 logic
bool connectPop3(const QString &host, int port, const QString &user, const QString &password);
QVector<MailItem> fetchFromPop3(const QString &folderId, int offset);
bool sendViaPop3(const MailItem &mail, const QString &accountId);
void moveViaPop3(const QString &mailItemId, const QString &targetFolderId);
void deleteViaPop3(const QString &mailItemId);
void markReadViaPop3(const QString &mailItemId, bool read);
};
#endif // POP_SYNCHRONIZER_H
+83
View File
@@ -0,0 +1,83 @@
#include "request.h"
Request::Request(QObject *parent)
: QObject(parent)
{
}
Request::Request(Method method, const QUrl &url, const QByteArray &body)
: QObject(nullptr)
{
setMethod(method);
setUrl(url);
setBody(body);
}
void Request::setMethod(Request::Method method)
{
m_method = method;
}
Request::Method Request::method() const
{
return m_method;
}
void Request::setUrl(const QUrl &url)
{
m_url = url;
}
QUrl Request::url() const
{
return m_url;
}
void Request::setHeader(const QString &key, const QString &value)
{
m_headers.insert(key, value);
}
QMap<QString, QString> Request::headers() const
{
return m_headers;
}
void Request::setBody(const QByteArray &body)
{
m_body = body;
}
QByteArray Request::body() const
{
return m_body;
}
void Request::setType(RequestType type)
{
m_requestType = type;
}
RequestType Request::type() const
{
return m_requestType;
}
void Request::setAccountId(const QString &accountId)
{
m_accountId = accountId;
}
QString Request::accountId() const
{
return m_accountId;
}
QNetworkRequest Request::toNetworkRequest() const
{
QNetworkRequest netRequest(m_url);
for (auto it = m_headers.constBegin(); it != m_headers.constEnd(); ++it) {
netRequest.setRawHeader(it.key().toUtf8(), it.value().toUtf8());
}
return netRequest;
}
+62
View File
@@ -0,0 +1,62 @@
#ifndef REQUEST_H
#define REQUEST_H
#include <QObject>
#include <QUrl>
#include <QMap>
#include <QByteArray>
#include <QNetworkRequest>
enum class RequestType {
GmailApi,
GraphApi,
Imap,
Custom
};
class Request : public QObject
{
Q_OBJECT
public:
enum class Method {
GET,
POST,
PUT,
DELETE,
PATCH
};
explicit Request(QObject *parent = nullptr);
Request(Method method, const QUrl &url, const QByteArray &body = QByteArray());
~Request() override = default;
void setMethod(Method method);
Method method() const;
void setUrl(const QUrl &url);
QUrl url() const;
void setHeader(const QString &key, const QString &value);
QMap<QString, QString> headers() const;
void setBody(const QByteArray &body);
QByteArray body() const;
void setType(RequestType type);
RequestType type() const;
void setAccountId(const QString &accountId);
QString accountId() const;
QNetworkRequest toNetworkRequest() const;
private:
Method m_method = Method::GET;
QUrl m_url;
QMap<QString, QString> m_headers;
QByteArray m_body;
RequestType m_requestType = RequestType::Custom;
QString m_accountId;
};
#endif // REQUEST_H
+82
View File
@@ -0,0 +1,82 @@
#include "requestprocessor.h"
#include <QDebug>
#include <QNetworkReply>
RequestProcessor::RequestProcessor(QObject *parent)
: QObject(parent)
, m_networkManager(new QNetworkAccessManager(this))
{
}
void RequestProcessor::execute(Request *request)
{
if (!request) {
qWarning() << "[RequestProcessor] Null request";
return;
}
QNetworkRequest netRequest = request->toNetworkRequest();
QNetworkReply *reply = nullptr;
switch (request->method()) {
case Request::Method::GET:
reply = m_networkManager->get(netRequest);
break;
case Request::Method::POST:
reply = m_networkManager->post(netRequest, request->body());
break;
case Request::Method::PUT:
reply = m_networkManager->put(netRequest, request->body());
break;
case Request::Method::DELETE:
reply = m_networkManager->deleteResource(netRequest);
break;
case Request::Method::PATCH:
reply = m_networkManager->sendCustomRequest(netRequest, "PATCH", request->body());
break;
}
if (reply) {
m_pendingRequests.insert(reply, request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
onReplyFinished(reply);
});
}
}
void RequestProcessor::cancelAll()
{
for (auto it = m_pendingRequests.begin(); it != m_pendingRequests.end(); ++it) {
it.key()->abort();
it.key()->deleteLater();
}
m_pendingRequests.clear();
}
void RequestProcessor::onReplyFinished(QNetworkReply *reply)
{
if (!reply) return;
Request *request = m_pendingRequests.value(reply, nullptr);
if (!request) {
reply->deleteLater();
return;
}
QString accountId = request->accountId();
int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (reply->error() == QNetworkReply::NoError) {
QByteArray data = reply->readAll();
QJsonDocument doc = QJsonDocument::fromJson(data);
emit requestFinished(accountId, doc);
} else {
QString errorMsg = reply->errorString();
qWarning() << "[RequestProcessor] Error:" << httpCode << errorMsg;
emit requestFailed(accountId, httpCode, errorMsg);
}
m_pendingRequests.remove(reply);
reply->deleteLater();
request->deleteLater();
}
+34
View File
@@ -0,0 +1,34 @@
#ifndef REQUESTPROCESSOR_H
#define REQUESTPROCESSOR_H
#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include "request.h"
class RequestProcessor : public QObject
{
Q_OBJECT
public:
explicit RequestProcessor(QObject *parent = nullptr);
~RequestProcessor() override = default;
void execute(Request *request);
void cancelAll();
signals:
void requestFinished(const QString &accountId, const QJsonDocument &response);
void requestFailed(const QString &accountId, int httpCode, const QString &errorMessage);
private slots:
void onReplyFinished(QNetworkReply *reply);
private:
QNetworkAccessManager *m_networkManager;
QMap<QNetworkReply*, Request*> m_pendingRequests;
};
#endif // REQUESTPROCESSOR_H
+31 -1
View File
@@ -1,6 +1,36 @@
#include "synchronizer.h"
Synchronizer::Synchronizer(QObject* parent)
Synchronizer::Synchronizer(QObject *parent)
: QObject(parent)
{
}
QVector<MailItem> Synchronizer::fetchMailItems(const QString &folderId, qint64 sinceUid)
{
Q_UNUSED(folderId);
Q_UNUSED(sinceUid);
return {};
}
bool Synchronizer::appendMailItem(const QString &folderId, const MailItem &item)
{
Q_UNUSED(folderId);
Q_UNUSED(item);
return false;
}
bool Synchronizer::updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged)
{
Q_UNUSED(folderId);
Q_UNUSED(itemUid);
Q_UNUSED(read);
Q_UNUSED(flagged);
return false;
}
bool Synchronizer::deleteMailItem(const QString &folderId, const QString &itemUid)
{
Q_UNUSED(folderId);
Q_UNUSED(itemUid);
return false;
}
+28
View File
@@ -0,0 +1,28 @@
#ifndef SYNCHRONIZER_H
#define SYNCHRONIZER_H
#include <QObject>
#include <QVector>
#include "core/models/account.h"
#include "core/models/folder.h"
#include "core/mailitem.h"
class Synchronizer : public QObject
{
public:
explicit Synchronizer(QObject *parent = nullptr);
~Synchronizer() override = default;
virtual bool initialize(const Account &account) = 0;
virtual bool syncFolder(const Folder &folder) = 0;
virtual QVector<Folder> getFolders() const = 0;
virtual QVector<MailItem> fetchMailItems(const QString &folderId, qint64 sinceUid = 0) = 0;
virtual bool appendMailItem(const QString &folderId, const MailItem &item) = 0;
virtual bool updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged) = 0;
virtual bool deleteMailItem(const QString &folderId, const QString &itemUid) = 0;
protected:
Account m_account;
};
#endif // SYNCHRONIZER_H
+15
View File
@@ -0,0 +1,15 @@
// src/services/synchronizerprovider.cpp
#include "synchronizerprovider.h"
#include "imapsynchronizer.h" // Assuming IMAP implementation exists or will be created
#include "popsynchronizer.h" // Already implemented
SynchronizerProvider* SynchronizerProviderInstance::instance() {
if (m_provider == nullptr) {
return nullptr;
}
return m_provider;
}
void SynchronizerProviderInstance::setProvider(SynchronizerProvider* provider) {
m_provider = provider;
}
+32
View File
@@ -0,0 +1,32 @@
// src/services/synchronizerprovider.h
#ifndef SYNCHRONIZER_PROVIDER_H
#define SYNCHRONIZER_PROVIDER_H
#include <QString>
#include <QObject>
#include "mailservice.h" // Include MailService definitions if needed for context
class SynchronizerProvider {
public:
virtual ~SynchronizerProvider() = default;
virtual bool isPop3Supported() const = 0;
virtual QVector<MailItem> fetchMailItems(const QString &folderId, int offset) = 0;
virtual bool sendMail(const MailItem &mail, const QString &accountId) = 0;
virtual void fetchMails(const QString &accountId, const QString &folderId) = 0;
virtual void moveMail(const QString &mailItemId, const QString &targetFolderId) = 0;
virtual void deleteMail(const QString &mailItemId) = 0;
virtual void markAsRead(const QString &mailItemId, bool read) = 0;
};
// Singleton pattern for easy access
class SynchronizerProviderInstance {
public:
static SynchronizerProvider* instance();
void setProvider(SynchronizerProvider* provider);
private:
SynchronizerProviderInstance() = default;
SynchronizerProvider* m_provider = nullptr;
};
#endif // SYNCHRONIZER_PROVIDER_H
+19
View File
@@ -0,0 +1,19 @@
// smtprequest.cpp
#include "smtprequest.h"
#include "request.h"
#include <QDebug>
SmtpRequest::SmtpRequest(Request* req) : Concreterequests(req) {}
Request* SmtpRequest::createRequest(const QString& type, const QString& target) {
qDebug() << "SmtpRequest is handling request for target:" << target;
// In a real scenario, this would construct an SMTP-specific Request object.
Request* req = new Request(Request::POST, target, QByteArray()); // Typically POST for sending mail
// Add specific context or headers if necessary for SMTP connection setup (to be implemented in Phase 2)
req->headers["SMTP_SERVER"] = "smtp.example.com"; // Placeholder
qDebug() << "SMTP request object created successfully.";
return req;
}
+54
View File
@@ -0,0 +1,54 @@
// /mnt/c/Users/javie/wino-mail-dtkqt/src/synchronizerprovider.cpp
#include "synchronizerprovider.h"
#include <QDebug>
SynchronizerProvider::SynchronizerProvider(QObject *parent) : QObject(parent) {}
void SynchronizerProvider::requestSync()
{
qDebug() << "SynchronizerProvider: Initiating data synchronization request...";
// Simulate the time taken for a network operation
// In a real app, this is where you'd handle threading/asynchronous calls.
// Simulated Data Fetching (This should be replaced by actual IMAP/SMTP calls)
QList<QString> folders = fetchFoldersFromBackend();
QList<EmailItem> emails = QList<EmailItem>();
if (!folders.isEmpty()) {
// Simulate fetching emails for the first folder if available, or all of them
qDebug() << "Synchronizing data for" << folders.count() << "folders.";
for (const QString& folder : folders) {
// Simulate fetching emails from each folder
QList<EmailItem> folderEmails = fetchEmailsFromBackend(folder);
emails.append(folderEmails);
}
}
qDebug() << "Data synchronization complete. Total emails fetched:" << emails.count();
// Emit the result signal
emit dataSynchronized(folders, emails);
}
QList<QString> SynchronizerProvider::fetchFoldersFromBackend() const
{
// Simulated API call result
return {"INBOX", "Sent", "Drafts", "Archive"};
}
QList<EmailItem> SynchronizerProvider::fetchEmailsFromBackend(const QString& folder) const
{
// Simulated API call result based on folder logic
if (folder == "INBOX") {
return {
{1, "Test Email 1", "test@example.com"},
{2, "Important Update", "other@example.com"}
};
} else if (folder == "Sent") {
return {
{3, "Sent Mail A", "me@example.com"}
};
}
return QList<EmailItem>();
}
+2 -1
View File
@@ -123,4 +123,5 @@ void SyncScheduler::setLastSyncTimestamp(qint64 timestamp)
{
QSettings settings;
settings.setValue("lastSyncTimestamp", timestamp);
}
}
#include "syncscheduler.moc"
+32
View File
@@ -0,0 +1,32 @@
#include "ui/calendarview.h"
CalendarView::CalendarView(QWidget *parent) : QWidget(parent) {
setupUI();
}
void CalendarView::setupUI() {
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setAlignment(Qt::AlignCenter);
QLabel *icon = new QLabel("📅");
icon->setAlignment(Qt::AlignCenter);
QFont iconFont = icon->font();
iconFont.setPointSize(48);
icon->setFont(iconFont);
QLabel *title = new QLabel("Calendar");
title->setAlignment(Qt::AlignCenter);
QFont titleFont = title->font();
titleFont.setPointSize(20);
titleFont.setBold(true);
title->setFont(titleFont);
title->setStyleSheet("color: #333;");
QLabel *subtitle = new QLabel("Coming soon — integrated calendar with email");
subtitle->setAlignment(Qt::AlignCenter);
subtitle->setStyleSheet("color: #888; font-size: 13px;");
layout->addWidget(icon);
layout->addWidget(title);
layout->addWidget(subtitle);
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <QWidget>
#include <QLabel>
#include <QVBoxLayout>
class CalendarView : public QWidget {
Q_OBJECT
public:
explicit CalendarView(QWidget *parent = nullptr);
~CalendarView() override = default;
private:
void setupUI();
};
+486
View File
@@ -0,0 +1,486 @@
#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("\xe2\x87\x94L");
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
QAction *alignCenter = m_toolbar->addAction("\xe2\x86\x94C");
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
QAction *alignRight = m_toolbar->addAction("\xe2\x87\x94R");
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
QAction *alignJustify = m_toolbar->addAction("\xe2\x87\x94J");
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
m_toolbar->addSeparator();
// Lists
QAction *bulletAct = m_toolbar->addAction("\xe2\x80\xa2 List");
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("\xe2\x86\x92 Indent");
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
QAction *outdentAct = m_toolbar->addAction("\xe2\x86\x90 Outdent");
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
m_toolbar->addSeparator();
// Insert image
QAction *imgAct = m_toolbar->addAction("\xf0\x9f\x96\xbc Image");
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
// Insert table
QAction *tableAct = m_toolbar->addAction("\xe2\x96\xa4 Table");
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
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::ComposeView(QWidget *parent) : QWidget(parent) {
setupUI();
}
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);
// 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
);
}
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()
);
}
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); }
+103
View File
@@ -0,0 +1,103 @@
#pragma once
#include <QWidget>
#include <QLineEdit>
#include <QTextEdit>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QDateTime>
#include <QDateTimeEdit>
#include <QMenu>
#include <QToolBar>
#include <QToolButton>
#include <QFontComboBox>
#include <QSpinBox>
#include <QAction>
class RichTextEditor : public QTextEdit {
Q_OBJECT
public:
explicit RichTextEditor(QWidget *parent = nullptr);
void setupToolbar(QVBoxLayout *layout);
private 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;
};
class ComposeView : public QWidget {
Q_OBJECT
public:
explicit ComposeView(QWidget *parent = nullptr);
~ComposeView() override = default;
void setTo(const QString &to);
void setSubject(const QString &subject);
void setBody(const QString &body);
signals:
void sendRequested(const QString &to, const QString &cc, const QString &bcc,
const QString &subject, const QString &body,
const QDateTime &scheduleTime);
void discardRequested();
void detachRequested(QWidget *composeView);
private slots:
void onSendClicked();
void onScheduleClicked();
void onCcToggle();
void onBccToggle();
void onDetachClicked();
private:
void setupUI();
QLineEdit *m_toField;
QWidget *m_ccRow;
QLineEdit *m_ccField;
QWidget *m_bccRow;
QLineEdit *m_bccField;
QLineEdit *m_subjectField;
RichTextEditor *m_bodyEditor;
QToolButton *m_sendSplit;
QMenu *m_sendMenu;
QAction *m_sendNowAction;
QAction *m_scheduleAction;
QPushButton *m_discardButton;
QPushButton *m_detachButton;
QPushButton *m_ccButton;
QPushButton *m_bccButton;
QPushButton *m_hideCcButton;
QPushButton *m_hideBccButton;
QWidget *m_schedulePanel;
QDateTimeEdit *m_schedulePicker;
QPushButton *m_scheduleSendButton;
bool m_ccVisible = false;
bool m_bccVisible = false;
};
+32
View File
@@ -0,0 +1,32 @@
#include "ui/contactsview.h"
ContactsView::ContactsView(QWidget *parent) : QWidget(parent) {
setupUI();
}
void ContactsView::setupUI() {
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setAlignment(Qt::AlignCenter);
QLabel *icon = new QLabel("👥");
icon->setAlignment(Qt::AlignCenter);
QFont iconFont = icon->font();
iconFont.setPointSize(48);
icon->setFont(iconFont);
QLabel *title = new QLabel("Contacts");
title->setAlignment(Qt::AlignCenter);
QFont titleFont = title->font();
titleFont.setPointSize(20);
titleFont.setBold(true);
title->setFont(titleFont);
title->setStyleSheet("color: #333;");
QLabel *subtitle = new QLabel("Coming soon — manage your contacts here");
subtitle->setAlignment(Qt::AlignCenter);
subtitle->setStyleSheet("color: #888; font-size: 13px;");
layout->addWidget(icon);
layout->addWidget(title);
layout->addWidget(subtitle);
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <QWidget>
#include <QLabel>
#include <QVBoxLayout>
class ContactsView : public QWidget {
Q_OBJECT
public:
explicit ContactsView(QWidget *parent = nullptr);
~ContactsView() override = default;
private:
void setupUI();
};
+103
View File
@@ -0,0 +1,103 @@
#include "ui/maillistview.h"
#include <QDateTime>
MailListView::MailListView(QWidget *parent) : QWidget(parent) {
setupUI();
}
void MailListView::setupUI() {
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
// Header bar
QWidget *headerBar = new QWidget();
headerBar->setFixedHeight(48);
headerBar->setStyleSheet("background-color: #1976D2;");
QHBoxLayout *headerLayout = new QHBoxLayout(headerBar);
headerLayout->setContentsMargins(16, 0, 10, 0);
QLabel *title = new QLabel("Wino Mail");
title->setStyleSheet("color: white; font-size: 18px; font-weight: bold;");
headerLayout->addWidget(title);
headerLayout->addStretch();
m_composeButton = new QPushButton("");
m_composeButton->setFixedSize(36, 36);
m_composeButton->setStyleSheet(
"QPushButton { background-color: #e0e0e0; border-radius: 4px; font-size: 18px; color: #333; }"
"QPushButton:hover { background-color: #d0d0d0; }"
);
headerLayout->addWidget(m_composeButton);
connect(m_composeButton, &QPushButton::clicked, this, &MailListView::composeRequested);
layout->addWidget(headerBar);
// Table
m_tableView = new QTableView();
m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
m_tableView->setSelectionMode(QAbstractItemView::SingleSelection);
m_tableView->setShowGrid(false);
m_tableView->setAlternatingRowColors(true);
m_tableView->verticalHeader()->hide();
m_tableView->horizontalHeader()->setStretchLastSection(true);
m_tableView->horizontalHeader()->setSectionsClickable(true);
m_tableView->setSortingEnabled(true);
m_tableView->setFrameShape(QFrame::NoFrame);
m_tableView->setStyleSheet(
"QTableView { background-color: #ffffff; alternate-background-color: #f9f9fb; border: none; }"
"QTableView::item { padding: 8px; border-bottom: 1px solid #e8e8ed; }"
"QTableView::item:selected { background-color: #e3f2fd; color: #1a1a2e; }"
"QHeaderView::section { background-color: #f5f5f7; padding: 8px; border: none; border-bottom: 1px solid #d1d1d6; font-weight: 600; color: #555; }"
);
m_proxyModel = new QSortFilterProxyModel(this);
m_proxyModel->setSortRole(EmailListModel::DateRole);
m_proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
m_proxyModel->setDynamicSortFilter(true);
m_tableView->setModel(m_proxyModel);
connect(m_tableView, &QTableView::clicked, this, &MailListView::onRowSelected);
layout->addWidget(m_tableView);
}
void MailListView::setModel(EmailListModel *model) {
m_proxyModel->setSourceModel(model);
m_tableView->setColumnHidden(EmailListModel::IdRole - Qt::UserRole - 1, true);
m_tableView->setColumnHidden(EmailListModel::RecipientRole - Qt::UserRole - 1, true);
m_tableView->setColumnHidden(EmailListModel::ReadRole - Qt::UserRole - 1, true);
m_tableView->setColumnHidden(EmailListModel::FlaggedRole - Qt::UserRole - 1, true);
m_tableView->setColumnHidden(EmailListModel::AttachmentsRole - Qt::UserRole - 1, true);
m_tableView->setColumnHidden(EmailListModel::FileIdRole - Qt::UserRole - 1, true);
m_tableView->setColumnHidden(EmailListModel::SizeRole - Qt::UserRole - 1, true);
m_tableView->setColumnHidden(EmailListModel::MessageIdRole - Qt::UserRole - 1, true);
m_tableView->setColumnHidden(EmailListModel::SenderInitialRole - Qt::UserRole - 1, true);
m_tableView->horizontalHeader()->setSectionResizeMode(EmailListModel::SenderRole - Qt::UserRole - 1, QHeaderView::Stretch);
m_tableView->horizontalHeader()->setSectionResizeMode(EmailListModel::SubjectRole - Qt::UserRole - 1, QHeaderView::Stretch);
// Default sort by date descending
m_tableView->sortByColumn(EmailListModel::DateRole - Qt::UserRole - 1, Qt::DescendingOrder);
// Set column titles manually
QHeaderView *header = m_tableView->horizontalHeader();
for (int i = 0; i < m_proxyModel->columnCount(); ++i) {
int role = Qt::UserRole + 1 + i;
switch (role) {
case EmailListModel::SenderRole: header->setSectionHidden(i, false); break;
case EmailListModel::SubjectRole: header->setSectionHidden(i, false); break;
case EmailListModel::DateRole: header->setSectionHidden(i, false); break;
default: header->setSectionHidden(i, true); break;
}
}
}
void MailListView::onRowSelected(const QModelIndex &index) {
if (!index.isValid()) return;
QModelIndex sourceIndex = m_proxyModel->mapToSource(index);
int mailId = sourceIndex.data(EmailListModel::IdRole).toInt();
emit emailSelected(mailId);
}
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <QWidget>
#include <QTableView>
#include <QHeaderView>
#include <QSortFilterProxyModel>
#include <QVBoxLayout>
#include <QPushButton>
#include <QLabel>
#include "ui/models/EmailListModel.h"
class MailListView : public QWidget {
Q_OBJECT
public:
explicit MailListView(QWidget *parent = nullptr);
~MailListView() override = default;
void setModel(EmailListModel *model);
signals:
void emailSelected(int mailId);
void composeRequested();
private slots:
void onRowSelected(const QModelIndex &index);
private:
void setupUI();
QTableView *m_tableView;
QSortFilterProxyModel *m_proxyModel;
QPushButton *m_composeButton;
};
+270
View File
@@ -0,0 +1,270 @@
#include "mainmainwindow.h"
#include "core/models/account.h"
#include "core/mailitem.h"
#include "db/dao/mailitemdao.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QDateTime>
#include <QLabel>
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-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]() {
statusBar()->showMessage("Account setup dialog would open here", 3000);
});
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);
m_folderSplitter->addWidget(m_mailListView);
// Reader
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
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);
m_emailModel->refresh();
}
}
void MainMainWindow::onEmailSelected(int 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::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 *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(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
});
}
+79
View File
@@ -0,0 +1,79 @@
#pragma once
#include <QMainWindow>
#include <QStackedWidget>
#include <QListWidget>
#include <QSplitter>
#include <QTreeView>
#include <QStatusBar>
#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();
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;
};
+3 -4
View File
@@ -3,8 +3,7 @@
#include <QDateTime>
EmailListModel::EmailListModel(QObject *parent)
: QAbstractListModel(parent),
m_mailItemDao(MailItemDao::instance())
: QAbstractListModel(parent)
{
refresh();
}
@@ -89,9 +88,9 @@ void EmailListModel::refresh()
{
beginResetModel();
if (m_folderId == -1) {
m_emails = m_mailItemDao.findAll();
m_emails = MailItemDao::findAll();
} else {
m_emails = m_mailItemDao.findByFolderId(m_folderId);
m_emails = MailItemDao::findByFolderId(m_folderId);
}
endResetModel();
qDebug() << "EmailListModel refreshed with" << m_emails.size() << "emails for folderId" << m_folderId;
+1 -1
View File
@@ -40,7 +40,7 @@ public:
private:
QVector<MailItem> m_emails;
int m_folderId{-1}; // -1 means all folders
MailItemDao& m_mailItemDao;
// REMOVED: MailItemDao& m_mailItemDao; // Methods are static, no instance needed
};
#endif // EMAILLISTMODEL_H
+203 -16
View File
@@ -1,36 +1,177 @@
#include "FolderListModel.h"
#include <QDebug>
FolderListModel::FolderListModel(QObject *parent)
: QAbstractListModel(parent),
m_folderDao(FolderDao::instance())
// --- FolderTreeItem ---
FolderTreeItem::FolderTreeItem(Type type, QVariant data, FolderTreeItem *parent)
: m_type(type), m_data(std::move(data)), m_parentItem(parent)
{
}
FolderTreeItem::~FolderTreeItem()
{
qDeleteAll(m_childItems);
}
void FolderTreeItem::appendChild(FolderTreeItem *child)
{
m_childItems.append(child);
}
void FolderTreeItem::clearChildren()
{
qDeleteAll(m_childItems);
m_childItems.clear();
}
FolderTreeItem *FolderTreeItem::child(int row)
{
if (row < 0 || row >= m_childItems.size())
return nullptr;
return m_childItems.at(row);
}
int FolderTreeItem::childCount() const
{
return m_childItems.size();
}
int FolderTreeItem::row() const
{
if (m_parentItem)
return m_parentItem->m_childItems.indexOf(const_cast<FolderTreeItem*>(this));
return 0;
}
FolderTreeItem *FolderTreeItem::parentItem()
{
return m_parentItem;
}
// --- FolderListModel ---
FolderListModel::FolderListModel(AccountService *accountService, QObject *parent)
: QAbstractItemModel(parent),
m_accountService(accountService),
m_rootItem(new FolderTreeItem(FolderTreeItem::AccountNode, "Root"))
{
refresh();
}
FolderListModel::~FolderListModel()
{
delete m_rootItem;
}
QModelIndex FolderListModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent))
return QModelIndex();
FolderTreeItem *parentItem;
if (!parent.isValid())
parentItem = m_rootItem;
else
parentItem = static_cast<FolderTreeItem*>(parent.internalPointer());
FolderTreeItem *childItem = parentItem->child(row);
if (childItem)
return createIndex(row, column, childItem);
return QModelIndex();
}
QModelIndex FolderListModel::parent(const QModelIndex &index) const
{
if (!index.isValid())
return QModelIndex();
FolderTreeItem *childItem = static_cast<FolderTreeItem*>(index.internalPointer());
FolderTreeItem *parentItem = childItem->parentItem();
if (!parentItem || parentItem == m_rootItem)
return QModelIndex();
return createIndex(parentItem->row(), 0, parentItem);
}
int FolderListModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid())
if (parent.column() > 0)
return 0;
return m_folderNames.size();
FolderTreeItem *parentItem;
if (!parent.isValid())
parentItem = m_rootItem;
else
parentItem = static_cast<FolderTreeItem*>(parent.internalPointer());
return parentItem->childCount();
}
int FolderListModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return 1;
}
QVariant FolderListModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= m_folderNames.size())
if (!index.isValid())
return QVariant();
if (role == FolderNameRole)
return m_folderNames.at(index.row());
else if (role == UnreadCountRole)
return m_unreadCounts.at(index.row());
FolderTreeItem *item = static_cast<FolderTreeItem*>(index.internalPointer());
if (role == Qt::DisplayRole || role == NameRole) {
if (item->type() == FolderTreeItem::AccountNode) {
Account acc = item->data().value<Account>();
return acc.displayName().isEmpty() ? acc.email() : acc.displayName();
} else {
Folder folder = item->data().value<Folder>();
return folder.name();
}
}
if (role == ItemTypeRole)
return item->type();
if (role == AccountIdRole) {
if (item->type() == FolderTreeItem::AccountNode) {
Account acc = item->data().value<Account>();
return acc.id();
}
}
if (role == FolderIdRole) {
if (item->type() == FolderTreeItem::FolderNode) {
Folder folder = item->data().value<Folder>();
return folder.id();
}
}
if (role == UnreadCountRole) {
if (item->type() == FolderTreeItem::FolderNode) {
Folder folder = item->data().value<Folder>();
return folder.unreadCount();
}
}
return QVariant();
}
Qt::ItemFlags FolderListModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::NoItemFlags;
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
QHash<int, QByteArray> FolderListModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[FolderNameRole] = "folderName";
roles[ItemTypeRole] = "itemType";
roles[AccountIdRole] = "accountId";
roles[FolderIdRole] = "folderId";
roles[NameRole] = "name";
roles[UnreadCountRole] = "unreadCount";
return roles;
}
@@ -38,9 +179,55 @@ QHash<int, QByteArray> FolderListModel::roleNames() const
void FolderListModel::refresh()
{
beginResetModel();
// For now, we'll just use hardcoded folders until we implement the DAO properly
m_folderNames = {"Inbox", "Sent", "Drafts", "Trash", "Spam"};
m_unreadCounts = {5, 0, 0, 0, 0};
clearModel();
setupModelData();
endResetModel();
qDebug() << "FolderListModel refreshed with" << m_folderNames.size() << "folders";
}
qDebug() << "FolderListModel refreshed";
}
void FolderListModel::setupModelData()
{
QVector<Account> accounts = m_accountService->getAllAccounts();
// If no accounts exist yet, add one sample account with default folders
if (accounts.isEmpty()) {
Account sample(1, "javi@example.com", "Javi's Email",
AccountType::IMAP);
accounts.append(sample);
}
for (const Account &acc : accounts) {
QVariant accVariant;
accVariant.setValue(acc);
FolderTreeItem *accountItem = new FolderTreeItem(FolderTreeItem::AccountNode, accVariant, m_rootItem);
m_rootItem->appendChild(accountItem);
// Get folders for this account
QVector<Folder> folders = FolderDao::findByAccountId(acc.id());
// If no folders exist yet, create default ones
if (folders.isEmpty()) {
QStringList defaultFolders = {"Inbox", "Sent", "Drafts", "Trash", "Spam"};
for (const QString &name : defaultFolders) {
Folder f;
f.setName(name);
f.setAccountId(acc.id());
folders.append(f);
}
}
for (const Folder &folder : folders) {
QVariant folderVariant;
folderVariant.setValue(folder);
FolderTreeItem *folderItem = new FolderTreeItem(FolderTreeItem::FolderNode, folderVariant, accountItem);
accountItem->appendChild(folderItem);
}
}
}
void FolderListModel::clearModel()
{
m_rootItem->clearChildren();
}
#include "FolderListModel.moc"
+47 -11
View File
@@ -1,34 +1,70 @@
#ifndef FOLDERLISTMODEL_H
#define FOLDERLISTMODEL_H
#include <QAbstractListModel>
#include <QAbstractItemModel>
#include <QHash>
#include <QByteArray>
#include "../db/dao/folderdao.h"
#include <QVector>
#include "services/accountservice.h"
#include "db/dao/folderdao.h"
#include "core/models/account.h"
#include "core/models/folder.h"
class FolderListModel : public QAbstractListModel
class FolderTreeItem
{
public:
enum Type { AccountNode, FolderNode };
explicit FolderTreeItem(Type type, QVariant data, FolderTreeItem *parent = nullptr);
~FolderTreeItem();
void appendChild(FolderTreeItem *child);
void clearChildren();
FolderTreeItem *child(int row);
int childCount() const;
int row() const;
FolderTreeItem *parentItem();
Type type() const { return m_type; }
QVariant data() const { return m_data; }
private:
Type m_type;
QVariant m_data;
QVector<FolderTreeItem*> m_childItems;
FolderTreeItem *m_parentItem;
};
class FolderListModel : public QAbstractItemModel
{
Q_OBJECT
public:
explicit FolderListModel(QObject *parent = nullptr);
~FolderListModel() override = default;
explicit FolderListModel(AccountService *accountService, QObject *parent = nullptr);
~FolderListModel() override;
enum FolderRoles {
FolderNameRole = Qt::UserRole + 1,
enum Roles {
ItemTypeRole = Qt::UserRole + 1,
AccountIdRole,
FolderIdRole,
NameRole,
UnreadCountRole
};
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &index) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QHash<int, QByteArray> roleNames() const override;
// Optional: method to refresh the model from the database
void refresh();
private:
QVector<QString> m_folderNames;
QVector<int> m_unreadCounts;
FolderDao& m_folderDao;
void setupModelData();
void clearModel();
FolderTreeItem *m_rootItem;
AccountService *m_accountService;
};
#endif // FOLDERLISTMODEL_H
+407
View File
@@ -0,0 +1,407 @@
#include "newmessagedialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFormLayout>
#include <QMessageBox>
#include <QFileDialog>
#include <QDateTime>
#include <QTextList>
#include <QTextTable>
#include <QFileInfo>
#include <QCloseEvent>
NewMessageDialog::NewMessageDialog(const QStringList &accounts, QWidget *parent)
: QDialog(parent)
{
setWindowTitle("New Message");
setMinimumSize(750, 600);
resize(850, 700);
setupUI();
// Init state
m_ccEdit->hide();
m_bccEdit->hide();
m_schedulePicker->hide();
// Capture initial content for dirty detection
m_initialContent = m_bodyEdit->toPlainText();
// Signals
connect(m_sendNowBtn, &QPushButton::clicked, this, &NewMessageDialog::onSendNow);
connect(m_sendLaterBtn, &QPushButton::clicked, this, &NewMessageDialog::onSendLater);
}
void NewMessageDialog::setupUI()
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(16, 12, 16, 12);
mainLayout->setSpacing(8);
// ===== HEADER FIELDS =====
QVBoxLayout *headerLayout = new QVBoxLayout();
headerLayout->setSpacing(6);
// From
QHBoxLayout *fromRow = new QHBoxLayout();
m_fromCombo = new QComboBox(this);
m_fromCombo->setMinimumWidth(400);
fromRow->addWidget(new QLabel("From:", this));
fromRow->addWidget(m_fromCombo, 1);
headerLayout->addLayout(fromRow);
// To + CC/CCO buttons
QHBoxLayout *toRow = new QHBoxLayout();
m_toEdit = new QLineEdit(this);
m_toEdit->setPlaceholderText("To");
toRow->addWidget(new QLabel("To:", this));
toRow->addWidget(m_toEdit, 1);
m_ccBtn = new QPushButton("CC", this);
m_ccBtn->setFixedWidth(48);
m_ccBtn->setStyleSheet("QPushButton { border: 1px solid #ccc; border-radius: 4px; padding: 4px 8px; font-size: 11px; background: #f0f0f0; } QPushButton:hover { background: #e0e0e0; }");
m_bccBtn = new QPushButton("BCC", this);
m_bccBtn->setFixedWidth(48);
m_bccBtn->setStyleSheet(m_ccBtn->styleSheet());
toRow->addWidget(m_ccBtn);
toRow->addWidget(m_bccBtn);
headerLayout->addLayout(toRow);
connect(m_ccBtn, &QPushButton::clicked, this, &NewMessageDialog::toggleCc);
connect(m_bccBtn, &QPushButton::clicked, this, &NewMessageDialog::toggleBcc);
// CC
m_ccEdit = new QLineEdit(this);
m_ccEdit->setPlaceholderText("Cc");
QHBoxLayout *ccRow = new QHBoxLayout();
ccRow->addWidget(new QLabel("Cc:", this));
ccRow->addWidget(m_ccEdit, 1);
headerLayout->addLayout(ccRow);
// BCC
m_bccEdit = new QLineEdit(this);
m_bccEdit->setPlaceholderText("Bcc");
QHBoxLayout *bccRow = new QHBoxLayout();
bccRow->addWidget(new QLabel("Bcc:", this));
bccRow->addWidget(m_bccEdit, 1);
headerLayout->addLayout(bccRow);
// Subject
QHBoxLayout *subjRow = new QHBoxLayout();
m_subjectEdit = new QLineEdit(this);
m_subjectEdit->setPlaceholderText("Subject");
subjRow->addWidget(new QLabel("Subject:", this));
subjRow->addWidget(m_subjectEdit, 1);
headerLayout->addLayout(subjRow);
mainLayout->addLayout(headerLayout);
// ===== RICH TEXT TOOLBAR =====
m_richToolbar = new QToolBar("Format", this);
m_richToolbar->setIconSize(QSize(16, 16));
m_richToolbar->setStyleSheet(
"QToolBar { border: 1px solid #d1d1d6; border-radius: 4px; background: #f9f9f9; spacing: 4px; padding: 2px; }"
);
setupRichTextToolbar(m_richToolbar);
mainLayout->addWidget(m_richToolbar);
// ===== BODY EDITOR =====
m_bodyEdit = new QTextEdit(this);
m_bodyEdit->setAcceptRichText(true);
m_bodyEdit->setPlaceholderText("Write your message...");
m_bodyEdit->setFrameShape(QFrame::StyledPanel);
m_bodyEdit->setStyleSheet(
"QTextEdit { border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px; font-size: 13px; font-family: 'Segoe UI', Roboto, Helvetica; }"
"QTextEdit:focus { border-color: #0078d4; }"
);
mainLayout->addWidget(m_bodyEdit, 1);
connect(m_bodyEdit, &QTextEdit::currentCharFormatChanged, this, &NewMessageDialog::currentCharFormatChanged);
// ===== ATTACHMENTS =====
QHBoxLayout *attachRow = new QHBoxLayout();
m_attachBtn = new QPushButton("Attach Files...", this);
m_attachBtn->setStyleSheet("QPushButton { border: 1px solid #ccc; border-radius: 4px; padding: 6px 12px; font-size: 12px; background: #f0f0f0; } QPushButton:hover { background: #e0e0e0; }");
m_attachmentList = new QListWidget(this);
m_attachmentList->setMaximumHeight(60);
m_attachmentList->setMinimumWidth(300);
m_attachmentList->setStyleSheet("QListWidget { border: 1px solid #e0e0e0; border-radius: 4px; font-size: 11px; }");
m_attachmentList->setVisible(false);
attachRow->addWidget(m_attachBtn);
attachRow->addWidget(m_attachmentList, 1);
mainLayout->addLayout(attachRow);
connect(m_attachBtn, &QPushButton::clicked, this, &NewMessageDialog::onAttachFile);
connect(m_attachmentList, &QListWidget::itemDoubleClicked, this, &NewMessageDialog::removeAttachment);
// ===== BOTTOM ACTION BAR =====
QHBoxLayout *actionBar = new QHBoxLayout();
// Schedule picker
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600), this);
m_schedulePicker->setCalendarPopup(true);
m_schedulePicker->setDisplayFormat("dd/MM/yyyy hh:mm AP");
m_schedulePicker->setStyleSheet("QDateTimeEdit { border: 1px solid #ccc; border-radius: 4px; padding: 6px 8px; font-size: 12px; }");
actionBar->addStretch();
m_cancelBtn = new QPushButton("Discard", this);
m_cancelBtn->setStyleSheet("QPushButton { border: 1px solid #ccc; border-radius: 6px; padding: 8px 20px; font-size: 13px; font-weight: bold; background: #f0f0f0; color: #333; } QPushButton:hover { background: #e0e0e0; }");
m_sendLaterBtn = new QPushButton("Schedule...", this);
m_sendLaterBtn->setStyleSheet("QPushButton { border: 1px solid #0078d4; border-radius: 6px; padding: 8px 20px; font-size: 13px; font-weight: bold; background: #ffffff; color: #0078d4; } QPushButton:hover { background: #e8f4ff; }");
m_sendNowBtn = new QPushButton("Send Now", this);
m_sendNowBtn->setStyleSheet("QPushButton { border: none; border-radius: 6px; padding: 8px 24px; font-size: 13px; font-weight: bold; background: #0078d4; color: white; } QPushButton:hover { background: #106ebe; }");
actionBar->addWidget(m_cancelBtn);
actionBar->addWidget(m_sendLaterBtn);
actionBar->addWidget(m_sendNowBtn);
mainLayout->addLayout(actionBar);
connect(m_cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
}
void NewMessageDialog::setupRichTextToolbar(QToolBar *tb)
{
m_boldAction = tb->addAction("B");
m_boldAction->setCheckable(true);
m_boldAction->setToolTip("Bold (Ctrl+B)");
m_boldAction->setFont(QFont("Segoe UI", 11, QFont::Bold));
m_italicAction = tb->addAction("I");
m_italicAction->setCheckable(true);
m_italicAction->setToolTip("Italic (Ctrl+I)");
m_italicAction->setFont(QFont("Segoe UI", 11, QFont::StyleItalic));
m_underlineAction = tb->addAction("U");
m_underlineAction->setCheckable(true);
m_underlineAction->setToolTip("Underline (Ctrl+U)");
QFont uFont("Segoe UI", 11);
uFont.setUnderline(true);
m_underlineAction->setFont(uFont);
tb->addSeparator();
QAction *bulletAction = tb->addAction("• List");
bulletAction->setToolTip("Bullet list");
QAction *numAction = tb->addAction("1. List");
numAction->setToolTip("Numbered list");
tb->addSeparator();
QAction *tableAction = tb->addAction("▦ Table");
tableAction->setToolTip("Insert table");
QAction *imageAction = tb->addAction("🖼 Image");
imageAction->setToolTip("Insert image");
connect(m_boldAction, &QAction::triggered, this, &NewMessageDialog::onFormatBold);
connect(m_italicAction, &QAction::triggered, this, &NewMessageDialog::onFormatItalic);
connect(m_underlineAction, &QAction::triggered, this, &NewMessageDialog::onFormatUnderline);
connect(bulletAction, &QAction::triggered, this, &NewMessageDialog::onInsertBulletList);
connect(numAction, &QAction::triggered, this, &NewMessageDialog::onInsertNumberedList);
connect(tableAction, &QAction::triggered, this, &NewMessageDialog::onInsertTable);
connect(imageAction, &QAction::triggered, this, &NewMessageDialog::onInsertImage);
tb->addSeparator();
QAction *undoAction = tb->addAction("");
undoAction->setToolTip("Undo");
connect(undoAction, &QAction::triggered, m_bodyEdit, &QTextEdit::undo);
}
// ===== FORMAT SLOTS =====
void NewMessageDialog::mergeFormatOnWordOrSelection(const QTextCharFormat &fmt)
{
QTextCursor cursor = m_bodyEdit->textCursor();
if (!cursor.hasSelection())
cursor.select(QTextCursor::WordUnderCursor);
cursor.mergeCharFormat(fmt);
m_bodyEdit->mergeCurrentCharFormat(fmt);
}
void NewMessageDialog::currentCharFormatChanged(const QTextCharFormat &fmt)
{
m_boldAction->setChecked(fmt.fontWeight() >= QFont::Bold);
m_italicAction->setChecked(fmt.fontItalic());
m_underlineAction->setChecked(fmt.fontUnderline());
}
void NewMessageDialog::onFormatBold()
{
QTextCharFormat fmt;
fmt.setFontWeight(m_boldAction->isChecked() ? QFont::Bold : QFont::Normal);
mergeFormatOnWordOrSelection(fmt);
}
void NewMessageDialog::onFormatItalic()
{
QTextCharFormat fmt;
fmt.setFontItalic(m_italicAction->isChecked());
mergeFormatOnWordOrSelection(fmt);
}
void NewMessageDialog::onFormatUnderline()
{
QTextCharFormat fmt;
fmt.setFontUnderline(m_underlineAction->isChecked());
mergeFormatOnWordOrSelection(fmt);
}
void NewMessageDialog::onInsertBulletList()
{
QTextCursor cursor = m_bodyEdit->textCursor();
cursor.insertList(QTextListFormat::ListDisc);
}
void NewMessageDialog::onInsertNumberedList()
{
QTextCursor cursor = m_bodyEdit->textCursor();
cursor.insertList(QTextListFormat::ListDecimal);
}
void NewMessageDialog::onInsertTable()
{
QTextCursor cursor = m_bodyEdit->textCursor();
QTextTable *table = cursor.insertTable(3, 3);
Q_UNUSED(table);
}
void NewMessageDialog::onInsertImage()
{
QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(),
"Images (*.png *.jpg *.jpeg *.gif *.bmp)");
if (filePath.isEmpty()) return;
QImage image(filePath);
if (image.isNull()) return;
// Scale large images to fit
if (image.width() > 600)
image = image.scaledToWidth(600, Qt::SmoothTransformation);
QTextCursor cursor = m_bodyEdit->textCursor();
cursor.insertImage(image);
}
// ===== CC / BCC TOGGLES =====
void NewMessageDialog::toggleCc()
{
m_ccVisible = !m_ccVisible;
m_ccEdit->setVisible(m_ccVisible);
m_ccBtn->setStyleSheet(m_ccVisible
? "QPushButton { border: 1px solid #0078d4; border-radius: 4px; padding: 4px 8px; font-size: 11px; background: #e8f4ff; color: #0078d4; }"
: "QPushButton { border: 1px solid #ccc; border-radius: 4px; padding: 4px 8px; font-size: 11px; background: #f0f0f0; } QPushButton:hover { background: #e0e0e0; }");
}
void NewMessageDialog::toggleBcc()
{
m_bccVisible = !m_bccVisible;
m_bccEdit->setVisible(m_bccVisible);
m_bccBtn->setStyleSheet(m_bccVisible
? "QPushButton { border: 1px solid #0078d4; border-radius: 4px; padding: 4px 8px; font-size: 11px; background: #e8f4ff; color: #0078d4; }"
: "QPushButton { border: 1px solid #ccc; border-radius: 4px; padding: 4px 8px; font-size: 11px; background: #f0f0f0; } QPushButton:hover { background: #e0e0e0; }");
}
// ===== SEND =====
void NewMessageDialog::onSendNow()
{
if (m_toEdit->text().trimmed().isEmpty()) {
QMessageBox::warning(this, "Missing Recipient", "Please enter at least one recipient.");
m_toEdit->setFocus();
return;
}
m_sendLater = false;
accept();
}
void NewMessageDialog::onSendLater()
{
if (m_toEdit->text().trimmed().isEmpty()) {
QMessageBox::warning(this, "Missing Recipient", "Please enter at least one recipient.");
m_toEdit->setFocus();
return;
}
if (m_schedulePicker->dateTime() <= QDateTime::currentDateTime()) {
QMessageBox::warning(this, "Invalid Time", "Scheduled time must be in the future.");
return;
}
m_sendLater = true;
m_scheduledTime = m_schedulePicker->dateTime();
accept();
}
// ===== ATTACHMENTS =====
void NewMessageDialog::onAttachFile()
{
QStringList files = QFileDialog::getOpenFileNames(this, "Attach Files");
if (files.isEmpty()) return;
for (const QString &file : files) {
QFileInfo fi(file);
m_attachedFiles << file;
m_attachmentList->addItem(fi.fileName());
}
m_attachmentList->setVisible(!m_attachedFiles.isEmpty());
}
void NewMessageDialog::removeAttachment()
{
QListWidgetItem *item = m_attachmentList->currentItem();
if (!item) return;
int row = m_attachmentList->row(item);
m_attachedFiles.removeAt(row);
delete m_attachmentList->takeItem(row);
m_attachmentList->setVisible(!m_attachedFiles.isEmpty());
}
// ===== CLOSE WITH UNSAVED CHANGES =====
bool NewMessageDialog::hasUnsavedChanges() const
{
// Check if anything was typed
if (!m_toEdit->text().trimmed().isEmpty()) return true;
if (!m_subjectEdit->text().trimmed().isEmpty()) return true;
if (m_bodyEdit->toPlainText() != m_initialContent &&
!m_bodyEdit->toPlainText().trimmed().isEmpty()) return true;
if (!m_attachedFiles.isEmpty()) return true;
return false;
}
void NewMessageDialog::closeEvent(QCloseEvent *event)
{
if (hasUnsavedChanges()) {
QMessageBox::StandardButton reply = QMessageBox::question(
this, "Unsaved Changes",
"You have unsaved changes. Would you like to save to drafts before closing?",
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
QMessageBox::Save);
if (reply == QMessageBox::Save) {
// Accept the dialog so the caller can handle saving to drafts
accept();
return;
} else if (reply == QMessageBox::Cancel) {
event->ignore();
return;
}
// Discard: fall through to reject
}
event->accept();
}
// ===== GETTERS =====
QString NewMessageDialog::fromAccount() const { return m_fromCombo->currentText(); }
QString NewMessageDialog::to() const { return m_toEdit->text().trimmed(); }
QString NewMessageDialog::cc() const { return m_ccEdit->text().trimmed(); }
QString NewMessageDialog::bcc() const { return m_bccEdit->text().trimmed(); }
QString NewMessageDialog::subject() const { return m_subjectEdit->text().trimmed(); }
QString NewMessageDialog::body() const { return m_bodyEdit->toHtml(); }
+95
View File
@@ -0,0 +1,95 @@
#ifndef NEWMESSAGEDIALOG_H
#define NEWMESSAGEDIALOG_H
#include <QDialog>
#include <QLineEdit>
#include <QTextEdit>
#include <QPushButton>
#include <QComboBox>
#include <QLabel>
#include <QToolBar>
#include <QDateTimeEdit>
#include <QListWidget>
#include <QStringList>
#include <QTextCharFormat>
class NewMessageDialog : public QDialog
{
Q_OBJECT
public:
explicit NewMessageDialog(const QStringList &accounts, QWidget *parent = nullptr);
~NewMessageDialog() override = default;
QString fromAccount() const;
QString to() const;
QString cc() const;
QString bcc() const;
QString subject() const;
QString body() const;
QStringList attachments() const { return m_attachedFiles; }
bool sendLater() const { return m_sendLater; }
QDateTime scheduledTime() const { return m_scheduledTime; }
protected:
void closeEvent(QCloseEvent *event) override;
private slots:
void toggleCc();
void toggleBcc();
void onSendNow();
void onSendLater();
void onAttachFile();
void removeAttachment();
void onFormatBold();
void onFormatItalic();
void onFormatUnderline();
void onInsertBulletList();
void onInsertNumberedList();
void onInsertTable();
void onInsertImage();
void mergeFormatOnWordOrSelection(const QTextCharFormat &fmt);
void currentCharFormatChanged(const QTextCharFormat &fmt);
private:
void setupUI();
void setupRichTextToolbar(QToolBar *tb);
bool hasUnsavedChanges() const;
// Header fields
QComboBox *m_fromCombo;
QLineEdit *m_toEdit;
QPushButton *m_ccBtn;
QPushButton *m_bccBtn;
QLineEdit *m_ccEdit;
QLineEdit *m_bccEdit;
QLineEdit *m_subjectEdit;
// Rich text editor
QToolBar *m_richToolbar;
QAction *m_boldAction;
QAction *m_italicAction;
QAction *m_underlineAction;
// Body
QTextEdit *m_bodyEdit;
// Attachments
QPushButton *m_attachBtn;
QListWidget *m_attachmentList;
QStringList m_attachedFiles;
// Bottom actions
QPushButton *m_sendNowBtn;
QPushButton *m_sendLaterBtn;
QPushButton *m_cancelBtn;
QDateTimeEdit *m_schedulePicker;
bool m_ccVisible{false};
bool m_bccVisible{false};
bool m_sendLater{false};
QDateTime m_scheduledTime;
QString m_initialContent; // For detecting unsaved changes
};
#endif // NEWMESSAGEDIALOG_H
+77
View File
@@ -0,0 +1,77 @@
#include "ui/readerview.h"
#include <QFont>
ReaderView::ReaderView(QWidget *parent) : QWidget(parent) {
setupUI();
}
void ReaderView::setupUI() {
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(15, 15, 15, 15);
mainLayout->setSpacing(10);
// Header Section
QWidget *headerWidget = new QWidget();
QVBoxLayout *headerLayout = new QVBoxLayout(headerWidget);
headerLayout->setSpacing(5);
m_subjectLabel = new QLabel();
QFont subjectFont = m_subjectLabel->font();
subjectFont.setBold(true);
subjectFont.setPointSize(14);
m_subjectLabel->setFont(subjectFont);
m_subjectLabel->setText("No subject");
m_subjectLabel->setWordWrap(true);
m_fromLabel = new QLabel();
m_fromLabel->setText("From: ");
m_dateLabel = new QLabel();
m_dateLabel->setText("Date: ");
m_dateLabel->setStyleSheet("color: gray; font-style: italic;");
headerLayout->addWidget(m_subjectLabel);
headerLayout->addWidget(m_fromLabel);
headerLayout->addWidget(m_dateLabel);
// Action Buttons
QHBoxLayout *actionsLayout = new QHBoxLayout();
m_replyButton = new QPushButton("Reply");
m_forwardButton = new QPushButton("Forward");
m_deleteButton = new QPushButton("Delete");
m_deleteButton->setStyleSheet("color: red;");
actionsLayout->addWidget(m_replyButton);
actionsLayout->addWidget(m_forwardButton);
actionsLayout->addStretch();
actionsLayout->addWidget(m_deleteButton);
// Body Viewer
m_bodyViewer = new QTextBrowser();
m_bodyViewer->setOpenExternalLinks(true);
m_bodyViewer->setFrameStyle(QFrame::NoFrame);
mainLayout->addWidget(headerWidget);
mainLayout->addLayout(actionsLayout);
mainLayout->addWidget(m_bodyViewer);
// Connections
connect(m_replyButton, &QPushButton::clicked, [this]() {
if (m_bodyViewer->toPlainText().isEmpty()) return;
});
}
void ReaderView::setMailItem(const MailItem* item) {
if (!item) {
m_subjectLabel->setText("No mail selected");
m_fromLabel->setText("From: ");
m_dateLabel->setText("Date: ");
m_bodyViewer->setHtml("<i>Please select a message to read</i>");
return;
}
m_subjectLabel->setText(item->subject());
m_fromLabel->setText(QString("From: %1").arg(item->sender()));
m_dateLabel->setText(QString("Date: %1").arg(item->date().toString("ddd, d MMM yyyy hh:mm")));
m_bodyViewer->setHtml(item->bodyHtml());
}
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <QWidget>
#include <QLabel>
#include <QTextBrowser>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include "core/mailitem.h"
class ReaderView : public QWidget {
Q_OBJECT
public:
explicit ReaderView(QWidget *parent = nullptr);
~ReaderView() override = default;
void setMailItem(const MailItem* item);
signals:
void replyRequested(const MailItem* item);
void forwardRequested(const MailItem* item);
void deleteRequested(const MailItem* item);
private:
void setupUI();
QLabel *m_subjectLabel;
QLabel *m_fromLabel;
QLabel *m_dateLabel;
QTextBrowser *m_bodyViewer;
QPushButton *m_replyButton;
QPushButton *m_forwardButton;
QPushButton *m_deleteButton;
};
+164
View File
@@ -0,0 +1,164 @@
#include "ui/settingsview.h"
#include "services/accountservice.h"
#include "core/models/account.h"
SettingsView::SettingsView(QWidget *parent) : QWidget(parent) {
setupUI();
}
void SettingsView::setupUI() {
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
m_tabWidget = new QTabWidget();
m_tabWidget->addTab(createAccountsTab(), "Accounts");
m_tabWidget->addTab(createGeneralTab(), "General");
m_tabWidget->addTab(createAppearanceTab(), "Appearance");
mainLayout->addWidget(m_tabWidget);
}
QWidget* SettingsView::createAccountsTab() {
QWidget *w = new QWidget();
QVBoxLayout *layout = new QVBoxLayout(w);
layout->setContentsMargins(20, 20, 20, 20);
layout->setSpacing(15);
QLabel *sectionTitle = new QLabel("Email Accounts");
QFont titleFont = sectionTitle->font();
titleFont.setPointSize(16);
titleFont.setBold(true);
sectionTitle->setFont(titleFont);
layout->addWidget(sectionTitle);
m_accountList = new QListWidget();
m_accountList->setAlternatingRowColors(true);
m_accountList->setFrameShape(QFrame::NoFrame);
m_accountList->setStyleSheet(
"QListWidget { background: #ffffff; border: 1px solid #e0e0e0; border-radius: 4px; }"
"QListWidget::item { padding: 12px; border-bottom: 1px solid #f0f0f0; }"
"QListWidget::item:selected { background: #e3f2fd; }"
);
layout->addWidget(m_accountList, 1);
QPushButton *addBtn = new QPushButton("+ Add Email Account");
addBtn->setStyleSheet(
"QPushButton { background: #1976D2; color: white; border: none; border-radius: 4px; padding: 10px 20px; font-weight: bold; }"
"QPushButton:hover { background: #1565C0; }"
);
connect(addBtn, &QPushButton::clicked, this, &SettingsView::accountAddRequested);
layout->addWidget(addBtn);
return w;
}
QWidget* SettingsView::createGeneralTab() {
QWidget *w = new QWidget();
QVBoxLayout *layout = new QVBoxLayout(w);
layout->setContentsMargins(20, 20, 20, 20);
layout->setSpacing(15);
QLabel *sectionTitle = new QLabel("General Settings");
QFont titleFont = sectionTitle->font();
titleFont.setPointSize(16);
titleFont.setBold(true);
sectionTitle->setFont(titleFont);
layout->addWidget(sectionTitle);
QCheckBox *startOnLogin = new QCheckBox("Start application on login");
startOnLogin->setChecked(true);
connect(startOnLogin, &QCheckBox::toggled, [this](bool checked) {
emit settingChanged("start_on_login", checked);
});
layout->addWidget(startOnLogin);
QCheckBox *enableNotifications = new QCheckBox("Enable notifications");
enableNotifications->setChecked(true);
connect(enableNotifications, &QCheckBox::toggled, [this](bool checked) {
emit settingChanged("notifications_enabled", checked);
});
layout->addWidget(enableNotifications);
QCheckBox *minimizeToTray = new QCheckBox("Minimize to tray on close");
minimizeToTray->setChecked(true);
connect(minimizeToTray, &QCheckBox::toggled, [this](bool checked) {
emit settingChanged("minimize_to_tray", checked);
});
layout->addWidget(minimizeToTray);
layout->addSpacing(10);
QHBoxLayout *syncLayout = new QHBoxLayout();
QLabel *syncLabel = new QLabel("Sync Interval:");
syncLabel->setStyleSheet("font-weight: bold; color: #555;");
m_syncIntervalCombo = new QComboBox();
m_syncIntervalCombo->addItem("15 minutes", 15);
m_syncIntervalCombo->addItem("30 minutes", 30);
m_syncIntervalCombo->addItem("60 minutes", 60);
m_syncIntervalCombo->addItem("120 minutes", 120);
m_syncIntervalCombo->setCurrentIndex(1);
connect(m_syncIntervalCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), [this](int idx) {
emit settingChanged("sync_interval", m_syncIntervalCombo->itemData(idx));
});
syncLayout->addWidget(syncLabel);
syncLayout->addWidget(m_syncIntervalCombo);
syncLayout->addStretch();
layout->addLayout(syncLayout);
layout->addStretch();
return w;
}
QWidget* SettingsView::createAppearanceTab() {
QWidget *w = new QWidget();
QVBoxLayout *layout = new QVBoxLayout(w);
layout->setContentsMargins(20, 20, 20, 20);
layout->setSpacing(15);
QLabel *sectionTitle = new QLabel("Appearance");
QFont titleFont = sectionTitle->font();
titleFont.setPointSize(16);
titleFont.setBold(true);
sectionTitle->setFont(titleFont);
layout->addWidget(sectionTitle);
QLabel *themeLabel = new QLabel("Theme:");
themeLabel->setStyleSheet("font-weight: bold; color: #555;");
layout->addWidget(themeLabel);
m_themeGroup = new QButtonGroup(this);
QRadioButton *lightRadio = new QRadioButton("Light");
QRadioButton *darkRadio = new QRadioButton("Dark");
QRadioButton *systemRadio = new QRadioButton("System");
lightRadio->setChecked(true);
m_themeGroup->addButton(lightRadio, 0);
m_themeGroup->addButton(darkRadio, 1);
m_themeGroup->addButton(systemRadio, 2);
connect(m_themeGroup, QOverload<int>::of(&QButtonGroup::idClicked), [this](int id) {
QString theme;
switch (id) {
case 0: theme = "light"; break;
case 1: theme = "dark"; break;
case 2: theme = "system"; break;
}
emit themeChanged(theme);
});
layout->addWidget(lightRadio);
layout->addWidget(darkRadio);
layout->addWidget(systemRadio);
layout->addSpacing(15);
QCheckBox *deepinTheme = new QCheckBox("Use Deepin theme");
deepinTheme->setChecked(true);
connect(deepinTheme, &QCheckBox::toggled, [this](bool checked) {
emit settingChanged("deepin_theme", checked);
});
layout->addWidget(deepinTheme);
layout->addStretch();
return w;
}
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include <QWidget>
#include <QTabWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QLabel>
#include <QListWidget>
#include <QComboBox>
#include <QCheckBox>
#include <QRadioButton>
#include <QButtonGroup>
#include <QVariant>
class SettingsView : public QWidget {
Q_OBJECT
public:
explicit SettingsView(QWidget *parent = nullptr);
~SettingsView() override = default;
signals:
void accountAddRequested();
void accountEditRequested(int accountIndex);
void accountDeleteRequested(int accountIndex);
void themeChanged(const QString &theme);
void settingChanged(const QString &key, const QVariant &value);
private:
void setupUI();
QWidget* createAccountsTab();
QWidget* createGeneralTab();
QWidget* createAppearanceTab();
QTabWidget *m_tabWidget;
QListWidget *m_accountList;
QComboBox *m_syncIntervalCombo;
QButtonGroup *m_themeGroup;
};
+9 -15
View File
@@ -8,8 +8,6 @@ NotificationManager::NotificationManager(QObject *parent)
, m_trayMenu(nullptr)
, m_showHideAction(nullptr)
, m_quitAction(nullptr)
, m_newMailSound(nullptr)
, m_errorSound(nullptr)
{
// Defer initialization to after QApplication is fully ready
// We'll initialize in a separate method called from main after the event loop starts?
@@ -35,8 +33,6 @@ void NotificationManager::initialize()
m_trayMenu = new QMenu(); // parent to NotificationManager so it gets deleted with us
m_showHideAction = new QAction("Show/Hide", m_trayMenu);
m_quitAction = new QAction("Quit", m_trayMenu);
m_newMailSound = new QSoundEffect(this);
m_errorSound = new QSoundEffect(this);
setupTrayIcon();
setupConnections();
@@ -53,11 +49,11 @@ void NotificationManager::setupTrayIcon()
m_trayMenu->addAction(m_quitAction);
m_trayIcon->setContextMenu(m_trayMenu);
m_newMailSound->setSource(QUrl::fromLocalFile("/usr/share/sounds/freedesktop/stereo/message-new-instant.oga"));
m_newMailSound->setVolume(0.5);
// // m_newMailSound->setSource (disabled)(QUrl::fromLocalFile("/usr/share/sounds/freedesktop/stereo/message-new-instant.oga"));
// m_newMailSound->setVolume(0.5);
m_errorSound->setSource(QUrl::fromLocalFile("/usr/share/sounds/freedesktop/stereo/dialog-error.oga"));
m_errorSound->setVolume(0.5);
// // m_errorSound->setSource (disabled)(QUrl::fromLocalFile("/usr/share/sounds/freedesktop/stereo/dialog-error.oga"));
// m_errorSound->setVolume(0.5);
}
void NotificationManager::setupConnections()
@@ -223,19 +219,17 @@ void NotificationManager::onQuitRequested()
void NotificationManager::playNewMailSound()
{
if (m_newMailSound->isLoaded()) {
m_newMailSound->play();
}
Q_UNUSED(m_newMailSound);
qDebug() << "New mail sound disabled (no Multimedia)";
}
void NotificationManager::playErrorSound()
{
if (m_errorSound->isLoaded()) {
m_errorSound->play();
}
Q_UNUSED(m_errorSound);
qDebug() << "Error sound disabled (no Multimedia)";
}
void NotificationManager::showNotification(const QString& title, const QString& message, QSystemTrayIcon::MessageIcon icon, int timeout)
{
m_trayIcon->showMessage(title, message, icon, timeout);
}
}
+3 -3
View File
@@ -5,7 +5,7 @@
#include <QSystemTrayIcon>
#include <QMenu>
#include <QAction>
#include <QSoundEffect>
//// QSoundEffect disabled (unavailable, sound disabled)
#include <QIcon>
#include "core/eventbus.h"
#include "../core/events.h"
@@ -39,8 +39,8 @@ private:
QMenu* m_trayMenu;
QAction* m_showHideAction;
QAction* m_quitAction;
QSoundEffect* m_newMailSound;
QSoundEffect* m_errorSound;
void* m_newMailSound = nullptr; // was QSoundEffect* (disabled)
void* m_errorSound = nullptr; // was QSoundEffect* (disabled)
void setupTrayIcon();
void setupConnections();