Files
postmoogle/email/email.go

226 lines
5.2 KiB
Go

package email
import (
"crypto"
"crypto/x509"
"encoding/pem"
"strings"
"github.com/emersion/go-msgauth/dkim"
"github.com/jhillyerd/enmime"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"gitlab.com/etke.cc/postmoogle/utils"
)
// Email object
type Email struct {
Date string
MessageID string
InReplyTo string
References string
From string
To string
RcptTo string
CC []string
Subject string
Text string
HTML string
Files []*utils.File
}
// New constructs Email object
func New(messageID, inReplyTo, references, subject, from, to, rcptto, cc, text, html string, files []*utils.File) *Email {
email := &Email{
Date: dateNow(),
MessageID: messageID,
InReplyTo: inReplyTo,
References: references,
From: Address(from),
To: Address(to),
CC: AddressList(cc),
RcptTo: rcptto,
Subject: subject,
Text: text,
HTML: html,
Files: files,
}
if html != "" {
html = styleRegex.ReplaceAllString(html, "")
email.HTML = html
}
return email
}
// FromEnvelope constructs Email object from envelope
func FromEnvelope(rcptto string, envelope *enmime.Envelope) *Email {
datetime, _ := envelope.Date() //nolint:errcheck // handled in dateNow()
date := dateNow(datetime)
var html string
if envelope.HTML != "" {
html = styleRegex.ReplaceAllString(envelope.HTML, "")
}
files := make([]*utils.File, 0, len(envelope.Attachments))
for _, attachment := range envelope.Attachments {
file := utils.NewFile(attachment.FileName, attachment.Content)
files = append(files, file)
}
email := &Email{
Date: date,
MessageID: envelope.GetHeader("Message-Id"),
InReplyTo: envelope.GetHeader("In-Reply-To"),
References: envelope.GetHeader("References"),
From: Address(envelope.GetHeader("From")),
To: Address(envelope.GetHeader("To")),
RcptTo: Address(rcptto),
CC: AddressList(envelope.GetHeader("Cc")),
Subject: envelope.GetHeader("Subject"),
Text: envelope.Text,
HTML: html,
Files: files,
}
return email
}
// Mailbox returns postmoogle's mailbox, parsing it from FROM (if incoming=false) or TO (incoming=true)
func (e *Email) Mailbox(incoming bool) string {
if incoming {
return utils.Mailbox(e.RcptTo)
}
return utils.Mailbox(e.From)
}
// Content converts the email object to a Matrix event content
func (e *Email) Content(threadID id.EventID, options *ContentOptions) *event.Content {
var text strings.Builder
if options.Sender {
text.WriteString(e.From)
}
if options.Recipient {
text.WriteString(" ➡️ ")
text.WriteString(e.To)
}
if options.CC && len(e.CC) > 0 {
text.WriteString("\ncc: ")
text.WriteString(strings.Join(e.CC, ", "))
}
if options.Sender || options.Recipient || options.CC {
text.WriteString("\n\n")
}
if options.Subject && threadID == "" {
text.WriteString("# ")
text.WriteString(e.Subject)
text.WriteString("\n\n")
}
if e.HTML != "" && options.HTML {
text.WriteString(format.HTMLToMarkdown(e.HTML))
} else {
text.WriteString(e.Text)
}
parsed := format.RenderMarkdown(text.String(), true, true)
parsed.RelatesTo = utils.RelatesTo(options.Threads, threadID)
var cc string
if len(e.CC) > 0 {
cc = strings.Join(e.CC, ", ")
}
content := event.Content{
Raw: map[string]interface{}{
options.MessageIDKey: e.MessageID,
options.InReplyToKey: e.InReplyTo,
options.ReferencesKey: e.References,
options.SubjectKey: e.Subject,
options.RcptToKey: e.RcptTo,
options.FromKey: e.From,
options.ToKey: e.To,
options.CcKey: cc,
},
Parsed: &parsed,
}
return &content
}
// Compose converts the email object to a string (to be used for delivery via SMTP) and possibly DKIM-signs it
func (e *Email) Compose(privkey string) string {
textSize := len(e.Text)
htmlSize := len(e.HTML)
if textSize == 0 && htmlSize == 0 {
return ""
}
mail := enmime.Builder().
From("", e.From).
To("", e.To).
Header("Message-Id", e.MessageID).
Subject(e.Subject)
if textSize > 0 {
mail = mail.Text([]byte(e.Text))
}
if htmlSize > 0 {
mail = mail.HTML([]byte(e.HTML))
}
if e.InReplyTo != "" {
mail = mail.Header("In-Reply-To", e.InReplyTo)
}
if e.References != "" {
mail = mail.Header("References", e.References)
}
if len(e.CC) > 0 {
for _, addr := range e.CC {
mail = mail.CC("", addr)
}
}
root, err := mail.Build()
if err != nil {
return ""
}
var data strings.Builder
err = root.Encode(&data)
if err != nil {
return ""
}
domain := strings.SplitN(e.From, "@", 2)[1]
return e.sign(domain, privkey, data)
}
func (e *Email) sign(domain, privkey string, data strings.Builder) string {
if privkey == "" {
return data.String()
}
pemblock, _ := pem.Decode([]byte(privkey))
if pemblock == nil {
return data.String()
}
parsedkey, err := x509.ParsePKCS8PrivateKey(pemblock.Bytes)
if err != nil {
return data.String()
}
signer := parsedkey.(crypto.Signer)
options := &dkim.SignOptions{
Domain: domain,
Selector: "postmoogle",
Signer: signer,
}
var msg strings.Builder
err = dkim.Sign(&msg, strings.NewReader(data.String()), options)
if err != nil {
return data.String()
}
return msg.String()
}