" format
+ QRegularExpression re("^\\s*(.*?)\\s*<([^>]+)>\\s*$");
+ QRegularExpressionMatch match = re.match(raw);
+ if (match.hasMatch()) {
+ QString name = match.captured(1).trimmed();
+ QString email = match.captured(2).trimmed();
+ if (!name.isEmpty()) {
+ return QString("%2 <%1>").arg(email, name);
+ }
+ return QString("%1").arg(email);
+ }
+
+ // Just an email
+ if (raw.contains("@")) {
+ return QString("%1").arg(raw);
+ }
+
+ // Just a name
+ return raw.toHtmlEscaped();
+}
+
+QString ReaderView::formatDate(const QDateTime &dt) {
+ if (!dt.isValid()) return "";
+
+ QDateTime now = QDateTime::currentDateTime();
+ if (dt.date() == now.date()) {
+ return dt.toString("'Hoy' hh:mm");
+ } else if (dt.date() == now.date().addDays(-1)) {
+ return dt.toString("'Ayer' hh:mm");
+ } else if (dt.date().year() == now.date().year()) {
+ return dt.toString("ddd, d MMM 'a las' hh:mm");
+ } else {
+ return dt.toString("ddd, d MMM yyyy 'a las' hh:mm");
+ }
+}
+
+QIcon ReaderView::mimeIcon(const QString &mimeType) {
+ QStyle *style = QApplication::style();
+ if (mimeType.startsWith("image/")) return QIcon::fromTheme("image-x-generic", style->standardIcon(QStyle::SP_FileIcon));
+ if (mimeType.startsWith("application/pdf")) return QIcon::fromTheme("application-pdf", style->standardIcon(QStyle::SP_FileIcon));
+ if (mimeType.startsWith("application/msword") || mimeType.contains("wordprocessingml")) return QIcon::fromTheme("application-msword", style->standardIcon(QStyle::SP_FileIcon));
+ if (mimeType.contains("spreadsheet") || mimeType.contains("excel")) return QIcon::fromTheme("application-vnd.ms-excel", style->standardIcon(QStyle::SP_FileIcon));
+ if (mimeType.contains("presentation") || mimeType.contains("powerpoint")) return QIcon::fromTheme("application-vnd.ms-powerpoint", style->standardIcon(QStyle::SP_FileIcon));
+ if (mimeType.startsWith("text/")) return QIcon::fromTheme("text-plain", style->standardIcon(QStyle::SP_FileIcon));
+ if (mimeType.startsWith("audio/")) return QIcon::fromTheme("audio-x-generic", style->standardIcon(QStyle::SP_FileIcon));
+ if (mimeType.startsWith("video/")) return QIcon::fromTheme("video-x-generic", style->standardIcon(QStyle::SP_FileIcon));
+ if (mimeType.contains("zip") || mimeType.contains("compressed") || mimeType.contains("archive")) return QIcon::fromTheme("package-x-generic", style->standardIcon(QStyle::SP_FileIcon));
+ return QIcon::fromTheme("unknown", style->standardIcon(QStyle::SP_FileIcon));
+}
+
+void ReaderView::showAttachmentContextMenu(const QPoint &pos, const QString &path, const QString &name) {
+ QMenu menu(this);
+ QAction *openAct = menu.addAction("Abrir", [this, path]() {
+ emit openAttachmentRequested(path);
+ QDesktopServices::openUrl(QUrl::fromLocalFile(path));
+ });
+ openAct->setIcon(mimeIcon(QMimeDatabase().mimeTypeForFile(path).name()));
+
+ QAction *saveAct = menu.addAction("Guardar como...", [this, path, name]() {
+ QString savePath = QFileDialog::getSaveFileName(this, "Guardar adjunto", QDir::homePath() + "/" + name);
+ if (!savePath.isEmpty()) {
+ QFile::copy(path, savePath);
+ }
+ });
+ saveAct->setIcon(QIcon::fromTheme("document-save", QApplication::style()->standardIcon(QStyle::SP_DialogSaveButton)));
+
+ QAction *copyPathAct = menu.addAction("Copiar ruta", [path]() {
+ QApplication::clipboard()->setText(path);
+ });
+ copyPathAct->setIcon(QIcon::fromTheme("edit-copy", QApplication::style()->standardIcon(QStyle::SP_DialogApplyButton)));
+
+ menu.addSeparator();
+ QAction *deleteAct = menu.addAction("Eliminar adjunto", [this, name]() {
+ // TODO: Implement attachment deletion from stored mail
+ QMessageBox::information(this, "No implementado", "Eliminar adjuntos del almacenamiento local aún no está implementado.");
+ });
+ deleteAct->setIcon(QIcon::fromTheme("edit-delete", QApplication::style()->standardIcon(QStyle::SP_TrashIcon)));
+
+ menu.exec(m_attachmentList->mapToGlobal(pos));
}
void ReaderView::setMailItem(const MailItem* item) {
if (!item) {
- m_subjectLabel->setText("No mail selected");
- m_fromLabel->setText("From: ");
- m_dateLabel->setText("Date: ");
+ m_currentMailId = -1;
+ m_allowExternalImages = false;
+ m_loadImagesButton->setVisible(false);
+ m_subjectLabel->setText("(Sin asunto)");
+ m_avatarLabel->setText("?");
+ m_fromLabel->setText("Remitente: —");
+ m_toLabel->setText("Para: —");
+ m_dateLabel->setText("Fecha: —");
m_attachmentList->clear();
- m_attachmentList->setVisible(false);
- m_bodyViewer->setHtml("Please select a message to read");
+ m_attachmentsFrame->setVisible(false);
+ m_bodyViewer->setHtml("(Seleccione un correo para leerlo)
");
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_currentMailId = item->id();
+ m_allowExternalImages = false;
+ m_loadImagesButton->setVisible(false);
+
+ // Subject
+ m_subjectLabel->setText(item->subject().isEmpty() ? "(Sin asunto)" : item->subject());
+
+ // Avatar - use first letter of sender name
+ QString sender = item->sender();
+ QString avatarChar = "?";
+ QRegularExpression re("^\\s*(.*?)\\s*<[^>]+>\\s*$");
+ QRegularExpressionMatch match = re.match(sender);
+ if (match.hasMatch()) {
+ QString name = match.captured(1).trimmed();
+ if (!name.isEmpty()) avatarChar = name[0].toUpper();
+ } else if (!sender.isEmpty() && sender.contains("@")) {
+ avatarChar = sender[0].toUpper();
+ }
+ m_avatarLabel->setText(avatarChar);
+
+ // From
+ m_fromLabel->setText("De: " + formatAddress(sender));
+
+ // To - MailItem doesn't have recipients easily accessible, skip for now
+ m_toLabel->setText("Para: —"); // TODO: fetch from MailItem or DB
+
+ // Date
+ m_dateLabel->setText("Fecha: " + formatDate(item->date()));
+
+ // Attachments
m_attachmentList->clear();
const QVector attachments = MailItemDao::attachmentsForMail(item->id());
for (const StoredAttachmentRecord &attachment : attachments) {
- auto *listItem = new QListWidgetItem(attachment.fileName, m_attachmentList);
+ QListWidgetItem *listItem = new QListWidgetItem(m_attachmentList);
+ QWidget *widget = new QWidget();
+ QHBoxLayout *layout = new QHBoxLayout(widget);
+ layout->setContentsMargins(8, 4, 8, 4);
+ layout->setSpacing(10);
+
+ // Icon
+ QLabel *iconLabel = new QLabel();
+ QMimeDatabase mimeDb;
+ QMimeType mime = mimeDb.mimeTypeForFileNameAndData(attachment.fileName, QByteArray());
+ QIcon icon = mimeIcon(mime.name());
+ iconLabel->setPixmap(icon.pixmap(24, 24));
+ iconLabel->setFixedSize(28, 28);
+ layout->addWidget(iconLabel);
+
+ // Name + size
+ QVBoxLayout *textLayout = new QVBoxLayout();
+ textLayout->setSpacing(1);
+ textLayout->setContentsMargins(0, 0, 0, 0);
+ QLabel *nameLabel = new QLabel(attachment.fileName);
+ nameLabel->setStyleSheet("font-weight: 500; font-size: 12px; color: #333;");
+ QLabel *sizeLabel = new QLabel(QString("%1 KB").arg(attachment.size / 1024));
+ sizeLabel->setStyleSheet("font-size: 10px; color: #888;");
+ textLayout->addWidget(nameLabel);
+ textLayout->addWidget(sizeLabel);
+ layout->addLayout(textLayout, 1);
+
+ widget->setLayout(layout);
+ listItem->setSizeHint(widget->sizeHint());
listItem->setData(Qt::UserRole, attachment.storedPath);
- listItem->setToolTip(attachment.storedPath);
+ listItem->setToolTip(QString("%1 (%2 KB)").arg(attachment.fileName).arg(attachment.size / 1024));
+ m_attachmentList->addItem(listItem);
+ m_attachmentList->setItemWidget(listItem, widget);
}
- m_attachmentList->setVisible(!attachments.isEmpty());
- m_bodyViewer->setHtml(item->bodyHtml());
+ m_attachmentsFrame->setVisible(!attachments.isEmpty());
+
+ // Body with sanitization + CSS
+ QString cleanHtml = sanitizeHtml(item->bodyHtml());
+ m_bodyViewer->setHtml(cleanHtml);
+
+ // Scroll to top
+ m_bodyViewer->moveCursor(QTextCursor::Start);
+ QScrollBar *vbar = m_scrollArea->verticalScrollBar();
+ if (vbar) vbar->setValue(0);
+}
+
+void ReaderView::refreshCurrentMail() {
+ if (m_currentMailId < 0) return;
+ std::optional item = MailItemDao::findById(m_currentMailId);
+ if (!item.has_value()) {
+ m_currentMailId = -1;
+ setMailItem(nullptr);
+ return;
+ }
+ // Re-apply without resetting zoom/state
+ MailItem &mail = item.value();
+ m_subjectLabel->setText(mail.subject().isEmpty() ? "(Sin asunto)" : mail.subject());
+ QString sender = mail.sender();
+ QString avatarChar = "?";
+ QRegularExpression re("^\\s*(.*?)\\s*<[^>]+>\\s*$");
+ QRegularExpressionMatch match = re.match(sender);
+ if (match.hasMatch()) {
+ QString name = match.captured(1).trimmed();
+ if (!name.isEmpty()) avatarChar = name[0].toUpper();
+ } else if (!sender.isEmpty() && sender.contains("@")) {
+ avatarChar = sender[0].toUpper();
+ }
+ m_avatarLabel->setText(avatarChar);
+ m_fromLabel->setText("De: " + formatAddress(sender));
+ m_dateLabel->setText("Fecha: " + formatDate(mail.date()));
+
+ QString cleanHtml = sanitizeHtml(mail.bodyHtml());
+ m_bodyViewer->setHtml(cleanHtml);
+}
+
+void ReaderView::setDarkMode(bool dark) {
+ m_darkMode = dark;
+ QString bg = dark ? "#1e1e1e" : "white";
+ QString text = dark ? "#e0e0e0" : "#333";
+ QString border = dark ? "#333" : "#e0e0e0";
+ QString headerBg = dark ? "#252525" : "#fafafa";
+ QString toolbarBg = dark ? "#2a2a2a" : "#fafafa";
+ QString findBg = dark ? "#3e2723" : "#fff3cd";
+ QString findBorder = dark ? "#ffb74d" : "#ffc107";
+
+ m_headerWidget->setStyleSheet(QString("QWidget { background: %1; border-bottom: 1px solid %2; }").arg(headerBg).arg(border));
+ m_toolbar->setStyleSheet(QString("QFrame { background: %1; border-bottom: 1px solid %2; }").arg(toolbarBg).arg(border));
+ m_findBar->setStyleSheet(QString("QFrame { background: %1; border-bottom: 1px solid %2; }").arg(findBg).arg(findBorder));
+ m_bodyViewer->setStyleSheet(QString("QTextBrowser { background: transparent; border: none; font-family: 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 13px; line-height: 1.5; color: %1; }").arg(text));
+ m_scrollArea->setStyleSheet(QString("QScrollArea { background: %1; border: none; }").arg(bg));
+ m_attachmentsFrame->setStyleSheet(QString("QFrame { background: %1; border-top: 1px solid %2; border-bottom: 1px solid %2; }").arg(headerBg).arg(border));
+ m_attachmentList->setStyleSheet(QString(
+ "QListWidget { background: %1; border: 1px solid %2; border-radius: 6px; padding: 4px; }"
+ "QListWidget::item { border: none; padding: 6px 8px; border-radius: 4px; color: %3; }"
+ "QListWidget::item:hover { background: %4; }"
+ "QListWidget::item:selected { background: %5; color: %6; }"
+ ).arg(dark ? "#2a2a2a" : "white").arg(border).arg(text).arg(dark ? "#333" : "#f0f0f0").arg(dark ? "#1976D2" : "#e3f2fd").arg(dark ? "#90caf9" : "#1976D2"));
+
+ m_subjectLabel->setStyleSheet(QString("color: %1;").arg(text));
+ m_fromLabel->setStyleSheet(QString("color: %1; font-size: 13px;").arg(text));
+ m_toLabel->setStyleSheet(QString("color: %1; font-size: 12px;").arg(dark ? "#aaa" : "#666"));
+ m_dateLabel->setStyleSheet(QString("color: %1; font-size: 12px;").arg(dark ? "#888" : "#888"));
+ m_attachmentsHeader->setStyleSheet(QString("color: %1;").arg(dark ? "#ccc" : "#555"));
+ m_findCountLabel->setStyleSheet(QString("color: %1; font-size: 11px;").arg(dark ? "#ffb74d" : "#856404"));
+ m_zoomLabel->setStyleSheet(QString("font-size: 11px; color: %1;").arg(dark ? "#aaa" : "#555"));
+
+ // Update load images button for dark mode
+ if (m_loadImagesButton) {
+ if (dark) {
+ m_loadImagesButton->setStyleSheet(
+ "QToolButton {"
+ " background: #1e3a5f;"
+ " border: 1px solid #64b5f6;"
+ " border-radius: 4px;"
+ " padding: 4px 10px;"
+ " font-size: 11px;"
+ " color: #90caf9;"
+ " font-weight: 500;"
+ "}"
+ "QToolButton:hover {"
+ " background: #2a4a6f;"
+ "}"
+ );
+ } else {
+ m_loadImagesButton->setStyleSheet(
+ "QToolButton {"
+ " background: #e3f2fd;"
+ " border: 1px solid #1976D2;"
+ " border-radius: 4px;"
+ " padding: 4px 10px;"
+ " font-size: 11px;"
+ " color: #1976D2;"
+ " font-weight: 500;"
+ "}"
+ "QToolButton:hover {"
+ " background: #bbdefb;"
+ "}"
+ );
+ }
+ }
+
+ // Re-render body with new theme
+ refreshCurrentMail();
+}
+
+// Slots for action buttons
+void ReaderView::onReplyClicked() {
+ if (m_currentMailId >= 0) emit replyRequested(m_currentMailId);
+}
+
+void ReaderView::onForwardClicked() {
+ if (m_currentMailId >= 0) emit forwardRequested(m_currentMailId);
+}
+
+void ReaderView::onDeleteClicked() {
+ if (m_currentMailId >= 0) emit deleteRequested(m_currentMailId);
+}
+
+void ReaderView::onDetachClicked() {
+ emit detachRequested();
}
diff --git a/src/ui/readerview.h b/src/ui/readerview.h
index d12fa22..ead5420 100644
--- a/src/ui/readerview.h
+++ b/src/ui/readerview.h
@@ -7,6 +7,11 @@
#include
#include
#include
+#include
+#include
+#include
+#include
+#include
#include "core/mailitem.h"
class ReaderView : public QWidget {
@@ -17,24 +22,81 @@ public:
~ReaderView() override = default;
void setMailItem(const MailItem* item);
+ void setDarkMode(bool dark);
signals:
- void replyRequested(const MailItem* item);
- void forwardRequested(const MailItem* item);
- void deleteRequested(const MailItem* item);
+ void replyRequested(int mailId);
+ void forwardRequested(int mailId);
+ void deleteRequested(int mailId);
void detachRequested();
+ void openAttachmentRequested(const QString &path);
+private slots:
+ void onReplyClicked();
+ void onForwardClicked();
+ void onDeleteClicked();
+ void onDetachClicked();
private:
void setupUI();
+ void setupBodyViewer();
+ void setupAttachmentsArea();
+ void setupFindBar();
+ void applyEmailCss();
+ void updateZoom();
+ void refreshCurrentMail();
+ QString sanitizeHtml(const QString &html);
+ QString formatAddress(const QString &raw);
+ QString formatDate(const QDateTime &dt);
+ QIcon mimeIcon(const QString &mimeType);
+ void showAttachmentContextMenu(const QPoint &pos, const QString &path, const QString &name);
+ // Header
+ QWidget *m_headerWidget;
+ QLabel *m_avatarLabel;
QLabel *m_subjectLabel;
QLabel *m_fromLabel;
QLabel *m_dateLabel;
+ QLabel *m_toLabel;
+
+ // Actions
+ QToolButton *m_replyButton;
+ QToolButton *m_forwardButton;
+ QToolButton *m_deleteButton;
+ QToolButton *m_detachButton;
+ QToolButton *m_moreButton;
+
+ // Body
+ QScrollArea *m_scrollArea;
QTextBrowser *m_bodyViewer;
+ QWidget *m_bodyContainer;
+ QVBoxLayout *m_bodyLayout;
+ qreal m_zoomFactor = 1.0;
+
+ // Attachments
+ QFrame *m_attachmentsFrame;
+ QVBoxLayout *m_attachmentsLayout;
+ QLabel *m_attachmentsHeader;
QListWidget *m_attachmentList;
- QPushButton *m_replyButton;
- QPushButton *m_forwardButton;
- QPushButton *m_deleteButton;
+ // Find bar
+ QFrame *m_findBar;
+ QLineEdit *m_findInput;
+ QToolButton *m_findPrevButton;
+ QToolButton *m_findNextButton;
+ QToolButton *m_findCloseButton;
+ QLabel *m_findCountLabel;
+
+ // Toolbar (zoom, etc.)
+ QFrame *m_toolbar;
+ QToolButton *m_zoomInButton;
+ QToolButton *m_zoomOutButton;
+ QToolButton *m_zoomResetButton;
+ QLabel *m_zoomLabel;
+
+ int m_currentMailId = -1;
+ bool m_darkMode = false;
+ QString m_baseCss;
+ bool m_allowExternalImages = false;
+ QToolButton *m_loadImagesButton = nullptr;
};
diff --git a/src/ui/ruleditordialog.cpp b/src/ui/ruleditordialog.cpp
new file mode 100644
index 0000000..b8c6964
--- /dev/null
+++ b/src/ui/ruleditordialog.cpp
@@ -0,0 +1,585 @@
+#include "ruleditordialog.h"
+#include "db/dao/ruledao.h"
+#include "db/dao/categorydao.h"
+#include "db/dao/folderdao.h"
+#include "services/rulesengine.h"
+#include "services/accountservice.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+RuleEditorDialog::RuleEditorDialog(const Rule& rule, qint64 accountId, QWidget *parent)
+ : QDialog(parent), m_rule(rule), m_accountId(accountId)
+{
+ setWindowTitle(rule.isValid() ? tr("Editar regla") : tr("Nueva regla"));
+ resize(800, 600);
+ setupUI();
+ populateFromRule();
+}
+
+void RuleEditorDialog::setupUI()
+{
+ QVBoxLayout* mainLayout = new QVBoxLayout(this);
+
+ // Form header
+ QGroupBox* headerGroup = new QGroupBox(tr("Información básica"));
+ QFormLayout* headerLayout = new QFormLayout(headerGroup);
+
+ m_nameEdit = new QLineEdit();
+ m_nameEdit->setPlaceholderText(tr("Nombre de la regla"));
+ headerLayout->addRow(tr("Nombre:"), m_nameEdit);
+
+ m_descEdit = new QTextEdit();
+ m_descEdit->setMaximumHeight(60);
+ m_descEdit->setPlaceholderText(tr("Descripción opcional"));
+ headerLayout->addRow(tr("Descripción:"), m_descEdit);
+
+ m_accountCombo = new QComboBox();
+ loadAccounts();
+ headerLayout->addRow(tr("Cuenta:"), m_accountCombo);
+
+ QHBoxLayout* optionsLayout = new QHBoxLayout();
+ m_enabledCheck = new QCheckBox(tr("Activada"));
+ m_enabledCheck->setChecked(true);
+ m_matchAllCheck = new QCheckBox(tr("Todas las condiciones (Y)"));
+ m_matchAllCheck->setChecked(true);
+ m_matchAllCheck->setToolTip(tr("Desactivar = Cualquier condición (O)"));
+ m_prioritySpin = new QSpinBox();
+ m_prioritySpin->setRange(1, 999);
+ m_prioritySpin->setValue(100);
+ m_prioritySpin->setToolTip(tr("Menor = mayor prioridad"));
+
+ optionsLayout->addWidget(m_enabledCheck);
+ optionsLayout->addWidget(m_matchAllCheck);
+ optionsLayout->addStretch();
+ optionsLayout->addWidget(new QLabel(tr("Prioridad:")));
+ optionsLayout->addWidget(m_prioritySpin);
+ headerLayout->addRow(optionsLayout);
+
+ mainLayout->addWidget(headerGroup);
+
+ // Conditions table
+ QGroupBox* condGroup = new QGroupBox(tr("Condiciones"));
+ QVBoxLayout* condLayout = new QVBoxLayout(condGroup);
+
+ m_conditionsTable = new QTableWidget();
+ m_conditionsTable->setColumnCount(5);
+ m_conditionsTable->setHorizontalHeaderLabels({tr("Campo"), tr("Operador"), tr("Valor"), tr("Mayúsculas"), ""});
+ m_conditionsTable->horizontalHeader()->setStretchLastSection(true);
+ m_conditionsTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch);
+ m_conditionsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_conditionsTable->verticalHeader()->setVisible(false);
+ condLayout->addWidget(m_conditionsTable);
+
+ QHBoxLayout* condBtnLayout = new QHBoxLayout();
+ QPushButton* btnAddCond = new QPushButton(tr("+ Añadir condición"));
+ btnAddCond->setStyleSheet("QPushButton { background: #1976D2; color: white; padding: 6px 12px; border-radius: 4px; }");
+ connect(btnAddCond, &QPushButton::clicked, this, &RuleEditorDialog::onAddCondition);
+ QPushButton* btnRemCond = new QPushButton(tr("- Eliminar"));
+ connect(btnRemCond, &QPushButton::clicked, this, &RuleEditorDialog::onRemoveCondition);
+ condBtnLayout->addWidget(btnAddCond);
+ condBtnLayout->addWidget(btnRemCond);
+ condBtnLayout->addStretch();
+ condLayout->addLayout(condBtnLayout);
+
+ mainLayout->addWidget(condGroup, 1);
+
+ // Actions table
+ QGroupBox* actGroup = new QGroupBox(tr("Acciones"));
+ QVBoxLayout* actLayout = new QVBoxLayout(actGroup);
+
+ m_actionsTable = new QTableWidget();
+ m_actionsTable->setColumnCount(4);
+ m_actionsTable->setHorizontalHeaderLabels({tr("Tipo"), tr("Parámetro"), tr("Detalles"), ""});
+ m_actionsTable->horizontalHeader()->setStretchLastSection(true);
+ m_actionsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
+ m_actionsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
+ m_actionsTable->verticalHeader()->setVisible(false);
+ actLayout->addWidget(m_actionsTable);
+
+ QHBoxLayout* actBtnLayout = new QHBoxLayout();
+ QPushButton* btnAddAct = new QPushButton(tr("+ Añadir acción"));
+ btnAddAct->setStyleSheet("QPushButton { background: #4CAF50; color: white; padding: 6px 12px; border-radius: 4px; }");
+ connect(btnAddAct, &QPushButton::clicked, this, &RuleEditorDialog::onAddAction);
+ QPushButton* btnRemAct = new QPushButton(tr("- Eliminar"));
+ connect(btnRemAct, &QPushButton::clicked, this, &RuleEditorDialog::onRemoveAction);
+ actBtnLayout->addWidget(btnAddAct);
+ actBtnLayout->addWidget(btnRemAct);
+ actBtnLayout->addStretch();
+ actLayout->addLayout(actBtnLayout);
+
+ mainLayout->addWidget(actGroup, 1);
+
+ // Buttons
+ QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+ connect(buttonBox, &QDialogButtonBox::accepted, this, &RuleEditorDialog::onAccept);
+ connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+ mainLayout->addWidget(buttonBox);
+}
+
+void RuleEditorDialog::loadAccounts()
+{
+ m_accountCombo->clear();
+ m_accountCombo->addItem(tr("Global (todas las cuentas)"), -1);
+
+ // Get accounts via AccountService (singleton doesn't have instance(), use parent's accountService)
+ // For now, we'll just add the global option and let the main window handle account-specific rules
+ if (m_accountId >= 0) {
+ int idx = m_accountCombo->findData(m_accountId);
+ if (idx >= 0) m_accountCombo->setCurrentIndex(idx);
+ }
+}
+
+void RuleEditorDialog::populateFromRule()
+{
+ if (!m_rule.isValid()) return;
+
+ m_nameEdit->setText(m_rule.name);
+ m_descEdit->setPlainText(m_rule.description);
+ m_enabledCheck->setChecked(m_rule.enabled);
+ m_matchAllCheck->setChecked(m_rule.matchAll);
+ m_prioritySpin->setValue(m_rule.priority);
+
+ int idx = m_accountCombo->findData(m_rule.accountId >= 0 ? m_rule.accountId : -1);
+ if (idx >= 0) m_accountCombo->setCurrentIndex(idx);
+
+ for (const RuleCondition& cond : m_rule.conditions) {
+ int row = m_conditionsTable->rowCount();
+ m_conditionsTable->insertRow(row);
+ setupConditionRow(row, cond);
+ }
+
+ for (const RuleAction& act : m_rule.actions) {
+ int row = m_actionsTable->rowCount();
+ m_actionsTable->insertRow(row);
+ setupActionRow(row, act);
+ }
+}
+
+void RuleEditorDialog::setupConditionRow(int row, const RuleCondition& cond)
+{
+ // Field combo
+ QComboBox* fieldCombo = new QComboBox();
+ fieldCombo->addItem(tr("De"), RuleCondition::From);
+ fieldCombo->addItem(tr("Para"), RuleCondition::To);
+ fieldCombo->addItem(tr("CC"), RuleCondition::Cc);
+ fieldCombo->addItem(tr("Asunto"), RuleCondition::Subject);
+ fieldCombo->addItem(tr("Cuerpo"), RuleCondition::Body);
+ fieldCombo->addItem(tr("Tiene adjuntos"), RuleCondition::HasAttachment);
+ fieldCombo->addItem(tr("Tamaño"), RuleCondition::Size);
+ fieldCombo->addItem(tr("Fecha"), RuleCondition::Date);
+ fieldCombo->addItem(tr("Marcado"), RuleCondition::Flagged);
+ fieldCombo->addItem(tr("Leído"), RuleCondition::Read);
+ fieldCombo->setCurrentIndex(fieldCombo->findData(cond.field));
+ connect(fieldCombo, QOverload::of(&QComboBox::currentIndexChanged),
+ [this, row](int) { onConditionFieldChanged(row); });
+ m_conditionsTable->setCellWidget(row, 0, fieldCombo);
+
+ // Operator combo
+ QComboBox* opCombo = new QComboBox();
+ opCombo->addItem(tr("Contiene"), RuleCondition::Contains);
+ opCombo->addItem(tr("No contiene"), RuleCondition::NotContains);
+ opCombo->addItem(tr("Es igual a"), RuleCondition::Equals);
+ opCombo->addItem(tr("No es igual a"), RuleCondition::NotEquals);
+ opCombo->addItem(tr("Empieza por"), RuleCondition::StartsWith);
+ opCombo->addItem(tr("Termina en"), RuleCondition::EndsWith);
+ opCombo->addItem(tr("Expresión regular"), RuleCondition::Regex);
+ opCombo->addItem(tr("Mayor que"), RuleCondition::GreaterThan);
+ opCombo->addItem(tr("Menor que"), RuleCondition::LessThan);
+ opCombo->addItem(tr("Antes de"), RuleCondition::Before);
+ opCombo->addItem(tr("Después de"), RuleCondition::After);
+ opCombo->setCurrentIndex(opCombo->findData(cond.op));
+ m_conditionsTable->setCellWidget(row, 1, opCombo);
+
+ // Value edit
+ QLineEdit* valueEdit = new QLineEdit(cond.value);
+ m_conditionsTable->setCellWidget(row, 2, valueEdit);
+
+ // Case sensitive checkbox
+ QCheckBox* caseCheck = new QCheckBox();
+ caseCheck->setChecked(cond.caseSensitive);
+ caseCheck->setStyleSheet("QCheckBox { margin-left: 50%; }");
+ m_conditionsTable->setCellWidget(row, 3, caseCheck);
+
+ // Remove button
+ QPushButton* removeBtn = new QPushButton("✕");
+ removeBtn->setFixedSize(24, 24);
+ removeBtn->setToolTip(tr("Eliminar condición"));
+ removeBtn->setStyleSheet("QPushButton { background: transparent; color: #d32f2f; border: none; font-weight: bold; } QPushButton:hover { background: #fdeaea; border-radius: 3px; }");
+ connect(removeBtn, &QPushButton::clicked, [this, row]() {
+ m_conditionsTable->removeRow(row);
+ });
+ m_conditionsTable->setCellWidget(row, 4, removeBtn);
+
+ onConditionFieldChanged(row);
+}
+
+void RuleEditorDialog::onConditionFieldChanged(int row)
+{
+ QComboBox* fieldCombo = qobject_cast(m_conditionsTable->cellWidget(row, 0));
+ QComboBox* opCombo = qobject_cast(m_conditionsTable->cellWidget(row, 1));
+ QLineEdit* valueEdit = qobject_cast(m_conditionsTable->cellWidget(row, 2));
+ QCheckBox* caseCheck = qobject_cast(m_conditionsTable->cellWidget(row, 3));
+
+ if (!fieldCombo || !opCombo) return;
+
+ RuleCondition::Field field = static_cast(fieldCombo->currentData().toInt());
+
+ // Clear and repopulate operators based on field type
+ opCombo->clear();
+
+ bool isTextField = (field == RuleCondition::From || field == RuleCondition::To ||
+ field == RuleCondition::Cc || field == RuleCondition::Subject ||
+ field == RuleCondition::Body);
+ bool isBooleanField = (field == RuleCondition::HasAttachment || field == RuleCondition::Flagged || field == RuleCondition::Read);
+ bool isNumericField = (field == RuleCondition::Size);
+ bool isDateField = (field == RuleCondition::Date);
+
+ if (isTextField) {
+ opCombo->addItem(tr("Contiene"), RuleCondition::Contains);
+ opCombo->addItem(tr("No contiene"), RuleCondition::NotContains);
+ opCombo->addItem(tr("Es igual a"), RuleCondition::Equals);
+ opCombo->addItem(tr("No es igual a"), RuleCondition::NotEquals);
+ opCombo->addItem(tr("Empieza por"), RuleCondition::StartsWith);
+ opCombo->addItem(tr("Termina en"), RuleCondition::EndsWith);
+ opCombo->addItem(tr("Expresión regular"), RuleCondition::Regex);
+ valueEdit->setPlaceholderText(tr("Texto a buscar..."));
+ caseCheck->setVisible(true);
+ } else if (isBooleanField) {
+ opCombo->addItem(tr("Es"), RuleCondition::Equals);
+ opCombo->addItem(tr("No es"), RuleCondition::NotEquals);
+ valueEdit->setPlaceholderText("true/false");
+ valueEdit->setText("true");
+ caseCheck->setVisible(false);
+ } else if (isNumericField) {
+ opCombo->addItem(tr("Mayor que"), RuleCondition::GreaterThan);
+ opCombo->addItem(tr("Menor que"), RuleCondition::LessThan);
+ opCombo->addItem(tr("Es igual a"), RuleCondition::Equals);
+ valueEdit->setPlaceholderText("Tamaño en bytes");
+ caseCheck->setVisible(false);
+ } else if (isDateField) {
+ opCombo->addItem(tr("Antes de"), RuleCondition::Before);
+ opCombo->addItem(tr("Después de"), RuleCondition::After);
+ opCombo->addItem(tr("Es igual a"), RuleCondition::Equals);
+ valueEdit->setPlaceholderText("YYYY-MM-DD");
+ caseCheck->setVisible(false);
+ }
+}
+
+void RuleEditorDialog::setupActionRow(int row, const RuleAction& act)
+{
+ // Type combo
+ QComboBox* typeCombo = new QComboBox();
+ typeCombo->addItem(tr("Mover a carpeta"), RuleAction::MoveToFolder);
+ typeCombo->addItem(tr("Marcar como leído/no leído"), RuleAction::MarkAsRead);
+ typeCombo->addItem(tr("Marcar/Desmarcar"), RuleAction::MarkAsFlagged);
+ typeCombo->addItem(tr("Eliminar"), RuleAction::Delete);
+ typeCombo->addItem(tr("Asignar categoría"), RuleAction::AssignCategory);
+ typeCombo->addItem(tr("Quitar categoría"), RuleAction::RemoveCategory);
+ typeCombo->addItem(tr("Reenviar a"), RuleAction::ForwardTo);
+ typeCombo->addItem(tr("Establecer prioridad"), RuleAction::SetPriority);
+ typeCombo->addItem(tr("Detener procesamiento"), RuleAction::StopProcessing);
+ typeCombo->setCurrentIndex(typeCombo->findData(act.type));
+ connect(typeCombo, QOverload::of(&QComboBox::currentIndexChanged),
+ [this, row](int) { onActionTypeChanged(row); });
+ m_actionsTable->setCellWidget(row, 0, typeCombo);
+
+ // Parameter edit (dynamic based on type)
+ QWidget* paramWidget = createParameterWidget(act);
+ m_actionsTable->setCellWidget(row, 1, paramWidget);
+
+ // Details label
+ QLabel* detailsLabel = new QLabel();
+ detailsLabel->setStyleSheet("color: #666; font-size: 11px;");
+ updateActionDetails(act, detailsLabel);
+ m_actionsTable->setCellWidget(row, 2, detailsLabel);
+
+ // Remove button
+ QPushButton* removeBtn = new QPushButton("✕");
+ removeBtn->setFixedSize(24, 24);
+ removeBtn->setToolTip(tr("Eliminar acción"));
+ removeBtn->setStyleSheet("QPushButton { background: transparent; color: #d32f2f; border: none; font-weight: bold; } QPushButton:hover { background: #fdeaea; border-radius: 3px; }");
+ connect(removeBtn, &QPushButton::clicked, [this, row]() {
+ m_actionsTable->removeRow(row);
+ });
+ m_actionsTable->setCellWidget(row, 3, removeBtn);
+}
+
+QWidget* RuleEditorDialog::createParameterWidget(const RuleAction& act)
+{
+ QWidget* container = new QWidget();
+ QHBoxLayout* layout = new QHBoxLayout(container);
+ layout->setContentsMargins(0, 0, 0, 0);
+
+ switch (act.type) {
+ case RuleAction::MoveToFolder: {
+ QComboBox* folderCombo = new QComboBox();
+ folderCombo->addItem(tr("Seleccionar carpeta..."), -1);
+ // Load folders from database
+ QSqlDatabase db = DatabaseManager::instance().database();
+ QSqlQuery query(db);
+ query.exec("SELECT id, name FROM Folder ORDER BY name");
+ while (query.next()) {
+ folderCombo->addItem(query.value(1).toString(), query.value(0).toLongLong());
+ }
+ if (act.value.toLongLong() > 0) {
+ int idx = folderCombo->findData(act.value.toLongLong());
+ if (idx >= 0) folderCombo->setCurrentIndex(idx);
+ }
+ layout->addWidget(folderCombo, 1);
+ break;
+ }
+ case RuleAction::MarkAsRead: {
+ QComboBox* combo = new QComboBox();
+ combo->addItem(tr("Leído"), "true");
+ combo->addItem(tr("No leído"), "false");
+ combo->setCurrentIndex(combo->findData(act.value));
+ layout->addWidget(combo, 1);
+ break;
+ }
+ case RuleAction::MarkAsFlagged: {
+ QComboBox* combo = new QComboBox();
+ combo->addItem(tr("Marcado"), "true");
+ combo->addItem(tr("No marcado"), "false");
+ combo->setCurrentIndex(combo->findData(act.value));
+ layout->addWidget(combo, 1);
+ break;
+ }
+ case RuleAction::AssignCategory:
+ case RuleAction::RemoveCategory: {
+ QComboBox* catCombo = new QComboBox();
+ catCombo->addItem(tr("Seleccionar categoría..."), -1);
+ QVector cats = CategoryDao::findAll();
+ for (const Category& cat : cats) {
+ catCombo->addItem(cat.name, cat.id);
+ }
+ if (act.value.toLongLong() > 0) {
+ int idx = catCombo->findData(act.value.toLongLong());
+ if (idx >= 0) catCombo->setCurrentIndex(idx);
+ }
+ layout->addWidget(catCombo, 1);
+ break;
+ }
+ case RuleAction::ForwardTo: {
+ QLineEdit* edit = new QLineEdit(act.value);
+ edit->setPlaceholderText("email@dominio.com");
+ layout->addWidget(edit, 1);
+ break;
+ }
+ case RuleAction::SetPriority: {
+ QComboBox* combo = new QComboBox();
+ combo->addItem(tr("Alta"), "high");
+ combo->addItem(tr("Normal"), "normal");
+ combo->addItem(tr("Baja"), "low");
+ combo->setCurrentIndex(combo->findData(act.value));
+ layout->addWidget(combo, 1);
+ break;
+ }
+ case RuleAction::Delete:
+ case RuleAction::StopProcessing:
+ default: {
+ QLabel* label = new QLabel(tr("(sin parámetros)"));
+ label->setStyleSheet("color: #999; font-style: italic;");
+ layout->addWidget(label, 1);
+ break;
+ }
+ }
+
+ return container;
+}
+
+void RuleEditorDialog::updateActionDetails(const RuleAction& act, QLabel* label)
+{
+ QString details;
+ switch (act.type) {
+ case RuleAction::MoveToFolder:
+ details = tr("Mueve el correo a la carpeta seleccionada");
+ break;
+ case RuleAction::MarkAsRead:
+ details = tr("Cambia el estado de lectura del correo");
+ break;
+ case RuleAction::MarkAsFlagged:
+ details = tr("Cambia el estado de marcado/estrella");
+ break;
+ case RuleAction::Delete:
+ details = tr("Elimina el correo permanentemente");
+ break;
+ case RuleAction::AssignCategory:
+ details = tr("Añade la categoría al correo");
+ break;
+ case RuleAction::RemoveCategory:
+ details = tr("Quita la categoría del correo");
+ break;
+ case RuleAction::ForwardTo:
+ details = tr("Reenvía el correo a la dirección indicada");
+ break;
+ case RuleAction::SetPriority:
+ details = tr("Marca el correo como alta prioridad (flagged)");
+ break;
+ case RuleAction::StopProcessing:
+ details = tr("Detiene la evaluación de más reglas para este correo");
+ break;
+ }
+ label->setText(details);
+}
+
+void RuleEditorDialog::onActionTypeChanged(int row)
+{
+ QComboBox* typeCombo = qobject_cast(m_actionsTable->cellWidget(row, 0));
+ if (!typeCombo) return;
+
+ RuleAction::Type type = static_cast(typeCombo->currentData().toInt());
+
+ // Replace parameter widget
+ QWidget* oldWidget = m_actionsTable->cellWidget(row, 1);
+ if (oldWidget) oldWidget->deleteLater();
+
+ RuleAction act;
+ act.type = type;
+ QWidget* newWidget = createParameterWidget(act);
+ m_actionsTable->setCellWidget(row, 1, newWidget);
+
+ // Update details
+ QLabel* detailsLabel = qobject_cast(m_actionsTable->cellWidget(row, 2));
+ if (detailsLabel) updateActionDetails(act, detailsLabel);
+}
+
+void RuleEditorDialog::onAddCondition()
+{
+ int row = m_conditionsTable->rowCount();
+ m_conditionsTable->insertRow(row);
+ setupConditionRow(row);
+}
+
+void RuleEditorDialog::onRemoveCondition()
+{
+ int row = m_conditionsTable->currentRow();
+ if (row >= 0) {
+ m_conditionsTable->removeRow(row);
+ }
+}
+
+void RuleEditorDialog::onAddAction()
+{
+ int row = m_actionsTable->rowCount();
+ m_actionsTable->insertRow(row);
+ setupActionRow(row);
+}
+
+void RuleEditorDialog::onRemoveAction()
+{
+ int row = m_actionsTable->currentRow();
+ if (row >= 0) {
+ m_actionsTable->removeRow(row);
+ }
+}
+
+RuleCondition RuleEditorDialog::conditionFromRow(int row) const
+{
+ RuleCondition cond;
+ QComboBox* fieldCombo = qobject_cast(m_conditionsTable->cellWidget(row, 0));
+ QComboBox* opCombo = qobject_cast(m_conditionsTable->cellWidget(row, 1));
+ QLineEdit* valueEdit = qobject_cast(m_conditionsTable->cellWidget(row, 2));
+ QCheckBox* caseCheck = qobject_cast(m_conditionsTable->cellWidget(row, 3));
+
+ if (fieldCombo) cond.field = static_cast(fieldCombo->currentData().toInt());
+ if (opCombo) cond.op = static_cast(opCombo->currentData().toInt());
+ if (valueEdit) cond.value = valueEdit->text();
+ if (caseCheck) cond.caseSensitive = caseCheck->isChecked();
+
+ return cond;
+}
+
+RuleAction RuleEditorDialog::actionFromRow(int row) const
+{
+ RuleAction act;
+ QComboBox* typeCombo = qobject_cast