aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/composecontext.cpp5
-rw-r--r--src/composewindow.cpp29
-rw-r--r--src/composewindow.h11
-rw-r--r--src/messagebuilder.cpp17
-rw-r--r--src/types.h27
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.
///