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.4 % 541 473
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 41 41
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 44.3 % 1244 551

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

Generated by: LCOV version 2.0-1