// Copyright (c) 2021 Proton Technologies AG // // This file is part of ProtonMail Bridge. // // ProtonMail Bridge is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // ProtonMail Bridge is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . package message import ( "bytes" "encoding/base64" "io/ioutil" "mime" "net/mail" "strings" "time" "unicode/utf8" "github.com/ProtonMail/go-rfc5322" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/emersion/go-message" "github.com/pkg/errors" ) func buildRFC822(kr *crypto.KeyRing, msg *pmapi.Message, attData [][]byte, opts JobOptions) ([]byte, error) { switch { case len(msg.Attachments) > 0: return buildMultipartRFC822(kr, msg, attData, opts) case msg.MIMEType == "multipart/mixed": return buildEncryptedRFC822(kr, msg, opts) default: return buildSimpleRFC822(kr, msg, opts) } } func buildSimpleRFC822(kr *crypto.KeyRing, msg *pmapi.Message, opts JobOptions) ([]byte, error) { dec, err := msg.Decrypt(kr) if err != nil { if !opts.IgnoreDecryptionErrors { return nil, errors.Wrap(ErrDecryptionFailed, err.Error()) } return buildMultipartRFC822(kr, msg, nil, opts) } hdr := getTextPartHeader(getMessageHeader(msg, opts), dec, msg.MIMEType) buf := new(bytes.Buffer) w, err := message.CreateWriter(buf, hdr) if err != nil { return nil, err } if _, err := w.Write(dec); err != nil { return nil, err } if err := w.Close(); err != nil { return nil, err } return buf.Bytes(), nil } func buildMultipartRFC822( kr *crypto.KeyRing, msg *pmapi.Message, attData [][]byte, opts JobOptions, ) ([]byte, error) { boundary := newBoundary(msg.ID) hdr := getMessageHeader(msg, opts) hdr.SetContentType("multipart/mixed", map[string]string{"boundary": boundary.gen()}) buf := new(bytes.Buffer) w, err := message.CreateWriter(buf, hdr) if err != nil { return nil, err } var ( inlineAtts []*pmapi.Attachment inlineData [][]byte attachAtts []*pmapi.Attachment attachData [][]byte ) for i, att := range msg.Attachments { if att.Disposition == pmapi.DispositionInline { inlineAtts = append(inlineAtts, att) inlineData = append(inlineData, attData[i]) } else { attachAtts = append(attachAtts, att) attachData = append(attachData, attData[i]) } } if len(inlineAtts) > 0 { if err := writeRelatedParts(w, kr, boundary, msg, inlineAtts, inlineData, opts); err != nil { return nil, err } } else if err := writeTextPart(w, kr, msg, opts); err != nil { return nil, err } for i, att := range attachAtts { if err := writeAttachmentPart(w, kr, att, attachData[i], opts); err != nil { return nil, err } } if err := w.Close(); err != nil { return nil, err } return buf.Bytes(), nil } func writeTextPart( w *message.Writer, kr *crypto.KeyRing, msg *pmapi.Message, opts JobOptions, ) error { dec, err := msg.Decrypt(kr) if err != nil { if !opts.IgnoreDecryptionErrors { return errors.Wrap(ErrDecryptionFailed, err.Error()) } /* if len(msg.Attachments) > 0 { return writeCustomTextPartAsAttachment(w, msg, err) } */ return writeCustomTextPart(w, msg, err) } part, err := w.CreatePart(getTextPartHeader(message.Header{}, dec, msg.MIMEType)) if err != nil { return err } if _, err := part.Write(dec); err != nil { return err } return part.Close() } func writeAttachmentPart( w *message.Writer, kr *crypto.KeyRing, att *pmapi.Attachment, attData []byte, opts JobOptions, ) error { kps, err := base64.StdEncoding.DecodeString(att.KeyPackets) if err != nil { return err } msg := crypto.NewPGPSplitMessage(kps, attData).GetPGPMessage() dec, err := kr.Decrypt(msg, nil, crypto.GetUnixTime()) if err != nil { if !opts.IgnoreDecryptionErrors { return errors.Wrap(ErrDecryptionFailed, err.Error()) } return writeCustomAttachmentPart(w, att, msg, err) } part, err := w.CreatePart(getAttachmentPartHeader(att)) if err != nil { return err } if _, err := part.Write(dec.GetBinary()); err != nil { return err } return part.Close() } func writeRelatedParts( w *message.Writer, kr *crypto.KeyRing, boundary *boundary, msg *pmapi.Message, atts []*pmapi.Attachment, attData [][]byte, opts JobOptions, ) error { hdr := message.Header{} hdr.SetContentType("multipart/related", map[string]string{"boundary": boundary.gen()}) rel, err := w.CreatePart(hdr) if err != nil { return err } if err := writeTextPart(rel, kr, msg, opts); err != nil { return err } for i, att := range atts { if err := writeAttachmentPart(rel, kr, att, attData[i], opts); err != nil { return err } } return rel.Close() } func buildEncryptedRFC822(kr *crypto.KeyRing, msg *pmapi.Message, opts JobOptions) ([]byte, error) { hdr := getMessageHeader(msg, opts) hdr.SetContentType("multipart/mixed", map[string]string{"boundary": newBoundary(msg.ID).gen()}) buf := new(bytes.Buffer) w, err := message.CreateWriter(buf, hdr) if err != nil { return nil, err } dec, err := msg.Decrypt(kr) if err != nil { return nil, errors.Wrap(ErrDecryptionFailed, err.Error()) } ent, err := message.Read(bytes.NewReader(dec)) if err != nil { return nil, err } part, err := w.CreatePart(ent.Header) if err != nil { return nil, err } body, err := ioutil.ReadAll(ent.Body) if err != nil { return nil, err } if _, err := part.Write(body); err != nil { return nil, err } if err := part.Close(); err != nil { return nil, err } if err := w.Close(); err != nil { return nil, err } return buf.Bytes(), nil } func getMessageHeader(msg *pmapi.Message, opts JobOptions) message.Header { // nolint[funlen] hdr := toMessageHeader(msg.Header) // SetText will RFC2047-encode. if msg.Subject != "" { hdr.SetText("Subject", msg.Subject) } // mail.Address.String() will RFC2047-encode if necessary. if msg.Sender != nil { hdr.Set("From", msg.Sender.String()) } if len(msg.ReplyTos) > 0 { hdr.Set("Reply-To", toAddressList(msg.ReplyTos)) } if len(msg.ToList) > 0 { hdr.Set("To", toAddressList(msg.ToList)) } if len(msg.CCList) > 0 { hdr.Set("Cc", toAddressList(msg.CCList)) } if len(msg.BCCList) > 0 { hdr.Set("Bcc", toAddressList(msg.BCCList)) } // Set Message-Id from ExternalID or ID if it's not already set. if hdr.Get("Message-Id") == "" { if msg.ExternalID != "" { hdr.Set("Message-Id", "<"+msg.ExternalID+">") } else { hdr.Set("Message-Id", "<"+msg.ID+"@"+pmapi.InternalIDDomain+">") } } // Sanitize the date; it needs to have a valid unix timestamp. if opts.SanitizeDate { if date, err := rfc5322.ParseDateTime(hdr.Get("Date")); err != nil || date.Before(time.Unix(0, 0)) { if msgTime := time.Unix(msg.Time, 0); msgTime.After(time.Unix(0, 0)) { hdr.Set("Date", msgTime.In(time.UTC).Format(time.RFC1123Z)) } else { // No message should realistically be older than RFC822 itself. hdr.Set("Date", time.Date(1982, 8, 13, 0, 0, 0, 0, time.UTC).Format(time.RFC1123Z)) } // We clobbered the date so we save it under X-Original-Date. hdr.Set("X-Original-Date", date.In(time.UTC).Format(time.RFC1123Z)) } } // Set our internal ID if requested. // This is important for us to detect whether APPENDed things are actually "move like outlook". if opts.AddInternalID { hdr.Set("X-Pm-Internal-Id", msg.ID) } // Set our external ID if requested. // This was useful during debugging of applemail recovered messages; doesn't help with any behaviour. if opts.AddExternalID { hdr.Set("X-Pm-External-Id", "<"+msg.ExternalID+">") } // Set our server date if requested. // Can be useful to see how long it took for a message to arrive. if opts.AddMessageDate { hdr.Set("X-Pm-Date", time.Unix(msg.Time, 0).In(time.UTC).Format(time.RFC1123Z)) } // Include the message ID in the references (supposedly this somehow improves outlook support...). if opts.AddMessageIDReference { if references := hdr.Get("References"); !strings.Contains(references, msg.ID) { hdr.Set("References", references+" <"+msg.ID+"@"+pmapi.InternalIDDomain+">") } } return hdr } func getTextPartHeader(hdr message.Header, body []byte, mimeType string) message.Header { params := make(map[string]string) if utf8.Valid(body) { params["charset"] = "utf-8" } hdr.SetContentType(mimeType, params) // Use quoted-printable for all text/... parts hdr.Set("Content-Transfer-Encoding", "quoted-printable") return hdr } func getAttachmentPartHeader(att *pmapi.Attachment) message.Header { hdr := toMessageHeader(mail.Header(att.Header)) // All attachments have a content type. hdr.SetContentType(att.MIMEType, map[string]string{"name": mime.QEncoding.Encode("utf-8", att.Name)}) // All attachments have a content disposition. hdr.SetContentDisposition(att.Disposition, map[string]string{"filename": mime.QEncoding.Encode("utf-8", att.Name)}) // Use base64 for all attachments except embedded RFC822 messages. if att.MIMEType != "message/rfc822" { hdr.Set("Content-Transfer-Encoding", "base64") } else { hdr.Del("Content-Transfer-Encoding") } return hdr } func toMessageHeader(hdr mail.Header) message.Header { var res message.Header for key, val := range hdr { for _, val := range val { res.Add(key, val) } } return res } func toAddressList(addrs []*mail.Address) string { res := make([]string, len(addrs)) for i, addr := range addrs { res[i] = addr.String() } return strings.Join(res, ", ") }