2017-12-02 17:23:06 +02:00
|
|
|
package smtp
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
2019-07-13 11:48:15 +03:00
|
|
|
"log"
|
2018-01-09 18:17:59 +02:00
|
|
|
"strings"
|
2017-12-02 17:23:06 +02:00
|
|
|
|
|
|
|
"github.com/emersion/go-message/mail"
|
|
|
|
"github.com/emersion/go-smtp"
|
|
|
|
"golang.org/x/crypto/openpgp"
|
2017-12-03 13:27:31 +02:00
|
|
|
"golang.org/x/crypto/openpgp/packet"
|
2017-12-02 17:23:06 +02:00
|
|
|
|
|
|
|
"github.com/emersion/hydroxide/auth"
|
|
|
|
"github.com/emersion/hydroxide/protonmail"
|
|
|
|
)
|
|
|
|
|
|
|
|
func toPMAddressList(addresses []*mail.Address) []*protonmail.MessageAddress {
|
|
|
|
l := make([]*protonmail.MessageAddress, len(addresses))
|
|
|
|
for i, addr := range addresses {
|
|
|
|
l[i] = &protonmail.MessageAddress{
|
|
|
|
Name: addr.Name,
|
|
|
|
Address: addr.Address,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return l
|
|
|
|
}
|
|
|
|
|
|
|
|
func formatHeader(h mail.Header) string {
|
|
|
|
var b bytes.Buffer
|
2019-05-15 18:51:58 +03:00
|
|
|
fields := h.Fields()
|
|
|
|
for fields.Next() {
|
|
|
|
b.WriteString(fmt.Sprintf("%s: %s\r\n", fields.Key(), fields.Value()))
|
2017-12-02 17:23:06 +02:00
|
|
|
}
|
|
|
|
return b.String()
|
|
|
|
}
|
|
|
|
|
2019-04-03 19:40:00 +03:00
|
|
|
type session struct {
|
2017-12-02 17:23:06 +02:00
|
|
|
c *protonmail.Client
|
|
|
|
u *protonmail.User
|
|
|
|
privateKeys openpgp.EntityList
|
2018-10-21 13:24:56 +03:00
|
|
|
addrs []*protonmail.Address
|
2017-12-02 17:23:06 +02:00
|
|
|
}
|
|
|
|
|
2019-12-08 13:25:26 +02:00
|
|
|
func (s *session) Mail(from string, options smtp.MailOptions) error {
|
2019-04-03 19:40:00 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *session) Rcpt(to string) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *session) Data(r io.Reader) error {
|
2017-12-03 13:27:31 +02:00
|
|
|
// Parse the incoming MIME message header
|
2017-12-02 17:23:06 +02:00
|
|
|
mr, err := mail.CreateReader(r)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
subject, _ := mr.Header.Subject()
|
|
|
|
fromList, _ := mr.Header.AddressList("From")
|
|
|
|
toList, _ := mr.Header.AddressList("To")
|
|
|
|
ccList, _ := mr.Header.AddressList("Cc")
|
|
|
|
bccList, _ := mr.Header.AddressList("Bcc")
|
|
|
|
|
|
|
|
if len(fromList) != 1 {
|
|
|
|
return errors.New("the From field must contain exactly one address")
|
|
|
|
}
|
|
|
|
if len(toList) == 0 && len(ccList) == 0 && len(bccList) == 0 {
|
|
|
|
return errors.New("no recipient specified")
|
|
|
|
}
|
|
|
|
|
2018-10-21 13:34:47 +03:00
|
|
|
rawFrom := fromList[0]
|
|
|
|
fromAddrStr := rawFrom.Address
|
2017-12-02 17:23:06 +02:00
|
|
|
var fromAddr *protonmail.Address
|
2019-04-03 19:40:00 +03:00
|
|
|
for _, addr := range s.addrs {
|
2018-01-09 18:17:59 +02:00
|
|
|
if strings.EqualFold(addr.Email, fromAddrStr) {
|
2017-12-02 17:23:06 +02:00
|
|
|
fromAddr = addr
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if fromAddr == nil {
|
|
|
|
return errors.New("unknown sender address")
|
|
|
|
}
|
2017-12-02 17:44:15 +02:00
|
|
|
if len(fromAddr.Keys) == 0 {
|
|
|
|
return errors.New("sender address has no private key")
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: get appropriate private key
|
|
|
|
encryptedPrivateKey, err := fromAddr.Keys[0].Entity()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("cannot parse sender private key: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var privateKey *openpgp.Entity
|
2019-04-03 19:40:00 +03:00
|
|
|
for _, e := range s.privateKeys {
|
2017-12-02 17:44:15 +02:00
|
|
|
if e.PrimaryKey.KeyId == encryptedPrivateKey.PrimaryKey.KeyId {
|
|
|
|
privateKey = e
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if privateKey == nil {
|
|
|
|
return errors.New("sender address key hasn't been decrypted")
|
|
|
|
}
|
2017-12-02 17:23:06 +02:00
|
|
|
|
|
|
|
msg := &protonmail.Message{
|
|
|
|
ToList: toPMAddressList(toList),
|
|
|
|
CCList: toPMAddressList(ccList),
|
|
|
|
BCCList: toPMAddressList(bccList),
|
|
|
|
Subject: subject,
|
|
|
|
Header: formatHeader(mr.Header),
|
|
|
|
AddressID: fromAddr.ID,
|
2018-10-21 15:07:46 +03:00
|
|
|
Sender: &protonmail.MessageAddress{
|
2018-10-21 13:34:47 +03:00
|
|
|
Address: rawFrom.Address,
|
|
|
|
Name: rawFrom.Name,
|
|
|
|
},
|
2017-12-02 17:23:06 +02:00
|
|
|
}
|
|
|
|
|
2017-12-03 12:55:59 +02:00
|
|
|
// Create an empty draft
|
2019-07-13 11:48:15 +03:00
|
|
|
log.Println("creating draft message")
|
|
|
|
|
2017-12-03 12:55:59 +02:00
|
|
|
plaintext, err := msg.Encrypt([]*openpgp.Entity{privateKey}, privateKey)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := plaintext.Close(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-07-11 13:48:45 +03:00
|
|
|
parentID := ""
|
|
|
|
inReplyToList, _ := mr.Header.AddressList("In-Reply-To")
|
|
|
|
if len(inReplyToList) == 1 {
|
|
|
|
inReplyTo := inReplyToList[0].Address
|
|
|
|
|
|
|
|
filter := protonmail.MessageFilter{
|
2018-10-21 13:15:20 +03:00
|
|
|
Limit: 1,
|
2018-07-11 13:48:45 +03:00
|
|
|
ExternalID: inReplyTo,
|
2018-10-21 13:15:20 +03:00
|
|
|
AddressID: fromAddr.ID,
|
2018-07-11 13:48:45 +03:00
|
|
|
}
|
2019-04-03 19:40:00 +03:00
|
|
|
total, msgs, err := s.c.ListMessages(&filter)
|
2018-07-11 13:48:45 +03:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if total == 1 {
|
|
|
|
parentID = msgs[0].ID
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-03 19:40:00 +03:00
|
|
|
msg, err = s.c.CreateDraftMessage(msg, parentID)
|
2017-12-03 12:55:59 +02:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("cannot create draft message: %v", err)
|
|
|
|
}
|
|
|
|
|
2017-12-03 13:27:31 +02:00
|
|
|
// Parse the incoming MIME message body
|
|
|
|
// Save the message text into a buffer
|
|
|
|
// Upload attachments
|
|
|
|
|
2017-12-02 17:23:06 +02:00
|
|
|
var body *bytes.Buffer
|
|
|
|
var bodyType string
|
2017-12-03 13:27:31 +02:00
|
|
|
attachmentKeys := make(map[string]*packet.EncryptedKey)
|
2017-12-02 17:23:06 +02:00
|
|
|
|
|
|
|
for {
|
|
|
|
p, err := mr.NextPart()
|
|
|
|
if err == io.EOF {
|
|
|
|
break
|
|
|
|
} else if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
switch h := p.Header.(type) {
|
2019-05-15 18:51:58 +03:00
|
|
|
case *mail.InlineHeader:
|
2017-12-02 17:23:06 +02:00
|
|
|
t, _, err := h.ContentType()
|
|
|
|
if err != nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
if body != nil && t != "text/html" {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
body = &bytes.Buffer{}
|
|
|
|
bodyType = t
|
|
|
|
if _, err := io.Copy(body, p.Body); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-05-15 18:51:58 +03:00
|
|
|
case *mail.AttachmentHeader:
|
2017-12-03 12:55:59 +02:00
|
|
|
t, _, err := h.ContentType()
|
|
|
|
if err != nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
filename, err := h.Filename()
|
|
|
|
if err != nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
att := &protonmail.Attachment{
|
|
|
|
MessageID: msg.ID,
|
|
|
|
Name: filename,
|
|
|
|
MIMEType: t,
|
|
|
|
ContentID: h.Get("Content-Id"),
|
|
|
|
// TODO: Header
|
|
|
|
}
|
|
|
|
|
2017-12-03 13:27:31 +02:00
|
|
|
attKey, err := att.GenerateKey([]*openpgp.Entity{privateKey})
|
2017-12-03 12:55:59 +02:00
|
|
|
if err != nil {
|
2017-12-03 13:27:31 +02:00
|
|
|
return fmt.Errorf("cannot generate attachment key: %v", err)
|
2017-12-03 12:55:59 +02:00
|
|
|
}
|
|
|
|
|
2019-07-13 11:48:15 +03:00
|
|
|
log.Printf("uploading message attachment %q", filename)
|
|
|
|
|
2017-12-03 13:27:31 +02:00
|
|
|
pr, pw := io.Pipe()
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
cleartext, err := att.Encrypt(pw, privateKey)
|
|
|
|
if err != nil {
|
|
|
|
pw.CloseWithError(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if _, err := io.Copy(cleartext, p.Body); err != nil {
|
|
|
|
pw.CloseWithError(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
pw.CloseWithError(cleartext.Close())
|
|
|
|
}()
|
|
|
|
|
2019-04-03 19:40:00 +03:00
|
|
|
att, err = s.c.CreateAttachment(att, pr)
|
2017-12-03 12:55:59 +02:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("cannot upload attachment: %v", err)
|
|
|
|
}
|
|
|
|
|
2017-12-03 13:27:31 +02:00
|
|
|
attachmentKeys[att.ID] = attKey
|
2017-12-02 17:23:06 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if body == nil {
|
|
|
|
return errors.New("message doesn't contain a body part")
|
|
|
|
}
|
|
|
|
|
2017-12-03 13:27:31 +02:00
|
|
|
// Encrypt the body and update the draft
|
2019-07-13 11:48:15 +03:00
|
|
|
log.Println("uploading message body")
|
|
|
|
|
2017-12-02 17:23:06 +02:00
|
|
|
msg.MIMEType = bodyType
|
2017-12-03 12:55:59 +02:00
|
|
|
plaintext, err = msg.Encrypt([]*openpgp.Entity{privateKey}, privateKey)
|
2017-12-02 17:23:06 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if _, err := io.Copy(plaintext, bytes.NewReader(body.Bytes())); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := plaintext.Close(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-04-03 19:40:00 +03:00
|
|
|
msg, err = s.c.UpdateDraftMessage(msg)
|
2017-12-02 17:23:06 +02:00
|
|
|
if err != nil {
|
2017-12-03 12:55:59 +02:00
|
|
|
return fmt.Errorf("cannot update draft message: %v", err)
|
2017-12-02 17:23:06 +02:00
|
|
|
}
|
|
|
|
|
2017-12-03 13:27:31 +02:00
|
|
|
// Split internal recipients and plaintext recipients
|
2017-12-02 17:23:06 +02:00
|
|
|
|
2017-12-03 12:55:59 +02:00
|
|
|
recipients := make([]*mail.Address, 0, len(toList)+len(ccList)+len(bccList))
|
2017-12-02 17:23:06 +02:00
|
|
|
recipients = append(recipients, toList...)
|
|
|
|
recipients = append(recipients, ccList...)
|
|
|
|
recipients = append(recipients, bccList...)
|
|
|
|
|
|
|
|
var plaintextRecipients []string
|
|
|
|
encryptedRecipients := make(map[string]*openpgp.Entity)
|
|
|
|
for _, rcpt := range recipients {
|
2019-04-03 19:40:00 +03:00
|
|
|
resp, err := s.c.GetPublicKeys(rcpt.Address)
|
2017-12-02 17:23:06 +02:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("cannot get public key for address %q: %v", rcpt.Address, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(resp.Keys) == 0 {
|
|
|
|
plaintextRecipients = append(plaintextRecipients, rcpt.Address)
|
2017-12-12 14:46:22 +02:00
|
|
|
continue
|
2017-12-02 17:23:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: only keys with Send == 1
|
|
|
|
pub, err := resp.Keys[0].Entity()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
encryptedRecipients[rcpt.Address] = pub
|
|
|
|
}
|
|
|
|
|
2017-12-03 13:27:31 +02:00
|
|
|
// Create and send the outgoing message
|
2019-07-13 11:48:15 +03:00
|
|
|
log.Println("sending message")
|
2017-12-03 13:27:31 +02:00
|
|
|
outgoing := &protonmail.OutgoingMessage{ID: msg.ID}
|
|
|
|
|
2017-12-02 17:23:06 +02:00
|
|
|
if len(plaintextRecipients) > 0 {
|
2017-12-03 13:27:31 +02:00
|
|
|
plaintextSet := protonmail.NewMessagePackageSet(attachmentKeys)
|
2017-12-02 17:23:06 +02:00
|
|
|
|
2017-12-03 12:55:59 +02:00
|
|
|
plaintext, err := plaintextSet.Encrypt(bodyType, privateKey)
|
2017-12-02 17:23:06 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if _, err := io.Copy(plaintext, bytes.NewReader(body.Bytes())); err != nil {
|
|
|
|
plaintext.Close()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := plaintext.Close(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, rcpt := range plaintextRecipients {
|
2018-01-15 12:53:16 +02:00
|
|
|
pkg, err := plaintextSet.AddCleartext(rcpt)
|
|
|
|
if err != nil {
|
2017-12-02 17:23:06 +02:00
|
|
|
return err
|
|
|
|
}
|
2018-01-15 12:53:16 +02:00
|
|
|
|
|
|
|
// Don't sign plaintext messages by default
|
|
|
|
// TODO: send inline singnature to opt-in contacts
|
|
|
|
pkg.Signature = 0
|
2017-12-02 17:23:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
outgoing.Packages = append(outgoing.Packages, plaintextSet)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(encryptedRecipients) > 0 {
|
2017-12-03 13:27:31 +02:00
|
|
|
encryptedSet := protonmail.NewMessagePackageSet(attachmentKeys)
|
2017-12-03 11:20:45 +02:00
|
|
|
|
2017-12-03 12:55:59 +02:00
|
|
|
plaintext, err := encryptedSet.Encrypt(bodyType, privateKey)
|
2017-12-03 11:20:45 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if _, err := io.Copy(plaintext, bytes.NewReader(body.Bytes())); err != nil {
|
|
|
|
plaintext.Close()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := plaintext.Close(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for rcpt, pub := range encryptedRecipients {
|
2018-01-15 12:53:16 +02:00
|
|
|
if _, err := encryptedSet.AddInternal(rcpt, pub); err != nil {
|
2017-12-03 11:20:45 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
outgoing.Packages = append(outgoing.Packages, encryptedSet)
|
2017-12-02 17:23:06 +02:00
|
|
|
}
|
|
|
|
|
2019-04-03 19:40:00 +03:00
|
|
|
_, _, err = s.c.SendMessage(outgoing)
|
2017-12-02 17:23:06 +02:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("cannot send message: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-04-03 19:40:00 +03:00
|
|
|
func (s *session) Reset() {}
|
|
|
|
|
|
|
|
func (s *session) Logout() error {
|
|
|
|
s.c = nil
|
|
|
|
s.u = nil
|
|
|
|
s.privateKeys = nil
|
2017-12-02 17:23:06 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type backend struct {
|
|
|
|
sessions *auth.Manager
|
|
|
|
}
|
|
|
|
|
2019-04-03 19:40:00 +03:00
|
|
|
func (be *backend) Login(_ *smtp.ConnectionState, username, password string) (smtp.Session, error) {
|
2017-12-02 17:23:06 +02:00
|
|
|
c, privateKeys, err := be.sessions.Auth(username, password)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
u, err := c.GetCurrentUser()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-10-21 13:24:56 +03:00
|
|
|
addrs, err := c.ListAddresses()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-12-02 17:44:15 +02:00
|
|
|
// TODO: decrypt private keys in u.Addresses
|
|
|
|
|
2019-07-19 00:35:58 +03:00
|
|
|
log.Printf("%s logged in", username)
|
2019-07-13 11:48:15 +03:00
|
|
|
|
2019-04-03 19:40:00 +03:00
|
|
|
return &session{c, u, privateKeys, addrs}, nil
|
2017-12-02 17:23:06 +02:00
|
|
|
}
|
|
|
|
|
2019-04-03 19:40:00 +03:00
|
|
|
func (be *backend) AnonymousLogin(_ *smtp.ConnectionState) (smtp.Session, error) {
|
2018-03-10 23:43:47 +02:00
|
|
|
return nil, smtp.ErrAuthRequired
|
|
|
|
}
|
|
|
|
|
2017-12-02 17:23:06 +02:00
|
|
|
func New(sessions *auth.Manager) smtp.Backend {
|
|
|
|
return &backend{sessions}
|
|
|
|
}
|