presentation/ComposeReplyPrefill.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "ComposeReplyPrefill.h" | ||
| 2 | |||
| 3 | #include "ComposeLog.h" | ||
| 4 | #include "ComposeQuoteSource.h" | ||
| 5 | #include "ComposeRecipients.h" | ||
| 6 | #include "QuickComposeForwardAttachments.h" | ||
| 7 | #include "WorkspaceState.h" | ||
| 8 | #include "app/core/MailSessionCore.h" | ||
| 9 | #include "app/core/AccountSession.h" | ||
| 10 | #include "controller/MailController.h" | ||
| 11 | #include "data/MailCache.h" | ||
| 12 | #include "service/MimeParser.h" | ||
| 13 | #include <QCoreApplication> | ||
| 14 | #include <QFutureWatcher> | ||
| 15 | #include <QLocale> | ||
| 16 | #include <QTemporaryDir> | ||
| 17 | #include <QTimer> | ||
| 18 | #include <QtConcurrent> | ||
| 19 | |||
| 20 | 795 | ComposeReplyPrefill::ComposeReplyPrefill(MailSessionCore &session, WorkspaceState &workspace, | |
| 21 | const QString &profileDirectory, | ||
| 22 | const QHash<qint64, QString> &folderPathById, | ||
| 23 | const QHash<qint64, quint32> &folderEpochById, | ||
| 24 | 795 | Deps deps, QObject *parent) | |
| 25 | 795 | : QObject(parent), m_session(session), m_workspace(workspace), | |
| 26 | 795 | m_profileDirectory(profileDirectory), m_folderPathById(folderPathById), | |
| 27 |
1/1✓ Branch 6 → 7 taken 795 times.
|
795 | m_folderEpochById(folderEpochById), m_deps(std::move(deps)) {} |
| 28 | |||
| 29 | 795 | void ComposeReplyPrefill::connectSource() { | |
| 30 | 795 | MailSessionCore &session = m_session; | |
| 31 | 795 | m_composeSourcePool.setMaxThreadCount(1); | |
| 32 | 795 | m_composeSourcePool.setExpiryTimeout(-1); | |
| 33 | // Another mail selected: the reply being prepared is no longer wanted. A | ||
| 34 | // selection signal for the same mail (the list re-selects it when its | ||
| 35 | // page loads) keeps it — Ctrl+R right after the start was dropped and said | ||
| 36 | // "could not be loaded" (KJ2, pipeline #7218). | ||
| 37 |
1/1✓ Branch 4 → 5 taken 795 times.
|
795 | connect(&m_workspace, &WorkspaceState::selectionChanged, this, [this] { |
| 38 |
5/6✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 7 taken 1568 times.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 7 not taken.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 1568 times.
|
1569 | if (m_composeSourceToken > 0 && m_workspace.mailId() == m_composeSourceMail) return; |
| 39 | 1568 | cancelReplyDraft(); | |
| 40 | }); | ||
| 41 | // The backend drops the folder's cached epoch in the receiver connected | ||
| 42 | // right before this one (formerly the first statement of this handler). | ||
| 43 |
1/1✓ Branch 7 → 8 taken 795 times.
|
795 | connect(session.cache(), &MailCache::folderIdentityInvalidated, this, |
| 44 | 795 | [this](qint64 folderId) { | |
| 45 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 16 times.
|
16 | if (m_composeSourceMail.folderId == folderId) cancelReplyDraft(); |
| 46 | 16 | }); | |
| 47 |
1/1✓ Branch 10 → 11 taken 795 times.
|
795 | connect(session.controller(), &MailController::folderIdentityInvalidated, |
| 48 | 795 | this, [this](qint64 folderId) { | |
| 49 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 11 times.
|
11 | if (m_composeSourceMail.folderId == folderId) cancelReplyDraft(); |
| 50 | 11 | }); | |
| 51 | 795 | } | |
| 52 | |||
| 53 | 795 | void ComposeReplyPrefill::shutdown() { | |
| 54 | 795 | m_composeSourceToken = 0; | |
| 55 | 795 | cancelReplyDraft(); | |
| 56 |
1/1✓ Branch 4 → 5 taken 795 times.
|
795 | m_composeSourcePool.waitForDone(); |
| 57 | 795 | } | |
| 58 | |||
| 59 | 2402 | void ComposeReplyPrefill::cancelReplyDraft() { | |
| 60 | 2402 | const int oldToken = m_composeSourceToken; | |
| 61 | 2402 | m_composeSourceToken = 0; | |
| 62 | 2402 | ++m_composeSourceGeneration; | |
| 63 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 2402 times.
|
2402 | if (m_composeSourceCancelled) m_composeSourceCancelled->store(true); |
| 64 | 2402 | m_composeSourceCancelled.reset(); | |
| 65 | 2402 | m_composeSourceMail = {}; | |
| 66 |
1/3✗ Branch 9 → 10 not taken.
✓ Branch 9 → 14 taken 2402 times.
✗ Branch 11 → 12 not taken.
|
2402 | if (oldToken > 0) emit replyDraftReady(oldToken, {}); |
| 67 | 2402 | } | |
| 68 | |||
| 69 | // F-20 (SPRINT-104): every common reply prefix counts as one — an | ||
| 70 | // already-prefixed subject keeps a single canonical "Re:" instead of | ||
| 71 | // growing "Re: AW: …" on every round. | ||
| 72 | 42 | QString ComposeReplyPrefill::normalizeReplySubject(const QString &subject) { | |
| 73 | static const QStringList prefixes = { | ||
| 74 | 7 | QStringLiteral("re"), QStringLiteral("aw"), QStringLiteral("fw"), | |
| 75 | 7 | QStringLiteral("fwd"), QStringLiteral("wg"), QStringLiteral("sv"), | |
| 76 | 7 | QStringLiteral("vs"), QStringLiteral("vb"), QStringLiteral("ref"), | |
| 77 |
5/10✓ Branch 2 → 3 taken 7 times.
✓ Branch 2 → 52 taken 35 times.
✓ Branch 4 → 5 taken 7 times.
✗ Branch 4 → 52 not taken.
✓ Branch 39 → 40 taken 70 times.
✓ Branch 39 → 41 taken 7 times.
✗ Branch 94 → 95 not taken.
✗ Branch 94 → 96 not taken.
✗ Branch 133 → 134 not taken.
✗ Branch 133 → 135 not taken.
|
126 | QStringLiteral("antwort")}; |
| 78 | 42 | QString rest = subject; | |
| 79 | forever { | ||
| 80 | 53 | bool matched = false; | |
| 81 |
2/2✓ Branch 79 → 55 taken 443 times.
✓ Branch 79 → 80 taken 42 times.
|
485 | for (const QString &prefix : prefixes) { |
| 82 | 443 | if (rest.size() > prefix.size() && | |
| 83 |
6/7✓ Branch 58 → 59 taken 427 times.
✓ Branch 58 → 68 taken 16 times.
✓ Branch 59 → 60 taken 427 times.
✓ Branch 60 → 61 taken 11 times.
✓ Branch 60 → 68 taken 416 times.
✓ Branch 66 → 67 taken 11 times.
✗ Branch 66 → 68 not taken.
|
454 | rest.startsWith(prefix, Qt::CaseInsensitive) && |
| 84 |
2/2✓ Branch 69 → 70 taken 11 times.
✓ Branch 69 → 77 taken 432 times.
|
454 | rest.at(prefix.size()) == QLatin1Char(':')) { |
| 85 |
2/2✓ Branch 71 → 72 taken 11 times.
✓ Branch 72 → 73 taken 11 times.
|
11 | rest = rest.mid(prefix.size() + 1).trimmed(); |
| 86 | 11 | matched = true; | |
| 87 | 11 | break; | |
| 88 | } | ||
| 89 | } | ||
| 90 |
2/2✓ Branch 80 → 81 taken 42 times.
✓ Branch 80 → 82 taken 11 times.
|
53 | if (!matched) break; |
| 91 | 11 | } | |
| 92 | // A subject without a prefix gets one, as every reply did before F-20. | ||
| 93 |
1/1✓ Branch 86 → 87 taken 42 times.
|
84 | return QStringLiteral("Re: ") + rest; |
| 94 | 119 | } | |
| 95 | |||
| 96 | 32 | bool ComposeReplyPrefill::startComposeSource(int requestToken, const std::function<SourceBuilder()> &prepare) { | |
| 97 |
1/1✓ Branch 2 → 3 taken 32 times.
|
32 | cancelReplyDraft(); |
| 98 | 32 | const MailId id = m_workspace.mailId(); | |
| 99 |
6/13✓ Branch 5 → 6 taken 32 times.
✗ Branch 5 → 13 not taken.
✓ Branch 7 → 8 taken 32 times.
✗ Branch 7 → 13 not taken.
✓ Branch 8 → 9 taken 32 times.
✓ Branch 10 → 11 taken 32 times.
✗ Branch 10 → 13 not taken.
✓ Branch 15 → 16 taken 32 times.
✗ Branch 15 → 17 not taken.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 32 times.
✗ Branch 58 → 59 not taken.
✗ Branch 58 → 60 not taken.
|
64 | if (requestToken <= 0 || !id.isValid() || id.accountId != m_deps.accountName() || |
| 100 |
1/2✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 32 times.
|
32 | !m_folderPathById.contains(id.folderId)) return false; |
| 101 | 32 | const quint32 expectedEpoch = m_folderEpochById.value(id.folderId, 0); | |
| 102 |
1/2✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 32 times.
|
32 | if (expectedEpoch == 0) return false; |
| 103 | 32 | const QString databasePath = m_session.cache()->databasePath(); | |
| 104 |
1/2✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 32 times.
|
32 | if (databasePath.isEmpty()) return false; |
| 105 | 32 | const quint64 generation = m_composeSourceGeneration; | |
| 106 | 32 | const quint64 accountGeneration = m_session.accounts()->generation(); | |
| 107 |
1/1✓ Branch 29 → 30 taken 32 times.
|
32 | auto cancelled = std::make_shared<std::atomic_bool>(false); |
| 108 | 32 | m_composeSourceCancelled = cancelled; | |
| 109 | 32 | m_composeSourceMail = id; | |
| 110 | 32 | m_composeSourceToken = requestToken; | |
| 111 |
3/6✓ Branch 32 → 33 taken 32 times.
✓ Branch 33 → 34 taken 32 times.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 32 times.
✗ Branch 63 → 64 not taken.
✗ Branch 63 → 65 not taken.
|
32 | auto *watcher = new QFutureWatcher<QVariantMap>(this); |
| 112 | 32 | connect(watcher, &QFutureWatcher<QVariantMap>::finished, this, | |
| 113 |
2/8✓ Branch 39 → 40 taken 32 times.
✓ Branch 40 → 41 taken 32 times.
✗ Branch 68 → 69 not taken.
✗ Branch 68 → 70 not taken.
✗ Branch 71 → 72 not taken.
✗ Branch 71 → 73 not taken.
✗ Branch 74 → 75 not taken.
✗ Branch 74 → 76 not taken.
|
64 | [this, watcher, cancelled, id, databasePath, expectedEpoch, generation, |
| 114 | accountGeneration, requestToken, prepare] { | ||
| 115 |
1/1✓ Branch 2 → 3 taken 32 times.
|
32 | const auto draft = watcher->result(); |
| 116 |
1/1✓ Branch 3 → 4 taken 32 times.
|
32 | watcher->deleteLater(); |
| 117 |
1/2✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 32 times.
|
64 | const char *dropped = cancelled->load() ? "cancelled" |
| 118 |
1/2✓ Branch 8 → 9 taken 32 times.
✗ Branch 8 → 30 not taken.
|
64 | : generation != m_composeSourceGeneration ? "superseded" |
| 119 |
1/2✓ Branch 11 → 12 taken 32 times.
✗ Branch 11 → 28 not taken.
|
64 | : accountGeneration != m_session.accounts()->generation() ? "account changed" |
| 120 |
1/2✓ Branch 14 → 15 taken 32 times.
✗ Branch 14 → 26 not taken.
|
64 | : m_workspace.mailId() != id ? "another mail selected" |
| 121 |
1/2✓ Branch 16 → 17 taken 32 times.
✗ Branch 16 → 24 not taken.
|
64 | : m_folderEpochById.value(id.folderId, 0) != expectedEpoch ? "folder epoch changed" |
| 122 |
1/2✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 32 times.
|
64 | : m_session.cache()->databasePath() != databasePath ? "database changed" |
| 123 |
1/2✓ Branch 32 → 33 taken 32 times.
✗ Branch 32 → 34 not taken.
|
32 | : nullptr; |
| 124 | // The folder was re-read (a new epoch, another database) or the cache | ||
| 125 | // had no ready snapshot while the same mail is still shown: once more, | ||
| 126 | // instead of swallowing the request (Ctrl+R right after the start, | ||
| 127 | // KJ2 in pipelines #7218/#7220). Cancelled and superseded requests | ||
| 128 | // were already answered by cancelReplyDraft. | ||
| 129 |
1/5✗ Branch 34 → 35 not taken.
✓ Branch 34 → 40 taken 32 times.
✗ Branch 35 → 36 not taken.
✗ Branch 36 → 37 not taken.
✗ Branch 36 → 39 not taken.
|
32 | const bool reread = dropped && (qstrcmp(dropped, "folder epoch changed") == 0 || |
| 130 | ✗ | qstrcmp(dropped, "database changed") == 0); | |
| 131 |
5/11✓ Branch 41 → 42 taken 32 times.
✗ Branch 41 → 45 not taken.
✓ Branch 42 → 43 taken 32 times.
✗ Branch 42 → 50 not taken.
✓ Branch 43 → 44 taken 32 times.
✗ Branch 44 → 45 not taken.
✓ Branch 44 → 50 taken 32 times.
✗ Branch 47 → 48 not taken.
✗ Branch 47 → 50 not taken.
✗ Branch 51 → 52 not taken.
✓ Branch 51 → 72 taken 32 times.
|
32 | if ((reread || (!dropped && draft.isEmpty())) && m_workspace.mailId() == id && |
| 132 | ✗ | m_composeSourceRetriedToken != requestToken) { | |
| 133 | ✗ | qCInfo(lcCompose) << "reply source read again:" << (dropped ? dropped : "no ready snapshot") << "token" | |
| 134 | ✗ | << requestToken; | |
| 135 | ✗ | m_composeSourceRetriedToken = requestToken; | |
| 136 | ✗ | m_composeSourceToken = 0; // the same request: no "could not be loaded" in between | |
| 137 | ✗ | QTimer::singleShot(0, this, [this, requestToken, prepare] { | |
| 138 | ✗ | if (!startComposeSource(requestToken, prepare)) emit replyDraftReady(requestToken, {}); | |
| 139 | ✗ | }); | |
| 140 | ✗ | return; | |
| 141 | } | ||
| 142 |
1/2✗ Branch 72 → 73 not taken.
✓ Branch 72 → 92 taken 32 times.
|
32 | if (dropped) { |
| 143 | ✗ | qCInfo(lcCompose) << "reply source dropped:" << dropped << "token" << requestToken; | |
| 144 | ✗ | if (reread) emit replyDraftReady(requestToken, {}); // nobody else tells the composer | |
| 145 | ✗ | return; | |
| 146 | } | ||
| 147 | 32 | m_composeSourceCancelled.reset(); | |
| 148 | 32 | m_composeSourceMail = {}; | |
| 149 | 32 | m_composeSourceToken = 0; | |
| 150 |
1/1✓ Branch 95 → 96 taken 32 times.
|
32 | emit replyDraftReady(requestToken, draft); |
| 151 |
1/2✓ Branch 98 → 99 taken 32 times.
✗ Branch 98 → 101 not taken.
|
32 | }); |
| 152 |
1/1✓ Branch 43 → 44 taken 32 times.
|
32 | const SourceBuilder build = prepare(); |
| 153 |
1/1✓ Branch 49 → 50 taken 32 times.
|
32 | watcher->setFuture(QtConcurrent::run(&m_composeSourcePool, |
| 154 |
2/8✓ Branch 47 → 48 taken 32 times.
✓ Branch 48 → 49 taken 32 times.
✗ Branch 83 → 84 not taken.
✗ Branch 83 → 85 not taken.
✗ Branch 86 → 87 not taken.
✗ Branch 86 → 88 not taken.
✗ Branch 89 → 90 not taken.
✗ Branch 89 → 91 not taken.
|
64 | [id, databasePath, expectedEpoch, cancelled, build]() -> QVariantMap { |
| 155 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 32 times.
|
32 | if (cancelled->load()) return {}; |
| 156 |
2/2✓ Branch 6 → 7 taken 22 times.
✓ Branch 6 → 8 taken 10 times.
|
32 | static thread_local std::unique_ptr<MailCache> reader; |
| 157 |
2/2✓ Branch 8 → 9 taken 22 times.
✓ Branch 8 → 10 taken 10 times.
|
32 | static thread_local QString readerPath; |
| 158 |
5/6✓ Branch 11 → 12 taken 10 times.
✓ Branch 11 → 14 taken 22 times.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 10 times.
✓ Branch 16 → 17 taken 22 times.
✓ Branch 16 → 28 taken 10 times.
|
32 | if (!reader || readerPath != databasePath) { |
| 159 | 22 | reader.reset(); | |
| 160 |
1/1✓ Branch 18 → 19 taken 22 times.
|
22 | reader = std::make_unique<MailCache>(); |
| 161 |
2/3✓ Branch 22 → 23 taken 22 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 27 taken 22 times.
|
22 | if (!reader->openReadOnly(databasePath)) { |
| 162 | ✗ | reader.reset(); | |
| 163 | ✗ | readerPath.clear(); | |
| 164 | ✗ | return {}; | |
| 165 | } | ||
| 166 | 22 | readerPath = databasePath; | |
| 167 | } | ||
| 168 | 32 | auto source = reader->snapshots().composeSourceSnapshot(id.accountId, id.folderId, | |
| 169 |
1/1✓ Branch 31 → 32 taken 32 times.
|
32 | id.uid, expectedEpoch, *cancelled); |
| 170 |
2/4✓ Branch 34 → 35 taken 32 times.
✗ Branch 34 → 36 not taken.
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 40 taken 32 times.
|
64 | if (cancelled->load() || |
| 171 |
1/2✗ Branch 35 → 36 not taken.
✓ Branch 35 → 37 taken 32 times.
|
32 | source.outcome != MailCache::ComposeSourceSnapshot::Outcome::Ready) |
| 172 | ✗ | return {}; | |
| 173 | // An HTML-only original has no text projection: the quote's text (the | ||
| 174 | // reply's plain part, a plain-text reply, a forward) comes from its HTML. | ||
| 175 |
11/18✓ Branch 40 → 41 taken 32 times.
✗ Branch 40 → 48 not taken.
✓ Branch 41 → 42 taken 32 times.
✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 48 taken 31 times.
✓ Branch 44 → 45 taken 1 time.
✓ Branch 46 → 47 taken 1 time.
✗ Branch 46 → 48 not taken.
✓ Branch 49 → 50 taken 1 time.
✓ Branch 49 → 51 taken 31 times.
✓ Branch 51 → 52 taken 32 times.
✗ Branch 51 → 53 not taken.
✓ Branch 53 → 54 taken 1 time.
✓ Branch 53 → 58 taken 31 times.
✗ Branch 71 → 72 not taken.
✗ Branch 71 → 73 not taken.
✗ Branch 75 → 76 not taken.
✗ Branch 75 → 77 not taken.
|
32 | if (source.bodyPresent && source.quoteText.trimmed().isEmpty() && !source.quoteHtml.trimmed().isEmpty()) |
| 176 |
1/1✓ Branch 54 → 55 taken 1 time.
|
1 | source.quoteText = ComposeQuoteSource::textOf(source.quoteHtml); |
| 177 |
1/1✓ Branch 59 → 60 taken 32 times.
|
32 | const QVariantMap draft = build(*reader, source, id); |
| 178 |
1/2✗ Branch 62 → 63 not taken.
✓ Branch 62 → 64 taken 32 times.
|
32 | return cancelled->load() ? QVariantMap{} : draft; |
| 179 | 32 | })); | |
| 180 | 32 | return true; | |
| 181 | 32 | } | |
| 182 | |||
| 183 | 30 | bool ComposeReplyPrefill::requestReplyDraft(int requestToken, bool all, | |
| 184 | bool forward) { | ||
| 185 |
1/1✓ Branch 3 → 4 taken 30 times.
|
60 | return startComposeSource(requestToken, [this, all, forward]() -> SourceBuilder { |
| 186 |
1/1✓ Branch 2 → 3 taken 30 times.
|
30 | const QString ownEmail = m_deps.accountEmail(); |
| 187 |
1/1✓ Branch 3 → 4 taken 30 times.
|
30 | const QString replyAttribution = QCoreApplication::translate("QuickComposeBackend", "On %1, %2 wrote:"); |
| 188 | const QString forwardAttribution = QCoreApplication::translate("QuickComposeBackend", "---------- Forwarded message ----------\n" | ||
| 189 |
1/1✓ Branch 4 → 5 taken 30 times.
|
30 | "From: %1\nDate: %2\nSubject: %3\n"); |
| 190 |
1/1✓ Branch 5 → 6 taken 30 times.
|
30 | const QString trimmedText = QCoreApplication::translate("QuickComposeBackend", "… [trimmed]"); |
| 191 |
1/1✓ Branch 6 → 7 taken 30 times.
|
30 | const QLocale locale; |
| 192 |
1/1✓ Branch 7 → 8 taken 30 times.
|
30 | const QString messageLanguage = locale.name(); |
| 193 | // F-06 (SPRINT-104): the forward's attachment scratch directory is | ||
| 194 | // created and owned on the GUI thread; the worker fills it, and the | ||
| 195 | // files live until the next forward or teardown (the send reads them). | ||
| 196 | 30 | std::shared_ptr<QTemporaryDir> forwardScratch; | |
| 197 |
2/2✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 14 taken 27 times.
|
30 | if (forward) |
| 198 |
1/1✓ Branch 9 → 10 taken 3 times.
|
3 | m_forwardScratch = forwardScratch = QuickComposeForwardAttachments::makeScratch(m_profileDirectory); |
| 199 |
0/12✗ Branch 36 → 37 not taken.
✗ Branch 36 → 38 not taken.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 44 not taken.
✗ Branch 45 → 46 not taken.
✗ Branch 45 → 47 not taken.
✗ Branch 48 → 49 not taken.
✗ Branch 48 → 50 not taken.
✗ Branch 51 → 52 not taken.
✗ Branch 51 → 53 not taken.
|
60 | return [ownEmail, all, forward, locale, messageLanguage, replyAttribution, forwardAttribution, trimmedText, |
| 200 | forwardScratch](MailCache &reader, const MailCache::ComposeSourceSnapshot &source, | ||
| 201 | const MailId &id) -> QVariantMap { | ||
| 202 | // F-06 (SPRINT-104, D-3 Must): a forward keeps the original's | ||
| 203 | // attachments. The cached blobs are exported to files of a forwarding | ||
| 204 | // scratch directory (the composer and the send path work with file | ||
| 205 | // paths); only the compose budget's worth travels, the rest is named. | ||
| 206 | // F-06 (SPRINT-104, D-3 Must): the forward's attachments travel as | ||
| 207 | // exported files (the composer and send path work with paths). | ||
| 208 | const QStringList forwardFiles = | ||
| 209 |
1/2✓ Branch 4 → 5 taken 3 times.
✗ Branch 4 → 9 not taken.
|
3 | forward && forwardScratch |
| 210 | 33 | ? QuickComposeForwardAttachments::exportTo(reader, id.folderId, id.uid, | |
| 211 |
3/5✓ Branch 6 → 7 taken 3 times.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 12 taken 27 times.
✗ Branch 231 → 232 not taken.
✗ Branch 231 → 233 not taken.
|
33 | forwardScratch->path()) |
| 212 |
3/3✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 9 taken 27 times.
✓ Branch 7 → 8 taken 3 times.
|
33 | : QStringList{}; |
| 213 | 30 | const auto &header = source.header; | |
| 214 | 30 | const QString date = locale.toString(header.date.toLocalTime(), | |
| 215 |
2/2✓ Branch 12 → 13 taken 30 times.
✓ Branch 13 → 14 taken 30 times.
|
30 | QLocale::ShortFormat); |
| 216 | 30 | QString subject = header.subject; | |
| 217 | 30 | QVariantMap draft; | |
| 218 |
2/2✓ Branch 16 → 17 taken 3 times.
✓ Branch 16 → 92 taken 27 times.
|
30 | if (forward) { |
| 219 |
2/3✓ Branch 18 → 19 taken 3 times.
✓ Branch 19 → 20 taken 3 times.
✗ Branch 19 → 29 not taken.
|
3 | if (!subject.startsWith(QLatin1String("Fwd:"), Qt::CaseInsensitive)) |
| 220 |
1/1✓ Branch 23 → 24 taken 3 times.
|
3 | subject = QStringLiteral("Fwd: ") + subject; |
| 221 |
1/1✓ Branch 33 → 34 taken 3 times.
|
6 | draft.insert(QStringLiteral("subject"), subject); |
| 222 |
1/2✓ Branch 37 → 38 taken 3 times.
✗ Branch 37 → 81 not taken.
|
3 | if (source.bodyPresent) { |
| 223 |
1/1✓ Branch 41 → 42 taken 3 times.
|
3 | QStringList lines = source.quoteText.split(QLatin1Char('\n')); |
| 224 |
2/4✓ Branch 42 → 43 taken 3 times.
✗ Branch 42 → 45 not taken.
✗ Branch 44 → 45 not taken.
✓ Branch 44 → 46 taken 3 times.
|
3 | const bool trimmed = source.quoteTruncated || lines.size() > 200; |
| 225 |
1/3✗ Branch 48 → 49 not taken.
✓ Branch 48 → 53 taken 3 times.
✗ Branch 49 → 50 not taken.
|
3 | if (lines.size() > 200) lines = lines.mid(0, 200); |
| 226 |
1/3✗ Branch 53 → 54 not taken.
✓ Branch 53 → 55 taken 3 times.
✗ Branch 54 → 55 not taken.
|
3 | if (trimmed) lines.append(trimmedText); |
| 227 |
1/1✓ Branch 70 → 71 taken 3 times.
|
6 | draft.insert(QStringLiteral("body"), |
| 228 |
1/1✓ Branch 62 → 63 taken 3 times.
|
6 | forwardAttribution.arg(header.from, date, header.subject) + |
| 229 |
4/4✓ Branch 59 → 60 taken 3 times.
✓ Branch 63 → 64 taken 3 times.
✓ Branch 64 → 65 taken 3 times.
✓ Branch 65 → 66 taken 3 times.
|
12 | QLatin1Char('\n') + lines.join(QLatin1Char('\n')) + QLatin1Char('\n')); |
| 230 | 3 | } | |
| 231 |
1/2✗ Branch 82 → 83 not taken.
✓ Branch 82 → 225 taken 3 times.
|
3 | if (!forwardFiles.isEmpty()) |
| 232 | ✗ | draft.insert(QStringLiteral("attachments"), forwardFiles); | |
| 233 | } else { | ||
| 234 |
1/1✓ Branch 92 → 93 taken 27 times.
|
27 | subject = normalizeReplySubject(subject); |
| 235 |
1/1✓ Branch 95 → 96 taken 27 times.
|
27 | const auto envelope = MimeParser::messageHeaders(source.envelopePrefix); |
| 236 | const auto recipients = ComposeRecipients::replyTargets( | ||
| 237 |
2/2✓ Branch 104 → 105 taken 27 times.
✓ Branch 105 → 106 taken 27 times.
|
54 | envelope.value("from", header.from), |
| 238 |
4/4✓ Branch 100 → 101 taken 27 times.
✓ Branch 101 → 102 taken 27 times.
✓ Branch 102 → 103 taken 27 times.
✓ Branch 103 → 104 taken 27 times.
|
54 | envelope.value("to", header.to), envelope.value("cc"), |
| 239 |
3/3✓ Branch 97 → 98 taken 27 times.
✓ Branch 98 → 99 taken 27 times.
✓ Branch 106 → 107 taken 27 times.
|
81 | envelope.value("reply-to"), ownEmail, all); |
| 240 |
1/1✓ Branch 121 → 122 taken 27 times.
|
54 | draft.insert(QStringLiteral("to"), recipients.to); |
| 241 |
1/1✓ Branch 129 → 130 taken 27 times.
|
54 | draft.insert(QStringLiteral("cc"), recipients.cc); |
| 242 |
1/1✓ Branch 137 → 138 taken 27 times.
|
54 | draft.insert(QStringLiteral("subject"), subject); |
| 243 |
1/2✓ Branch 141 → 142 taken 27 times.
✗ Branch 141 → 201 not taken.
|
27 | if (source.bodyPresent) { |
| 244 |
1/1✓ Branch 145 → 146 taken 27 times.
|
27 | QStringList lines = source.quoteText.split(QLatin1Char('\n')); |
| 245 |
2/4✓ Branch 146 → 147 taken 27 times.
✗ Branch 146 → 149 not taken.
✗ Branch 148 → 149 not taken.
✓ Branch 148 → 150 taken 27 times.
|
27 | const bool trimmed = source.quoteTruncated || lines.size() > 200; |
| 246 |
1/3✗ Branch 152 → 153 not taken.
✓ Branch 152 → 157 taken 27 times.
✗ Branch 153 → 154 not taken.
|
27 | if (lines.size() > 200) lines = lines.mid(0, 200); |
| 247 |
1/3✗ Branch 157 → 158 not taken.
✓ Branch 157 → 159 taken 27 times.
✗ Branch 158 → 159 not taken.
|
27 | if (trimmed) lines.append(trimmedText); |
| 248 | 27 | QStringList quotedLines; | |
| 249 |
1/1✓ Branch 160 → 161 taken 27 times.
|
27 | quotedLines.reserve(lines.size()); |
| 250 |
4/4✓ Branch 161 → 162 taken 27 times.
✓ Branch 162 → 163 taken 27 times.
✓ Branch 173 → 164 taken 77 times.
✓ Branch 173 → 174 taken 27 times.
|
104 | for (const QString &line : lines) |
| 251 |
3/3✓ Branch 166 → 167 taken 77 times.
✓ Branch 167 → 168 taken 77 times.
✓ Branch 168 → 169 taken 77 times.
|
77 | quotedLines.append(QLatin1String("> ") + line); |
| 252 |
1/1✓ Branch 189 → 190 taken 27 times.
|
54 | draft.insert(QStringLiteral("body"), |
| 253 |
2/2✓ Branch 181 → 182 taken 27 times.
✓ Branch 182 → 183 taken 27 times.
|
54 | replyAttribution.arg(date, header.from) + QLatin1Char('\n') + |
| 254 |
3/3✓ Branch 178 → 179 taken 27 times.
✓ Branch 183 → 184 taken 27 times.
✓ Branch 184 → 185 taken 27 times.
|
108 | quotedLines.join(QLatin1Char('\n')) + QLatin1Char('\n')); |
| 255 | 27 | } | |
| 256 | 27 | QStringList references = header.references; | |
| 257 |
3/3✓ Branch 203 → 204 taken 24 times.
✓ Branch 203 → 205 taken 3 times.
✓ Branch 204 → 205 taken 24 times.
|
27 | if (!header.messageId.isEmpty()) references.append(header.messageId); |
| 258 |
1/1✓ Branch 209 → 210 taken 27 times.
|
54 | draft.insert(QStringLiteral("inReplyTo"), header.messageId); |
| 259 |
1/1✓ Branch 217 → 218 taken 27 times.
|
54 | draft.insert(QStringLiteral("references"), references); |
| 260 | 27 | } | |
| 261 | // Sprint 108: the new composer's quote, format and identity inputs. | ||
| 262 |
1/1✓ Branch 225 → 226 taken 30 times.
|
30 | ComposeQuoteSource::add(draft, source, forward, messageLanguage); |
| 263 | 30 | return draft; | |
| 264 |
1/1✓ Branch 21 → 22 taken 30 times.
|
90 | }; |
| 265 | 90 | }); | |
| 266 | } | ||
| 267 | |||
| 268 | 10 | QVariantMap ComposeReplyPrefill::replyDraft(bool all) const { | |
| 269 | 10 | const auto id = m_workspace.mailId(); | |
| 270 |
2/3✓ Branch 4 → 5 taken 10 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 10 times.
|
10 | if (!m_deps.acceptsIdentity(id)) return {}; |
| 271 |
1/1✓ Branch 8 → 9 taken 10 times.
|
10 | const auto header = m_session.cache()->header(id.folderId, id.uid); |
| 272 |
1/2✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 10 times.
|
10 | if (!header) return {}; |
| 273 |
1/1✓ Branch 13 → 14 taken 10 times.
|
10 | const QString subject = normalizeReplySubject(header->subject); // F-20 |
| 274 | // Quote the stored plain text: the standard attribution header plus | ||
| 275 | // "> "-prefixed lines, bounded so huge mails cannot flood the editor. | ||
| 276 | // P90.14: the envelope prefix + plain text projection replaces the full | ||
| 277 | // raw-source load; header-row fallbacks cover pathological envelopes. | ||
| 278 | 10 | QString quoted; | |
| 279 | 10 | QMap<QString, QString> envelopeHeaders; | |
| 280 |
3/3✓ Branch 15 → 16 taken 10 times.
✓ Branch 17 → 18 taken 6 times.
✓ Branch 17 → 81 taken 4 times.
|
10 | if (const auto original = m_session.cache()->replySnapshot(id.folderId, id.uid)) { |
| 281 |
1/1✓ Branch 19 → 20 taken 6 times.
|
6 | envelopeHeaders = MimeParser::messageHeaders(original->rawSource); |
| 282 |
1/1✓ Branch 22 → 23 taken 6 times.
|
6 | const QString attribution = QCoreApplication::translate("QuickComposeBackend", "On %1, %2 wrote:") |
| 283 |
2/2✓ Branch 26 → 27 taken 6 times.
✓ Branch 27 → 28 taken 6 times.
|
12 | .arg(QLocale().toString(header->date.toLocalTime(), QLocale::ShortFormat), |
| 284 |
2/2✓ Branch 24 → 25 taken 6 times.
✓ Branch 28 → 29 taken 6 times.
|
12 | header->from); |
| 285 |
1/1✓ Branch 37 → 38 taken 6 times.
|
6 | QStringList lines = original->textPlain.split(QLatin1Char('\n')); |
| 286 |
1/2✗ Branch 39 → 40 not taken.
✓ Branch 39 → 47 taken 6 times.
|
6 | if (lines.size() > 200) { |
| 287 | ✗ | lines = lines.mid(0, 200); | |
| 288 | ✗ | lines.append(QCoreApplication::translate("QuickComposeBackend", "… [trimmed]")); | |
| 289 | } | ||
| 290 | 6 | QStringList quotedLines; | |
| 291 |
1/1✓ Branch 48 → 49 taken 6 times.
|
6 | quotedLines.reserve(lines.size()); |
| 292 |
7/7✓ Branch 49 → 50 taken 6 times.
✓ Branch 50 → 51 taken 6 times.
✓ Branch 54 → 55 taken 6 times.
✓ Branch 55 → 56 taken 6 times.
✓ Branch 56 → 57 taken 6 times.
✓ Branch 61 → 52 taken 6 times.
✓ Branch 61 → 62 taken 6 times.
|
12 | for (const QString &line : lines) quotedLines.append(QLatin1String("> ") + line); |
| 293 |
4/4✓ Branch 66 → 67 taken 6 times.
✓ Branch 69 → 70 taken 6 times.
✓ Branch 70 → 71 taken 6 times.
✓ Branch 71 → 72 taken 6 times.
|
6 | quoted = attribution + QLatin1Char('\n') + quotedLines.join(QLatin1Char('\n')) + QLatin1Char('\n'); |
| 294 | 16 | } | |
| 295 | 10 | QStringList references = header->references; | |
| 296 |
3/3✓ Branch 86 → 87 taken 4 times.
✓ Branch 86 → 89 taken 6 times.
✓ Branch 88 → 89 taken 4 times.
|
10 | if (!header->messageId.isEmpty()) references.append(header->messageId); |
| 297 | const auto recipients = ComposeRecipients::replyTargets( | ||
| 298 |
4/4✓ Branch 97 → 98 taken 10 times.
✓ Branch 98 → 99 taken 10 times.
✓ Branch 100 → 101 taken 10 times.
✓ Branch 101 → 102 taken 10 times.
|
20 | envelopeHeaders.value("from", header->from), envelopeHeaders.value("to", header->to), |
| 299 |
6/6✓ Branch 89 → 90 taken 10 times.
✓ Branch 91 → 92 taken 10 times.
✓ Branch 92 → 93 taken 10 times.
✓ Branch 94 → 95 taken 10 times.
✓ Branch 95 → 96 taken 10 times.
✓ Branch 102 → 103 taken 10 times.
|
30 | envelopeHeaders.value("cc"), envelopeHeaders.value("reply-to"), m_deps.accountEmail(), all); |
| 300 | 10 | QVariantMap draft; | |
| 301 |
1/1✓ Branch 118 → 119 taken 10 times.
|
20 | draft.insert(QStringLiteral("to"), recipients.to); |
| 302 |
1/1✓ Branch 126 → 127 taken 10 times.
|
20 | draft.insert(QStringLiteral("cc"), recipients.cc); |
| 303 |
1/1✓ Branch 134 → 135 taken 10 times.
|
20 | draft.insert(QStringLiteral("subject"), subject); |
| 304 |
1/1✓ Branch 142 → 143 taken 10 times.
|
20 | draft.insert(QStringLiteral("body"), quoted); |
| 305 |
1/1✓ Branch 151 → 152 taken 10 times.
|
20 | draft.insert(QStringLiteral("inReplyTo"), header->messageId); |
| 306 |
1/1✓ Branch 159 → 160 taken 10 times.
|
20 | draft.insert(QStringLiteral("references"), references); |
| 307 | 10 | return draft; | |
| 308 | 10 | } | |
| 309 | |||
| 310 | 1 | QVariantMap ComposeReplyPrefill::forwardDraft() const { | |
| 311 | 1 | const auto id = m_workspace.mailId(); | |
| 312 |
2/3✓ Branch 4 → 5 taken 1 time.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1 time.
|
1 | if (!m_deps.acceptsIdentity(id)) return {}; |
| 313 |
1/1✓ Branch 8 → 9 taken 1 time.
|
1 | const auto header = m_session.cache()->header(id.folderId, id.uid); |
| 314 |
1/2✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 1 time.
|
1 | if (!header) return {}; |
| 315 | 1 | QString subject = header->subject; | |
| 316 |
2/3✓ Branch 15 → 16 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 26 not taken.
|
1 | if (!subject.startsWith(QLatin1String("Fwd:"), Qt::CaseInsensitive)) |
| 317 |
1/1✓ Branch 20 → 21 taken 1 time.
|
1 | subject = QStringLiteral("Fwd: ") + subject; |
| 318 | 1 | QString body; | |
| 319 | // P90.14: forwarding quotes the plain text only; no raw-source load. | ||
| 320 |
2/3✓ Branch 27 → 28 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
✗ Branch 29 → 74 not taken.
|
1 | if (const auto original = m_session.cache()->displayBody(id.folderId, id.uid)) { |
| 321 |
1/1✓ Branch 30 → 31 taken 1 time.
|
1 | const QString attribution = QCoreApplication::translate("QuickComposeBackend", "---------- Forwarded message ----------\n" |
| 322 | "From: %1\nDate: %2\nSubject: %3\n") | ||
| 323 | 1 | .arg(header->from, | |
| 324 |
2/2✓ Branch 34 → 35 taken 1 time.
✓ Branch 35 → 36 taken 1 time.
|
2 | QLocale().toString(header->date.toLocalTime(), QLocale::ShortFormat), |
| 325 |
2/2✓ Branch 32 → 33 taken 1 time.
✓ Branch 37 → 38 taken 1 time.
|
2 | header->subject); |
| 326 |
1/1✓ Branch 46 → 47 taken 1 time.
|
1 | QStringList lines = original->textPlain.split(QLatin1Char('\n')); |
| 327 |
1/2✗ Branch 48 → 49 not taken.
✓ Branch 48 → 56 taken 1 time.
|
1 | if (lines.size() > 200) { |
| 328 | ✗ | lines = lines.mid(0, 200); | |
| 329 | ✗ | lines.append(QCoreApplication::translate("QuickComposeBackend", "… [trimmed]")); | |
| 330 | } | ||
| 331 |
4/4✓ Branch 60 → 61 taken 1 time.
✓ Branch 63 → 64 taken 1 time.
✓ Branch 64 → 65 taken 1 time.
✓ Branch 65 → 66 taken 1 time.
|
1 | body = attribution + QLatin1Char('\n') + lines.join(QLatin1Char('\n')) + QLatin1Char('\n'); |
| 332 | 2 | } | |
| 333 | 2 | return {{QStringLiteral("subject"), subject}, | |
| 334 |
2/4✓ Branch 85 → 86 taken 2 times.
✓ Branch 85 → 87 taken 1 time.
✗ Branch 149 → 150 not taken.
✗ Branch 149 → 151 not taken.
|
4 | {QStringLiteral("body"), body}}; |
| 335 | 4 | } | |
| 336 |