MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - ui - ComposeWindow.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 87.6 % 547 479
Test Date: 2026-07-27 17:53:44 Functions: 100.0 % 43 43
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 44.3 % 1248 553

             Branch data     Line data    Source code
       1                 :             : #include "ComposeWindow.h"
       2                 :             : 
       3                 :             : #include "ui/ThemeManager.h"
       4                 :             : 
       5                 :             : #include <QAction>
       6                 :             : #include <QCloseEvent>
       7                 :             : #include <QDragEnterEvent>
       8                 :             : #include <QDir>
       9                 :             : #include <QFile>
      10                 :             : #include <QFileDialog>
      11                 :             : #include <QFileIconProvider>
      12                 :             : #include <QFileInfo>
      13                 :             : #include <QFormLayout>
      14                 :             : #include <QIcon>
      15                 :             : #include <QLabel>
      16                 :             : #include <QLineEdit>
      17                 :             : #include <QMessageBox>
      18                 :             : #include <QMimeData>
      19                 :             : #include <QPushButton>
      20                 :             : #include <QRegularExpression>
      21                 :             : #include <QTemporaryDir>
      22                 :             : #include <QToolBar>
      23                 :             : #include <QTimer>
      24                 :             : #include <QVBoxLayout>
      25                 :             : 
      26                 :             : #include "data/AccountConfig.h"
      27                 :             : #include "service/SmtpService.h"
      28                 :             : #include "service/Rfc2822Builder.h"
      29                 :             : #include "ui/ContactCompleter.h"
      30                 :             : #include "data/ContactStore.h"
      31                 :             : #include "ui/MentionTextEdit.h"
      32                 :             : #include "ui/PlainTextMessageBox.h"
      33                 :             : #include <QEvent>
      34                 :             : 
      35                 :             : 
      36                 :             : // 67.B3: all colors come from ThemeManager tokens (docs/DESIGN.md §2);
      37                 :             : // stylesheets are built at construction time with the active palette.
      38                 :         162 : static QString tok(const char *token) {
      39   [ +  -  +  - ]:         162 :   return ThemeManager::instance().color(QLatin1String(token));
      40                 :             : }
      41                 :             : 
      42         [ +  - ]:          81 : ComposeWindow::ComposeWindow(QWidget *parent) : QDialog(parent) {
      43         [ +  - ]:          81 :   setupUi();
      44                 :             : 
      45   [ +  -  +  -  :          81 :   m_smtp = new SmtpService(this);
             -  +  -  - ]
      46         [ +  - ]:          81 :   connect(m_smtp, &SmtpService::sendSuccess, this, [this]() {
      47   [ +  -  +  - ]:           1 :     m_statusLabel->setText(tr("Mail sent!"));
      48                 :           1 :     m_sendAction->setEnabled(true);
      49                 :           1 :     m_wasSentSuccessfully = true; // T-177: prevent draft-save prompt
      50                 :             :     // T-155: Emit recipients for contact tracking
      51   [ +  -  +  -  :           1 :     emit recipientsSent(parseRecipients(m_toEdit->text()),
                   +  - ]
      52   [ +  -  +  - ]:           2 :                         parseRecipients(m_ccEdit->text()),
      53   [ +  -  +  - ]:           2 :                         parseRecipients(m_bccEdit->text()));
      54                 :           1 :     emit messageSent();
      55         [ +  - ]:           1 :     QTimer::singleShot(1500, this, &QDialog::accept);
      56                 :           1 :   });
      57         [ +  - ]:          81 :   connect(m_smtp, &SmtpService::sendFailed, this, [this](const QString &err) {
      58   [ +  -  +  -  :           2 :     m_statusLabel->setText(tr("Error: %1").arg(err));
                   +  - ]
      59                 :           1 :     m_sendAction->setEnabled(true);
      60                 :           1 :   });
      61                 :          81 :   connect(m_smtp, &SmtpService::statusMessage, this,
      62         [ +  - ]:          82 :           [this](const QString &msg) { m_statusLabel->setText(msg); });
      63                 :          81 : }
      64                 :             : 
      65                 :          81 : void ComposeWindow::setupUi() {
      66   [ +  -  +  - ]:          81 :   setWindowTitle(tr("New Message"));
      67                 :          81 :   setMinimumSize(550, 400);
      68                 :          81 :   resize(750, 600);
      69                 :             : 
      70   [ +  -  -  +  :          81 :   auto *mainLayout = new QVBoxLayout(this);
                   -  - ]
      71                 :          81 :   mainLayout->setSpacing(0);
      72                 :          81 :   mainLayout->setContentsMargins(0, 0, 0, 0);
      73                 :             : 
      74                 :             :   // ═══ Toolbar ═══
      75   [ +  -  -  +  :          81 :   m_toolbar = new QToolBar(this);
                   -  - ]
      76         [ +  - ]:         162 :   m_toolbar->setObjectName(QStringLiteral("composeToolbar"));
      77                 :          81 :   m_toolbar->setMovable(false);
      78         [ +  - ]:          81 :   m_toolbar->setIconSize(QSize(18, 18));
      79                 :          81 :   m_toolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
      80                 :             : 
      81         [ +  - ]:          81 :   m_discardAction = m_toolbar->addAction(
      82         [ +  - ]:         162 :       QIcon::fromTheme(QStringLiteral("edit-delete"),
      83         [ +  - ]:         162 :                        QIcon::fromTheme(QStringLiteral("user-trash"))),
      84         [ +  - ]:         162 :       tr("Discard"));
      85                 :          81 :   connect(m_discardAction, &QAction::triggered, this,
      86         [ +  - ]:          81 :           &ComposeWindow::onDiscardClicked);
      87                 :             : 
      88         [ +  - ]:          81 :   m_attachAction = m_toolbar->addAction(
      89         [ +  - ]:         162 :       QIcon::fromTheme(QStringLiteral("mail-attachment"),
      90         [ +  - ]:         162 :                        QIcon::fromTheme(QStringLiteral("attachment"))),
      91         [ +  - ]:         162 :       tr("Attach"));
      92                 :          81 :   connect(m_attachAction, &QAction::triggered, this,
      93         [ +  - ]:          81 :           &ComposeWindow::onAttachClicked);
      94                 :             : 
      95                 :          81 :   m_toolbar->addSeparator();
      96   [ +  -  -  +  :          81 :   auto *spacer = new QWidget(this);
                   -  - ]
      97                 :          81 :   spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
      98                 :          81 :   m_toolbar->addWidget(spacer);
      99                 :             : 
     100         [ +  - ]:          81 :   m_sendAction = m_toolbar->addAction(
     101         [ +  - ]:         162 :       QIcon::fromTheme(QStringLiteral("mail-send"),
     102         [ +  - ]:         162 :                        QIcon::fromTheme(QStringLiteral("document-send"))),
     103         [ +  - ]:         162 :       tr("Send"));
     104                 :          81 :   connect(m_sendAction, &QAction::triggered, this,
     105         [ +  - ]:          81 :           &ComposeWindow::onSendClicked);
     106                 :             : 
     107                 :             :   // Style the send button blue (object name → main.qss rule)
     108                 :          81 :   auto *sendWidget = m_toolbar->widgetForAction(m_sendAction);
     109         [ +  - ]:          81 :   if (sendWidget) {
     110         [ +  - ]:         162 :     sendWidget->setObjectName(QStringLiteral("composeSendButton"));
     111                 :             :   }
     112                 :             : 
     113         [ +  - ]:          81 :   mainLayout->addWidget(m_toolbar);
     114                 :             : 
     115                 :             :   // ═══ Header Fields ═══
     116   [ +  -  -  +  :          81 :   auto *headerWidget = new QWidget(this);
                   -  - ]
     117   [ +  -  -  +  :          81 :   auto *headerLayout = new QFormLayout(headerWidget);
                   -  - ]
     118                 :          81 :   headerLayout->setContentsMargins(12, 8, 12, 8);
     119                 :          81 :   headerLayout->setSpacing(8);
     120                 :          81 :   headerLayout->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter);
     121                 :             :   // Sprint 69: header field styling comes from global QLineEdit/QLabel rules
     122                 :             :   // in main.qss — no inline QSS (was 3px radius, non-token).
     123                 :             : 
     124   [ +  -  -  +  :          81 :   m_fromEdit = new QLineEdit(this);
                   -  - ]
     125         [ +  - ]:         162 :   m_fromEdit->setObjectName(QStringLiteral("composeFromEdit"));
     126                 :          81 :   m_fromEdit->setReadOnly(true);
     127   [ +  -  +  - ]:          81 :   headerLayout->addRow(tr("From:"), m_fromEdit);
     128                 :             : 
     129   [ +  -  -  +  :          81 :   m_toEdit = new QLineEdit(this);
                   -  - ]
     130   [ +  -  +  - ]:          81 :   m_toEdit->setPlaceholderText(tr("recipient@example.com"));
     131   [ +  -  +  - ]:          81 :   headerLayout->addRow(tr("To:"), m_toEdit);
     132                 :             : 
     133                 :             :   // CC field
     134   [ +  -  -  +  :          81 :   m_ccEdit = new QLineEdit(this);
                   -  - ]
     135   [ +  -  +  - ]:          81 :   m_ccEdit->setPlaceholderText(tr("Optional"));
     136   [ +  -  +  - ]:          81 :   headerLayout->addRow(tr("CC:"), m_ccEdit);
     137                 :             : 
     138                 :             :   // BCC field (hidden by default)
     139   [ +  -  -  +  :          81 :   m_bccEdit = new QLineEdit(this);
                   -  - ]
     140   [ +  -  +  - ]:          81 :   m_bccEdit->setPlaceholderText(tr("Optional"));
     141                 :          81 :   m_bccEdit->hide();
     142                 :             : 
     143                 :             :   // CC/BCC toggle label
     144         [ -  - ]:           0 :   m_ccBccLabel = new QLabel(
     145                 :         162 :       QStringLiteral("<a href='#' style='color: %1; font-size: 11px;'>"
     146                 :             :                      "BCC anzeigen</a>")
     147   [ +  -  +  - ]:         162 :           .arg(tok("@link")),
     148   [ +  -  -  + ]:         243 :       this);
     149   [ +  -  +  - ]:          81 :   m_ccBccLabel->setCursor(Qt::PointingHandCursor);
     150         [ +  - ]:          81 :   connect(m_ccBccLabel, &QLabel::linkActivated, this, [this]() {
     151         [ #  # ]:           0 :     if (m_bccEdit->isVisible()) {
     152                 :           0 :       m_bccEdit->hide();
     153         [ #  # ]:           0 :       m_ccBccLabel->setText(QStringLiteral(
     154                 :             :           "<a href='#' style='color: %1; font-size: 11px;'>"
     155   [ #  #  #  # ]:           0 :           "BCC anzeigen</a>").arg(tok("@link")));
     156                 :             :     } else {
     157                 :           0 :       m_bccEdit->show();
     158         [ #  # ]:           0 :       m_ccBccLabel->setText(QStringLiteral(
     159                 :             :           "<a href='#' style='color: %1; font-size: 11px;'>"
     160   [ #  #  #  # ]:           0 :           "BCC ausblenden</a>").arg(tok("@link")));
     161                 :             :     }
     162                 :           0 :   });
     163         [ +  - ]:          81 :   headerLayout->addRow(QString(), m_ccBccLabel);
     164   [ +  -  +  - ]:          81 :   headerLayout->addRow(tr("BCC:"), m_bccEdit);
     165                 :             : 
     166   [ +  -  -  +  :          81 :   m_subjectEdit = new QLineEdit(this);
                   -  - ]
     167   [ +  -  +  - ]:          81 :   headerLayout->addRow(tr("Subject:"), m_subjectEdit);
     168                 :             : 
     169         [ +  - ]:          81 :   mainLayout->addWidget(headerWidget);
     170                 :             : 
     171                 :             :   // ═══ Separator ═══
     172   [ +  -  -  +  :          81 :   auto *separator = new QFrame(this);
                   -  - ]
     173         [ +  - ]:         162 :   separator->setObjectName(QStringLiteral("composeSeparator"));
     174                 :          81 :   separator->setFixedHeight(1);
     175         [ +  - ]:          81 :   mainLayout->addWidget(separator);
     176                 :             : 
     177                 :             :   // ═══ Body Editor ═══
     178   [ +  -  -  +  :          81 :   m_bodyEdit = new MentionTextEdit(this);
                   -  - ]
     179         [ +  - ]:         162 :   m_bodyEdit->setObjectName(QStringLiteral("composeBodyEdit"));
     180                 :          81 :   m_bodyEdit->setAcceptRichText(false);
     181                 :             :   // Sprint 69: styling lives in main.qss (QTextEdit#composeBodyEdit);
     182                 :             :   // UI typography (Inter) via the global QWidget font-family rule.
     183         [ +  - ]:          81 :   mainLayout->addWidget(m_bodyEdit, 1);
     184                 :             :   // T-161: Wire @-mention to auto-add to CC
     185                 :          81 :   connect(m_bodyEdit, &MentionTextEdit::mentionSelected, this,
     186         [ +  - ]:          81 :           [this](const QString &email, const QString &displayName) {
     187   [ +  -  +  - ]:           4 :             QString cc = m_ccEdit->text().trimmed();
     188                 :             :             // Check if email already in CC
     189   [ +  -  +  + ]:           4 :             if (cc.contains(email, Qt::CaseInsensitive))
     190                 :           1 :               return;
     191                 :           3 :             QString entry = displayName.isEmpty()
     192                 :           3 :                 ? email
     193   [ +  +  +  -  :           5 :                 : QStringLiteral("%1 <%2>").arg(displayName, email);
          +  +  +  +  -  
                -  -  - ]
     194         [ +  + ]:           3 :             if (!cc.isEmpty())
     195         [ +  - ]:           1 :               cc += QStringLiteral(", ");
     196         [ +  - ]:           3 :             cc += entry;
     197         [ +  - ]:           3 :             m_ccEdit->setText(cc);
     198         [ +  - ]:           3 :             m_statusLabel->setText(
     199   [ +  -  +  +  :           9 :                 tr("%1 added to CC").arg(
                   +  - ]
     200                 :           3 :                     displayName.isEmpty() ? email : displayName));
     201         [ +  + ]:           4 :           });
     202                 :             : 
     203                 :             :   // ═══ Attachment Chip Bar (T-106) ═══
     204   [ +  -  -  +  :          81 :   m_attachmentBar = new QWidget(this);
                   -  - ]
     205         [ +  - ]:         162 :   m_attachmentBar->setObjectName(QStringLiteral("composeAttachmentBar"));
     206   [ +  -  -  +  :          81 :   auto *attachLayout = new QVBoxLayout(m_attachmentBar);
                   -  - ]
     207                 :          81 :   attachLayout->setContentsMargins(12, 8, 12, 8);
     208                 :          81 :   attachLayout->setSpacing(4);
     209                 :          81 :   m_attachmentBar->hide(); // Only show when attachments exist
     210                 :             : 
     211   [ +  -  -  +  :          81 :   m_attachmentSizeLabel = new QLabel(m_attachmentBar);
                   -  - ]
     212         [ +  - ]:         162 :   m_attachmentSizeLabel->setObjectName(
     213                 :         162 :       QStringLiteral("composeAttachmentSizeLabel"));
     214                 :          81 :   m_attachmentSizeLabel->setTextFormat(Qt::PlainText);
     215         [ +  - ]:          81 :   attachLayout->addWidget(m_attachmentSizeLabel);
     216                 :             : 
     217         [ +  - ]:          81 :   mainLayout->addWidget(m_attachmentBar);
     218                 :             : 
     219                 :             :   // ═══ Status Bar ═══
     220   [ +  -  -  +  :          81 :   m_statusLabel = new QLabel(this);
                   -  - ]
     221         [ +  - ]:         162 :   m_statusLabel->setObjectName(QStringLiteral("composeStatusBar"));
     222                 :          81 :   m_statusLabel->setTextFormat(Qt::PlainText);
     223         [ +  - ]:          81 :   mainLayout->addWidget(m_statusLabel);
     224                 :             : 
     225                 :             :   // ═══ Drop Overlay (T-107) ═══
     226                 :          81 :   setAcceptDrops(true);
     227   [ +  -  -  +  :          81 :   m_dropOverlay = new QWidget(this);
                   -  - ]
     228         [ +  - ]:         162 :   m_dropOverlay->setObjectName(QStringLiteral("composeDropOverlay"));
     229                 :             :   {
     230                 :             :     // Sprint 69: only the dynamic translucent RGBA fill stays inline;
     231                 :             :     // the dashed border + radius live in main.qss.
     232         [ +  - ]:          81 :     QColor overlayBg(tok("@accent"));
     233         [ +  - ]:          81 :     overlayBg.setAlphaF(0.12f);
     234         [ +  - ]:         162 :     m_dropOverlay->setStyleSheet(
     235                 :         162 :         QStringLiteral("background: rgba(%1, %2, %3, 0.12);")
     236         [ +  - ]:         162 :             .arg(overlayBg.red())
     237         [ +  - ]:         162 :             .arg(overlayBg.green())
     238         [ +  - ]:         162 :             .arg(overlayBg.blue()));
     239                 :             :   }
     240   [ +  -  -  +  :          81 :   auto *overlayLayout = new QVBoxLayout(m_dropOverlay);
                   -  - ]
     241   [ +  -  +  -  :          81 :   auto *dropLabel = new QLabel(tr("↓ Drop files here"), m_dropOverlay);
             -  +  -  - ]
     242         [ +  - ]:         162 :   dropLabel->setObjectName(QStringLiteral("composeDropLabel"));
     243         [ +  - ]:          81 :   dropLabel->setAlignment(Qt::AlignCenter);
     244         [ +  - ]:          81 :   overlayLayout->addWidget(dropLabel);
     245                 :          81 :   m_dropOverlay->hide();
     246                 :             : 
     247                 :          81 :   m_toEdit->setFocus();
     248                 :             : 
     249                 :             :   // T-177: Ctrl+S = Save Draft
     250   [ +  -  +  -  :          81 :   auto *saveDraftAction = new QAction(tr("Save Draft"), this);
             -  +  -  - ]
     251   [ +  -  +  - ]:          81 :   saveDraftAction->setShortcut(QKeySequence::Save); // Ctrl+S
     252         [ +  - ]:          81 :   connect(saveDraftAction, &QAction::triggered, this, &ComposeWindow::saveDraft);
     253                 :          81 :   addAction(saveDraftAction);
     254                 :             : 
     255                 :             :   // T-177: Auto-save timer (60 seconds)
     256   [ +  -  -  +  :          81 :   m_autoSaveTimer = new QTimer(this);
                   -  - ]
     257                 :          81 :   m_autoSaveTimer->setInterval(60 * 1000);
     258         [ +  - ]:          81 :   connect(m_autoSaveTimer, &QTimer::timeout, this, [this]() {
     259   [ #  #  #  #  :           0 :     if (hasUnsavedChanges() && !m_draftsFolder.isEmpty()) {
                   #  # ]
     260                 :           0 :       saveDraft();
     261                 :             :     }
     262                 :           0 :   });
     263                 :             :   // Timer starts only after draftsFolder is set via setDraftsFolder()
     264                 :          81 : }
     265                 :             : 
     266                 :          47 : void ComposeWindow::setTo(const QString &to) { m_toEdit->setText(to); }
     267                 :           6 : void ComposeWindow::setCc(const QString &cc) { m_ccEdit->setText(cc); }
     268                 :           6 : void ComposeWindow::setBcc(const QString &bcc) {
     269                 :           6 :   m_bccEdit->setText(bcc);
     270   [ +  -  +  - ]:           6 :   if (!bcc.trimmed().isEmpty()) m_bccEdit->show(); // T-177: show BCC if pre-filled
     271                 :           6 : }
     272                 :          49 : void ComposeWindow::setSubject(const QString &subject) {
     273                 :          49 :   m_subjectEdit->setText(subject);
     274                 :          49 : }
     275                 :          35 : void ComposeWindow::setBody(const QString &body) {
     276                 :          35 :   m_bodyEdit->setPlainText(body);
     277                 :          35 : }
     278                 :           2 : QString ComposeWindow::subject() const { return m_subjectEdit->text(); }
     279                 :           1 : QString ComposeWindow::body() const { return m_bodyEdit->toPlainText(); }
     280                 :          53 : void ComposeWindow::setFrom(const QString &from) {
     281                 :          53 :   m_fromEdit->setText(from);
     282                 :          53 : }
     283                 :             : 
     284                 :          20 : void ComposeWindow::setSmtpConfig(const SmtpConfig &config) {
     285                 :             :   // T-507: Use unique_ptr to prevent memory leak
     286         [ +  - ]:          20 :   m_smtpConfig = std::make_unique<SmtpConfig>(config);
     287                 :          20 : }
     288                 :             : 
     289                 :             : // ═══════════════════════════════════════════════════════
     290                 :             : // T-177: Draft management
     291                 :             : // ═══════════════════════════════════════════════════════
     292                 :             : 
     293                 :           5 : void ComposeWindow::saveDraft() {
     294         [ -  + ]:           5 :   if (m_draftsFolder.isEmpty()) {
     295   [ #  #  #  # ]:           0 :     m_statusLabel->setText(tr("No Drafts folder found"));
     296                 :           1 :     return;
     297                 :             :   }
     298                 :             : 
     299                 :           5 :   QString buildError;
     300         [ +  - ]:           5 :   QByteArray msg = buildMessage(/*includeBcc=*/true, &buildError);
     301         [ +  + ]:           5 :   if (msg.isEmpty()) {
     302         [ +  - ]:           2 :     m_statusLabel->setText(buildError.isEmpty()
     303   [ -  +  -  - ]:           2 :                                ? tr("Could not build draft")
     304                 :             :                                : buildError);
     305                 :           1 :     return;
     306                 :             :   }
     307                 :             : 
     308   [ +  -  +  - ]:           4 :   m_statusLabel->setText(tr("Saving draft…"));
     309                 :             : 
     310         [ +  - ]:           4 :   emit draftSaveRequested(msg);
     311   [ +  +  +  + ]:           6 : }
     312                 :             : 
     313                 :          10 : void ComposeWindow::markDraftSaved() {
     314         [ +  - ]:          10 :   m_lastSavedContent = currentContentSnapshot();
     315   [ +  -  +  - ]:          10 :   m_statusLabel->setText(tr("Draft saved"));
     316         [ -  + ]:          10 :   if (m_closeAfterDraftSave) {
     317                 :           0 :     m_closeAfterDraftSave = false;
     318                 :           0 :     close();
     319                 :             :   }
     320                 :          10 : }
     321                 :             : 
     322                 :           3 : void ComposeWindow::markDraftSaveFailed(const QString &error) {
     323                 :           3 :   m_closeAfterDraftSave = false;
     324         [ +  - ]:           6 :   m_statusLabel->setText(error.isEmpty()
     325   [ +  +  +  -  :           8 :                              ? tr("Could not save draft")
                   +  - ]
     326   [ +  -  +  +  :           5 :                              : tr("Could not save draft: %1")
                   -  - ]
     327                 :             :                                    .arg(error));
     328   [ +  -  +  +  :           3 :   if (m_autoSaveTimer && !m_draftsFolder.isEmpty())
                   +  + ]
     329                 :           1 :     m_autoSaveTimer->start();
     330                 :           3 : }
     331                 :             : 
     332                 :          13 : bool ComposeWindow::hasUnsavedChanges() const {
     333         [ +  - ]:          13 :   return currentContentSnapshot() != m_lastSavedContent;
     334                 :             : }
     335                 :             : 
     336                 :          23 : QString ComposeWindow::currentContentSnapshot() const {
     337                 :          23 :   QStringList attachmentState;
     338         [ +  - ]:          23 :   attachmentState.reserve(m_attachmentPaths.size());
     339         [ +  + ]:          29 :   for (const auto &path : m_attachmentPaths) {
     340         [ +  - ]:           6 :     const QFileInfo info(path);
     341         [ +  - ]:           6 :     attachmentState.append(
     342                 :          12 :         QStringLiteral("%1|%2|%3|%4")
     343         [ +  - ]:          12 :             .arg(path)
     344   [ +  -  +  +  :          12 :             .arg(info.exists() ? 1 : 0)
                   +  - ]
     345   [ +  -  +  +  :          12 :             .arg(info.exists() ? info.size() : -1)
             +  -  +  - ]
     346   [ +  -  +  +  :          12 :             .arg(info.exists() ? info.lastModified().toMSecsSinceEpoch()
          +  -  +  -  +  
             -  +  +  -  
                      - ]
     347                 :             :                                : -1));
     348                 :           6 :   }
     349                 :             : 
     350   [ +  -  +  -  :          46 :   return m_toEdit->text() + m_ccEdit->text() + m_bccEdit->text() +
          +  -  +  -  +  
                      - ]
     351   [ +  -  +  -  :          92 :          m_subjectEdit->text() + m_bodyEdit->toPlainText() +
             +  -  +  - ]
     352   [ +  -  +  -  :         115 :          QStringLiteral("\n--attachments--\n") + attachmentState.join('\n');
                   +  - ]
     353                 :          23 : }
     354                 :             : 
     355                 :          29 : void ComposeWindow::setContactStore(ContactStore *store) {
     356                 :          29 :   m_contactStore = store;
     357         [ -  + ]:          29 :   if (!store)
     358                 :           0 :     return;
     359                 :             :   // Install completers on address fields
     360   [ +  -  -  +  :          29 :   m_toCompleter = new ContactCompleter(store, m_toEdit, this);
                   -  - ]
     361   [ +  -  -  +  :          29 :   m_ccCompleter = new ContactCompleter(store, m_ccEdit, this);
                   -  - ]
     362   [ +  -  -  +  :          29 :   m_bccCompleter = new ContactCompleter(store, m_bccEdit, this);
                   -  - ]
     363                 :             :   // Fix A: Wire @-mention in body editor
     364                 :          29 :   m_bodyEdit->setContactStore(store);
     365                 :             : }
     366                 :             : 
     367                 :           6 : bool ComposeWindow::addAttachmentData(const QString &filename,
     368                 :             :                                       const QByteArray &data,
     369                 :             :                                       const QByteArray &mimeType) {
     370                 :             :   Q_UNUSED(mimeType);
     371                 :             : 
     372         [ +  + ]:           6 :   if (!m_restoredAttachmentDir) {
     373         [ +  - ]:           5 :     m_restoredAttachmentDir = std::make_unique<QTemporaryDir>();
     374                 :             :   }
     375   [ +  -  -  + ]:           6 :   if (!m_restoredAttachmentDir->isValid()) {
     376                 :           0 :     return false;
     377                 :             :   }
     378                 :             : 
     379   [ +  -  +  - ]:           6 :   QString safeFilename = QFileInfo(filename).fileName();
     380         [ -  + ]:           6 :   if (safeFilename.isEmpty()) {
     381                 :           0 :     safeFilename = QStringLiteral("attachment.bin");
     382                 :             :   }
     383                 :             : 
     384         [ +  - ]:           6 :   const QFileInfo safeInfo(safeFilename);
     385         [ +  - ]:           6 :   const QString baseName = safeInfo.completeBaseName().isEmpty()
     386   [ -  +  -  - ]:           6 :                                ? QStringLiteral("attachment")
     387   [ -  +  +  - ]:           6 :                                : safeInfo.completeBaseName();
     388         [ +  - ]:           6 :   const QString suffix = safeInfo.suffix();
     389                 :             : 
     390                 :           6 :   QString candidate = safeFilename;
     391                 :           6 :   for (int counter = 2;
     392   [ +  -  +  -  :           6 :        QFileInfo::exists(m_restoredAttachmentDir->filePath(candidate));
                   -  + ]
     393                 :             :        ++counter) {
     394                 :           0 :     candidate = suffix.isEmpty()
     395   [ #  #  #  #  :           0 :                     ? QStringLiteral("%1-%2").arg(baseName).arg(counter)
          #  #  #  #  #  
          #  #  #  #  #  
             #  #  #  # ]
     396   [ #  #  #  #  :           0 :                     : QStringLiteral("%1-%2.%3")
             #  #  #  # ]
     397   [ #  #  #  #  :           0 :                           .arg(baseName)
                   #  # ]
     398   [ #  #  #  #  :           0 :                           .arg(counter)
             #  #  #  # ]
     399                 :           0 :                           .arg(suffix);
     400                 :             :   }
     401                 :             : 
     402         [ +  - ]:           6 :   const QString path = m_restoredAttachmentDir->filePath(candidate);
     403         [ +  - ]:           6 :   QFile file(path);
     404   [ +  -  -  + ]:           6 :   if (!file.open(QIODevice::WriteOnly)) {
     405                 :           0 :     return false;
     406                 :             :   }
     407   [ +  -  +  -  :           6 :   if (!data.isEmpty() && file.write(data) != data.size()) {
             -  +  -  + ]
     408         [ #  # ]:           0 :     file.close();
     409         [ #  # ]:           0 :     file.remove();
     410                 :           0 :     return false;
     411                 :             :   }
     412         [ +  - ]:           6 :   file.close();
     413                 :             : 
     414         [ +  - ]:           6 :   addAttachment(path);
     415                 :           6 :   return true;
     416                 :           6 : }
     417                 :             : 
     418                 :             : // ═══════════════════════════════════════════════════════
     419                 :             : // Actions
     420                 :             : // ═══════════════════════════════════════════════════════
     421                 :             : 
     422                 :           3 : void ComposeWindow::onSendClicked() {
     423   [ +  -  +  -  :           3 :   if (m_toEdit->text().trimmed().isEmpty()) {
                   +  + ]
     424   [ +  -  +  - ]:           1 :     QMessageBox::warning(this, tr("Missing recipients"),
     425         [ +  - ]:           2 :                          tr("Please enter at least one recipient."));
     426         [ +  - ]:           1 :     m_toEdit->setFocus();
     427                 :           3 :     return;
     428                 :             :   }
     429         [ +  + ]:           2 :   if (!m_smtpConfig) {
     430         [ +  - ]:           1 :     QMessageBox::warning(
     431         [ +  - ]:           2 :         this, tr("SMTP not configured"),
     432         [ +  - ]:           2 :         tr("Please configure SMTP in the settings."));
     433                 :           1 :     return;
     434                 :             :   }
     435                 :             : 
     436   [ +  -  +  - ]:           1 :   QStringList recipients = parseRecipients(m_toEdit->text());
     437   [ +  -  +  -  :           1 :   recipients.append(parseRecipients(m_ccEdit->text()));
                   +  - ]
     438   [ +  -  +  -  :           1 :   recipients.append(parseRecipients(m_bccEdit->text()));
                   +  - ]
     439                 :             : 
     440                 :             :   // T-502: Build message WITHOUT Bcc header for SMTP transport
     441                 :           1 :   QString buildError;
     442         [ +  - ]:           1 :   QByteArray message = buildMessage(/*includeBcc=*/false, &buildError);
     443         [ +  - ]:           1 :   if (message.isEmpty()) {
     444                 :           1 :     const QString errorText = buildError.isEmpty()
     445         [ -  + ]:           1 :                                   ? tr("Could not build message")
     446         [ -  - ]:           1 :                                   : buildError;
     447         [ +  - ]:           1 :     m_statusLabel->setText(errorText);
     448   [ +  -  +  - ]:           1 :     PlainTextMessageBox::warning(this, tr("Attachment unreadable"),
     449                 :             :                                  errorText);
     450                 :           1 :     return;
     451                 :           1 :   }
     452                 :             : 
     453         [ #  # ]:           0 :   m_sendAction->setEnabled(false);
     454   [ #  #  #  # ]:           0 :   m_statusLabel->setText(tr("Sending..."));
     455   [ #  #  #  # ]:           0 :   m_smtp->sendMail(*m_smtpConfig, m_fromEdit->text(), recipients, message);
     456   [ -  +  -  +  :           3 : }
                   -  + ]
     457                 :             : 
     458                 :           2 : void ComposeWindow::onAttachClicked() {
     459                 :           2 :   QStringList files = QFileDialog::getOpenFileNames(
     460   [ +  -  +  - ]:           2 :       this, tr("Attach files"), QString());
     461   [ +  -  +  -  :           3 :   for (const auto &file : files) {
                   +  + ]
     462         [ +  - ]:           1 :     addAttachment(file);
     463                 :             :   }
     464                 :           2 : }
     465                 :             : 
     466                 :           1 : void ComposeWindow::onDiscardClicked() {
     467                 :             :   // If there is content, show confirmation
     468   [ +  -  +  -  :           2 :   bool hasContent = !m_toEdit->text().trimmed().isEmpty() ||
             -  -  -  - ]
     469   [ +  -  +  -  :           2 :                     !m_subjectEdit->text().trimmed().isEmpty() ||
          +  -  +  -  +  
             -  -  -  -  
                      - ]
     470   [ +  -  +  -  :           5 :                     !m_bodyEdit->toPlainText().trimmed().isEmpty() ||
          +  -  +  -  +  
          -  +  -  +  -  
             -  -  -  - ]
     471   [ -  +  +  - ]:           2 :                     !m_attachmentPaths.isEmpty();
     472                 :             : 
     473         [ -  + ]:           1 :   if (hasContent) {
     474         [ #  # ]:           0 :     auto result = QMessageBox::question(
     475         [ #  # ]:           0 :         this, tr("Discard draft?"),
     476         [ #  # ]:           0 :         tr("Do you really want to discard this message?"),
     477                 :             :         QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
     478         [ #  # ]:           0 :     if (result != QMessageBox::Yes)
     479                 :           0 :       return;
     480                 :             :   }
     481                 :             : 
     482                 :           1 :   reject();
     483                 :             : }
     484                 :             : 
     485                 :           4 : void ComposeWindow::closeEvent(QCloseEvent *event) {
     486                 :             :   // T-177: If mail was sent → close immediately
     487         [ -  + ]:           4 :   if (m_wasSentSuccessfully) {
     488                 :           0 :     event->accept();
     489                 :           0 :     return;
     490                 :             :   }
     491                 :             : 
     492                 :             :   // If no unsaved changes and a draft already exists → close
     493   [ +  +  +  -  :           4 :   if (!hasUnsavedChanges() && m_draftUid > 0) {
                   +  + ]
     494                 :           1 :     event->accept();
     495                 :           1 :     return;
     496                 :             :   }
     497                 :             : 
     498   [ +  -  +  -  :           6 :   bool hasContent = !m_toEdit->text().trimmed().isEmpty() ||
             -  -  -  - ]
     499   [ +  -  +  -  :           6 :                     !m_subjectEdit->text().trimmed().isEmpty() ||
          +  -  +  -  +  
             -  -  -  -  
                      - ]
     500   [ +  -  +  -  :          14 :                     !m_bodyEdit->toPlainText().trimmed().isEmpty() ||
          +  -  +  -  +  
          +  +  -  +  -  
             -  -  -  - ]
     501   [ -  +  +  - ]:           5 :                     !m_attachmentPaths.isEmpty();
     502                 :             : 
     503         [ +  + ]:           3 :   if (!hasContent) {
     504                 :           2 :     event->accept();
     505                 :           2 :     return;
     506                 :             :   }
     507                 :             : 
     508                 :             :   // T-177: Stop auto-save while dialog is open
     509         [ +  - ]:           1 :   if (m_autoSaveTimer) m_autoSaveTimer->stop();
     510                 :             : 
     511                 :             :   // T-177: 3-way dialog: Save / Discard / Cancel
     512         [ +  - ]:           1 :   if (!m_draftsFolder.isEmpty()) {
     513         [ +  - ]:           1 :     QMessageBox msgBox(this);
     514   [ +  -  +  - ]:           1 :     msgBox.setWindowTitle(tr("Draft"));
     515   [ +  -  +  - ]:           1 :     msgBox.setText(tr("What do you want to do with the draft?"));
     516         [ +  - ]:           1 :     msgBox.setTextFormat(Qt::PlainText);
     517         [ +  - ]:           1 :     msgBox.setIcon(QMessageBox::Question);
     518                 :             : 
     519   [ +  -  +  - ]:           1 :     auto *saveBtn = msgBox.addButton(tr("Save Draft"),
     520                 :             :                                       QMessageBox::AcceptRole);
     521   [ +  -  +  - ]:           1 :     auto *discardBtn = msgBox.addButton(tr("Discard"),
     522                 :             :                                          QMessageBox::DestructiveRole);
     523         [ +  - ]:           1 :     msgBox.addButton(QMessageBox::Cancel);
     524                 :             : 
     525         [ +  - ]:           1 :     msgBox.exec();
     526                 :             : 
     527   [ +  -  -  + ]:           1 :     if (msgBox.clickedButton() == saveBtn) {
     528                 :           0 :       m_closeAfterDraftSave = true;
     529         [ #  # ]:           0 :       saveDraft();
     530                 :           0 :       event->ignore();
     531   [ +  -  +  - ]:           1 :     } else if (msgBox.clickedButton() == discardBtn) {
     532         [ -  + ]:           1 :       if (m_draftUid > 0) {
     533         [ #  # ]:           0 :         emit draftDiscarded(m_draftUid);
     534                 :             :       }
     535                 :           1 :       event->accept();
     536                 :             :     } else {
     537   [ #  #  #  # ]:           0 :       if (m_autoSaveTimer) m_autoSaveTimer->start();
     538                 :           0 :       event->ignore();
     539                 :             :     }
     540                 :           1 :   } else {
     541                 :             :     // Fallback: no Drafts folder configured → simple discard dialog
     542         [ #  # ]:           0 :     auto result = QMessageBox::question(
     543         [ #  # ]:           0 :         this, tr("Discard draft?"),
     544         [ #  # ]:           0 :         tr("Do you really want to discard this message?"),
     545                 :             :         QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
     546         [ #  # ]:           0 :     if (result == QMessageBox::Yes) {
     547                 :           0 :       event->accept();
     548                 :             :     } else {
     549         [ #  # ]:           0 :       if (m_autoSaveTimer) m_autoSaveTimer->start();
     550                 :           0 :       event->ignore();
     551                 :             :     }
     552                 :             :   }
     553                 :             : }
     554                 :             : 
     555                 :             : // ═══════════════════════════════════════════════════════
     556                 :             : // Message Building
     557                 :             : // ═══════════════════════════════════════════════════════
     558                 :             : 
     559                 :          36 : QByteArray ComposeWindow::buildMessage(bool includeBcc, QString *errorMessage) {
     560         [ +  + ]:          36 :   if (errorMessage)
     561                 :           7 :     errorMessage->clear();
     562                 :             : 
     563                 :             :   // T-184: Delegate to Rfc2822Builder for proper RFC-2822 compliance
     564                 :          36 :   Rfc2822Builder builder;
     565                 :             : 
     566                 :             :   // From: parse "Display Name <email>" or plain email
     567   [ +  -  +  - ]:          36 :   QString fromText = m_fromEdit->text().trimmed();
     568                 :          36 :   int ltIdx = fromText.indexOf('<');
     569                 :          36 :   int gtIdx = fromText.indexOf('>');
     570   [ -  +  -  - ]:          36 :   if (ltIdx > 0 && gtIdx > ltIdx) {
     571   [ #  #  #  #  :           0 :     builder.setFrom(fromText.left(ltIdx).trimmed(),
                   #  # ]
     572   [ #  #  #  # ]:           0 :                     fromText.mid(ltIdx + 1, gtIdx - ltIdx - 1).trimmed());
     573                 :             :   } else {
     574         [ +  - ]:          36 :     builder.setFrom({}, fromText);
     575                 :             :   }
     576                 :             : 
     577   [ +  -  +  -  :          36 :   builder.setTo(parseRecipientList(m_toEdit->text()));
                   +  - ]
     578   [ +  -  +  -  :          36 :   if (!m_ccEdit->text().trimmed().isEmpty())
                   +  + ]
     579   [ +  -  +  -  :           6 :     builder.setCc(parseRecipientList(m_ccEdit->text()));
                   +  - ]
     580   [ +  -  +  -  :          36 :   if (!m_bccEdit->text().trimmed().isEmpty())
                   +  + ]
     581   [ +  -  +  -  :           6 :     builder.setBcc(parseRecipientList(m_bccEdit->text()));
                   +  - ]
     582                 :             : 
     583   [ +  -  +  - ]:          36 :   builder.setSubject(m_subjectEdit->text());
     584                 :             : 
     585                 :             :   // Threading: In-Reply-To + References (set by reply/forward)
     586         [ +  + ]:          36 :   if (!m_inReplyTo.isEmpty())
     587         [ +  - ]:           4 :     builder.setInReplyTo(m_inReplyTo);
     588         [ +  + ]:          36 :   if (!m_references.isEmpty())
     589         [ +  - ]:           4 :     builder.setReferences(m_references);
     590                 :             : 
     591   [ +  -  +  - ]:          36 :   builder.setBodyText(m_bodyEdit->toPlainText());
     592                 :             : 
     593                 :             :   // Attachments
     594   [ +  -  +  -  :          41 :   for (const auto &path : m_attachmentPaths) {
                   +  + ]
     595   [ +  -  +  + ]:           8 :     if (!builder.addAttachmentFromFile(path)) {
     596                 :             :       const QString errorText =
     597         [ +  - ]:           3 :           tr("Attachment cannot be read: %1")
     598   [ +  -  +  - ]:           3 :               .arg(QDir::toNativeSeparators(path));
     599         [ +  - ]:           3 :       if (errorMessage)
     600                 :           3 :         *errorMessage = errorText;
     601         [ +  - ]:           3 :       m_lastBuiltMessage.clear();
     602                 :           3 :       m_lastMessageId.clear();
     603                 :           3 :       return {};
     604                 :           3 :     }
     605                 :             :   }
     606                 :             : 
     607                 :             :   // T-502: Control BCC header inclusion
     608                 :          33 :   builder.setIncludeBcc(includeBcc);
     609                 :             : 
     610         [ +  - ]:          33 :   m_lastBuiltMessage = builder.build();
     611                 :          33 :   m_lastMessageId = builder.messageId();
     612                 :          33 :   return m_lastBuiltMessage;
     613                 :          36 : }
     614                 :             : 
     615                 :          54 : QStringList ComposeWindow::parseRecipientList(const QString &field) const {
     616                 :             :   // Split on comma/semicolon but preserve "Name <email>" groups
     617                 :          54 :   QStringList result;
     618   [ +  -  +  -  :         116 :   for (auto &part : field.split(QRegularExpression(QStringLiteral("[,;]")))) {
          +  -  +  -  +  
                      + ]
     619         [ +  - ]:          62 :     QString trimmed = part.trimmed();
     620         [ +  + ]:          62 :     if (!trimmed.isEmpty())
     621         [ +  - ]:          56 :       result.append(trimmed);
     622                 :         116 :   }
     623                 :          54 :   return result;
     624                 :           0 : }
     625                 :             : 
     626                 :          11 : QStringList ComposeWindow::parseRecipients(const QString &field) const {
     627                 :          11 :   QStringList result;
     628   [ +  -  +  -  :          25 :   for (auto &addr : field.split(QRegularExpression("[,;]"))) {
          +  -  +  -  +  
                -  +  + ]
     629         [ +  - ]:          14 :     QString trimmed = addr.trimmed();
     630                 :          14 :     int ltIdx = trimmed.indexOf('<');
     631                 :          14 :     int gtIdx = trimmed.indexOf('>');
     632   [ +  +  +  - ]:          14 :     if (ltIdx >= 0 && gtIdx > ltIdx) {
     633   [ +  -  +  - ]:           5 :       trimmed = trimmed.mid(ltIdx + 1, gtIdx - ltIdx - 1).trimmed();
     634                 :             :     }
     635         [ +  + ]:          14 :     if (!trimmed.isEmpty()) {
     636         [ +  - ]:           8 :       result.append(trimmed);
     637                 :             :     }
     638                 :          25 :   }
     639                 :          11 :   return result;
     640                 :           0 : }
     641                 :             : 
     642                 :             : // ═══════════════════════════════════════════════════════
     643                 :             : // Attachment Chips (T-106)
     644                 :             : // ═══════════════════════════════════════════════════════
     645                 :             : 
     646                 :          34 : static QString formatFileSize(qint64 bytes) {
     647         [ +  + ]:          34 :   if (bytes < 1024)
     648   [ +  -  +  - ]:          29 :     return QString::number(bytes) + QStringLiteral(" B");
     649         [ +  + ]:           5 :   if (bytes < 1024 * 1024)
     650   [ +  -  +  - ]:           3 :     return QString::number(bytes / 1024.0, 'f', 1) + QStringLiteral(" KB");
     651         [ +  - ]:           4 :   return QString::number(bytes / (1024.0 * 1024.0), 'f', 1) +
     652         [ +  - ]:           6 :          QStringLiteral(" MB");
     653                 :             : }
     654                 :             : 
     655                 :          15 : void ComposeWindow::addAttachment(const QString &path) {
     656         [ -  + ]:          15 :   if (m_attachmentPaths.contains(path))
     657                 :           0 :     return;
     658                 :          15 :   m_attachmentPaths.append(path);
     659                 :          15 :   refreshAttachmentBar();
     660                 :             : }
     661                 :             : 
     662                 :           3 : void ComposeWindow::removeAttachment(int index) {
     663   [ +  -  +  -  :           3 :   if (index >= 0 && index < m_attachmentPaths.size()) {
                   +  - ]
     664                 :           3 :     m_attachmentPaths.removeAt(index);
     665                 :           3 :     refreshAttachmentBar();
     666                 :             :   }
     667                 :           3 : }
     668                 :             : 
     669                 :          18 : void ComposeWindow::refreshAttachmentBar() {
     670                 :             :   // Clear existing chip widgets
     671                 :          18 :   auto *layout = m_attachmentBar->layout();
     672         [ +  + ]:          40 :   while (layout->count() > 0) {
     673                 :          22 :     auto *item = layout->takeAt(0);
     674         [ +  - ]:          22 :     if (auto *widget = item->widget()) {
     675         [ +  + ]:          22 :       if (widget != m_attachmentSizeLabel)
     676                 :           5 :         widget->deleteLater();
     677                 :             :     }
     678         [ +  - ]:          22 :     delete item;
     679                 :             :   }
     680                 :             : 
     681         [ +  + ]:          18 :   if (m_attachmentPaths.isEmpty()) {
     682                 :           2 :     m_attachmentBar->hide();
     683         [ +  - ]:           2 :     m_statusLabel->setText(QString());
     684                 :           2 :     return;
     685                 :             :   }
     686                 :             : 
     687                 :             :   // Chip row
     688   [ +  -  -  +  :          16 :   auto *chipRow = new QWidget(m_attachmentBar);
                   -  - ]
     689   [ +  -  -  +  :          16 :   auto *chipLayout = new QHBoxLayout(chipRow);
                   -  - ]
     690                 :          16 :   chipLayout->setContentsMargins(0, 0, 0, 0);
     691                 :          16 :   chipLayout->setSpacing(6);
     692                 :             : 
     693                 :          16 :   qint64 totalSize = 0;
     694                 :             : 
     695         [ +  + ]:          34 :   for (int i = 0; i < m_attachmentPaths.size(); ++i) {
     696   [ +  -  +  - ]:          18 :     QFileInfo info(m_attachmentPaths[i]);
     697         [ +  - ]:          18 :     totalSize += info.size();
     698                 :             : 
     699                 :             :     // Chip widget
     700   [ +  -  +  -  :          18 :     auto *chip = new QWidget(chipRow);
             -  +  -  - ]
     701         [ +  - ]:          36 :     chip->setObjectName(QStringLiteral("attachmentChip"));
     702   [ +  -  +  -  :          18 :     auto *chipInner = new QHBoxLayout(chip);
             -  +  -  - ]
     703         [ +  - ]:          18 :     chipInner->setContentsMargins(8, 2, 4, 2);
     704         [ +  - ]:          18 :     chipInner->setSpacing(4);
     705                 :             : 
     706                 :             :     // Filename + size
     707                 :             :     auto *nameLabel = new QLabel(
     708                 :          36 :         QStringLiteral("%1 (%2)")
     709   [ +  -  +  -  :          36 :             .arg(info.fileName(), formatFileSize(info.size())),
             +  -  +  - ]
     710   [ +  -  +  -  :          54 :         chip);
             -  +  -  - ]
     711         [ +  - ]:          36 :     nameLabel->setObjectName(QStringLiteral("attachmentChipName"));
     712         [ +  - ]:          18 :     nameLabel->setTextFormat(Qt::PlainText);
     713         [ +  - ]:          18 :     chipInner->addWidget(nameLabel);
     714                 :             : 
     715                 :             :     // Remove button
     716   [ +  -  +  -  :          36 :     auto *removeBtn = new QPushButton(QStringLiteral("×"), chip);
             -  +  -  - ]
     717         [ +  - ]:          36 :     removeBtn->setObjectName(QStringLiteral("attachmentChipRemove"));
     718         [ +  - ]:          18 :     removeBtn->setFixedSize(16, 16);
     719                 :             :     // T-507/H7: Capture path instead of index — prevents stale index
     720                 :             :     // after earlier items are removed
     721         [ +  - ]:          18 :     connect(removeBtn, &QPushButton::clicked, this,
     722         [ +  - ]:          36 :             [this, path = m_attachmentPaths[i]]() {
     723                 :           0 :               int idx = m_attachmentPaths.indexOf(path);
     724         [ #  # ]:           0 :               if (idx >= 0) removeAttachment(idx);
     725                 :           0 :             });
     726         [ +  - ]:          18 :     chipInner->addWidget(removeBtn);
     727                 :             : 
     728         [ +  - ]:          18 :     chipLayout->addWidget(chip);
     729                 :          18 :   }
     730                 :             : 
     731                 :          16 :   chipLayout->addStretch();
     732                 :          16 :   layout->addWidget(chipRow);
     733                 :             : 
     734                 :             :   // Total size label
     735         [ +  - ]:          16 :   m_attachmentSizeLabel->setText(
     736   [ +  -  +  -  :          48 :       tr("Gesamt: %1").arg(formatFileSize(totalSize)));
                   +  - ]
     737                 :          16 :   layout->addWidget(m_attachmentSizeLabel);
     738                 :             : 
     739                 :             :   // Size warning — toggle a property the QSS reads; the base rule
     740                 :             :   // (QLabel#composeAttachmentSizeLabel) lives in main.qss.
     741         [ -  + ]:          16 :   if (totalSize > 25 * 1024 * 1024) {
     742         [ #  # ]:           0 :     m_attachmentSizeLabel->setStyleSheet(
     743                 :           0 :         QStringLiteral("color: %1; font-weight: bold;")
     744   [ #  #  #  # ]:           0 :             .arg(tok("@danger")));
     745         [ #  # ]:           0 :     m_attachmentSizeLabel->setText(
     746   [ #  #  #  #  :           0 :         tr("⚠ Gesamt: %1 (> 25 MB!)").arg(formatFileSize(totalSize)));
                   #  # ]
     747                 :             :   } else {
     748         [ +  - ]:          16 :     m_attachmentSizeLabel->setStyleSheet(QString());
     749                 :             :   }
     750                 :             : 
     751                 :          16 :   m_attachmentBar->show();
     752         [ +  - ]:          16 :   m_statusLabel->setText(
     753   [ +  -  +  - ]:          48 :       tr("%1 attachment(s)").arg(m_attachmentPaths.size()));
     754                 :             : }
     755                 :             : 
     756                 :             : // ═══════════════════════════════════════════════════════
     757                 :             : // Drag & Drop (T-107)
     758                 :             : // ═══════════════════════════════════════════════════════
     759                 :             : 
     760                 :           3 : void ComposeWindow::showDropOverlay(bool show) {
     761         [ +  + ]:           3 :   if (show) {
     762         [ +  - ]:           1 :     m_dropOverlay->setGeometry(rect().adjusted(8, 8, -8, -8));
     763                 :           1 :     m_dropOverlay->raise();
     764                 :           1 :     m_dropOverlay->show();
     765                 :             :   } else {
     766                 :           2 :     m_dropOverlay->hide();
     767                 :             :   }
     768                 :           3 : }
     769                 :             : 
     770                 :           1 : void ComposeWindow::dragEnterEvent(QDragEnterEvent *event) {
     771         [ +  - ]:           1 :   if (event->mimeData()->hasUrls()) {
     772                 :             :     // Only accept file URLs
     773                 :           1 :     bool hasFiles = false;
     774   [ +  -  +  -  :           1 :     for (const auto &url : event->mimeData()->urls()) {
             +  -  +  - ]
     775   [ +  -  +  -  :           1 :       if (url.isLocalFile() && QFileInfo(url.toLocalFile()).isFile()) {
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  -  
                -  -  - ]
     776                 :           1 :         hasFiles = true;
     777                 :           1 :         break;
     778                 :             :       }
     779                 :           1 :     }
     780         [ +  - ]:           1 :     if (hasFiles) {
     781                 :           1 :       event->acceptProposedAction();
     782                 :           1 :       showDropOverlay(true);
     783                 :           1 :       return;
     784                 :             :     }
     785                 :             :   }
     786                 :           0 :   event->ignore();
     787                 :             : }
     788                 :             : 
     789                 :           1 : void ComposeWindow::dragLeaveEvent(QDragLeaveEvent *event) {
     790                 :           1 :   showDropOverlay(false);
     791                 :           1 :   QDialog::dragLeaveEvent(event);
     792                 :           1 : }
     793                 :             : 
     794                 :           1 : void ComposeWindow::dropEvent(QDropEvent *event) {
     795                 :           1 :   showDropOverlay(false);
     796                 :             : 
     797         [ -  + ]:           1 :   if (!event->mimeData()->hasUrls()) {
     798                 :           0 :     event->ignore();
     799                 :           0 :     return;
     800                 :             :   }
     801                 :             : 
     802                 :           1 :   int added = 0;
     803   [ +  -  +  -  :           2 :   for (const auto &url : event->mimeData()->urls()) {
             +  -  +  + ]
     804   [ +  -  +  - ]:           1 :     if (url.isLocalFile()) {
     805   [ +  -  +  - ]:           1 :       QFileInfo info(url.toLocalFile());
     806   [ +  -  +  - ]:           1 :       if (info.isFile()) {
     807   [ +  -  +  - ]:           1 :         addAttachment(info.absoluteFilePath());
     808                 :           1 :         ++added;
     809                 :             :       }
     810                 :           1 :     }
     811                 :           1 :   }
     812                 :             : 
     813         [ +  - ]:           1 :   if (added > 0) {
     814                 :           1 :     event->acceptProposedAction();
     815                 :             :   } else {
     816                 :           0 :     event->ignore();
     817                 :             :   }
     818                 :             : }
     819                 :             : 
     820                 :             : // T-304: Runtime language switching
     821                 :         209 : void ComposeWindow::changeEvent(QEvent *event) {
     822         [ +  + ]:         209 :   if (event->type() == QEvent::LanguageChange)
     823                 :           1 :     retranslateUi();
     824                 :         209 :   QDialog::changeEvent(event);
     825                 :         209 : }
     826                 :             : 
     827                 :           1 : void ComposeWindow::retranslateUi() {
     828   [ +  -  +  - ]:           1 :   setWindowTitle(tr("New Message"));
     829   [ +  -  +  -  :           1 :   if (m_sendAction) m_sendAction->setText(tr("Send"));
                   +  - ]
     830   [ +  -  +  -  :           1 :   if (m_discardAction) m_discardAction->setText(tr("Discard"));
                   +  - ]
     831   [ +  -  +  -  :           1 :   if (m_attachAction) m_attachAction->setText(tr("Attach"));
                   +  - ]
     832                 :           1 : }
        

Generated by: LCOV version 2.0-1