mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
413 lines
9.7 KiB
Go
413 lines
9.7 KiB
Go
// 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 <https://www.gnu.org/licenses/>.
|
|
|
|
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())
|
|
}
|
|
|
|
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, ", ")
|
|
}
|