From a219374bdf49d3b54599eaf27c5b817b74e7fe2f Mon Sep 17 00:00:00 2001 From: emersion Date: Sat, 2 Dec 2017 16:23:06 +0100 Subject: [PATCH] Sending plaintext messages works --- README.md | 2 +- cmd/hydroxide/hydroxide.go | 26 ++++- protonmail/messages.go | 24 ++-- protonmail/protonmail.go | 2 + smtp/smtp.go | 231 +++++++++++++++++++++++++++++++++++++ 5 files changed, 270 insertions(+), 15 deletions(-) create mode 100644 smtp/smtp.go diff --git a/README.md b/README.md index 7f72e63..fe59126 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ You must setup an HTTPS reverse proxy to forward requests to `hydroxide`. ```shell go get github.com/emersion/hydroxide hydroxide auth -hydroxide +hydroxide carddav ``` Tested on GNOME (Evolution) and Android (DAVDroid). diff --git a/cmd/hydroxide/hydroxide.go b/cmd/hydroxide/hydroxide.go index 435e242..86bdf1b 100644 --- a/cmd/hydroxide/hydroxide.go +++ b/cmd/hydroxide/hydroxide.go @@ -10,9 +10,12 @@ import ( "os" "time" + "github.com/emersion/go-smtp" + "github.com/emersion/hydroxide/auth" "github.com/emersion/hydroxide/carddav" "github.com/emersion/hydroxide/protonmail" + smtpbackend "github.com/emersion/hydroxide/smtp" ) func newClient() *protonmail.Client { @@ -122,7 +125,23 @@ func main() { } fmt.Println("Bridge password:", bridgePassword) - case "": + case "smtp": + port := os.Getenv("PORT") + if port == "" { + port = "1465" + } + + sessions := auth.NewManager(newClient) + + be := smtpbackend.New(sessions) + s := smtp.NewServer(be) + s.Addr = "127.0.0.1:"+port + s.Domain = "localhost" // TODO: make this configurable + s.AllowInsecureAuth = true // TODO: remove this + + log.Println("Starting SMTP server at", s.Addr) + log.Fatal(s.ListenAndServe()) + case "carddav": port := os.Getenv("PORT") if port == "" { port = "8080" @@ -167,10 +186,11 @@ func main() { }), } - log.Println("Starting server at", s.Addr) + log.Println("Starting CardDAV server at", s.Addr) log.Fatal(s.ListenAndServe()) default: - log.Fatal("usage: hydroxide") + log.Fatal("usage: hydroxide carddav") + log.Fatal("usage: hydroxide smtp") log.Fatal("usage: hydroxide auth ") } } diff --git a/protonmail/messages.go b/protonmail/messages.go index 1df5cd4..00738ea 100644 --- a/protonmail/messages.go +++ b/protonmail/messages.go @@ -241,6 +241,7 @@ type MessagePackageSet struct { func NewMessagePackageSet(attachmentKeys map[string]*packet.EncryptedKey) *MessagePackageSet { return &MessagePackageSet{ + Addresses: make(map[string]*MessagePackage), attachmentKeys: attachmentKeys, } } @@ -261,7 +262,7 @@ func (set *MessagePackageSet) generateBodyKey(cipher packet.CipherFunction, conf type outgoingMessageWriter struct { cleartext io.WriteCloser ciphertext io.WriteCloser - armored *bytes.Buffer + encoded *bytes.Buffer set *MessagePackageSet } @@ -276,23 +277,22 @@ func (w *outgoingMessageWriter) Close() error { if err := w.ciphertext.Close(); err != nil { return err } - w.set.Body = w.armored.String() - w.armored = nil + w.set.Body = w.encoded.String() + w.encoded = nil return nil } -func (set *MessagePackageSet) Encrypt() (io.WriteCloser, error) { +func (set *MessagePackageSet) Encrypt(mimeType string) (io.WriteCloser, error) { + set.MIMEType = mimeType + config := &packet.Config{} if err := set.generateBodyKey(packet.CipherAES256, config); err != nil { return nil, err } - var armored bytes.Buffer - ciphertext, err := armor.Encode(&armored, "PGP MESSAGE", nil) - if err != nil { - return nil, err - } + var encoded bytes.Buffer + ciphertext := base64.NewEncoder(base64.StdEncoding, &encoded) encryptedData, err := packet.SerializeSymmetricallyEncrypted(ciphertext, set.bodyKey.CipherFunc, set.bodyKey.Key, config) if err != nil { @@ -309,13 +309,14 @@ func (set *MessagePackageSet) Encrypt() (io.WriteCloser, error) { return &outgoingMessageWriter{ cleartext: literalData, ciphertext: ciphertext, - armored: &armored, + encoded: &encoded, set: set, }, nil } func (set *MessagePackageSet) AddCleartext(addr string) error { set.Addresses[addr] = &MessagePackage{Type: MessagePackageCleartext} + set.Type |= MessagePackageCleartext if set.BodyKey == "" || set.AttachmentKeys == nil { set.BodyKey = base64.StdEncoding.EncodeToString(set.bodyKey.Key) @@ -366,6 +367,7 @@ func (set *MessagePackageSet) AddInternal(addr string, pub *openpgp.Entity) erro attachmentKeys[att] = attKey } + set.Type |= MessagePackageInternal set.Addresses[addr] = &MessagePackage{ Type: MessagePackageInternal, BodyKeyPacket: bodyKey, @@ -384,7 +386,7 @@ type OutgoingMessage struct { } func (c *Client) SendMessage(msg *OutgoingMessage) (sent, parent *Message, err error) { - req, err := c.newJSONRequest(http.MethodPut, "/messages/"+msg.ID, msg) + req, err := c.newJSONRequest(http.MethodPost, "/messages/send/"+msg.ID, msg) if err != nil { return nil, nil, err } diff --git a/protonmail/protonmail.go b/protonmail/protonmail.go index c61a769..a5a723f 100644 --- a/protonmail/protonmail.go +++ b/protonmail/protonmail.go @@ -82,6 +82,8 @@ func (c *Client) newJSONRequest(method, path string, body interface{}) (*http.Re } b := buf.Bytes() + //log.Printf(">> %v %v\n%v", method, path, string(b)) + req, err := c.newRequest(method, path, bytes.NewReader(b)) if err != nil { return nil, err diff --git a/smtp/smtp.go b/smtp/smtp.go new file mode 100644 index 0000000..dd0cb4c --- /dev/null +++ b/smtp/smtp.go @@ -0,0 +1,231 @@ +package smtp + +import ( + "bytes" + "errors" + "fmt" + "io" + + "github.com/emersion/go-message/mail" + "github.com/emersion/go-smtp" + "golang.org/x/crypto/openpgp" + + "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 + for k, values := range h.Header { + for _, v := range values { + b.WriteString(fmt.Sprintf("%s: %s\r\n", k, v)) + } + } + return b.String() +} + +type user struct { + c *protonmail.Client + u *protonmail.User + privateKeys openpgp.EntityList +} + +func (u *user) Send(from string, to []string, r io.Reader) error { + 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") + } + + fromAddrStr := fromList[0].Address + var fromAddr *protonmail.Address + for _, addr := range u.u.Addresses { + if addr.Email == fromAddrStr { + fromAddr = addr + break + } + } + if fromAddr == nil { + return errors.New("unknown sender address") + } + + msg := &protonmail.Message{ + ToList: toPMAddressList(toList), + CCList: toPMAddressList(ccList), + BCCList: toPMAddressList(bccList), + Subject: subject, + Header: formatHeader(mr.Header), + AddressID: fromAddr.ID, + } + + var body *bytes.Buffer + var bodyType string + + for { + p, err := mr.NextPart() + if err == io.EOF { + break + } else if err != nil { + return err + } + + switch h := p.Header.(type) { + case mail.TextHeader: + 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 + } + case mail.AttachmentHeader: + // TODO + } + } + + if body == nil { + return errors.New("message doesn't contain a body part") + } + + msg.MIMEType = bodyType + + privateKey := u.privateKeys[0] + plaintext, err := msg.Encrypt([]*openpgp.Entity{privateKey}, privateKey) + 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 + } + + // TODO: parentID from In-Reply-To + msg, err = u.c.CreateDraftMessage(msg, "") + if err != nil { + return fmt.Errorf("cannot create draft message: %v", err) + } + + outgoing := &protonmail.OutgoingMessage{ID: msg.ID} + + recipients := make([]*mail.Address, 0, len(toList) + len(ccList) + len(bccList)) + 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 { + resp, err := u.c.GetPublicKeys(rcpt.Address) + 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) + break + } + + // TODO: only keys with Send == 1 + pub, err := resp.Keys[0].Entity() + if err != nil { + return err + } + + encryptedRecipients[rcpt.Address] = pub + } + + if len(plaintextRecipients) > 0 { + plaintextSet := protonmail.NewMessagePackageSet(nil) + + plaintext, err := plaintextSet.Encrypt(bodyType) + 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 { + if err := plaintextSet.AddCleartext(rcpt); err != nil { + return err + } + } + + outgoing.Packages = append(outgoing.Packages, plaintextSet) + } + + if len(encryptedRecipients) > 0 { + // TODO + } + + _, _, err = u.c.SendMessage(outgoing) + if err != nil { + return fmt.Errorf("cannot send message: %v", err) + } + + return nil +} + +func (u *user) Logout() error { + u.c = nil + u.privateKeys = nil + return nil +} + +type backend struct { + sessions *auth.Manager +} + +func (be *backend) Login(username, password string) (smtp.User, error) { + c, privateKeys, err := be.sessions.Auth(username, password) + if err != nil { + return nil, err + } + + u, err := c.GetCurrentUser() + if err != nil { + return nil, err + } + + return &user{c, u, privateKeys}, nil +} + +func New(sessions *auth.Manager) smtp.Backend { + return &backend{sessions} +}