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:
+206
-42
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user