smtp: upload attachments
This commit is contained in:
parent
0e1b866880
commit
72b56494bc
|
@ -1,13 +1,18 @@
|
||||||
package protonmail
|
package protonmail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/openpgp"
|
||||||
|
"golang.org/x/crypto/openpgp/packet"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AttachmentKey struct {
|
type AttachmentKey struct {
|
||||||
|
@ -28,6 +33,51 @@ type Attachment struct {
|
||||||
Signature string
|
Signature string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encrypt generates an encrypted key for the provided recipients and encrypts
|
||||||
|
// to w the data that will be written to the returned io.WriteCloser.
|
||||||
|
//
|
||||||
|
// signed is ignored for now.
|
||||||
|
func (att *Attachment) Encrypt(ciphertext io.Writer, to []*openpgp.Entity, signed *openpgp.Entity) (cleartext io.WriteCloser, err 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.KeyPackets = encodedKeyPackets.String()
|
||||||
|
|
||||||
|
encryptedData, err := packet.SerializeSymmetricallyEncrypted(ciphertext, unencryptedKey.CipherFunc, 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
|
// GetAttachment downloads an attachment's payload. The returned io.ReadCloser
|
||||||
// may be encrypted.
|
// may be encrypted.
|
||||||
func (c *Client) GetAttachment(id string) (io.ReadCloser, error) {
|
func (c *Client) GetAttachment(id string) (io.ReadCloser, error) {
|
||||||
|
@ -88,7 +138,7 @@ func (c *Client) CreateAttachment(att *Attachment, r io.Reader) (created *Attach
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if w, err := mw.CreateFormFile("DataPackets", "DataPackets.pgp"); err != nil {
|
if w, err := mw.CreateFormFile("DataPacket", "DataPacket.pgp"); err != nil {
|
||||||
pw.CloseWithError(err)
|
pw.CloseWithError(err)
|
||||||
return
|
return
|
||||||
} else if _, err := io.Copy(w, r); err != nil {
|
} else if _, err := io.Copy(w, r); err != nil {
|
||||||
|
@ -109,8 +159,7 @@ func (c *Client) CreateAttachment(att *Attachment, r io.Reader) (created *Attach
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
params := map[string]string{"boundary": mw.Boundary()}
|
req.Header.Set("Content-Type", mw.FormDataContentType())
|
||||||
req.Header.Set("Content-Type", mime.FormatMediaType("multipart/form-data", params))
|
|
||||||
|
|
||||||
var respData struct {
|
var respData struct {
|
||||||
resp
|
resp
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
@ -246,19 +246,6 @@ func NewMessagePackageSet(attachmentKeys map[string]*packet.EncryptedKey) *Messa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
@ -282,14 +269,20 @@ func (w *outgoingMessageWriter) Close() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (set *MessagePackageSet) Encrypt(mimeType string) (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
|
set.MIMEType = mimeType
|
||||||
|
|
||||||
config := &packet.Config{}
|
config := &packet.Config{}
|
||||||
|
|
||||||
if err := set.generateBodyKey(packet.CipherAES256, config); err != nil {
|
key, err := generateUnencryptedKey(packet.CipherAES256, config)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
set.bodyKey = key
|
||||||
|
|
||||||
var encoded bytes.Buffer
|
var encoded bytes.Buffer
|
||||||
ciphertext := base64.NewEncoder(base64.StdEncoding, &encoded)
|
ciphertext := base64.NewEncoder(base64.StdEncoding, &encoded)
|
||||||
|
|
66
smtp/smtp.go
66
smtp/smtp.go
|
@ -101,8 +101,24 @@ func (u *user) Send(from string, to []string, r io.Reader) error {
|
||||||
AddressID: fromAddr.ID,
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
var body *bytes.Buffer
|
var body *bytes.Buffer
|
||||||
var bodyType string
|
var bodyType string
|
||||||
|
var attachments []*protonmail.Attachment
|
||||||
|
|
||||||
for {
|
for {
|
||||||
p, err := mr.NextPart()
|
p, err := mr.NextPart()
|
||||||
|
@ -129,7 +145,42 @@ func (u *user) Send(from string, to []string, r io.Reader) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case mail.AttachmentHeader:
|
case mail.AttachmentHeader:
|
||||||
// TODO
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
cleartext, err := att.Encrypt(&b, []*openpgp.Entity{privateKey}, privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot encrypt attachment: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(cleartext, p.Body); err != nil {
|
||||||
|
return fmt.Errorf("cannot encrypt attachment: %v", err)
|
||||||
|
}
|
||||||
|
if err := cleartext.Close(); err != nil {
|
||||||
|
return fmt.Errorf("cannot encrypt attachment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
att, err = u.c.CreateAttachment(att, &b)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot upload attachment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments = append(attachments, att)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +190,7 @@ func (u *user) Send(from string, to []string, r io.Reader) error {
|
||||||
|
|
||||||
msg.MIMEType = bodyType
|
msg.MIMEType = bodyType
|
||||||
|
|
||||||
plaintext, err := msg.Encrypt([]*openpgp.Entity{privateKey}, privateKey)
|
plaintext, err = msg.Encrypt([]*openpgp.Entity{privateKey}, privateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -150,10 +201,9 @@ func (u *user) Send(from string, to []string, r io.Reader) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: parentID from In-Reply-To
|
msg, err = u.c.UpdateDraftMessage(msg)
|
||||||
msg, err = u.c.CreateDraftMessage(msg, "")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot create draft message: %v", err)
|
return fmt.Errorf("cannot update draft message: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
outgoing := &protonmail.OutgoingMessage{ID: msg.ID}
|
outgoing := &protonmail.OutgoingMessage{ID: msg.ID}
|
||||||
|
@ -186,9 +236,10 @@ func (u *user) Send(from string, to []string, r io.Reader) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(plaintextRecipients) > 0 {
|
if len(plaintextRecipients) > 0 {
|
||||||
|
// TODO: attachments
|
||||||
plaintextSet := protonmail.NewMessagePackageSet(nil)
|
plaintextSet := protonmail.NewMessagePackageSet(nil)
|
||||||
|
|
||||||
plaintext, err := plaintextSet.Encrypt(bodyType)
|
plaintext, err := plaintextSet.Encrypt(bodyType, privateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -210,9 +261,10 @@ func (u *user) Send(from string, to []string, r io.Reader) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(encryptedRecipients) > 0 {
|
if len(encryptedRecipients) > 0 {
|
||||||
|
// TODO: attachments
|
||||||
encryptedSet := protonmail.NewMessagePackageSet(nil)
|
encryptedSet := protonmail.NewMessagePackageSet(nil)
|
||||||
|
|
||||||
plaintext, err := encryptedSet.Encrypt(bodyType)
|
plaintext, err := encryptedSet.Encrypt(bodyType, privateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue