presentation/QuickSourceViewer.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "QuickSourceViewer.h" | ||
| 2 | |||
| 3 | #include "WorkspaceState.h" | ||
| 4 | #include "app/core/MailSessionCore.h" | ||
| 5 | #include "data/MailCache.h" | ||
| 6 | #include "service/MailPayloadExportService.h" | ||
| 7 | #include "util/PdfAdmission.h" | ||
| 8 | #include <QDir> | ||
| 9 | #include <QStandardPaths> | ||
| 10 | |||
| 11 | // F6: viewer heuristic, not a security boundary — the extension/content | ||
| 12 | // type only selects the inline PDF viewer, never grants trust. Downloads | ||
| 13 | // and the sanitizer path treat every attachment as untrusted bytes. | ||
| 14 | namespace { | ||
| 15 | 3 | bool contentTypeIsPdf(const QString &contentType, const QString &filename) { | |
| 16 |
3/9✓ Branch 5 → 6 taken 3 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 12 taken 3 times.
✓ Branch 20 → 21 taken 3 times.
✗ Branch 20 → 22 not taken.
✗ Branch 32 → 33 not taken.
✗ Branch 32 → 34 not taken.
✗ Branch 36 → 37 not taken.
✗ Branch 36 → 38 not taken.
|
9 | return contentType.startsWith(QStringLiteral("application/pdf"), Qt::CaseInsensitive) || |
| 17 |
3/13✗ Branch 10 → 11 not taken.
✗ Branch 11 → 12 not taken.
✗ Branch 11 → 13 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 3 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 3 times.
✓ Branch 18 → 19 taken 3 times.
✗ Branch 18 → 20 not taken.
✗ Branch 24 → 25 not taken.
✗ Branch 24 → 26 not taken.
✗ Branch 28 → 29 not taken.
✗ Branch 28 → 30 not taken.
|
6 | filename.endsWith(QStringLiteral(".pdf"), Qt::CaseInsensitive); |
| 18 | } | ||
| 19 | } | ||
| 20 | |||
| 21 | // F-71 (SPRINT-104): the number of bytes at the END of `window` that form | ||
| 22 | // a possibly-incomplete multi-byte UTF-8 sequence (1..3), 0 when the tail | ||
| 23 | // is complete or malformed. Held back until the next window so a valid | ||
| 24 | // character crossing the source-window boundary decodes once, instead of | ||
| 25 | // two replacement characters from two independent decodes. | ||
| 26 | 2 | int trailingPartialUtf8(const QByteArray &window) { | |
| 27 | 2 | const int size = window.size(); | |
| 28 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
|
2 | if (size == 0) return 0; |
| 29 |
2/2✓ Branch 28 → 7 taken 8 times.
✓ Branch 28 → 29 taken 1 time.
|
9 | for (int back = qMin(3, size - 1); back >= 0; --back) { |
| 30 | 8 | const uchar lead = static_cast<uchar>(window.at(size - 1 - back)); | |
| 31 |
3/4✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 7 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 1 time.
|
8 | if (lead < 0xC0 || lead > 0xF4) |
| 32 | 7 | continue; // not a lead byte further back | |
| 33 |
2/4✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 16 not taken.
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 14 not taken.
|
1 | const int length = lead >= 0xF0 ? 4 : lead >= 0xE0 ? 3 : 2; |
| 34 | 1 | const int available = back + 1; | |
| 35 |
1/2✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 1 time.
|
1 | if (available >= length) |
| 36 | ✗ | return 0; // the sequence is complete within this window | |
| 37 | // every byte after the lead must be a continuation byte | ||
| 38 |
1/2✗ Branch 25 → 20 not taken.
✓ Branch 25 → 26 taken 1 time.
|
1 | for (int k = 0; k < back; ++k) { |
| 39 | ✗ | const uchar tail = static_cast<uchar>(window.at(size - 1 - k)); | |
| 40 | ✗ | if (tail < 0x80 || tail >= 0xC0) | |
| 41 | ✗ | return 0; // malformed: let fromUtf8 produce the replacement | |
| 42 | } | ||
| 43 | 1 | return length - available; | |
| 44 | } | ||
| 45 | 1 | return 0; | |
| 46 | } | ||
| 47 | |||
| 48 | 795 | QuickSourceViewer::QuickSourceViewer(MailSessionCore &session, WorkspaceState &workspace, | |
| 49 | const QString &profileDirectory, Deps deps, | ||
| 50 | 795 | QObject *parent) | |
| 51 | 795 | : QObject(parent), m_session(session), m_workspace(workspace), | |
| 52 |
2/2✓ Branch 7 → 8 taken 795 times.
✓ Branch 9 → 10 taken 795 times.
|
795 | m_profileDirectory(profileDirectory), m_deps(std::move(deps)) {} |
| 53 | |||
| 54 | // Source view and PDF preview close on selection change. | ||
| 55 | // Formerly QuickMailBackend::wireSourceView(); the backend calls this at the | ||
| 56 | // same constructor position, which keeps this handler the 6th receiver of | ||
| 57 | // WorkspaceState::selectionChanged (Sprint 91 rule 4). | ||
| 58 | 795 | void QuickSourceViewer::connectSignals() { | |
| 59 | 795 | WorkspaceState &workspace = m_workspace; | |
| 60 | // The raw-source view describes exactly the selected mail; any selection | ||
| 61 | // change (tab switch, new mail, account reload) closes it. | ||
| 62 |
1/1✓ Branch 2 → 3 taken 795 times.
|
795 | connect(&workspace, &WorkspaceState::selectionChanged, this, [this] { |
| 63 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1569 times.
|
1569 | if (m_sourceActive) closeSource(); |
| 64 | // F3: stop a preview that is still in flight for the previous selection. | ||
| 65 | // The dedicated owner keeps attachment saves and source reads running. | ||
| 66 | 1569 | MailPayloadExportService::shared()->cancel(&m_pdfOwner); | |
| 67 |
1/2✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 1569 times.
|
1569 | if (!m_pdfRejectedAttachments.isEmpty()) { |
| 68 | ✗ | m_pdfRejectedAttachments.clear(); | |
| 69 | ✗ | emit pdfChanged(); | |
| 70 | } | ||
| 71 | 1569 | closePdfInternal(); | |
| 72 | 1569 | }); | |
| 73 | 795 | } | |
| 74 | |||
| 75 | 3 | bool QuickSourceViewer::saveAttachment(qint64 attachmentId) { const auto id = m_workspace.mailId(); | |
| 76 |
7/9✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 9 taken 1 time.
✓ Branch 6 → 7 taken 2 times.
✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 9 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 2 times.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 2 times.
|
3 | if (!id.isValid() || !m_deps.configured() || attachmentId <= 0) return false; |
| 77 |
1/1✓ Branch 14 → 15 taken 2 times.
|
2 | const auto epoch = m_session.cache()->uidValidity(id.folderId); |
| 78 |
1/2✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 2 times.
|
2 | if (epoch == 0) return false; |
| 79 | // Admission against the mail's cached metadata: an id that does not belong | ||
| 80 | // to this mail can never be exported. | ||
| 81 | 2 | QString suggestedName; | |
| 82 |
5/5✓ Branch 19 → 20 taken 2 times.
✓ Branch 20 → 21 taken 2 times.
✓ Branch 21 → 22 taken 2 times.
✓ Branch 28 → 23 taken 2 times.
✓ Branch 28 → 29 taken 1 time.
|
3 | for (const auto &attachment : m_session.cache()->attachments().attachments(id.folderId, id.uid)) { |
| 83 |
2/2✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 26 taken 1 time.
|
2 | if (attachment.id == attachmentId) { suggestedName = attachment.filename; break; } |
| 84 | 2 | } | |
| 85 |
2/2✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 33 taken 1 time.
|
2 | if (suggestedName.isEmpty()) return false; |
| 86 | const QString directory = | ||
| 87 |
1/1✓ Branch 33 → 34 taken 1 time.
|
1 | QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); |
| 88 |
1/2✗ Branch 35 → 36 not taken.
✓ Branch 35 → 40 taken 1 time.
|
1 | if (directory.isEmpty()) { |
| 89 | ✗ | m_deps.setStatus(tr("No download directory is available.")); | |
| 90 | ✗ | return false; | |
| 91 | } | ||
| 92 | 1 | MailPayloadRequest request{m_session.cache()->databasePath(), id, epoch, | |
| 93 | 1 | MailPayloadRequest::Kind::Attachment, attachmentId}; | |
| 94 |
3/3✓ Branch 43 → 44 taken 1 time.
✓ Branch 45 → 46 taken 1 time.
✓ Branch 46 → 47 taken 1 time.
|
2 | m_deps.setStatus(tr("Saving %1…").arg(suggestedName)); |
| 95 |
3/3✓ Branch 49 → 50 taken 1 time.
✓ Branch 51 → 52 taken 1 time.
✓ Branch 55 → 56 taken 1 time.
|
2 | MailPayloadExportService::shared()->saveInDirectory( |
| 96 | this, request, directory, suggestedName, | ||
| 97 | 2 | [this, suggestedName](MailPayloadReadResult result) { | |
| 98 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1 time.
|
1 | if (!result.completed) m_deps.setStatus(result.error); |
| 99 |
3/3✓ Branch 4 → 5 taken 1 time.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 7 → 8 taken 1 time.
|
2 | else m_deps.setStatus(tr("Saved %1.").arg(suggestedName)); |
| 100 | 1 | }); | |
| 101 | 1 | return true; | |
| 102 | 3 | } | |
| 103 | |||
| 104 | 5 | void QuickSourceViewer::openSource() { | |
| 105 |
5/6✓ Branch 2 → 3 taken 5 times.
✗ Branch 2 → 6 not taken.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 4 times.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 4 times.
|
5 | if (m_sourceActive || !m_workspace.mailId().isValid()) return; |
| 106 | 4 | m_sourceActive = true; | |
| 107 | // F-70: a new view lifetime — completions of the previous lifetime | ||
| 108 | // (closed view, other message) can never publish into this one. | ||
| 109 | 4 | ++m_sourceGeneration; | |
| 110 | 4 | m_sourceText.clear(); | |
| 111 | 4 | m_sourceSkip = 0; | |
| 112 | 4 | m_sourceTotal = 0; | |
| 113 | 4 | m_sourceFullyLoaded = false; | |
| 114 | 4 | m_sourcePendingBytes.clear(); // F-71: decoder state resets with the view | |
| 115 | 4 | emit sourceChanged(); | |
| 116 | 4 | requestSourceWindow(); | |
| 117 | } | ||
| 118 | |||
| 119 | 2 | void QuickSourceViewer::loadMoreSource() { | |
| 120 |
3/6✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 5 not taken.
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 2 times.
|
2 | if (!m_sourceActive || m_sourcePending || m_sourceFullyLoaded) return; |
| 121 | 2 | requestSourceWindow(); | |
| 122 | } | ||
| 123 | |||
| 124 | 2 | void QuickSourceViewer::closeSource() { | |
| 125 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 2 times.
|
2 | if (!m_sourceActive) return; |
| 126 | 2 | m_sourceActive = false; | |
| 127 | // F-70: the closed lifetime's in-flight reads own nothing anymore. | ||
| 128 | 2 | ++m_sourceGeneration; | |
| 129 | 2 | m_sourcePendingBytes.clear(); | |
| 130 | 2 | emit sourceChanged(); | |
| 131 | } | ||
| 132 | |||
| 133 | 1 | bool QuickSourceViewer::saveSource() { | |
| 134 | 1 | const auto id = m_workspace.mailId(); | |
| 135 |
4/7✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 8 not taken.
✓ Branch 6 → 7 taken 1 time.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 1 time.
|
1 | if (!id.isValid() || !m_deps.configured()) return false; |
| 136 |
1/1✓ Branch 13 → 14 taken 1 time.
|
1 | const auto epoch = m_session.cache()->uidValidity(id.folderId); |
| 137 |
1/2✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 1 time.
|
1 | if (epoch == 0) return false; |
| 138 | const QString directory = | ||
| 139 |
1/1✓ Branch 16 → 17 taken 1 time.
|
1 | QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); |
| 140 |
1/2✗ Branch 18 → 19 not taken.
✓ Branch 18 → 23 taken 1 time.
|
1 | if (directory.isEmpty()) { |
| 141 | ✗ | m_deps.setStatus(tr("No download directory is available.")); | |
| 142 | ✗ | return false; | |
| 143 | } | ||
| 144 | 1 | MailPayloadRequest request{m_session.cache()->databasePath(), id, epoch, | |
| 145 | 1 | MailPayloadRequest::Kind::RawSource, 0, 0, -1}; | |
| 146 |
2/2✓ Branch 26 → 27 taken 1 time.
✓ Branch 27 → 28 taken 1 time.
|
1 | m_deps.setStatus(tr("Saving source…")); |
| 147 |
2/2✓ Branch 29 → 30 taken 1 time.
✓ Branch 36 → 37 taken 1 time.
|
2 | MailPayloadExportService::shared()->save( |
| 148 |
1/1✓ Branch 34 → 35 taken 1 time.
|
2 | this, request, directory + QStringLiteral("/source.eml"), true, |
| 149 | 1 | [this](MailPayloadReadResult result) { | |
| 150 |
3/4✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
✓ Branch 5 → 6 taken 1 time.
|
1 | m_deps.setStatus(result.completed ? tr("Source saved.") : result.error); |
| 151 | 1 | }); | |
| 152 | 1 | return true; | |
| 153 | 1 | } | |
| 154 | |||
| 155 | 6 | void QuickSourceViewer::requestSourceWindow() { | |
| 156 | 6 | const auto id = m_workspace.mailId(); | |
| 157 |
4/7✓ Branch 5 → 6 taken 6 times.
✗ Branch 5 → 8 not taken.
✓ Branch 6 → 7 taken 6 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 6 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 6 times.
|
6 | if (!id.isValid() || !m_deps.configured()) return; |
| 158 |
1/1✓ Branch 13 → 14 taken 6 times.
|
6 | const auto epoch = m_session.cache()->uidValidity(id.folderId); |
| 159 | 6 | MailPayloadRequest request{m_session.cache()->databasePath(), id, epoch, | |
| 160 | MailPayloadRequest::Kind::RawSource, 0, | ||
| 161 | 6 | m_sourceSkip, SourceWindowBytes}; | |
| 162 | 6 | m_sourcePending = true; | |
| 163 | // F-70: a completion may only publish into the lifetime and byte | ||
| 164 | // position it was requested for — a reopened view (same or another | ||
| 165 | // message) must not admit an older read. | ||
| 166 | 6 | const quint64 generation = m_sourceGeneration; | |
| 167 | 6 | const qint64 expectedSkip = m_sourceSkip; | |
| 168 |
1/1✓ Branch 17 → 18 taken 6 times.
|
6 | emit sourceChanged(); |
| 169 |
2/2✓ Branch 18 → 19 taken 6 times.
✓ Branch 21 → 22 taken 6 times.
|
12 | MailPayloadExportService::shared()->readWindow( |
| 170 |
1/1✓ Branch 19 → 20 taken 6 times.
|
6 | this, request, [this, generation, expectedSkip](QByteArray bytes, MailPayloadReadResult result) { |
| 171 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 5 times.
|
6 | if (generation != m_sourceGeneration) |
| 172 | 1 | return; // a newer view lifetime owns the state | |
| 173 | 5 | m_sourcePending = false; | |
| 174 |
2/4✓ Branch 4 → 5 taken 5 times.
✗ Branch 4 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 12 taken 5 times.
|
5 | if (!m_sourceActive || !result.completed) { |
| 175 | ✗ | if (m_sourceActive && !result.completed) { | |
| 176 | ✗ | m_deps.setStatus(result.error); | |
| 177 | ✗ | m_sourceFullyLoaded = true; | |
| 178 | } | ||
| 179 | ✗ | emit sourceChanged(); | |
| 180 | ✗ | return; | |
| 181 | } | ||
| 182 |
1/2✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 5 times.
|
5 | if (m_sourceSkip != expectedSkip) |
| 183 | ✗ | return; // not the window this state is built from | |
| 184 |
4/8✓ Branch 14 → 15 taken 3 times.
✓ Branch 14 → 19 taken 2 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 19 taken 3 times.
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 19 not taken.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 26 taken 5 times.
|
5 | if (m_sourceSkip == 0 && bytes.isEmpty() && result.totalBytes == 0) { |
| 185 | ✗ | m_deps.setStatus(tr("No source is stored for this message.")); | |
| 186 | ✗ | m_sourceFullyLoaded = true; | |
| 187 | ✗ | emit sourceChanged(); | |
| 188 | ✗ | return; | |
| 189 | } | ||
| 190 | // F-71: hold a trailing partial UTF-8 sequence back until the | ||
| 191 | // next window (or the true end) so a character split across the | ||
| 192 | // window boundary decodes once. Byte offsets keep counting | ||
| 193 | // source bytes, not QString length. | ||
| 194 | 5 | m_sourceSkip += bytes.size(); | |
| 195 | 5 | m_sourceTotal = result.totalBytes; | |
| 196 |
2/2✓ Branch 27 → 28 taken 2 times.
✓ Branch 27 → 29 taken 3 times.
|
7 | m_sourceFullyLoaded = m_sourceSkip >= m_sourceTotal || |
| 197 |
1/2✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 2 times.
|
2 | m_sourceSkip >= SourceMaximumLoadedBytes; |
| 198 |
1/1✓ Branch 31 → 32 taken 5 times.
|
5 | QByteArray window = m_sourcePendingBytes + bytes; |
| 199 |
1/1✓ Branch 32 → 33 taken 5 times.
|
5 | m_sourcePendingBytes.clear(); |
| 200 | 5 | const int hold = m_sourceFullyLoaded | |
| 201 |
2/2✓ Branch 33 → 34 taken 3 times.
✓ Branch 33 → 35 taken 2 times.
|
5 | ? 0 |
| 202 |
1/1✓ Branch 35 → 36 taken 2 times.
|
2 | : trailingPartialUtf8(window); |
| 203 |
2/2✓ Branch 39 → 40 taken 5 times.
✓ Branch 40 → 41 taken 5 times.
|
5 | m_sourceText += QString::fromUtf8(window.constData(), window.size() - hold); |
| 204 |
2/2✓ Branch 42 → 43 taken 1 time.
✓ Branch 42 → 47 taken 4 times.
|
5 | if (hold > 0) |
| 205 |
1/1✓ Branch 43 → 44 taken 1 time.
|
1 | m_sourcePendingBytes = window.right(hold); |
| 206 |
1/1✓ Branch 47 → 48 taken 5 times.
|
5 | emit sourceChanged(); |
| 207 | 5 | }); | |
| 208 | 6 | } | |
| 209 | |||
| 210 | 4 | void QuickSourceViewer::viewAttachmentPdf(qint64 attachmentId) { | |
| 211 | 4 | const auto id = m_workspace.mailId(); | |
| 212 |
7/9✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 9 taken 1 time.
✓ Branch 6 → 7 taken 3 times.
✓ Branch 7 → 8 taken 3 times.
✗ Branch 7 → 9 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 3 times.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 3 times.
|
4 | if (!id.isValid() || !m_deps.configured() || attachmentId <= 0) return; |
| 213 |
1/1✓ Branch 14 → 15 taken 3 times.
|
3 | const auto epoch = m_session.cache()->uidValidity(id.folderId); |
| 214 |
1/2✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 3 times.
|
3 | if (epoch == 0) return; |
| 215 | // Admission against the mail's cached metadata; only PDFs render inline. | ||
| 216 | 3 | QString filename; | |
| 217 | 3 | bool isPdf = false; | |
| 218 |
4/5✓ Branch 19 → 20 taken 3 times.
✓ Branch 20 → 21 taken 3 times.
✓ Branch 21 → 22 taken 3 times.
✓ Branch 40 → 23 taken 3 times.
✗ Branch 40 → 41 not taken.
|
3 | for (const auto &attachment : m_session.cache()->attachments().attachments(id.folderId, id.uid)) { |
| 219 |
1/2✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 3 times.
|
3 | if (attachment.id != attachmentId) continue; |
| 220 | 3 | filename = attachment.filename; | |
| 221 |
1/2✗ Branch 27 → 28 not taken.
✓ Branch 27 → 36 taken 3 times.
|
3 | if (attachment.size > PdfAdmission::maximumBytes) { |
| 222 | ✗ | m_pdfRejectedAttachments.append(attachmentId); | |
| 223 | ✗ | emit pdfChanged(); | |
| 224 | ✗ | m_deps.setStatus(tr("This attachment is too large to preview. Save it to open it separately.")); | |
| 225 | ✗ | return; | |
| 226 | } | ||
| 227 |
1/1✓ Branch 36 → 37 taken 3 times.
|
3 | isPdf = contentTypeIsPdf(attachment.contentType, attachment.filename); |
| 228 | 3 | break; | |
| 229 | 3 | } | |
| 230 |
1/2✗ Branch 45 → 47 not taken.
✓ Branch 45 → 51 taken 3 times.
|
3 | if (!isPdf) { |
| 231 | ✗ | m_deps.setStatus(tr("Only PDF attachments can be shown inline.")); | |
| 232 | ✗ | return; | |
| 233 | } | ||
| 234 |
2/8✗ Branch 52 → 53 not taken.
✓ Branch 52 → 57 taken 3 times.
✗ Branch 54 → 55 not taken.
✗ Branch 54 → 57 not taken.
✗ Branch 55 → 56 not taken.
✗ Branch 55 → 57 not taken.
✗ Branch 58 → 59 not taken.
✓ Branch 58 → 60 taken 3 times.
|
3 | if (m_pdfPreview && m_pdfName == filename && m_pdfActive) return; |
| 235 | 3 | MailPayloadRequest request{m_session.cache()->databasePath(), id, epoch, | |
| 236 | 3 | MailPayloadRequest::Kind::Attachment, attachmentId}; | |
| 237 |
2/2✓ Branch 63 → 64 taken 3 times.
✓ Branch 67 → 68 taken 3 times.
|
6 | const QString cacheDirectory = QDir(m_profileDirectory).filePath(QStringLiteral("pdf-preview")); |
| 238 |
3/3✓ Branch 71 → 72 taken 3 times.
✓ Branch 73 → 74 taken 3 times.
✓ Branch 74 → 75 taken 3 times.
|
6 | m_deps.setStatus(tr("Preparing %1…").arg(filename)); |
| 239 |
2/2✓ Branch 77 → 78 taken 3 times.
✓ Branch 83 → 84 taken 3 times.
|
6 | MailPayloadExportService::shared()->preparePreview( |
| 240 | &m_pdfOwner, request, cacheDirectory, | ||
| 241 |
1/5✓ Branch 80 → 81 taken 3 times.
✗ Branch 139 → 140 not taken.
✗ Branch 139 → 141 not taken.
✗ Branch 142 → 143 not taken.
✗ Branch 142 → 144 not taken.
|
6 | [this, id, epoch, filename, attachmentId](std::shared_ptr<const MailPayloadPreview> preview, |
| 242 | MailPayloadReadResult result) { | ||
| 243 | // F3: only the request that still belongs to the current selection | ||
| 244 | // may publish or report. A stale result is dropped silently: its | ||
| 245 | // lease releases the snapshot file on scope exit, and it must not | ||
| 246 | // clobber the new selection's status. | ||
| 247 |
1/2✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 9 not taken.
|
4 | const bool current = m_workspace.mailId() == id && |
| 248 |
1/2✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 9 not taken.
|
2 | m_session.cache()->uidValidity(id.folderId) == epoch; |
| 249 |
4/8✓ Branch 10 → 11 taken 2 times.
✗ Branch 10 → 17 not taken.
✓ Branch 12 → 13 taken 2 times.
✗ Branch 12 → 17 not taken.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 2 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 28 taken 2 times.
|
2 | if (!result.completed || !preview || preview->path().isEmpty()) { |
| 250 | ✗ | if (current) | |
| 251 | ✗ | m_deps.setStatus(!result.completed ? result.error : tr("The attachment could not be prepared.")); | |
| 252 | ✗ | return; | |
| 253 | } | ||
| 254 |
1/2✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 2 times.
|
2 | if (!current) return; |
| 255 | 2 | const auto admission = PdfAdmission::checkFile(preview->path()); | |
| 256 |
2/2✓ Branch 33 → 34 taken 1 time.
✓ Branch 33 → 44 taken 1 time.
|
2 | if (admission != PdfAdmission::Result::Accepted) { |
| 257 |
1/1✓ Branch 35 → 36 taken 1 time.
|
1 | m_pdfRejectedAttachments.append(attachmentId); |
| 258 | 1 | emit pdfChanged(); | |
| 259 |
1/1✓ Branch 41 → 42 taken 1 time.
|
1 | m_deps.setStatus(admission == PdfAdmission::Result::TooLarge |
| 260 |
2/4✗ Branch 38 → 39 not taken.
✓ Branch 38 → 40 taken 1 time.
✗ Branch 39 → 41 not taken.
✓ Branch 40 → 41 taken 1 time.
|
2 | ? tr("This attachment is too large to preview. Save it to open it separately.") |
| 261 | : tr("This attachment cannot be previewed as a PDF. Save it to open it separately.")); | ||
| 262 | 1 | return; | |
| 263 | } | ||
| 264 | 1 | m_pdfPreview = std::move(preview); | |
| 265 | 1 | m_pdfActive = true; | |
| 266 |
1/1✓ Branch 48 → 49 taken 1 time.
|
1 | m_pdfSource = QUrl::fromLocalFile(m_pdfPreview->path()); |
| 267 | 1 | m_pdfName = filename; | |
| 268 | 1 | emit pdfChanged(); | |
| 269 | }); | ||
| 270 | 4 | } | |
| 271 | |||
| 272 | ✗ | void QuickSourceViewer::closePdf() { closePdfInternal(); } | |
| 273 | |||
| 274 | 1569 | void QuickSourceViewer::closePdfInternal() { | |
| 275 | 1569 | m_pdfPreview.reset(); | |
| 276 |
6/8✓ Branch 3 → 4 taken 1568 times.
✓ Branch 3 → 9 taken 1 time.
✓ Branch 5 → 6 taken 1568 times.
✗ Branch 5 → 9 not taken.
✓ Branch 7 → 8 taken 1568 times.
✗ Branch 7 → 9 not taken.
✓ Branch 10 → 11 taken 1568 times.
✓ Branch 10 → 12 taken 1 time.
|
1569 | if (!m_pdfActive && m_pdfSource.isEmpty() && m_pdfName.isEmpty()) return; |
| 277 | 1 | m_pdfActive = false; | |
| 278 | 1 | m_pdfSource.clear(); | |
| 279 | 1 | m_pdfName.clear(); | |
| 280 | 1 | emit pdfChanged(); | |
| 281 | } | ||
| 282 |