Merge pull request #12 from emersion/smtp

Add SMTP server
This commit is contained in:
emersion 2017-12-03 12:32:51 +01:00 committed by GitHub
commit 07532b8403
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 623 additions and 65 deletions

View File

@ -1,22 +1,38 @@
# hydroxide # hydroxide
A third-party, open-source ProtonMail CardDAV bridge. For power users only, A third-party, open-source ProtonMail bridge. For power users only, designed to
designed to run on a server. run on a server.
hydroxide supports CardDAV and SMTP.
## Usage ## Usage
Your credentials will be stored on disk encrypted with a 32-byte random Your credentials will be stored on disk encrypted with a 32-byte random
password. When configuring your CardDAV client, you'll need this password. password. When configuring your client, you'll need this password.
You must setup an HTTPS reverse proxy to forward requests to `hydroxide`.
```shell ```shell
go get github.com/emersion/hydroxide go get github.com/emersion/hydroxide
hydroxide auth <username> hydroxide auth <username>
hydroxide ```
### CardDAV
You must setup an HTTPS reverse proxy to forward requests to `hydroxide`.
```shell
hydroxide carddav
``` ```
Tested on GNOME (Evolution) and Android (DAVDroid). Tested on GNOME (Evolution) and Android (DAVDroid).
### SMTP
For now, it only supports unencrypted local connections.
```shell
hydroxide smtp
```
## License ## License
MIT MIT

View File

@ -10,9 +10,12 @@ import (
"os" "os"
"time" "time"
"github.com/emersion/go-smtp"
"github.com/emersion/hydroxide/auth" "github.com/emersion/hydroxide/auth"
"github.com/emersion/hydroxide/carddav" "github.com/emersion/hydroxide/carddav"
"github.com/emersion/hydroxide/protonmail" "github.com/emersion/hydroxide/protonmail"
smtpbackend "github.com/emersion/hydroxide/smtp"
) )
func newClient() *protonmail.Client { func newClient() *protonmail.Client {
@ -122,7 +125,23 @@ func main() {
} }
fmt.Println("Bridge password:", bridgePassword) 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") port := os.Getenv("PORT")
if port == "" { if port == "" {
port = "8080" 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()) log.Fatal(s.ListenAndServe())
default: default:
log.Fatal("usage: hydroxide") log.Fatal("usage: hydroxide carddav")
log.Fatal("usage: hydroxide smtp")
log.Fatal("usage: hydroxide auth <username>") log.Fatal("usage: hydroxide auth <username>")
} }
} }

View File

