@@ 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.")
+}
@@ 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
+)
@@ 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=
@@ 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
}
}