2026-05-13 01:18:04 +02:00
|
|
|
#include "gmailsynchronizer.h"
|
|
|
|
|
#include <QNetworkAccessManager>
|
|
|
|
|
#include <QNetworkReply>
|
|
|
|
|
#include <QNetworkRequest>
|
|
|
|
|
#include <QUrl>
|
|
|
|
|
#include <QJsonDocument>
|
|
|
|
|
#include <QJsonObject>
|
|
|
|
|
#include <QJsonArray>
|
|
|
|
|
#include <QDebug>
|
2026-05-17 01:09:01 +02:00
|
|
|
#include <QUrlQuery>
|
2026-05-13 01:18:04 +02:00
|
|
|
#include <QDateTime>
|
|
|
|
|
#include <QTimer>
|
2026-05-29 10:51:49 +02:00
|
|
|
#include <QEventLoop>
|
|
|
|
|
#include "../core/events.h"
|
|
|
|
|
#include "../core/eventbus.h"
|
2026-05-13 01:18:04 +02:00
|
|
|
|
|
|
|
|
GmailSynchronizer::GmailSynchronizer(QObject* parent)
|
|
|
|
|
: Synchronizer(parent),
|
|
|
|
|
m_networkManager(new QNetworkAccessManager(this)),
|
|
|
|
|
m_historyTimer(new QTimer(this))
|
|
|
|
|
{
|
|
|
|
|
// Conectar señales
|
|
|
|
|
connect(m_networkManager, &QNetworkAccessManager::finished,
|
|
|
|
|
this, &GmailSynchronizer::onGmailReplyFinished);
|
|
|
|
|
connect(m_historyTimer, &QTimer::timeout,
|
|
|
|
|
this, &GmailSynchronizer::onHistoryTimer);
|
|
|
|
|
|
|
|
|
|
// Configurar timer para history sync cada 2 minutos (Gmail permite cambios frecuentes)
|
|
|
|
|
m_historyTimer->setInterval(2 * 60 * 1000); // 2 minutos
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool GmailSynchronizer::initialize(const Account& account)
|
|
|
|
|
{
|
|
|
|
|
// Guardar información de la cuenta
|
|
|
|
|
m_account = account;
|
|
|
|
|
|
|
|
|
|
// Para Gmail, usamos tokens de acceso
|
|
|
|
|
m_accessToken = account.accessToken();
|
|
|
|
|
m_refreshToken = account.refreshToken();
|
|
|
|
|
m_tokenExpires = account.tokenExpires();
|
|
|
|
|
|
|
|
|
|
if (m_accessToken.isEmpty()) {
|
|
|
|
|
qWarning() << "Gmail account has no access token";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-29 10:51:49 +02:00
|
|
|
// Publish AccountConnectedEvent
|
|
|
|
|
WinoMail::Events::AccountConnectedEvent event;
|
|
|
|
|
event.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
|
|
|
|
|
event.timestamp = QDateTime::currentDateTimeUtc();
|
|
|
|
|
event.accountId = account.id();
|
|
|
|
|
event.provider = "gmail";
|
|
|
|
|
PUBLISH(event);
|
|
|
|
|
|
2026-05-13 01:18:04 +02:00
|
|
|
qDebug() << "Gmail Synchronizer initialized for account:" << account.email();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool GmailSynchronizer::syncFolder(const Folder& folder)
|
|
|
|
|
{
|
|
|
|
|
if (!m_account.isTokenValid()) {
|
|
|
|
|
if (!refreshAccessToken()) {
|
|
|
|
|
qWarning() << "Failed to refresh token for Gmail sync";
|
|
|
|
|
emit errorOccurred("Failed to refresh authentication token");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
qDebug() << "Starting Gmail sync for folder (label):" << folder.name();
|
2026-05-29 10:51:49 +02:00
|
|
|
|
|
|
|
|
// Publish SyncStartedEvent
|
|
|
|
|
WinoMail::Events::SyncStartedEvent syncStartEvent;
|
|
|
|
|
syncStartEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
|
|
|
|
|
syncStartEvent.timestamp = QDateTime::currentDateTimeUtc();
|
|
|
|
|
syncStartEvent.accountId = m_account.id();
|
|
|
|
|
syncStartEvent.folderId = QString::number(folder.id());
|
|
|
|
|
PUBLISH(syncStartEvent);
|
|
|
|
|
|
2026-05-17 01:09:01 +02:00
|
|
|
emit folderSyncStarted(QString::number(folder.id()));
|
2026-05-13 01:18:04 +02:00
|
|
|
|
|
|
|
|
// Obtener elementos de correo desde Gmail API
|
2026-05-17 01:09:01 +02:00
|
|
|
QVector<MailItem> items = fetchMailItems(QString::number(folder.id()));
|
2026-05-13 01:18:04 +02:00
|
|
|
|
|
|
|
|
// En una implementación real, aquí compararíamos con la base de datos local
|
|
|
|
|
// y emitiríamos las señales apropiadas para elementos nuevos/actualizados/eliminados
|
|
|
|
|
|
|
|
|
|
// Por ahora, simulamos que obtenemos algunos elementos
|
|
|
|
|
if (!items.isEmpty()) {
|
|
|
|
|
for (const MailItem& item : items) {
|
|
|
|
|
emit mailItemAdded(item);
|
2026-05-29 10:51:49 +02:00
|
|
|
// Publish MailItemAddedEvent
|
|
|
|
|
WinoMail::Events::MailItemAddedEvent mailEvent;
|
|
|
|
|
mailEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
|
|
|
|
|
mailEvent.timestamp = QDateTime::currentDateTimeUtc();
|
|
|
|
|
mailEvent.item = item;
|
|
|
|
|
PUBLISH(mailEvent);
|
2026-05-13 01:18:04 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-17 01:09:01 +02:00
|
|
|
emit folderSyncFinished(QString::number(folder.id()), true);
|
2026-05-29 10:51:49 +02:00
|
|
|
|
|
|
|
|
// Publish SyncFinishedEvent
|
|
|
|
|
WinoMail::Events::SyncFinishedEvent syncFinishEvent;
|
|
|
|
|
syncFinishEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
|
|
|
|
|
syncFinishEvent.timestamp = QDateTime::currentDateTimeUtc();
|
|
|
|
|
syncFinishEvent.accountId = m_account.id();
|
|
|
|
|
syncFinishEvent.folderId = QString::number(folder.id());
|
|
|
|
|
syncFinishEvent.success = true;
|
|
|
|
|
syncFinishEvent.errorMessage = "";
|
|
|
|
|
PUBLISH(syncFinishEvent);
|
|
|
|
|
|
2026-05-13 01:18:04 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVector<Folder> GmailSynchronizer::getFolders() const
|
|
|
|
|
{
|
|
|
|
|
// En una implementación real, llamaríamos a Gmail API para obtener las etiquetas (labels)
|
|
|
|
|
// Por ahora, devolvemos algunas etiquetas básicas
|
|
|
|
|
|
|
|
|
|
QVector<Folder> folders;
|
|
|
|
|
// En Gmail, las carpetas se llaman "labels"
|
|
|
|
|
folders.append(Folder(1, m_account.id(), "Inbox", "INBOX", true, false, false, false, 0, QDateTime::currentDateTime()));
|
|
|
|
|
folders.append(Folder(2, m_account.id(), "Drafts", "DRAFTS", false, true, false, false, 0, QDateTime::currentDateTime()));
|
|
|
|
|
folders.append(Folder(3, m_account.id(), "Sent Mail", "SENT", false, false, true, false, 0, QDateTime::currentDateTime()));
|
|
|
|
|
folders.append(Folder(4, m_account.id(), "Trash", "TRASH", false, false, false, true, 0, QDateTime::currentDateTime()));
|
|
|
|
|
folders.append(Folder(5, m_account.id(), "Spam", "SPAM", false, false, false, false, 0, QDateTime::currentDateTime()));
|
|
|
|
|
folders.append(Folder(6, m_account.id(), "Starred", "STARRED", false, false, false, false, 0, QDateTime::currentDateTime()));
|
|
|
|
|
folders.append(Folder(7, m_account.id(), "Important", "IMPORTANT", false, false, false, false, 0, QDateTime::currentDateTime()));
|
|
|
|
|
|
|
|
|
|
return folders;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVector<MailItem> GmailSynchronizer::fetchMailItems(const QString& folderId,
|
|
|
|
|
qint64 sinceUid)
|
|
|
|
|
{
|
|
|
|
|
Q_UNUSED(sinceUid); // En Gmail usamos history IDs en lugar de UIDs
|
|
|
|
|
|
|
|
|
|
if (!m_account.isTokenValid()) {
|
|
|
|
|
if (!refreshAccessToken()) {
|
|
|
|
|
qWarning() << "Failed to refresh token for fetching mail items";
|
|
|
|
|
return QVector<MailItem>();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
qDebug() << "Fetching mail items for folder (label):" << folderId;
|
|
|
|
|
|
|
|
|
|
// Construir la URL para Gmail API
|
|
|
|
|
// Nota: En Gmail, las carpetas se identifican por su nombre de label (ej: INBOX, DRAFTS, etc.)
|
2026-05-17 01:09:01 +02:00
|
|
|
QString endpoint = QString("/me/mailFolders/%1/messages").arg(folderId);
|
2026-05-13 01:18:04 +02:00
|
|
|
QString url = buildGmailUrl(endpoint);
|
|
|
|
|
|
|
|
|
|
// Parámetros de consulta
|
|
|
|
|
QUrlQuery query;
|
|
|
|
|
query.addQueryItem("maxResults", "50"); // Limitar a 50 mensajes por petición
|
|
|
|
|
query.addQueryItem("q", "in:" + folderId); // Filtrar por label
|
|
|
|
|
url += "?" + query.toString();
|
|
|
|
|
|
|
|
|
|
QNetworkRequest request = createAuthRequest(url);
|
|
|
|
|
QNetworkReply* reply = m_networkManager->get(request);
|
|
|
|
|
|
|
|
|
|
// Nota: En una implementación real, esperaríamos la respuesta asíncronamente
|
|
|
|
|
// pero por simplicidad en este stub, simulamos una respuesta
|
|
|
|
|
|
|
|
|
|
// Simular respuesta para desarrollo
|
|
|
|
|
QVector<MailItem> items;
|
|
|
|
|
items.append(MailItem(1, folderId.toInt(), "Actualización de proyecto",
|
|
|
|
|
"carlos.rodriguez@proveedor.com", m_account.email(),
|
|
|
|
|
QDateTime::currentDateTime().addSecs(-1800),
|
|
|
|
|
false, false));
|
|
|
|
|
items.append(MailItem(2, folderId.toInt(), "Factura adjunta",
|
|
|
|
|
"facturacion@servicios.com", m_account.email(),
|
|
|
|
|
QDateTime::currentDateTime().addSecs(-5400),
|
|
|
|
|
true, true)); // Marcada como importante y leída
|
|
|
|
|
|
|
|
|
|
return items;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool GmailSynchronizer::appendMailItem(const QString& folderId, const MailItem& item)
|
|
|
|
|
{
|
|
|
|
|
if (!m_account.isTokenValid()) {
|
|
|
|
|
if (!refreshAccessToken()) {
|
|
|
|
|
qWarning() << "Failed to refresh token for appending mail item";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
qDebug() << "Appending mail item to folder (label):" << folderId;
|
|
|
|
|
|
|
|
|
|
// En una implementación real, llamaríamos a Gmail API
|
|
|
|
|
// para crear un mensaje en la etiqueta especificada
|
|
|
|
|
|
|
|
|
|
// Por ahora, simulamos éxito
|
|
|
|
|
emit mailItemAdded(item);
|
2026-05-29 10:51:49 +02:00
|
|
|
|
|
|
|
|
// Publish MailItemAddedEvent
|
|
|
|
|
WinoMail::Events::MailItemAddedEvent mailEvent;
|
|
|
|
|
mailEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
|
|
|
|
|
mailEvent.timestamp = QDateTime::currentDateTimeUtc();
|
|
|
|
|
mailEvent.item = item;
|
|
|
|
|
PUBLISH(mailEvent);
|
|
|
|
|
|
2026-05-13 01:18:04 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool GmailSynchronizer::updateMailItemFlags(const QString& folderId,
|
|
|
|
|
const QString& itemUid,
|
|
|
|
|
bool read, bool flagged)
|
|
|
|
|
{
|
|
|
|
|
if (!m_account.isTokenValid()) {
|
|
|
|
|
if (!refreshAccessToken()) {
|
|
|
|
|
qWarning() << "Failed to refresh token for updating mail item flags";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
qDebug() << "Updating mail item flags:" << itemUid
|
|
|
|
|
<< "read:" << read << "flagged:" << flagged;
|
|
|
|
|
|
|
|
|
|
// En una implementación real, llamaríamos a Gmail API
|
|
|
|
|
// para actualizar las etiquetas del mensaje (leído, importante, etc.)
|
|
|
|
|
|
|
|
|
|
// Por ahora, simulamos éxito
|
|
|
|
|
MailItem updatedItem;
|
|
|
|
|
updatedItem.setId(itemUid.toLongLong());
|
|
|
|
|
updatedItem.setFolderId(folderId.toInt());
|
|
|
|
|
updatedItem.setRead(read);
|
|
|
|
|
updatedItem.setFlagged(flagged);
|
|
|
|
|
emit mailItemUpdated(updatedItem);
|
|
|
|
|
|
2026-05-29 10:51:49 +02:00
|
|
|
// Publish MailItemUpdatedEvent
|
|
|
|
|
WinoMail::Events::MailItemUpdatedEvent updateEvent;
|
|
|
|
|
updateEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
|
|
|
|
|
updateEvent.timestamp = QDateTime::currentDateTimeUtc();
|
|
|
|
|
updateEvent.item = updatedItem;
|
|
|
|
|
updateEvent.changedFields = QStringList() << "read" << "flagged"; // Simplified
|
|
|
|
|
PUBLISH(updateEvent);
|
|
|
|
|
|
2026-05-13 01:18:04 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool GmailSynchronizer::deleteMailItem(const QString& folderId,
|
|
|
|
|
const QString& itemUid)
|
|
|
|
|
{
|
|
|
|
|
if (!m_account.isTokenValid()) {
|
|
|
|
|
if (!refreshAccessToken()) {
|
|
|
|
|
qWarning() << "Failed to refresh token for deleting mail item";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
qDebug() << "Deleting mail item:" << itemUid << "from folder (label):" << folderId;
|
|
|
|
|
|
|
|
|
|
// En una implementación real, llamaríamos a Gmail API
|
|
|
|
|
// para eliminar el mensaje (moverlo a la papelera)
|
|
|
|
|
|
|
|
|
|
// Por ahora, simulamos éxito
|
|
|
|
|
emit mailItemRemoved(itemUid);
|
2026-05-29 10:51:49 +02:00
|
|
|
|
|
|
|
|
// Publish MailItemRemovedEvent
|
|
|
|
|
WinoMail::Events::MailItemRemovedEvent removeEvent;
|
|
|
|
|
removeEvent.eventId = QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" + QString::number(rand());
|
|
|
|
|
removeEvent.timestamp = QDateTime::currentDateTimeUtc();
|
|
|
|
|
removeEvent.itemUid = itemUid;
|
|
|
|
|
removeEvent.folderId = folderId.toInt();
|
|
|
|
|
PUBLISH(removeEvent);
|
|
|
|
|
|
2026-05-13 01:18:04 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GmailSynchronizer::onGmailReplyFinished(QNetworkReply* reply)
|
|
|
|
|
{
|
|
|
|
|
if (reply->error() != QNetworkReply::NoError) {
|
|
|
|
|
qWarning() << "Gmail API error:" << reply->errorString();
|
|
|
|
|
emit errorOccurred(reply->errorString());
|
|
|
|
|
reply->deleteLater();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray responseData = reply->readAll();
|
|
|
|
|
reply->deleteLater();
|
|
|
|
|
|
|
|
|
|
QJsonParseError parseError;
|
|
|
|
|
QJsonDocument doc = QJsonDocument::fromJson(responseData, &parseError);
|
|
|
|
|
|
|
|
|
|
if (parseError.error != QJsonParseError::NoError) {
|
|
|
|
|
qWarning() << "Failed to parse Gmail API response:" << parseError.errorString();
|
|
|
|
|
emit errorOccurred("Failed to parse server response");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!doc.isObject()) {
|
|
|
|
|
qWarning() << "Gmail API response is not an object";
|
|
|
|
|
emit errorOccurred("Invalid server response");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QJsonObject obj = doc.object();
|
|
|
|
|
|
|
|
|
|
// Procesar según el tipo de llamada
|
|
|
|
|
// En una implementación completa, aquí manejaríamos las respuestas
|
|
|
|
|
// para obtener etiquetas, mensajes, etc.
|
|
|
|
|
|
|
|
|
|
qDebug() << "Gmail API response received successfully";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GmailSynchronizer::onHistoryTimer()
|
|
|
|
|
{
|
|
|
|
|
qDebug() << "History timer fired - checking for changes";
|
|
|
|
|
|
|
|
|
|
// En una implementación real, aquí usaríamos los history IDs
|
|
|
|
|
// obtenidos previamente para consultar solo los cambios
|
|
|
|
|
// desde la última sincronización
|
|
|
|
|
|
|
|
|
|
// Por ahora, simplemente iniciamos una sincronización completa
|
|
|
|
|
// de todas las etiquetas (esto sería ineficiente en producción)
|
|
|
|
|
QVector<Folder> folders = getFolders();
|
|
|
|
|
for (const Folder& folder : folders) {
|
2026-05-17 01:09:01 +02:00
|
|
|
syncFolder(Folder(folder.id(), 0, QString()));
|
2026-05-13 01:18:04 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString GmailSynchronizer::buildGmailUrl(const QString& endpoint) const
|
|
|
|
|
{
|
|
|
|
|
return QStringLiteral("https://gmail.googleapis.com/gmail/v1/users/%1/%2")
|
|
|
|
|
.arg(m_account.email())
|
|
|
|
|
.arg(endpoint);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QNetworkRequest GmailSynchronizer::createAuthRequest(const QString& url) const
|
|
|
|
|
{
|
2026-05-17 01:09:01 +02:00
|
|
|
QNetworkRequest request{QUrl(url)};
|
2026-05-13 01:18:04 +02:00
|
|
|
request.setRawHeader("Authorization",
|
|
|
|
|
QString("Bearer %1").arg(m_accessToken).toUtf8());
|
|
|
|
|
request.setHeader(QNetworkRequest::ContentTypeHeader,
|
|
|
|
|
"application/json");
|
|
|
|
|
return request;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool GmailSynchronizer::refreshAccessToken()
|
|
|
|
|
{
|
|
|
|
|
// En una implementación real, aquí usaríamos el refresh token
|
|
|
|
|
// para obtener un nuevo access token desde Google OAuth2
|
|
|
|
|
|
|
|
|
|
qWarning() << "Token refresh not implemented - using stub";
|
|
|
|
|
// Simulamos éxito por ahora
|
|
|
|
|
return !m_refreshToken.isEmpty();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
qint64 GmailSynchronizer::getHistoryId(const QString& folderId) const
|
|
|
|
|
{
|
|
|
|
|
return m_historyIds.value(folderId, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GmailSynchronizer::setHistoryId(const QString& folderId, qint64 historyId)
|
|
|
|
|
{
|
|
|
|
|
m_historyIds.insert(folderId, historyId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MailItem GmailSynchronizer::parseMailItemFromGmail(const QJsonObject& mailJson) const
|
|
|
|
|
{
|
|
|
|
|
// Esta función convertiría un objeto JSON de Gmail API
|
|
|
|
|
// a nuestro objeto MailItem
|
|
|
|
|
|
|
|
|
|
// Por ahora, devolvemos un objeto vacío como stub
|
|
|
|
|
return MailItem();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Folder GmailSynchronizer::parseFolderFromGmail(const QJsonObject& labelJson) const
|
|
|
|
|
{
|
|
|
|
|
// Esta función convertiría un objeto JSON de Gmail API (label)
|
|
|
|
|
// a nuestro objeto Folder
|
|
|
|
|
|
|
|
|
|
// Por ahora, devolvemos un objeto vacío como stub
|
|
|
|
|
return Folder();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QStringList GmailSynchronizer::extractHeaderValue(const QJsonArray& headers, const QString& name) const
|
|
|
|
|
{
|
|
|
|
|
QStringList values;
|
|
|
|
|
for (const QJsonValue& headerVal : headers) {
|
|
|
|
|
if (headerVal.isObject()) {
|
|
|
|
|
QJsonObject header = headerVal.toObject();
|
|
|
|
|
if (header.contains("name") && header["name"].toString().compare(name, Qt::CaseInsensitive) == 0) {
|
|
|
|
|
values.append(header["value"].toString());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return values;
|
|
|
|
|
}
|