feat: IMAP sync incremental + Account Setup Wizard + robust FETCH parser

- ImapSynchronizer::syncFolder(): detecta eliminados (UIDs locales no en servidor), fetch nuevos (sinceUid), actualiza flags (FLAGS batch)
- fetchAllUids(): SELECT + SEARCH ALL para lista completa UIDs servidor
- parseAndUpdateFlags(): FETCH FLAGS en lotes 100, update DB si cambió read/flagged
- AccountSetupDialog: integra ConnectionWizard para IMAP/POP3 con test de conexión real
- IMAP FETCH parser robusto: logging respuesta servidor + fallback UID-by-UID
- Fix: FETCH failed logging muestra first/last UID + respuesta truncada
This commit is contained in:
2026-08-17 15:34:49 +02:00
parent 364624aada
commit a004bdd60f
94 changed files with 19408 additions and 1047 deletions
+206 -42
View File
@@ -15,8 +15,11 @@
MainMainWindow::MainMainWindow(QWidget *parent)
: QMainWindow(parent), m_currentFolderId(-1), m_currentMailId(-1)
{
qDebug() << "[MainMainWindow] Constructor start";
setupUI();
qDebug() << "[MainMainWindow] setupUI done";
connectModels();
qDebug() << "[MainMainWindow] connectModels done";
setWindowTitle("Wino Mail DTK");
resize(1280, 820);
// Setup progress bar in status bar
@@ -27,12 +30,20 @@ MainMainWindow::MainMainWindow(QWidget *parent)
// Connect mail service progress/status signals
connect(m_mailService, &MailService::progressChanged, this, &MainMainWindow::onProgressChanged);
connect(m_mailService, &MailService::statusMessage, this, &MainMainWindow::onStatusMessage);
connect(m_mailService, &MailService::mailSent, this, [this](const QString &) {
statusBar()->showMessage(tr("Message sent"), 5000);
});
connect(m_mailService, &MailService::mailSendFailed, this, [this](const QString &, const QString &error) {
statusBar()->showMessage(tr("Send failed: %1").arg(error), 8000);
});
// Provide account service to settings view
m_settingsView->setAccountService(m_accountService);
qDebug() << "[MainMainWindow] Constructor complete";
}
void MainMainWindow::setupUI()
{
qDebug() << "[MainMainWindow::setupUI] Start";
// === Global stylesheet ===
this->setStyleSheet(
"QMainWindow { background-color: #f5f5f7; }"
@@ -40,8 +51,10 @@ void MainMainWindow::setupUI()
"QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
"QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }"
);
qDebug() << "[MainMainWindow::setupUI] Stylesheet set";
createToolBar();
qDebug() << "[MainMainWindow::setupUI] Toolbar created";
// === Central widget ===
QWidget *central = new QWidget();
@@ -51,6 +64,7 @@ void MainMainWindow::setupUI()
// === Sidebar ===
setupSidebar();
qDebug() << "[MainMainWindow::setupUI] Sidebar created";
centralLayout->addWidget(m_sidebar);
// === Separator line ===
@@ -62,23 +76,34 @@ void MainMainWindow::setupUI()
// === Stacked pages ===
m_stack = new QStackedWidget();
m_stack->setStyleSheet("background-color: #f5f5f7;");
qDebug() << "[MainMainWindow::setupUI] Stack created";
// Page 0: Mail (folder tree + mail list + reader)
setupMailPage();
qDebug() << "[MainMainWindow::setupUI] Mail page created";
m_stack->addWidget(m_mailPage);
// Page 1: Compose
m_composeView = new ComposeView();
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime, const QString &fromAddr) {
QString msg;
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime, const QString &fromAddr, const QStringList &attachmentPaths) {
if (scheduleTime.isValid()) {
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
} else {
msg = "Message sent (simulated)";
statusBar()->showMessage(tr("Scheduled sending is not available yet"), 5000);
return;
}
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
statusBar()->showMessage(msg, 5000);
if (fromAddr.isEmpty()) {
statusBar()->showMessage(tr("Select an account before sending"), 5000);
return;
}
MailItem mail;
mail.setTo(to);
mail.setRecipient(to);
mail.setCc(cc);
mail.setBcc(bcc);
mail.setSubject(subject);
mail.setBodyHtml(body);
mail.setDate(QDateTime::currentDateTimeUtc());
m_mailService->sendMail(mail, fromAddr, attachmentPaths);
statusBar()->showMessage(tr("Sending message…"), 5000);
switchToPage(PageMail);
});
connect(m_composeView, &ComposeView::discardRequested, [this]() {
@@ -120,6 +145,7 @@ void MainMainWindow::setupUI()
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
});
m_stack->addWidget(m_settingsView);
qDebug() << "[MainMainWindow::setupUI] Settings page created";
// Page 3: Contacts
m_contactsView = new ContactsView();
@@ -131,9 +157,11 @@ void MainMainWindow::setupUI()
centralLayout->addWidget(m_stack, 1);
setCentralWidget(central);
qDebug() << "[MainMainWindow::setupUI] Central widget set";
// Show mail page by default
switchToPage(PageMail);
qDebug() << "[MainMainWindow::setupUI] Done";
}
void MainMainWindow::setupSidebar()
@@ -161,8 +189,7 @@ void MainMainWindow::setupSidebar()
}
void MainMainWindow::setupMailPage()
{
m_mailPage = new QWidget();
{ m_mailPage = new QWidget();
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
mailLayout->setContentsMargins(0, 0, 0, 0);
mailLayout->setSpacing(0);
@@ -189,7 +216,30 @@ void MainMainWindow::setupMailPage()
});
m_folderSplitter->addWidget(m_mailListView);
// Reader
// Viewer stack: placeholder, reader, compose
m_viewerStack = new QStackedWidget();
// Placeholder widget
m_placeholderWidget = new QWidget();
QVBoxLayout *placeholderLayout = new QVBoxLayout(m_placeholderWidget);
placeholderLayout->setAlignment(Qt::AlignCenter);
QLabel *iconLabel = new QLabel();
QPixmap pixmap = QIcon::fromTheme("mail-unread").pixmap(48, 48);
if (pixmap.isNull()) {
pixmap = QPixmap(48, 48);
pixmap.fill(Qt::gray);
}
iconLabel->setPixmap(pixmap);
iconLabel->setAlignment(Qt::AlignCenter);
QLabel *textLabel = new QLabel(tr("Seleccione un correo para leerlo"));
textLabel->setAlignment(Qt::AlignCenter);
textLabel->setStyleSheet("color: #666; font-size: 14px;");
placeholderLayout->addStretch();
placeholderLayout->addWidget(iconLabel);
placeholderLayout->addWidget(textLabel);
placeholderLayout->addStretch();
m_placeholderWidget->setLayout(placeholderLayout);
// Reader view
m_emailViewer = new ReaderView();
m_emailViewer->setMinimumWidth(350);
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
@@ -198,9 +248,22 @@ void MainMainWindow::setupMailPage()
openMailInIndependentWindow(m_currentMailId);
}
});
m_folderSplitter->addWidget(m_emailViewer);
// Default sizes: folder 240, list 380, reader flex
// Embedded compose view
m_embeddedComposeView = new ComposeView();
connect(m_embeddedComposeView, &ComposeView::sendRequested, this, &MainMainWindow::onEmbeddedSendRequested);
connect(m_embeddedComposeView, &ComposeView::discardRequested, this, &MainMainWindow::onEmbeddedDiscardRequested);
connect(m_embeddedComposeView, &ComposeView::detachRequested, this, &MainMainWindow::onEmbeddedDetachRequested);
// Add to stack
m_viewerStack->addWidget(m_placeholderWidget);
m_viewerStack->addWidget(m_emailViewer);
m_viewerStack->addWidget(m_embeddedComposeView);
m_viewerStack->setCurrentIndex(0); // show placeholder by default
m_folderSplitter->addWidget(m_viewerStack);
// Default sizes: folder 240, list 380, viewer flex
m_folderSplitter->setSizes({240, 380, 600});
mailLayout->addWidget(m_folderSplitter);
@@ -208,12 +271,18 @@ void MainMainWindow::setupMailPage()
void MainMainWindow::connectModels()
{
qDebug() << "[MainMainWindow::connectModels] Start";
m_accountService = new AccountService(this);
qDebug() << "[MainMainWindow::connectModels] AccountService created";
m_mailService = new MailService(m_accountService, this);
qDebug() << "[MainMainWindow::connectModels] MailService created";
m_composeView->setAccountService(m_accountService);
m_embeddedComposeView->setAccountService(m_accountService);
m_folderModel = new FolderListModel(m_accountService, this);
qDebug() << "[MainMainWindow::connectModels] FolderListModel created";
m_emailModel = new EmailListModel(this);
qDebug() << "[MainMainWindow::connectModels] EmailListModel created";
m_folderTree->setModel(m_folderModel);
m_mailListView->setModel(m_emailModel);
@@ -221,6 +290,18 @@ void MainMainWindow::connectModels()
m_folderTree->expandAll();
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
connect(m_mailService, &MailService::mailFetched, this,
[this](const QString &, const QString &folderId, const QVector<MailItem> &) {
if (folderId.toInt() == m_currentFolderId) {
m_emailModel->refresh();
statusBar()->showMessage(tr("Mail synchronized"), 3000);
}
});
connect(m_mailService, &MailService::mailFetchError, this,
[this](const QString &, const QString &, const QString &error) {
statusBar()->showMessage(tr("Synchronization failed: %1").arg(error), 8000);
});
qDebug() << "[MainMainWindow::connectModels] Done";
}
void MainMainWindow::onNavChanged(int index)
@@ -239,8 +320,7 @@ void MainMainWindow::switchToPage(int pageIndex)
}
void MainMainWindow::onFolderSelected(const QModelIndex &index)
{
if (!index.isValid()) return;
{ if (!index.isValid()) return;
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
if (itemType == FolderTreeItem::FolderNode) {
@@ -257,43 +337,55 @@ void MainMainWindow::onFolderSelected(const QModelIndex &index)
}
}
m_emailModel->refresh();
// Clear selection and show placeholder
m_currentMailId = -1;
m_mailListView->tableView()->clearSelection();
m_viewerStack->setCurrentIndex(0); // placeholder
}
}
void MainMainWindow::onEmailSelected(int mailId)
{
m_currentMailId = mailId;
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
{ m_currentMailId = mailId;
if (mailId >= 0) {
std::optional<MailItem> item = MailItemDao::findById(mailId);
if (!item.has_value()) {
m_emailViewer->setMailItem(nullptr);
m_viewerStack->setCurrentIndex(0); // placeholder
return;
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
m_viewerStack->setCurrentIndex(1); // reader
} else {
m_emailViewer->setMailItem(nullptr);
return;
m_viewerStack->setCurrentIndex(0); // placeholder
}
MailItem &mail = item.value();
if (!mail.isRead()) {
mail.setRead(true);
MailItemDao::update(mail);
}
m_emailViewer->setMailItem(&mail);
}
void MainMainWindow::onComposeRequested()
{
switchToPage(PageCompose);
{ m_embeddedComposeView->initializeComposition();
m_viewerStack->setCurrentIndex(2); // compose
}
void MainMainWindow::onReaderReplyRequested(const MailItem *item)
{
if (item) {
m_composeView->setTo(item->sender());
m_composeView->setSubject("Re: " + item->subject());
{ if (item) {
m_embeddedComposeView->initializeComposition();
m_embeddedComposeView->setTo(item->sender());
m_embeddedComposeView->setSubject(QString("Re: %1").arg(item->subject()));
// Optionally set body with quoted text? We'll leave empty for now.
m_embeddedComposeView->setBody(QString()); // clear
}
switchToPage(PageCompose);
m_viewerStack->setCurrentIndex(2); // compose
}
void MainMainWindow::onNewMessage()
{
switchToPage(PageCompose);
{ // Show compose view in the viewer stack
m_embeddedComposeView->initializeComposition();
m_viewerStack->setCurrentIndex(2); // compose
}
void MainMainWindow::openMailInIndependentWindow(int mailId)
@@ -329,8 +421,11 @@ void MainMainWindow::createToolBar()
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
connect(syncAction, &QAction::triggered, [this]() {
if (m_currentFolderId >= 0) {
m_emailModel->refresh();
statusBar()->showMessage("Refreshed", 2000);
const auto folder = FolderDao::findById(m_currentFolderId);
if (folder) {
m_mailService->fetchMails(QString::number(folder->accountId()), QString::number(m_currentFolderId));
statusBar()->showMessage(tr("Synchronizing…"), 3000);
}
}
});
connect(openWinAction, &QAction::triggered, [this]() {
@@ -341,7 +436,12 @@ void MainMainWindow::createToolBar()
}
});
connect(deleteAction, &QAction::triggered, [this]() {
statusBar()->showMessage("Delete would be implemented here", 3000);
if (m_currentMailId >= 0) {
m_mailService->deleteMail(QString::number(m_currentMailId));
m_currentMailId = -1;
m_emailModel->refresh();
m_viewerStack->setCurrentIndex(0);
}
});
}
@@ -385,6 +485,70 @@ void MainMainWindow::onAccountEditRequested(int accountId)
void MainMainWindow::onAccountDeleteRequested(int accountId)
{
Q_UNUSED(accountId);
statusBar()->showMessage(QString("Delete account %1 requested").arg(accountId), 3000);
}
m_accountService->removeAccount(accountId);
m_folderModel->refresh();
m_emailModel->setFolderId(-1);
m_emailModel->refresh();
m_currentFolderId = -1;
m_currentMailId = -1;
m_viewerStack->setCurrentIndex(0);
statusBar()->showMessage(tr("Account removed"), 3000);
}
void MainMainWindow::onEmbeddedSendRequested(const QString &to, const QString &cc, const QString &bcc,
const QString &subject, const QString &body,
const QDateTime &scheduleTime,
const QString &fromAddress,
const QStringList &attachmentPaths)
{
if (scheduleTime.isValid()) {
statusBar()->showMessage(tr("Scheduled sending is not available yet"), 5000);
return;
}
if (fromAddress.isEmpty()) {
statusBar()->showMessage(tr("Select an account before sending"), 5000);
return;
}
MailItem mail;
mail.setTo(to);
mail.setRecipient(to);
mail.setCc(cc);
mail.setBcc(bcc);
mail.setSubject(subject);
mail.setBodyHtml(body);
mail.setDate(QDateTime::currentDateTimeUtc());
m_mailService->sendMail(mail, fromAddress, attachmentPaths);
statusBar()->showMessage(tr("Sending message…"), 5000);
m_viewerStack->setCurrentIndex(0); // placeholder
m_embeddedComposeView->initializeComposition();
}
void MainMainWindow::onEmbeddedDiscardRequested()
{
// Go back to placeholder
m_viewerStack->setCurrentIndex(0); // placeholder
m_embeddedComposeView->initializeComposition();
}
void MainMainWindow::onEmbeddedDetachRequested(QWidget *widget)
{
// Detach the compose view to a standalone window (similar to main compose view's detach)
QStackedWidget *stack = qobject_cast<QStackedWidget*>(widget->parentWidget());
if (stack) {
stack->removeWidget(widget);
}
// Create standalone window
QMainWindow *detachedWin = new QMainWindow();
detachedWin->setWindowTitle(tr("Compose - Wino Mail"));
// Assign central widget and ensure it's visible and sized
detachedWin->setCentralWidget(widget);
widget->setMinimumSize(800, 600);
widget->update();
detachedWin->resize(800, 600);
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
detachedWin->show();
statusBar()->showMessage(tr("Compose view detached to separate window"), 3000);
}