imap: add APPEND support

This commit is contained in:
emersion 2018-01-12 21:40:13 +01:00
parent a8b5fb864e
commit 0fdabd4447
No known key found for this signature in database
GPG Key ID: 0FDE7BE0E88F5E48
2 changed files with 204 additions and 1 deletions

View File

@ -348,7 +348,12 @@ func (mbox *mailbox) CreateMessage(flags []string, date time.Time, body imap.Lit
return errors.New("cannot create messages outside the Drafts mailbox")
}
return errNotYetImplemented
if err := mbox.init(); err != nil {
return err
}
_, err := createMessage(mbox.u.c, mbox.u.u, mbox.u.privateKeys, body)
return err
}
func (mbox *mailbox) fromSeqSet(isUID bool, seqSet *imap.SeqSet) ([]string, error) {

View File

@ -3,6 +3,7 @@ package imap
import (
"bytes"
"errors"
"fmt"
"io"
"log"
"strings"
@ -11,6 +12,7 @@ import (
"github.com/emersion/go-imap"
"github.com/emersion/go-message"
"github.com/emersion/go-message/mail"
"golang.org/x/crypto/openpgp"
"github.com/emersion/hydroxide/protonmail"
)
@ -23,6 +25,27 @@ func messageID(msg *protonmail.Message) string {
}
}
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()
}
func protonmailAddressList(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 imapAddress(addr *protonmail.MessageAddress) *imap.Address {
parts := strings.SplitN(addr.Address, "@", 2)
if len(parts) < 2 {
@ -352,3 +375,178 @@ func (mbox *mailbox) fetchBodySection(msg *protonmail.Message, section *imap.Bod
return l, nil
}
func createMessage(c *protonmail.Client, u *protonmail.User, privateKeys openpgp.EntityList, r io.Reader) (*protonmail.Message, error) {
// Parse the incoming MIME message header
mr, err := mail.CreateReader(r)
if err != nil {
return nil, 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 nil, errors.New("the From field must contain exactly one address")
}
if len(toList) == 0 && len(ccList) == 0 && len(bccList) == 0 {
return nil, errors.New("no recipient specified")
}
fromAddrStr := fromList[0].Address
var fromAddr *protonmail.Address
for _, addr := range u.Addresses {
if strings.EqualFold(addr.Email, fromAddrStr) {
fromAddr = addr
break
}
}
if fromAddr == nil {
return nil, errors.New("unknown sender address")
}
if len(fromAddr.Keys) == 0 {
return nil, errors.New("sender address has no private key")
}
// TODO: get appropriate private key
encryptedPrivateKey, err := fromAddr.Keys[0].Entity()
if err != nil {
return nil, fmt.Errorf("cannot parse sender private key: %v", err)
}
var privateKey *openpgp.Entity
for _, e := range privateKeys {
if e.PrimaryKey.KeyId == encryptedPrivateKey.PrimaryKey.KeyId {
privateKey = e
break
}
}
if privateKey == nil {
return nil, errors.New("sender address key hasn't been decrypted")
}
msg := &protonmail.Message{
ToList: protonmailAddressList(toList),
CCList: protonmailAddressList(ccList),
BCCList: protonmailAddressList(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 nil, err
}
if err := plaintext.Close(); err != nil {
return nil, err
}
// TODO: parentID from In-Reply-To
msg, err = c.CreateDraftMessage(msg, "")
if err != nil {
return nil, fmt.Errorf("cannot create draft message: %v", err)
}
var body *bytes.Buffer
var bodyType string
for {
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
return nil, 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 nil, 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
}
_, err = att.GenerateKey([]*openpgp.Entity{privateKey})
if err != nil {
return nil, 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 = c.CreateAttachment(att, pr)
if err != nil {
return nil, fmt.Errorf("cannot upload attachment: %v", err)
}
msg.Attachments = append(msg.Attachments, att)
}
}
if body == nil {
return nil, 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 nil, err
}
if _, err := io.Copy(plaintext, body); err != nil {
return nil, err
}
if err := plaintext.Close(); err != nil {
return nil, err
}
if _, err := c.UpdateDraftMessage(msg); err != nil {
return nil, fmt.Errorf("cannot update draft message: %v", err)
}
return msg, nil
}