Files
wino-mail-dtkqt/src/ui/composeview.cpp
T
javier a004bdd60f 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
2026-08-17 15:34:49 +02:00

537 lines
22 KiB
C++

// ===================== ComposeView =====================
#include "composeview.h"
#include <QDialog>
#include <QInputDialog>
#include <QVBoxLayout>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QFileInfo>
#include <QMessageBox>
#include <QLabel>
#include <QPushButton>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QPixmap>
#include <QStyle>
#include <QApplication>
#include <QFileIconProvider>
ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
setupUI();
// Connect the rich text editor's signature signal to our slot
connect(m_bodyEditor, &RichTextEditor::signatureClicked, this, &ComposeView::onSignatureClicked);
connect(m_bodyEditor, &RichTextEditor::signatureEditRequested, this, &ComposeView::onSignatureEditRequested);
}
ComposeView::~ComposeView() {
// Destructor implementation
}
void ComposeView::onSignatureClicked() {
// This slot is called when the signature button in the rich text editor is clicked
// The actual menu handling is done in RichTextEditor, so we just need to
// handle any ComposeView-specific logic here if needed
}
void ComposeView::onSignatureEditRequested() {
QDialog dialog(this);
dialog.setWindowTitle(tr("Edit Signature"));
dialog.setMinimumSize(600, 400);
QVBoxLayout *layout = new QVBoxLayout(&dialog);
RichTextEditor *editor = new RichTextEditor(&dialog);
editor->setupToolbar(layout);
layout->addWidget(editor);
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog);
layout->addWidget(buttonBox);
connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
if (dialog.exec() == QDialog::Accepted) {
QString signatureHtml = editor->toHtml();
if (!signatureHtml.isEmpty()) {
QTextCursor cursor = m_bodyEditor->textCursor();
cursor.insertHtml(signatureHtml);
}
}
}
void ComposeView::setAccountService(AccountService *service) {
m_accountService = service;
populateAccountCombo();
}
void ComposeView::populateAccountCombo() {
if (!m_accountService) {
qWarning() << "AccountService not set";
return;
}
m_accountCombo->clear();
m_accountCombo->addItem(tr("Select Account..."), QVariant());
QVector<Account> accounts = m_accountService->getAllAccounts();
for (const Account &account : accounts) {
m_accountCombo->addItem(account.email(), account.id());
}
// Select the first account by default if there are accounts
if (accounts.size() > 0) {
m_accountCombo->setCurrentIndex(1); // Skip the placeholder item
m_currentAccountId = accounts.first().id();
}
}
void ComposeView::onAccountChanged(int index) {
if (index <= 0) {
// Placeholder item selected or invalid index
m_currentAccountId = -1;
return;
}
m_currentAccountId = m_accountCombo->itemData(index).toInt();
loadSignatureForCurrentAccount();
}
void ComposeView::loadSignatureForCurrentAccount() {
if (!m_accountService || m_currentAccountId <= 0) {
// Clear signature if no account selected
m_bodyEditor->clear();
return;
}
Account* account = m_accountService->findAccountById(m_currentAccountId);
if (account) {
// For now, we'll just clear the editor since we don't have a signature field in Account yet
// In a real implementation, we would retrieve and display the signature
m_bodyEditor->clear();
delete account;
} else {
m_bodyEditor->clear();
}
}
void ComposeView::setupUI() {
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(20, 15, 20, 15);
mainLayout->setSpacing(8);
this->setStyleSheet(
"QLineEdit, QTextEdit { border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
"QLineEdit:focus, QTextEdit:focus { border-color: #1976D2; }"
);
// De: field + Account selector (renamed from From:)
QHBoxLayout *fromLayout = new QHBoxLayout();
QLabel *fromLabel = new QLabel(tr("De:"));
fromLabel->setFixedWidth(40);
fromLabel->setStyleSheet("font-weight: bold; color: #555;");
m_accountCombo = new QComboBox();
m_accountCombo->setFixedWidth(180);
m_accountCombo->setStyleSheet(
"QComboBox { border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 8px; "
"background: white; min-height: 20px; }"
"QComboBox:hover { border-color: #bdbdbd; }"
"QComboBox:focus { border-color: #1976D2; }"
"QComboBox::drop-down { border: none; width: 20px; }"
"QComboBox::down-arrow { image: url(:/icons/dropdown.png); width: 10px; height: 10px; }"
);
connect(m_accountCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ComposeView::onAccountChanged);
fromLayout->addWidget(fromLabel);
fromLayout->addWidget(m_accountCombo);
fromLayout->addStretch(); // Este resorte empuja el par (label+combo) a la izquierda
mainLayout->addLayout(fromLayout);
// Para: field + Cc/Bcc toggle buttons
QHBoxLayout *toLayout = new QHBoxLayout();
QLabel *toLabel = new QLabel(tr("Para:"));
toLabel->setFixedWidth(40);
toLabel->setStyleSheet("font-weight: bold; color: #555;");
m_toField = new everload_tags::TagsLineEdit();
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(tr("Cc:"));
ccLabel->setFixedWidth(40);
ccLabel->setStyleSheet("color: #555;");
m_ccField = new everload_tags::TagsLineEdit();
ccLayout->addWidget(ccLabel);
ccLayout->addWidget(m_ccField, 1);
m_hideCcButton = new QPushButton("");
m_hideCcButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Linear/Essentional, UI/Close Square.svg")));
m_hideCcButton->setFixedSize(20, 20);
m_hideCcButton->setToolTip(tr("Hide Cc"));
m_hideCcButton->setStyleSheet(
"QPushButton { background: transparent; border: none; color: blue; 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(tr("Bcc:"));
bccLabel->setFixedWidth(40);
bccLabel->setStyleSheet("color: #555;");
m_bccField = new everload_tags::TagsLineEdit();
bccLayout->addWidget(bccLabel);
bccLayout->addWidget(m_bccField, 1);
m_hideBccButton = new QPushButton("");
m_hideBccButton->setFixedSize(20, 20);
m_hideBccButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Linear/Essentional, UI/Close Square.svg")));
m_hideBccButton->setToolTip(tr("Hide Bcc"));
m_hideBccButton->setStyleSheet(
"QPushButton { background: transparent; border: none; color: blue; 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);
// Asunto: field
QHBoxLayout *subjectLayout = new QHBoxLayout();
QLabel *subjectLabel = new QLabel(tr("Asunto:"));
subjectLabel->setFixedWidth(60);
subjectLabel->setStyleSheet("font-weight: bold; color: #555;");
m_subjectField = new QLineEdit();
m_subjectField->setPlaceholderText(tr("Subject"));
m_subjectField->setFixedHeight(40);
QFont subjectFont = m_subjectField->font();
subjectFont.setPointSize(14);
subjectFont.setBold(true);
m_subjectField->setFont(subjectFont);
//subjectLayout->addWidget(subjectLabel);
subjectLayout->addWidget(m_subjectField, 1);
// Detach button placed to the right of the subject line
m_detachButton = new QPushButton("");//("↥ Detach");
m_detachButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Linear/Arrows Action/Square Top Down.svg")));
m_detachButton->setToolTip(tr("Open compose window in a separate window"));
m_detachButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 12px; color: #555; font-size: 12px; }"
"QPushButton:hover { background: #f0f0f0; }"
);
connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked);
subjectLayout->addWidget(m_detachButton);
mainLayout->addLayout(subjectLayout);
// Separator line admin@email.com
QFrame *line1 = new QFrame();
line1->setFrameShape(QFrame::HLine);
line1->setStyleSheet("color: #e0e0e0;");
mainLayout->addWidget(line1);
// Rich text editor with toolbar
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(tr("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(tr("Schedule Send"));
m_scheduleSendButton->setFixedWidth(150);
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, 2);
m_schedulePanel->setVisible(false);
m_schedulePanel->setVisible(false);
mainLayout->addWidget(m_schedulePanel);
// Attachment section
QHBoxLayout *attachmentLayout = new QHBoxLayout();
m_attachmentList = new QListWidget();
m_attachmentList->setViewMode(QListView::IconMode);
m_attachmentList->setResizeMode(QListView::Adjust);
m_attachmentList->setMovement(QListView::Static);
m_attachmentList->setGridSize(QSize(300, 50));
m_attachmentList->setSpacing(10);
m_attachmentList->setLayoutDirection(Qt::LeftToRight);
m_attachmentList->setSelectionMode(QAbstractItemView::SingleSelection);
m_attachmentList->setStyleSheet(
"QListWidget { border: 1px solid #d1d1d6; border-radius: 4px; }"
"QListWidget::item { border: none; padding: 5px; }"
"QListWidget::item:selected { background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #1976D2, stop:1 #1565C0); border-radius: 4px; }"
);
m_attachmentList->setVisible(false);
attachmentLayout->addWidget(m_attachmentList, 1);
mainLayout->addLayout(attachmentLayout);
// Action buttons row
QHBoxLayout *actionsLayout = new QHBoxLayout();
// Attachments and Templates buttons (right-aligned)
QHBoxLayout *buttonRow = new QHBoxLayout();
m_attachButton = new QToolButton();
m_attachButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Outline/Messages, Conversation/Paperclip.svg")));
m_attachButton->setToolTip(tr("Add attachment"));
m_attachButton->setIconSize(QSize(20,20));
m_attachButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }");
connect(m_attachButton, &QToolButton::clicked, this, &ComposeView::onAddAttachmentClicked);
buttonRow->addWidget(m_attachButton);
m_templateButton = new QToolButton();
m_templateButton->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Outline/Files/File Text.svg"))); // Assuming you have a template icon
m_templateButton->setToolTip(tr("Email templates"));
m_templateButton->setIconSize(QSize(20,20));
m_templateButton->setStyleSheet("QToolButton { border: none; padding: 5px; } QToolButton:hover { background: #e0e0e0; border-radius: 3px; }");
connect(m_templateButton, &QToolButton::clicked, this, &ComposeView::onTemplateClicked);
buttonRow->addWidget(m_templateButton);
buttonRow->setSpacing(4);
actionsLayout->addLayout(buttonRow);
actionsLayout->addStretch(); // Este resorte empuja el par (label+combo) a la izquierda
m_discardButton = new QPushButton(tr("Discard"));
m_discardButton->setStyleSheet(
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px 20px; color: #555; }"
"QPushButton:hover { background: #f5f5f5; }"
);
connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested);
actionsLayout->addWidget(m_discardButton);
// Split button for Send / Schedule
m_sendMenu = new QMenu(this);
m_sendNowAction = m_sendMenu->addAction(tr("Send Now"));
connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked);
m_scheduleAction = m_sendMenu->addAction(tr("Schedule for later..."));
connect(m_scheduleAction, &QAction::triggered, [this]() {
m_schedulePanel->setVisible(true);
});
m_sendSplit = new QToolButton();
m_sendSplit->setText(tr("Send"));
m_sendSplit->setIcon(QIcon(QStringLiteral(":/icons/resources/icons/SVG/Bold Duotone/Messages, Conversation/Plain.svg")));
m_templateButton->setIconSize(QSize(20,20));
//m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup);
m_sendSplit->setMenu(m_sendMenu);
m_sendSplit->setStyleSheet(
"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 8px 24px; font-weight: bold; }"
"QToolButton:hover { background-color: #1565C0; }"
"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; width: 20px;}"
"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->tags2().join(", "),
m_ccVisible ? m_ccField->tags2().join(", ") : QString(),
m_bccVisible ? m_bccField->tags2().join(", ") : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
QDateTime(), // null = send now
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString(),
m_attachmentFiles
);
}
void ComposeView::onScheduleClicked() {
emit sendRequested(
m_toField->tags2().join(", "),
m_ccVisible ? m_ccField->tags2().join(", ") : QString(),
m_bccVisible ? m_bccField->tags2().join(", ") : QString(),
m_subjectField->text(),
m_bodyEditor->toHtml(),
m_schedulePicker->dateTime(),
m_currentAccountId > 0 ? m_accountCombo->itemData(m_accountCombo->currentIndex()).toString() : QString(),
m_attachmentFiles
);
}
void ComposeView::onDetachClicked() {
emit detachRequested(this);
}
void ComposeView::setTo(const QString &to) { m_toField->tags(to.split(',', Qt::SkipEmptyParts)); }
void ComposeView::setSubject(const QString &subject) { m_subjectField->setText(subject); }
void ComposeView::setBody(const QString &body) { m_bodyEditor->setHtml(body); }
void ComposeView::initializeComposition() {
m_toField->tags(QStringList{});
m_ccField->tags(QStringList{});
m_bccField->tags(QStringList{});
m_subjectField->clear();
m_bodyEditor->clear();
m_attachmentFiles.clear();
m_attachmentList->clear();
m_attachmentList->setVisible(false);
m_ccRow->setVisible(false);
m_bccRow->setVisible(false);
m_schedulePanel->setVisible(false);
m_toField->setFocus();
}
void ComposeView::startNewEmail(const QString &initialRecipient) {
initializeComposition();
if (!initialRecipient.isEmpty())
m_toField->tags(initialRecipient.split(',', Qt::SkipEmptyParts));
m_toField->setFocus();
}
void ComposeView::onAddAttachmentClicked() {
qDebug() << "Attach button clicked";
QStringList files = QFileDialog::getOpenFileNames(this, tr("Select Attachments"), QString(), tr("All Files (*)"));
if (files.isEmpty())
return;
for (const QString &file : files) {
m_attachmentFiles.append(file);
QListWidgetItem *item = new QListWidgetItem(m_attachmentList);
item->setData(Qt::UserRole, file);
QWidget *widget = new QWidget;
widget->setFixedSize(300, 50);
widget->setStyleSheet(
"background-color: #f8f9fa; "
"border-radius: 8px; "
"border: 1px solid #e9ecef;"
);
QHBoxLayout *layout = new QHBoxLayout(widget);
layout->setContentsMargins(8, 4, 8, 4);
layout->setSpacing(8);
// Icon from system
QFileIconProvider provider;
QIcon icon = provider.icon(QFileInfo(file));
QLabel *iconLabel = new QLabel;
QPixmap pix = icon.pixmap(32, 32);
if (pix.isNull())
pix = QIcon::fromTheme("unknown").pixmap(32,32);
iconLabel->setPixmap(pix);
iconLabel->setFixedSize(32,32);
layout->addWidget(iconLabel);
// Text vertical layout
QVBoxLayout *textLayout = new QVBoxLayout;
textLayout->setSpacing(2);
QString fileName = QFileInfo(file).fileName();
QLabel *nameLabel = new QLabel(fileName);
nameLabel->setStyleSheet("font-weight: bold;");
QLabel *sizeLabel = new QLabel(tr("%1 KB").arg(QFileInfo(file).size()/1024));
sizeLabel->setStyleSheet("color: #666; font-size: 10px;");
textLayout->addWidget(nameLabel);
textLayout->addWidget(sizeLabel);
layout->addLayout(textLayout);
layout->addStretch();
// Delete button
QPushButton *delBtn = new QPushButton;
QIcon delIcon = QIcon::fromTheme("edit-delete");
if (delIcon.isNull())
delIcon = QIcon::fromTheme("list-remove");
if (delIcon.isNull())
delIcon = QIcon::fromTheme("gtk-delete");
delBtn->setIcon(delIcon);
delBtn->setToolTip(tr("Remove attachment"));
delBtn->setFixedSize(24,24);
delBtn->setStyleSheet(
"QPushButton { border: none; background: transparent; }"
"QPushButton:hover { background: #e9ecef; border-radius: 4px; }"
);
layout->addWidget(delBtn);
// Connect delete button
connect(delBtn, &QPushButton::clicked, [this, item]() {
int row = m_attachmentList->row(item);
if (row >= 0) {
QListWidgetItem *it = m_attachmentList->takeItem(row);
if (it) {
m_attachmentFiles.removeAt(row);
delete it;
}
m_attachmentList->setVisible(!m_attachmentFiles.isEmpty());
}
});
m_attachmentList->addItem(item);
m_attachmentList->setItemWidget(item, widget);
}
m_attachmentList->setVisible(!m_attachmentFiles.isEmpty());
}
void ComposeView::onRemoveAttachmentClicked() {
QListWidgetItem *item = m_attachmentList->currentItem();
if (!item)
return;
int row = m_attachmentList->row(item);
m_attachmentList->takeItem(row);
m_attachmentFiles.removeAt(row);
delete item;
m_attachmentList->setVisible(!m_attachmentFiles.isEmpty());
}
void ComposeView::onTemplateClicked() {
// Placeholder for template functionality
QMessageBox::information(this, tr("Email Templates"), tr("Email templates feature not yet implemented."));
}
#include "composeview.moc"