RecursiveAI.OneMail.Composition makes the replies and forwards a mail client would, from any item in the shared view (MIME, MSG,
PST/OST, TNEF, OLM, DBX). item.CreateReply(), item.CreateReplyAll() and item.CreateForward() are one call each over
MessageComposer. The result (ComposedMessage) holds the new message as a MIME entity tree, and, for Outlook originals, the same
message in the MAPI property model. Nothing is sent, and the original is not changed. The same inputs and options always give the same bytes
when the clock (TimeProvider) and the random source (RandomBytes) are fixed.
Subject
One prefix, never stacked. Every leading prefix of the original is removed (Re: RE: Fwd: AW: topic becomes topic), then the new
prefix is put on: Re: for replies, Fwd: for forwards (both can be replaced in the options). A prefix is a label from
SubjectPrefixes.Labels, ignoring case, an optional counter in square brackets or parentheses (Re[2]:, Re(3):), optional blanks, and
a colon (ASCII or full-width). A word that is not in the list (Status: x) is part of the subject. A subject that is only prefixes
becomes empty. Control characters and line separators in the original's subject become spaces, and trailing white space is dropped, so a
decoded subject can never start a new header.
The labels are the ones language packs of common clients write; they are an agent default and not a specification: Re, Fw, Fwd (English); AW, WG (German); SV, Svar, VS, VB, ATB (Scandinavian); Antw, Doorst (Dutch); TR, Ré, Réf, Rép (French); RV, PD, Reenv (Spanish); ENC, RES, Resp (Portuguese); R, RIF, Inoltro (Italian); Odp (Polish); VL (Finnish); YNT, ILT (Turkish). Labels in non-Latin scripts are not recognised.
MAPI: the MS-OXCMAIL import splits the subject (MS-OXCMAIL 2.2.3.2.6.1), so PidTagSubjectPrefix is Re: or Fwd: and
PidTagNormalizedSubject is the rest.
Threading headers (RFC 5322 §3.6.4)
Replies only. A forward starts no reply thread: it has no In-Reply-To or References (agent default; clients differ), but keeps the
conversation topic and index.
In-Reply-To: the original'sMessage-ID.References: the original'sReferences, or itsIn-Reply-Towhen it has none, then itsMessage-ID(not repeated when it is already last). Identifiers that cannot be written (white space, angle brackets, control characters, longer than 512 characters) are dropped withInvalidMessageIdDropped.- Too long a chain is cut to
MaxReferences(default 20, at least 2) by keeping the first identifier and the most recent ones, and raisesReferencesTruncated. - An original without a
Message-IDgets noIn-Reply-To(ParentHasNoMessageId);Referencesholds what it had. - The new
Message-IDcomes fromMessageIdFactory, orrandom@MessageIdDomain; with neither, none is written and the sending server adds one.
Conversation topic and index (MS-OXOMSG 2.2.1.3)
Thread-Topic and Thread-Index (MAPI PidTagConversationTopic, PidTagConversationIndex) are written when the original is an Outlook item
or already has an index, or when ConversationIndexMode.Always is set; MIME messages from other clients get none by default.
- The topic is the original's topic, else its normalised subject.
- A valid index (six bytes of FILETIME, a 16-byte GUID, then five-byte levels) gets one more level: a delta code bit and 31 bits of time
delta from the header's time, then a random byte. The delta is
TimeDiff >> 18when the high 32 bits of the difference AND0x00FE0000are zero (under about 1.7 years), elseTimeDiff >> 23with the delta code set. A clock before the header's time gives delta 0 (ConversationIndexTimeClamped). - No index, or one that is not in the form (
ConversationIndexInvalid), starts a new thread: a header block with the original's delivery or sent time (never later than now) and a new GUID, then the level. - An index that has reached 255 bytes is kept as it is (
ConversationIndexFull).
ConversationIndex.TryParse, CreateHeader and AppendLevel are public and decode and extend indexes by themselves.
Observed against the spec. MS-OXOMSG draws the header as a reserved byte 0x01 followed by a 40-bit time (the high 32 bits of the FILETIME and the top 8 bits of its low half). Indexes in real Outlook items (45 MSG files and the CFReDS OST) carry the time from the first byte on: `01 CD 50 A5 70 16` is the FILETIME's top six bytes, whose first byte is 0x01 for every date between 1829 and 2057. OneMail reads and writes that form, so the header time has 2^16 FILETIME ticks (6.5 ms) of resolution. The spec's delta rules are followed as written.
Recipients
- Reply:
Mail-Reply-Toif present (MIME items), elseReply-To, elseFrom(RFC 5322 §3.6.2). Replying to a message the user sent (every address is one ofOwnAddresses) goes to that message'sToinstead. - Reply all: the reply target plus the original's
To, thenCc; the user's own addresses are removed; each address appears once,Totaking precedence overCc. AMail-Followup-Toheader (MIME items) replaces the whole list. If onlyCcis left it becomesTo. If nobody is left, the people the user wrote to are used. - Addresses are compared ignoring case (the local part is technically case-sensitive; no real server treats it so).
- Recipients without an SMTP form (an Exchange distinguished name only) are never written and never lost silently: they are in
ComposedMessage.UnaddressablewithUnaddressableRecipient. An address that is not usable (no single@, white space, specials, longer than 320 characters) is dropped withInvalidAddressDropped. More thanMaxRecipients(default 500) stops the list withTooManyRecipients. No recipient at all raisesNoReplyTarget; the message is still made so the caller can address it. - Display names are written as atoms, quoted strings or RFC 2047 encoded words as needed; control characters become spaces.
Quoting
Top posting: the author's Text/Html (if given), then the quote.
- Text: the attribution line (
On Mon, 4 Mar 2024 09:00:00 +0100, Ann <[email protected]> wrote:by default;Attributionreplaces it), then every line of the original's text body prefixed with>(>alone for an empty line). Aformat=flowedoriginal gives aformat=flowedreply (RFC 3676 §4.5): quoted lines get>with no space, soft line breaks (trailing space) survive, our own lines are space-stuffed, anddelspis kept. - HTML:
<div>attribution</div><blockquote type="cite">…</blockquote>, the original's content between<body>and</body>(all of it when there is no body element). The quoted HTML is not sanitised: it is as hostile as the original, and anything that shows it goes through the display sanitiser. - A body larger than
MaxQuoteCharacters(default 1,048,576) is cut at a line end (QuoteTruncated). - Both representations are quoted when the original has both (
multipart/alternative); one when it has one.QuoteBodyFormat.TextOnlyorHtmlOnlyquotes only that one; if it does not exist, nothing is quoted (BodyNotAvailable): HTML is not turned into text here. - Inline images (
cid:) that the quoted HTML refers to are carried in amultipart/related. A reply carries other attachments only withIncludeAttachments. - Inline forward: the banner
---------- Forwarded message ----------and the header block (From, Date, Subject, To, Cc), then the original's body unquoted, with all attachments carried. Embedded messages (message/rfc822) are carried as messages. - Attachment forward: a
message/rfc822part withContent-Disposition: attachment; filename="<subject>.eml". MIME originals are embedded as their own bytes (a fresh parse of the item's message gives back exactly the source bytes, malformed input included). Outlook items are exported by the MS-OXCMAIL mapping. An original that is neither (an OLM record) is rebuilt from the shared view withParentSynthesized. - At most
MaxCarriedBytes(default 64 MiB) of attachments and embedded originals are carried; the rest is skipped withAttachmentSkipped.
MAPI result
When the original is an Outlook item (or CreateMapi is true) the finished MIME message is imported by MimeToMapiConverter, so the
MAPI message is exactly what the MS-OXCMAIL mapping makes of the MIME one, and ComposedMessage.Report is that import's loss report.
PidTagInReplyToId holds the one identifier, PidTagInternetReferences the chain, PidTagConversationTopic and PidTagConversationIndex the topic and
the index. An attachment forward carries the original as an embedded message.
Not covered
Sending, signing, sanitising the quote, HTML to text, meeting responses (accept/decline), drafts' X-Unsent, Disposition-Notification-To, and
translated attribution or banner text (pass your own).
How it is checked
tools/composition/run.sh: replies, reply-alls and both forwards of every sampled item, parsed back by OneMail, MimeKit, Python email and Go
net/mail, with subject, From, To, Cc, In-Reply-To and References compared to what the composer says it wrote. See docs/compatibility.md for the
latest numbers. fuzz/ has the composition target.