@ -1,16 +1,189 @@
package protonmail package protonmail
type Attachment struct { import (
ID string "bytes"
Name string "encoding/base64"
Size int "errors"
MIMEType string "fmt"
KeyPackets string "io"
//Headers map[string]string "mime/multipart"
} "net/http"
"strconv"
"strings"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/packet"
)
type AttachmentKey struct { type AttachmentKey struct {
ID string ID string
Key string Key string
Algo string Algo string
} }
type Attachment struct {
ID string
MessageID string
Name string
Size int
MIMEType string
ContentID string
KeyPackets string // encrypted with the user's key, base64-encoded
//Headers map[string]string
Signature string
unencryptedKey *packet.EncryptedKey
}
// GenerateKey generates an encrypted key and encrypts it to the provided
// recipients. Usually, the recipient is the user himself.
//
// The returned key is NOT encrypted.
func (att *Attachment) GenerateKey(to []*openpgp.Entity) (*packet.EncryptedKey, error) {
config := &packet.Config{}
var encodedKeyPackets bytes.Buffer
keyPackets := base64.NewEncoder(base64.StdEncoding, &encodedKeyPackets)
unencryptedKey, err := generateUnencryptedKey(packet.CipherAES256, config)
if err != nil {
return nil, err
}
for _, pub := range to {
encKey, ok := encryptionKey(pub, config.Now())
if !ok {
return nil, errors.New("cannot encrypt an attachment to key id " + strconv.FormatUint(pub.PrimaryKey.KeyId, 16) + " because it has no encryption keys")
}
err := packet.SerializeEncryptedKey(keyPackets, encKey.PublicKey, unencryptedKey.CipherFunc, unencryptedKey.Key, config)
if err != nil {
return nil, err
}
}
keyPackets.Close()
att.unencryptedKey = unencryptedKey
att.KeyPackets = encodedKeyPackets.String()
return unencryptedKey, nil
}
// Encrypt encrypts to w the data that will be written to the returned
// io.WriteCloser.
//
// Prior to calling Encrypt, an attachment key must have been generated with
// GenerateKey.
//
// signed is ignored for now.
func (att *Attachment) Encrypt(ciphertext io.Writer, signed *openpgp.Entity) (cleartext io.WriteCloser, err error) {
config := &packet.Config{}
if att.unencryptedKey == nil {
return nil, errors.New("cannot encrypt attachment: no attachment key available")
}
encryptedData, err := packet.SerializeSymmetricallyEncrypted(ciphertext, att.unencryptedKey.CipherFunc, att.unencryptedKey.Key, config)
if err != nil {
return nil, err
}
// TODO: sign, see https://github.com/golang/crypto/blob/master/openpgp/write.go#L287
literalData, err := packet.SerializeLiteral(encryptedData, true, att.Name, 0)
if err != nil {
return nil, err
}
return literalData, nil
}
// GetAttachment downloads an attachment's payload. The returned io.ReadCloser
// may be encrypted.
func (c *Client) GetAttachment(id string) (io.ReadCloser, error) {
req, err := c.newRequest(http.MethodGet, "/attachments/"+id, nil)
if err != nil {
return nil, err
}
resp, err := c.do(req)
if err != nil {
return nil, err
}
if resp.StatusCode/100 != 2 {
return nil, fmt.Errorf("cannot get attachment %q: %v %v", id, resp.Status, resp.StatusCode)
}
return resp.Body, nil
}
// CreateAttachment uploads a new attachment. r must be an PGP data packet
// encrypted with att.KeyPackets.
func (c *Client) CreateAttachment(att *Attachment, r io.Reader) (created *Attachment, err error) {
pr, pw := io.Pipe()
mw := multipart.NewWriter(pw)
go func() {
if err := mw.WriteField("Filename", att.Name); err != nil {
pw.CloseWithError(err)
return
}
if err := mw.WriteField("MessageID", att.MessageID); err != nil {
pw.CloseWithError(err)
return
}
if err := mw.WriteField("MIMEType", att.MIMEType); err != nil {
pw.CloseWithError(err)
return
}
if att.ContentID != "" {
if err := mw.WriteField("ContentID", att.ContentID); err != nil {
pw.CloseWithError(err)
return
}
}
if w, err := mw.CreateFormFile("KeyPackets", "KeyPackets.pgp"); err != nil {
pw.CloseWithError(err)
return
} else {
kpr := base64.NewDecoder(base64.StdEncoding, strings.NewReader(att.KeyPackets))
if _, err := io.Copy(w, kpr); err != nil {
pw.CloseWithError(err)
return
}
}
if w, err := mw.CreateFormFile("DataPacket", "DataPacket.pgp"); err != nil {
pw.CloseWithError(err)
return
} else if _, err := io.Copy(w, r); err != nil {
pw.CloseWithError(err)
return
}
// TODO: Signature
pw.CloseWithError(mw.Close())
}()
req, err := c.newRequest(http.MethodPost, "/attachments", pr)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", mw.FormDataContentType())
var respData struct {
resp
Attachment *Attachment
}
if err := c.doJSON(req, &respData); err != nil {
return nil, err
}
return respData.Attachment, nil
}

View File

@ -1,9 +1,11 @@
package protonmail package protonmail
import ( import (
"io"
"time" "time"
"golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/packet"
) )
// primaryIdentity returns the Identity marked as primary or the first identity // primaryIdentity returns the Identity marked as primary or the first identity
@ -89,3 +91,15 @@ func signingKey(e *openpgp.Entity, now time.Time) (openpgp.Key, bool) {
return openpgp.Key{}, false return openpgp.Key{}, false
} }
func generateUnencryptedKey(cipher packet.CipherFunction, config *packet.Config) (*packet.EncryptedKey, error) {
symKey := make([]byte, cipher.KeySize())
if _, err := io.ReadFull(config.Random(), symKey); err != nil {
return nil, err
}
return &packet.EncryptedKey{
CipherFunc: cipher,
Key: symKey,
}, nil
}

View File

