diff --git a/bot/command.go b/bot/command.go index 9cc1d94..4a8266b 100644 --- a/bot/command.go +++ b/bot/command.go @@ -364,15 +364,15 @@ func (b *Bot) runSend(ctx context.Context) { defer b.unlock(evt.RoomID) from := mailbox + "@" + b.domains[0] - ID := fmt.Sprintf("<%s@%s>", evt.ID, b.domains[0]) + ID := utils.MessageID(evt.ID, b.domains[0]) for _, to := range tos { - email := utils.NewEmail(ID, "", subject, from, to, body, "", nil) + email := utils.NewEmail(ID, "", " "+ID, subject, from, to, body, "", nil) data := email.Compose(b.getBotSettings().DKIMPrivateKey()) err = b.sendmail(from, to, data) if err != nil { b.Error(ctx, evt.RoomID, "cannot send email to %s: %v", to, err) } else { - b.forgeSentMetadata(ctx, email, &cfg) + b.saveSentMetadata(ctx, email, &cfg) } } if len(tos) > 1 { @@ -380,9 +380,9 @@ func (b *Bot) runSend(ctx context.Context) { } } -// forgeSentMetadata used to save metadata from !pm sent event to a separate notice message +// saveSentMetadata used to save metadata from !pm sent event to a separate notice message // because that metadata is needed to determine email thread relations -func (b *Bot) forgeSentMetadata(ctx context.Context, email *utils.Email, cfg *roomSettings) { +func (b *Bot) saveSentMetadata(ctx context.Context, email *utils.Email, cfg *roomSettings) { evt := eventFromContext(ctx) threadID := utils.EventParent(evt.ID, evt.Content.AsMessage()) content := email.Content(threadID, cfg.ContentOptions()) @@ -402,7 +402,7 @@ func (b *Bot) forgeSentMetadata(ctx context.Context, email *utils.Email, cfg *ro return } if threadID != "" { - b.setThreadID(evt.RoomID, fmt.Sprintf("<%s@%s>", msgID, b.domains[0]), threadID) + b.setThreadID(evt.RoomID, utils.MessageID(msgID, b.domains[0]), threadID) } b.setLastEventID(evt.RoomID, threadID, msgID) } diff --git a/bot/email.go b/bot/email.go index cfa5582..fd756eb 100644 --- a/bot/email.go +++ b/bot/email.go @@ -19,11 +19,12 @@ const ( // event keys const ( - eventMessageIDkey = "cc.etke.postmoogle.messageID" - eventInReplyToKey = "cc.etke.postmoogle.inReplyTo" - eventSubjectKey = "cc.etke.postmoogle.subject" - eventFromKey = "cc.etke.postmoogle.from" - eventToKey = "cc.etke.postmoogle.to" + eventMessageIDkey = "cc.etke.postmoogle.messageID" + eventReferencesKey = "cc.etke.postmoogle.references" + eventInReplyToKey = "cc.etke.postmoogle.inReplyTo" + eventSubjectKey = "cc.etke.postmoogle.subject" + eventFromKey = "cc.etke.postmoogle.from" + eventToKey = "cc.etke.postmoogle.to" ) // SetSendmail sets mail sending func to the bot @@ -115,46 +116,60 @@ func (b *Bot) IncomingEmail(ctx context.Context, email *utils.Email) error { return nil } -func (b *Bot) getParentEmail(evt *event.Event) (string, string, string, string) { +type parentEmail struct { + MessageID string + From string + To string + InReplyTo string + References string + Subject string +} + +func (b *Bot) getParentEmail(evt *event.Event) parentEmail { + var parent parentEmail content := evt.Content.AsMessage() parentID := utils.EventParent(evt.ID, content) if parentID == evt.ID { - return "", "", "", "" + return parent } parentID = b.getLastEventID(evt.RoomID, parentID) parentEvt, err := b.lp.GetClient().GetEvent(evt.RoomID, parentID) if err != nil { b.log.Error("cannot get parent event: %v", err) - return "", "", "", "" + return parent } if parentEvt.Content.Parsed == nil { perr := parentEvt.Content.ParseRaw(event.EventMessage) if perr != nil { b.log.Error("cannot parse event content: %v", perr) - return "", "", "", "" + return parent } } - from := utils.EventField[string](&parentEvt.Content, eventFromKey) - to := utils.EventField[string](&parentEvt.Content, eventToKey) - inReplyTo := utils.EventField[string](&parentEvt.Content, eventMessageIDkey) - if inReplyTo == "" { - inReplyTo = parentID.String() + parent.MessageID = utils.MessageID(parentID, b.domains[0]) + parent.From = utils.EventField[string](&parentEvt.Content, eventFromKey) + parent.To = utils.EventField[string](&parentEvt.Content, eventToKey) + parent.InReplyTo = utils.EventField[string](&parentEvt.Content, eventMessageIDkey) + parent.References = utils.EventField[string](&parentEvt.Content, eventReferencesKey) + if parent.InReplyTo == "" { + parent.InReplyTo = parent.MessageID + } + if parent.References == "" { + parent.References = " " + parent.MessageID } - subject := utils.EventField[string](&parentEvt.Content, eventSubjectKey) - if subject != "" { - subject = "Re: " + subject + parent.Subject = utils.EventField[string](&parentEvt.Content, eventSubjectKey) + if parent.Subject != "" { + parent.Subject = "Re: " + parent.Subject } else { - subject = strings.SplitN(content.Body, "\n", 1)[0] + parent.Subject = strings.SplitN(content.Body, "\n", 1)[0] } - return from, to, inReplyTo, subject + return parent } // SendEmailReply sends replies from matrix thread to email thread func (b *Bot) SendEmailReply(ctx context.Context) { - var inReplyTo string evt := eventFromContext(ctx) cfg, err := b.getRoomSettings(evt.RoomID) if err != nil { @@ -169,35 +184,37 @@ func (b *Bot) SendEmailReply(ctx context.Context) { b.lock(evt.RoomID) defer b.unlock(evt.RoomID) + fromMailbox := mailbox + "@" + b.domains[0] - from, to, inReplyTo, subject := b.getParentEmail(evt) + meta := b.getParentEmail(evt) // when email was sent from matrix and reply was sent from matrix again - if fromMailbox != from { - to = from + if fromMailbox != meta.From { + meta.To = meta.From } - if to == "" { + if meta.To == "" { b.Error(ctx, evt.RoomID, "cannot find parent email and continue the thread. Please, start a new email thread") return } content := evt.Content.AsMessage() - if subject == "" { - subject = strings.SplitN(content.Body, "\n", 1)[0] + if meta.Subject == "" { + meta.Subject = strings.SplitN(content.Body, "\n", 1)[0] } body := content.Body - ID := evt.ID.String()[1:] + "@" + b.domains[0] - b.log.Debug("send email reply ID=%s from=%s to=%s inReplyTo=%s subject=%s body=%s", ID, from, to, inReplyTo, subject, body) - data := utils. - NewEmail(ID, inReplyTo, subject, from, to, body, "", nil). - Compose(b.getBotSettings().DKIMPrivateKey()) + ID := utils.MessageID(evt.ID, b.domains[0]) + meta.References = meta.References + " " + ID + b.log.Debug("send email reply ID=%s meta=%+v", ID, meta) + email := utils.NewEmail(ID, meta.InReplyTo, meta.References, meta.Subject, meta.From, meta.To, body, "", nil) + data := email.Compose(b.getBotSettings().DKIMPrivateKey()) - err = b.sendmail(from, to, data) + err = b.sendmail(meta.From, meta.To, data) if err != nil { b.Error(ctx, evt.RoomID, "cannot send email: %v", err) return } + b.saveSentMetadata(ctx, email, &cfg) } func (b *Bot) sendFiles(ctx context.Context, roomID id.RoomID, files []*utils.File, noThreads bool, parentID id.EventID) { diff --git a/bot/settings_room.go b/bot/settings_room.go index 52d7514..50efc6f 100644 --- a/bot/settings_room.go +++ b/bot/settings_room.go @@ -146,11 +146,12 @@ func (s roomSettings) ContentOptions() *utils.ContentOptions { Subject: !s.NoSubject(), Threads: !s.NoThreads(), - ToKey: eventToKey, - FromKey: eventFromKey, - SubjectKey: eventSubjectKey, - MessageIDKey: eventMessageIDkey, - InReplyToKey: eventInReplyToKey, + ToKey: eventToKey, + FromKey: eventFromKey, + SubjectKey: eventSubjectKey, + MessageIDKey: eventMessageIDkey, + InReplyToKey: eventInReplyToKey, + ReferencesKey: eventReferencesKey, } } diff --git a/smtp/session.go b/smtp/session.go index 63650e4..482c2d7 100644 --- a/smtp/session.go +++ b/smtp/session.go @@ -80,6 +80,7 @@ func (s *incomingSession) Data(r io.Reader) error { email := utils.NewEmail( eml.GetHeader("Message-Id"), eml.GetHeader("In-Reply-To"), + eml.GetHeader("References"), eml.GetHeader("Subject"), s.from, s.to, @@ -132,6 +133,7 @@ func (s *outgoingSession) Data(r io.Reader) error { email := utils.NewEmail( eml.GetHeader("Message-Id"), eml.GetHeader("In-Reply-To"), + eml.GetHeader("References"), eml.GetHeader("Subject"), s.from, s.to, diff --git a/utils/email.go b/utils/email.go index ab3c0ca..cf6c80a 100644 --- a/utils/email.go +++ b/utils/email.go @@ -4,6 +4,7 @@ import ( "crypto" "crypto/x509" "encoding/pem" + "fmt" "net/mail" "strings" "time" @@ -23,15 +24,16 @@ type IncomingFilteringOptions interface { // Email object type Email struct { - Date string - MessageID string - InReplyTo string - From string - To string - Subject string - Text string - HTML string - Files []*File + Date string + MessageID string + InReplyTo string + References string + From string + To string + Subject string + Text string + HTML string + Files []*File } // ContentOptions represents settings that specify how an email is to be converted to a Matrix message @@ -44,11 +46,12 @@ type ContentOptions struct { Threads bool // Keys - MessageIDKey string - InReplyToKey string - SubjectKey string - FromKey string - ToKey string + MessageIDKey string + InReplyToKey string + ReferencesKey string + SubjectKey string + FromKey string + ToKey string } // AddressValid checks if email address is valid @@ -57,18 +60,24 @@ func AddressValid(email string) bool { return err == nil } +// MessageID generates email Message-Id from matrix event ID +func MessageID(eventID id.EventID, domain string) string { + return fmt.Sprintf("<%s@%s>", eventID, domain) +} + // NewEmail constructs Email object -func NewEmail(messageID, inReplyTo, subject, from, to, text, html string, files []*File) *Email { +func NewEmail(messageID, inReplyTo, references, subject, from, to, text, html string, files []*File) *Email { email := &Email{ - Date: time.Now().UTC().Format(time.RFC1123Z), - MessageID: messageID, - InReplyTo: inReplyTo, - From: from, - To: to, - Subject: subject, - Text: text, - HTML: html, - Files: files, + Date: time.Now().UTC().Format(time.RFC1123Z), + MessageID: messageID, + InReplyTo: inReplyTo, + References: references, + From: from, + To: to, + Subject: subject, + Text: text, + HTML: html, + Files: files, } if html != "" { @@ -119,11 +128,12 @@ func (e *Email) Content(threadID id.EventID, options *ContentOptions) *event.Con content := event.Content{ Raw: map[string]interface{}{ - options.MessageIDKey: e.MessageID, - options.InReplyToKey: e.InReplyTo, - options.SubjectKey: e.Subject, - options.FromKey: e.From, - options.ToKey: e.To, + options.MessageIDKey: e.MessageID, + options.InReplyToKey: e.InReplyTo, + options.ReferencesKey: e.References, + options.SubjectKey: e.Subject, + options.FromKey: e.From, + options.ToKey: e.To, }, Parsed: parsed, } @@ -167,6 +177,12 @@ func (e *Email) Compose(privkey string) string { data.WriteString("\r\n") } + if e.References != "" { + data.WriteString("References: ") + data.WriteString(e.References) + data.WriteString("\r\n") + } + data.WriteString("Subject: ") data.WriteString(e.Subject) data.WriteString("\r\n")