diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/composecontext.cpp | 5 | ||||
| -rw-r--r-- | src/composewindow.cpp | 29 | ||||
| -rw-r--r-- | src/composewindow.h | 11 | ||||
| -rw-r--r-- | src/messagebuilder.cpp | 17 | ||||
| -rw-r--r-- | src/types.h | 27 |
5 files changed, 82 insertions, 7 deletions
diff --git a/src/composecontext.cpp b/src/composecontext.cpp index b02b790..e594e24 100644 --- a/src/composecontext.cpp +++ b/src/composecontext.cpp @@ -572,6 +572,11 @@ ComposeContext ComposeContextBuilder::forDraft(const Config &config, // arriving one step later. context.draftPath = resolved; + // Item 165. The identity this draft already has, so the next autosave + // REPLACES the message on the server rather than minting a second one. + // The parser already reads this header; nothing used it until now. + context.draftMessageId = draft.messageId; + const auto addresses = [](const QString &header) { QStringList out; for (const Recipient &recipient : parseAddressHeader(header)) diff --git a/src/composewindow.cpp b/src/composewindow.cpp index 7952efa..218edef 100644 --- a/src/composewindow.cpp +++ b/src/composewindow.cpp @@ -902,6 +902,9 @@ void ComposeWindow::seedFields() // The draft file this composer is resuming, so the next autosave REPLACES // it rather than writing a second one beside it. m_draftPath = m_context.draftPath; + // And the identity that file already carries (item 165), so resuming does + // not start a second one. + m_draftMessageId = m_context.draftMessageId; // New and Forward seed from [compose] send_html; Reply and Reply-all seed // from whether the original carried a text/html part, ignoring the config @@ -1365,7 +1368,10 @@ bool ComposeWindow::saveDraftNow() return true; } - const OutgoingMessage message = currentMessage(); + // Not const: the draft's own Message-ID is assigned below (item 165). + // fingerprintOf() does not read that field, so the dirty check above is + // unaffected by it. + OutgoingMessage message = currentMessage(); // The dirty CHECK, not just the flag: an unchanged message means no file // is written and no sync is provoked. Every autosave produces a Maildir @@ -1374,10 +1380,13 @@ bool ComposeWindow::saveDraftNow() // // Checked BEFORE the build, and on the message rather than on the bytes. // The plan's draft compared built.bytes, which can never match: GMime is - // given a fresh Date and Message-ID on every build, so two builds of an - // unchanged message differ. That check would have read as working while - // writing a file on every debounce. Doing it first also skips the - // blocking build entirely for the no-change case, which is the common one. + // given a fresh Date on every build, so two builds of an unchanged message + // differ. That check would have read as working while writing a file on + // every debounce. Doing it first also skips the blocking build entirely + // for the no-change case, which is the common one. + // + // The Message-ID is no longer part of that difference since item 165, but + // the Date still is, so comparing bytes remains wrong. const QString fingerprint = fingerprintOf(message); if (!m_savedFingerprint.isEmpty() && fingerprint == m_savedFingerprint) { setDirty(false); @@ -1391,6 +1400,12 @@ bool ComposeWindow::saveDraftNow() // crosses the worker boundary, and a second threading model for one call // is worse than the stall. If someone is measuring a composer freeze, this // line is where to look. + // Item 165. Every revision of this draft builds under ONE id, so a save + // replaces the message on the server rather than adding one. Empty on the + // first save of a new draft, which mints one below; seeded from the file + // for a resumed draft. The SEND path deliberately does not do this. + message.messageId = m_draftMessageId; + const MessageBuilder::Result built = MessageBuilder::build(message, account); if (!built.ok()) { m_saveFailed = true; @@ -1426,6 +1441,10 @@ bool ComposeWindow::saveDraftNow() } m_draftPath = written.path; + // Kept for the NEXT revision (item 165). Assigned from the build rather + // than from m_draftMessageId so the first save adopts the id GMime just + // generated, which is what makes every later save a replacement. + m_draftMessageId = built.messageId; m_savedFingerprint = fingerprint; setDirty(false); m_saveFailed = false; diff --git a/src/composewindow.h b/src/composewindow.h index 9affce1..87b105a 100644 --- a/src/composewindow.h +++ b/src/composewindow.h @@ -371,6 +371,17 @@ private: QString m_draftPath; ///< The revision on disk, unlinked on the next write. + /// The Message-ID this draft's revisions share, empty until the first + /// save. + /// + /// Item 165. Fed back into the next build so a save REPLACES the message + /// mbsync uploaded rather than adding one; without it the server kept one + /// message per revision, measured as four for a single reply. Seeded from + /// the file when a draft is resumed, so reopening does not start a second + /// identity. Deliberately NOT used on the send path: the sent copy is a + /// different item and mints its own id. + QString m_draftMessageId; + /// A fingerprint of the message the last successful save wrote, for the /// dirty CHECK. /// diff --git a/src/messagebuilder.cpp b/src/messagebuilder.cpp index fe23862..ea428ee 100644 --- a/src/messagebuilder.cpp +++ b/src/messagebuilder.cpp @@ -308,8 +308,21 @@ Result build(const OutgoingMessage &message, const Account &account) // invariant the next early return forgets; it is assigned once, beside the // bytes, on the one path that succeeds. QString messageId; - char *generatedId = g_mime_utils_generate_message_id(domainUtf8.constData()); - if (generatedId) { + if (!message.messageId.isEmpty()) { + // Item 165. A draft revision reuses the id its previous build + // returned, so a save REPLACES the message on the server rather than + // adding one. Only the autosave path supplies this; a send leaves it + // empty and gets a fresh id below, because two sent messages must + // never share one. + // + // set_message_id takes the id WITHOUT angle brackets and adds them, + // which is the form generate_message_id returns and the form this is + // stored in. + const QByteArray supplied = message.messageId.toUtf8(); + g_mime_message_set_message_id(mime, supplied.constData()); + messageId = message.messageId; + } else if (char *generatedId = + g_mime_utils_generate_message_id(domainUtf8.constData())) { g_mime_message_set_message_id(mime, generatedId); messageId = QString::fromUtf8(generatedId); g_free(generatedId); diff --git a/src/types.h b/src/types.h index ee2627f..fb7901c 100644 --- a/src/types.h +++ b/src/types.h @@ -352,6 +352,15 @@ struct ComposeContext /// to make room. It is the message itself. QString body; + /// The Message-ID the resumed draft already has, empty for every other + /// kind. + /// + /// Item 165. Seeded into ComposeWindow::m_draftMessageId so a reopened + /// draft keeps the identity it was saved under instead of starting a + /// second one on its next autosave. Distinct from `inReplyTo`, which is + /// the ORIGINAL's id when the draft is a reply; this is the draft's own. + QString draftMessageId; + /// The draft file this composer OWNS, empty for every other kind. /// /// Seeded into ComposeWindow::m_draftPath so the next autosave REPLACES @@ -379,6 +388,24 @@ struct OutgoingMessage QString inReplyTo; QStringList references; + /// The Message-ID to build under, or empty to mint a fresh one. + /// + /// Item 165. A draft keeps ONE identity across its revisions, so an + /// autosave replaces the message it wrote last time instead of adding + /// another. Every save used to generate a new id, and mbsync uploads each + /// revision to the drafts folder before the next save removes the local + /// file, so the server ended up holding one message per revision: + /// measured as four for a single reply on the user's own mail. Deleting a + /// local file does not retract an uploaded one, which is why the local + /// cleanup, which is correct, could never fix this. + /// + /// **Empty on the SEND path, deliberately.** The sent copy is a different + /// item from the draft, at the user's own decision (item 165), and two + /// sent messages sharing an id would be far worse than two ids for one + /// draft. So this is opt-in: only ComposeWindow's autosave fills it, from + /// the id the previous build returned. + QString messageId; + /// Item 171. The forwarded original's HTML, already sanitised, appended to /// the HTML alternative below the user's own text. /// |
