@@ -249,6 +249,12 @@ func sendSTARTTLSWithConfig(addr, host string, tlsCfg *tls.Config, auth smtp.Aut
249249// otherwise the structure is unchanged (multipart/alternative only).
250250// htmlSignature, if non-empty, is injected before the closing </body> tag in the HTML part.
251251func BuildMessage (from , to , cc , subject , markdownBody string , attachments []string , htmlSignature string ) ([]byte , error ) {
252+ return BuildMessageWithThreading (from , to , cc , subject , markdownBody , attachments , htmlSignature , "" , "" )
253+ }
254+
255+ // BuildMessageWithThreading builds a MIME message with optional threading headers (In-Reply-To, References).
256+ // Used for replies and forwards to maintain proper email conversation threading.
257+ func BuildMessageWithThreading (from , to , cc , subject , markdownBody string , attachments []string , htmlSignature , inReplyTo , references string ) ([]byte , error ) {
252258 htmlBody , err := render .ToHTML (markdownBody )
253259 if err != nil {
254260 return nil , fmt .Errorf ("markdown to html: %w" , err )
@@ -262,7 +268,16 @@ func BuildMessage(from, to, cc, subject, markdownBody string, attachments []stri
262268 htmlBody = htmlBody [:idx ] + "\n " + htmlSignature + "\n " + htmlBody [idx :]
263269 }
264270 }
265- return buildMessage (from , to , cc , subject , markdownBody , htmlBody , attachments )
271+ // Build References chain: append inReplyTo to existing references
272+ refChain := references
273+ if inReplyTo != "" {
274+ if refChain != "" {
275+ refChain = refChain + " " + inReplyTo
276+ } else {
277+ refChain = inReplyTo
278+ }
279+ }
280+ return buildMessageWithBCC (from , to , cc , "" , subject , markdownBody , htmlBody , attachments , inReplyTo , refChain )
266281}
267282
268283// BuildDraftMessage constructs a raw MIME draft for IMAP APPEND.
@@ -271,8 +286,27 @@ func BuildMessage(from, to, cc, subject, markdownBody string, attachments []stri
271286// Drafts are stored as plain text only (no HTML conversion) to preserve the
272287// original markdown formatting exactly during save/load cycles.
273288func BuildDraftMessage (from , to , cc , bcc , subject , markdownBody string , attachments []string ) ([]byte , error ) {
274- // Pass empty htmlBody to store plain text only
275- return buildMessageWithBCC (from , to , cc , bcc , subject , markdownBody , "" , attachments )
289+ // Pass empty htmlBody to store plain text only; no threading headers for drafts
290+ return buildMessageWithBCC (from , to , cc , bcc , subject , markdownBody , "" , attachments , "" , "" )
291+ }
292+
293+ // BuildReactionMessage constructs a minimal reaction email with threading headers.
294+ // Used for emoji reactions sent as replies to emails.
295+ // plainBody and htmlBody are pre-formatted reaction messages (emoji + footer).
296+ // inReplyTo is the Message-ID of the original email.
297+ // references is the References chain from the original email (may be empty).
298+ func BuildReactionMessage (from , to , cc , subject , plainBody , htmlBody , inReplyTo , references string ) ([]byte , error ) {
299+ // Build References chain: append inReplyTo to existing references
300+ refChain := references
301+ if inReplyTo != "" {
302+ if refChain != "" {
303+ refChain = refChain + " " + inReplyTo
304+ } else {
305+ refChain = inReplyTo
306+ }
307+ }
308+ // No attachments for reactions
309+ return buildMessageWithBCC (from , to , cc , "" , subject , plainBody , htmlBody , nil , inReplyTo , refChain )
276310}
277311
278312// inlineImage holds a local image path and its assigned Content-ID.
@@ -291,10 +325,10 @@ type inlineImage struct {
291325// - images only → multipart/related > (multipart/alternative + inline images)
292326// - images + files → multipart/mixed > (multipart/related > alt+images) + files
293327func buildMessage (from , to , cc , subject , plainText , htmlBody string , attachments []string ) ([]byte , error ) {
294- return buildMessageWithBCC (from , to , cc , "" , subject , plainText , htmlBody , attachments )
328+ return buildMessageWithBCC (from , to , cc , "" , subject , plainText , htmlBody , attachments , "" , "" )
295329}
296330
297- func buildMessageWithBCC (from , to , cc , bcc , subject , plainText , htmlBody string , attachments []string ) ([]byte , error ) {
331+ func buildMessageWithBCC (from , to , cc , bcc , subject , plainText , htmlBody string , attachments []string , inReplyTo , references string ) ([]byte , error ) {
298332 // Find local image paths in htmlBody (<img src="/abs/path">), assign CIDs.
299333 var inlines []inlineImage
300334 processedHTML := imgSrcRe .ReplaceAllStringFunc (htmlBody , func (match string ) string {
@@ -332,6 +366,13 @@ func buildMessageWithBCC(from, to, cc, bcc, subject, plainText, htmlBody string,
332366 hdr ("Subject" , mime .QEncoding .Encode ("utf-8" , subject ))
333367 hdr ("Date" , time .Now ().Format (time .RFC1123Z ))
334368 hdr ("Message-ID" , "<" + msgID + "@neomd>" )
369+ // Threading headers for replies
370+ if inReplyTo != "" {
371+ hdr ("In-Reply-To" , inReplyTo )
372+ }
373+ if references != "" {
374+ hdr ("References" , references )
375+ }
335376 hdr ("MIME-Version" , "1.0" )
336377 hdr ("Content-Type" , contentType )
337378 hdr ("X-Mailer" , "neomd" )
@@ -356,6 +397,13 @@ func buildMessageWithBCC(from, to, cc, bcc, subject, plainText, htmlBody string,
356397 hdr ("Subject" , mime .QEncoding .Encode ("utf-8" , subject ))
357398 hdr ("Date" , time .Now ().Format (time .RFC1123Z ))
358399 hdr ("Message-ID" , "<" + msgID + "@neomd>" )
400+ // Threading headers for replies
401+ if inReplyTo != "" {
402+ hdr ("In-Reply-To" , inReplyTo )
403+ }
404+ if references != "" {
405+ hdr ("References" , references )
406+ }
359407 hdr ("MIME-Version" , "1.0" )
360408 hdr ("Content-Type" , "text/plain; charset=utf-8" )
361409 hdr ("Content-Transfer-Encoding" , "quoted-printable" )
0 commit comments