Initial work to add support for PGP signing and encrypting emails. Refs ~petersanchez/carrier#4
4 files changed, 154 insertions(+), 5 deletions(-)

M carrier_test.go
M go.mod
M go.sum
M message.go
M carrier_test.go +36 -1
@@ 8,7 8,6 @@ import (
 )
 
 func Example() {
-	// import "petersanchez.com/carrier"
 	svc := carrier.NewConsoleService()
 	msg := carrier.NewMessage()
 	msg.SetFrom("me@mydomain.com").

          
@@ 31,3 30,39 @@ func Example() {
 	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.")
+}

          
M go.mod +7 -1
@@ 4,4 4,10 @@ go 1.17
 
 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
+)

          
M go.sum +18 -0
@@ 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=

          
M message.go +93 -3
@@ 10,7 10,9 @@ import (
 	"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 @@ func addressToString(addys []*mail.Addre
 	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 @@ type Message struct {
 	mw       *mail.Writer
 	once     sync.Once
 	isClosed bool
+
+	pgpKeys *PGPKeys
+	ct      io.WriteCloser
 }
 
 // SetTo Set the To: header

          
@@ 137,12 182,54 @@ func (m *Message) SetFrom(address string
 	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) GetAllRcpts() []string
 func (m *Message) Close() {
 	if m.mw != nil && !m.isClosed {
 		m.mw.Close()
+		if m.ct != nil {
+			m.ct.Close()
+		}
 		m.isClosed = true
 	}
 }