@ -63,7 +63,7 @@ type Message struct {
SpamScore int SpamScore int
AddressID string AddressID string
Body string Body string
MIMEType string MIMEType string `json:",omitempty"`
CCList []*MessageAddress CCList []*MessageAddress
BCCList []*MessageAddress BCCList []*MessageAddress
Header string Header string
@ -214,55 +214,43 @@ const (
type MessagePackage struct { type MessagePackage struct {
Type MessagePackageType Type MessagePackageType
BodyKeyPacket string BodyKeyPacket string
AttachmentKeyPackets map[string]string AttachmentKeyPackets map[string]string
Signature int Signature int
// Only if encrypted for outside // Only if encrypted for outside
PasswordHint string PasswordHint string
Auth interface{} // TODO Auth interface{} // TODO
Token string Token string
EncToken string EncToken string
} }
type MessagePackageSet struct { type MessagePackageSet struct {
Type MessagePackageType // OR of each Type Type MessagePackageType // OR of each Type
Addresses map[string]*MessagePackage Addresses map[string]*MessagePackage
MIMEType string MIMEType string
Body string // Body data packet Body string // Body data packet
// Only if cleartext is sent // Only if cleartext is sent
BodyKey string BodyKey string
AttachmentKeys map[string]string AttachmentKeys map[string]string
bodyKey *packet.EncryptedKey bodyKey *packet.EncryptedKey
attachmentKeys map[string]*packet.EncryptedKey attachmentKeys map[string]*packet.EncryptedKey
} }
func NewMessagePackageSet(attachmentKeys map[string]*packet.EncryptedKey) *MessagePackageSet { func NewMessagePackageSet(attachmentKeys map[string]*packet.EncryptedKey) *MessagePackageSet {
return &MessagePackageSet{ return &MessagePackageSet{
Addresses: make(map[string]*MessagePackage),
attachmentKeys: attachmentKeys, attachmentKeys: attachmentKeys,
} }
} }
func (set *MessagePackageSet) generateBodyKey(cipher packet.CipherFunction, config *packet.Config) error {
symKey := make([]byte, cipher.KeySize())
if _, err := io.ReadFull(config.Random(), symKey); err != nil {
return err
}
set.bodyKey = &packet.EncryptedKey{
CipherFunc: cipher,
Key: symKey,
}
return nil
}
type outgoingMessageWriter struct { type outgoingMessageWriter struct {
cleartext io.WriteCloser cleartext io.WriteCloser
ciphertext io.WriteCloser ciphertext io.WriteCloser
armored *bytes.Buffer encoded *bytes.Buffer
set *MessagePackageSet set *MessagePackageSet
} }
func (w *outgoingMessageWriter) Write(p []byte) (int, error) { func (w *outgoingMessageWriter) Write(p []byte) (int, error) {
@ -276,23 +264,28 @@ func (w *outgoingMessageWriter) Close() error {
if err := w.ciphertext.Close(); err != nil { if err := w.ciphertext.Close(); err != nil {
return err return err
} }
w.set.Body = w.armored.String() w.set.Body = w.encoded.String()
w.armored = nil w.encoded = nil
return nil return nil
} }
func (set *MessagePackageSet) Encrypt() (io.WriteCloser, error) { // Encrypt encrypts the data that will be written to the returned
// io.WriteCloser.
//
// The signed parameter is ignored for now.
func (set *MessagePackageSet) Encrypt(mimeType string, signed *openpgp.Entity) (io.WriteCloser, error) {
set.MIMEType = mimeType
config := &packet.Config{} config := &packet.Config{}
if err := set.generateBodyKey(packet.CipherAES256, config); err != nil { key, err := generateUnencryptedKey(packet.CipherAES256, config)
return nil, err
}
var armored bytes.Buffer
ciphertext, err := armor.Encode(&armored, "PGP MESSAGE", nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
set.bodyKey = key
var encoded bytes.Buffer
ciphertext := base64.NewEncoder(base64.StdEncoding, &encoded)
encryptedData, err := packet.SerializeSymmetricallyEncrypted(ciphertext, set.bodyKey.CipherFunc, set.bodyKey.Key, config) encryptedData, err := packet.SerializeSymmetricallyEncrypted(ciphertext, set.bodyKey.CipherFunc, set.bodyKey.Key, config)
if err != nil { if err != nil {
@ -307,15 +300,16 @@ func (set *MessagePackageSet) Encrypt() (io.WriteCloser, error) {
} }
return &outgoingMessageWriter{ return &outgoingMessageWriter{
cleartext: literalData, cleartext: literalData,
ciphertext: ciphertext, ciphertext: ciphertext,
armored: &armored, encoded: &encoded,
set: set, set: set,
}, nil }, nil
} }
func (set *MessagePackageSet) AddCleartext(addr string) error { func (set *MessagePackageSet) AddCleartext(addr string) error {
set.Addresses[addr] = &MessagePackage{Type: MessagePackageCleartext} set.Addresses[addr] = &MessagePackage{Type: MessagePackageCleartext}
set.Type |= MessagePackageCleartext
if set.BodyKey == "" || set.AttachmentKeys == nil { if set.BodyKey == "" || set.AttachmentKeys == nil {
set.BodyKey = base64.StdEncoding.EncodeToString(set.bodyKey.Key) set.BodyKey = base64.StdEncoding.EncodeToString(set.bodyKey.Key)
@ -330,18 +324,17 @@ func (set *MessagePackageSet) AddCleartext(addr string) error {
} }
func serializeEncryptedKey(symKey *packet.EncryptedKey, pub *packet.PublicKey, config *packet.Config) (string, error) { func serializeEncryptedKey(symKey *packet.EncryptedKey, pub *packet.PublicKey, config *packet.Config) (string, error) {
var armored bytes.Buffer var encoded bytes.Buffer
ciphertext, err := armor.Encode(&armored, "PGP MESSAGE", nil) ciphertext := base64.NewEncoder(base64.StdEncoding, &encoded)
err := packet.SerializeEncryptedKey(ciphertext, pub, symKey.CipherFunc, symKey.Key, config)
if err != nil { if err != nil {
return "", err return "", err
} }
err = packet.SerializeEncryptedKey(ciphertext, pub, symKey.CipherFunc, symKey.Key, config) ciphertext.Close()
if err != nil {
return "", err
}
return armored.String(), nil return encoded.String(), nil
} }
func (set *MessagePackageSet) AddInternal(addr string, pub *openpgp.Entity) error { func (set *MessagePackageSet) AddInternal(addr string, pub *openpgp.Entity) error {
@ -366,9 +359,10 @@ func (set *MessagePackageSet) AddInternal(addr string, pub *openpgp.Entity) erro
attachmentKeys[att] = attKey attachmentKeys[att] = attKey
} }
set.Type |= MessagePackageInternal
set.Addresses[addr] = &MessagePackage{ set.Addresses[addr] = &MessagePackage{
Type: MessagePackageInternal, Type: MessagePackageInternal,
BodyKeyPacket: bodyKey, BodyKeyPacket: bodyKey,
AttachmentKeyPackets: attachmentKeys, AttachmentKeyPackets: attachmentKeys,
} }
return nil return nil
@ -384,7 +378,7 @@ type OutgoingMessage struct {
} }
func (c *Client) SendMessage(msg *OutgoingMessage) (sent, parent *Message, err error) { 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 { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@ -82,6 +82,8 @@ func (c *Client) newJSONRequest(method, path string, body interface{}) (*http.Re
} }
b := buf.Bytes() b := buf.Bytes()
//log.Printf(">> %v %v\n%v", method, path, string(b))
req, err := c.newRequest(method, path, bytes.NewReader(b)) req, err := c.newRequest(method, path, bytes.NewReader(b))
if err != nil { if err != nil {
return nil, err return nil, err

339
smtp/smtp.go Normal file
View File

@ -0,0 +1,339 @@
package smtp
import (
"bytes"
"errors"
"fmt"
"io"
"github.com/emersion/go-message/mail"
"github.com/emersion/go-smtp"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/packet"
"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 {
// Parse the incoming MIME message header
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")
}
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
for _, e := range u.privateKeys {
if e.PrimaryKey.KeyId == encryptedPrivateKey.PrimaryKey.KeyId {
privateKey = e
break
}
}
if privateKey == nil {
return errors.New("sender address key hasn't been decrypted")
}
msg := &protonmail.Message{
ToList: toPMAddressList(toList),
CCList: toPMAddressList(ccList),
BCCList: toPMAddressList(bccList),
Subject: subject,
Header: formatHeader(mr.Header),
AddressID: fromAddr.ID,
}
// Create an empty draft
plaintext, err := msg.Encrypt([]*openpgp.Entity{privateKey}, privateKey)
if 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)
}
// Parse the incoming MIME message body
// Save the message text into a buffer
// Upload attachments
var body *bytes.Buffer
var bodyType string
attachmentKeys := make(map[string]*packet.EncryptedKey)
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:
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
}
attKey, err := att.GenerateKey([]*openpgp.Entity{privateKey})
if err != nil {
return fmt.Errorf("cannot generate attachment key: %v", err)
}
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())
}()
att, err = u.c.CreateAttachment(att, pr)
if err != nil {
return fmt.Errorf("cannot upload attachment: %v", err)
}
attachmentKeys[att.ID] = attKey
}
}
if body == nil {
return errors.New("message doesn't contain a body part")
}
// Encrypt the body and update the draft
msg.MIMEType = bodyType
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
}
msg, err = u.c.UpdateDraftMessage(msg)
if err != nil {
return fmt.Errorf("cannot update draft message: %v", err)
}
// Split internal recipients and plaintext recipients
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
}
// Create and send the outgoing message
outgoing := &protonmail.OutgoingMessage{ID: msg.ID}
if len(plaintextRecipients) > 0 {
plaintextSet := protonmail.NewMessagePackageSet(attachmentKeys)
plaintext, err := plaintextSet.Encrypt(bodyType, privateKey)
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 {
encryptedSet := protonmail.NewMessagePackageSet(attachmentKeys)
plaintext, err := encryptedSet.Encrypt(bodyType, privateKey)
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 {
if err := encryptedSet.AddInternal(rcpt, pub); err != nil {
return err
}
}
outgoing.Packages = append(outgoing.Packages, encryptedSet)
}
_, _, 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
}
// TODO: decrypt private keys in u.Addresses
return &user{c, u, privateKeys}, nil
}
func New(sessions *auth.Manager) smtp.Backend {
return &backend{sessions}
}