Sending plaintext messages works
This commit is contained in:
parent
480af1016a
commit
a219374bdf
|
@ -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 <username>
|
||||
hydroxide
|
||||
hydroxide carddav
|
||||
```
|
||||
|
||||
Tested on GNOME (Evolution) and Android (DAVDroid).
|
||||
|
|
|
@ -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 <username>")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
}
|
Loading…
Reference in New Issue