# HG changeset patch # User Peter Sanchez # Date 1647477076 21600 # Wed Mar 16 18:31:16 2022 -0600 # Node ID b2b875f33be2325daf59c34b048ef6312fb5a86d # Parent 0bacf7594debebe202deb633f8b12757c19bdb37 Initial work to add support for PGP signing and encrypting emails. Refs ~petersanchez/carrier#4 diff --git a/carrier_test.go b/carrier_test.go --- a/carrier_test.go +++ b/carrier_test.go @@ -8,7 +8,6 @@ ) func Example() { - // import "petersanchez.com/carrier" svc := carrier.NewConsoleService() msg := carrier.NewMessage() msg.SetFrom("me@mydomain.com"). @@ -31,3 +30,39 @@ err = svc.Send(msg) log.Println("Successfully sent email.") } + +func ExamplePGPKeys() { + + // You should handle errors properly in all instances below + privkey, err := os.Open("privkey.asc") + if err != nil { + panic(err) + } + defer privkey.Close() + + // public key is optional. If present, the email will encrypted + // for the recipient using the public key + pubkey, err := os.Open("pubkey.asc") + defer pubkey.Close() + + // Give pubkey if you want the email encrypted. If just providing + // the private key, the email will be signed with the given key. + keys, err := carrier.NewPGPKeys(privkey, nil) + + svc := carrier.NewConsoleService() + msg := carrier.NewMessage(). + SetFrom("me@mydomain.com"). + SetTo("recipient@theirdomain.com"). + SetCc("copy@somedomain.com") + msg.SetSubject("Sending email from Go!") + + // You MUST assign keys before setting the body. + err = msg.AddPGPKeys(keys) + + err = msg.SetBody("This is the text email body.") + err = msg.SetBodyHTML("This is the HTML email body.") + + // Send email + err = svc.Send(msg) + log.Println("Successfully sent email.") +} diff --git a/go.mod b/go.mod --- a/go.mod +++ b/go.mod @@ -4,4 +4,10 @@ require github.com/emersion/go-message v0.15.0 -require github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect +require ( + github.com/ProtonMail/go-crypto v0.0.0-20220113124808-70ae35bab23f // indirect + github.com/emersion/go-pgpmail v0.2.0 // indirect + github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect + golang.org/x/crypto v0.0.0-20211202192323-5770296d904e // indirect + golang.org/x/text v0.3.7 // indirect +) diff --git a/go.sum b/go.sum --- a/go.sum +++ b/go.sum @@ -1,6 +1,24 @@ +github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= +github.com/ProtonMail/go-crypto v0.0.0-20220113124808-70ae35bab23f h1:J2FzIrXN82q5uyUraeJpLIm7U6PffRwje2ORho5yIik= +github.com/ProtonMail/go-crypto v0.0.0-20220113124808-70ae35bab23f/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY= github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= +github.com/emersion/go-pgpmail v0.2.0 h1:BU9kEGQcDVXi6n0v3JBsWAikyo63xsUGZ1lnVaWa6ks= +github.com/emersion/go-pgpmail v0.2.0/go.mod h1:8mQ8Rpn+w28DDaiP8HvJuZjSAymaWr87K3zA/bwwkU0= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211202192323-5770296d904e h1:MUP6MR3rJ7Gk9LEia0LP2ytiH6MuCfs7qYz+47jGdD8= +golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/message.go b/message.go --- a/message.go +++ b/message.go @@ -10,7 +10,9 @@ "sync" "time" + "github.com/ProtonMail/go-crypto/openpgp" "github.com/emersion/go-message/mail" + "github.com/emersion/go-pgpmail" ) // Service interface @@ -54,6 +56,46 @@ return taddys } +// PGPKeys holds signer and recipient PGP keys +type PGPKeys struct { + signer *openpgp.Entity + rcpt *openpgp.Entity +} + +// NewPGPKeys returns a PGPKey instance. If rkey is empty then +// the final email this PGPKeys instance is used for will only +// be signed. +func NewPGPKeys(skey, rkey io.Reader) (*PGPKeys, error) { + var se *openpgp.Entity + + keyring, err := openpgp.ReadArmoredKeyRing(skey) + if err != nil { + return nil, err + } + if len(keyring) != 1 { + return nil, fmt.Errorf("Expected signer PGP key to contain one key") + } + se = keyring[0] + if se.PrivateKey == nil || se.PrivateKey.Encrypted { + return nil, fmt.Errorf("Failed to load private key for signer") + } + gkey := &PGPKeys{signer: se} + + if rkey == nil { + return gkey, nil + } + + keyring, err = openpgp.ReadArmoredKeyRing(rkey) + if err != nil { + return nil, err + } + if len(keyring) != 1 { + return nil, fmt.Errorf("Expected recipient PGP key to contain one key") + } + gkey.rcpt = keyring[0] + return gkey, nil +} + // Message object which holds the entire email message. Each service will // take a *Message object and use it to parse out the required data to send // the email. @@ -64,6 +106,9 @@ mw *mail.Writer once sync.Once isClosed bool + + pgpKeys *PGPKeys + ct io.WriteCloser } // SetTo Set the To: header @@ -137,12 +182,54 @@ return m } +// AddPGPKeys sets the PGP keys for this message. If you plan to sign +// or encrypt the final email than this MUST be called before writing +// any message body. An error is raised if you try to assign keys +// after the body has been opened. +func (m *Message) AddPGPKeys(keys *PGPKeys) error { + if m.mw != nil || m.isClosed { + return fmt.Errorf("Message writer is already open. Can't assign keys") + } + m.pgpKeys = keys + return nil +} + func (m *Message) openWriter() { m.once.Do(func() { var err error - m.mw, err = mail.CreateWriter(&m.mb, m.Header) - if err != nil { - panic(err) + if m.pgpKeys != nil && m.pgpKeys.signer != nil { + var ( + eh mail.Header + cleartext io.WriteCloser + ) + eh.SetContentType("text/plain", nil) + + if m.pgpKeys.rcpt != nil { + to := []*openpgp.Entity{m.pgpKeys.rcpt} + cleartext, err = pgpmail.Encrypt( + &m.mb, m.Header.Header.Header, to, m.pgpKeys.signer, nil) + if err != nil { + panic(err) + } + } else { + // Signing only + cleartext, err = pgpmail.Sign( + &m.mb, m.Header.Header.Header, m.pgpKeys.signer, nil) + if err != nil { + panic(err) + } + } + + m.mw, err = mail.CreateWriter(cleartext, eh) + if err != nil { + panic(err) + } + m.ct = cleartext + } else { + m.mw, err = mail.CreateWriter(&m.mb, m.Header) + if err != nil { + panic(err) + } } }) } @@ -235,6 +322,9 @@ func (m *Message) Close() { if m.mw != nil && !m.isClosed { m.mw.Close() + if m.ct != nil { + m.ct.Close() + } m.isClosed = true } }