From 8c2729f421a1ebb923db7120d569465dbb5238fc Mon Sep 17 00:00:00 2001 From: Adnan Maolood Date: Wed, 4 May 2022 11:00:26 -0400 Subject: [PATCH] email.EnqueueStd: Don't overwrite headers Sometimes we need to specify the Message-Id, From, and Reply-To headers (e.g. for todo.sr.ht ticket notifications). Don't overwrite these headers if they are present. --- email/worker.go | 144 +++++++++++++++++++++++++---------------------- server/server.go | 10 ++-- 2 files changed, 83 insertions(+), 71 deletions(-) diff --git a/email/worker.go b/email/worker.go index df96a4bec93cec242be45cd1005c7fe8e9655e0b..b63b3a244f31d25764b1ec1f8ce4c34ca83fe57a 100644 --- a/email/worker.go +++ b/email/worker.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "errors" - "fmt" "io" "log" "net/http" @@ -12,13 +11,12 @@ import ( "strings" "time" - "git.sr.ht/~sircmpwn/dowork" + work "git.sr.ht/~sircmpwn/dowork" "github.com/ProtonMail/go-crypto/openpgp" _ "github.com/emersion/go-message/charset" "github.com/emersion/go-message/mail" "github.com/emersion/go-pgpmail" - - "git.sr.ht/~sircmpwn/core-go/config" + "github.com/vaughan0/go-ini" ) var emailCtxKey = &contextKey{"email"} @@ -48,11 +46,6 @@ func NewTask(msg *bytes.Buffer, rcpts []string) *work.Task { }) } -// Enqueues an email for sending with the default parameters. -func Enqueue(ctx context.Context, msg *bytes.Buffer, rcpts []string) { - ForContext(ctx).Enqueue(NewTask(msg, rcpts)) -} - func prepareEncrypted(rcptKey *string, header mail.Header, buf *bytes.Buffer, signed *openpgp.Entity) (io.WriteCloser, error) { keyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(*rcptKey)) @@ -81,29 +74,13 @@ func prepareSigned(header mail.Header, buf *bytes.Buffer, // encrypts it, and then queues it for delivery. // // Senders should fill in at least the To and Subject headers, and the message -// body. Message-ID, Date, From, and Reply-To will be added here. +// body. Message-ID, Date, From, and Reply-To will also be added if they are not +// already present. func EnqueueStd(ctx context.Context, header mail.Header, bodyReader io.Reader, rcptKey *string) error { - // XXX: Do we really need to load all this shit every time we send an email - conf := config.ForContext(ctx) - smtpFrom, ok := conf.Get("mail", "smtp-from") - if !ok { - panic(errors.New("Expected [mail]smtp-from in config")) - } - ownerName, ok := conf.Get("sr.ht", "owner-name") - if !ok { - panic(errors.New("Expected [sr.ht]owner-name in config")) - } - ownerEmail, ok := conf.Get("sr.ht", "owner-email") - if !ok { - panic(errors.New("Expected [sr.ht]owner-email in config")) - } + queue := ForContext(ctx) - from, err := header.AddressList("From") - if err != nil { - return err - } to, err := header.AddressList("To") if err != nil { return err @@ -113,12 +90,6 @@ func EnqueueStd(ctx context.Context, header mail.Header, return err } - if addr, err := mail.ParseAddress(smtpFrom); err == nil { - from = append(from, addr) - } else { - panic(err) - } - var rcpts []string for _, addr := range to { rcpts = append(rcpts, addr.Address) @@ -127,32 +98,17 @@ func EnqueueStd(ctx context.Context, header mail.Header, rcpts = append(rcpts, addr.Address) } - header.GenerateMessageID() - header.SetDate(time.Now().UTC()) - header.SetAddressList("From", from) - header.Header.SetText("Reply-To", - fmt.Sprintf("%s <%s>", ownerName, ownerEmail)) - - privKeyPath, ok := conf.Get("mail", "pgp-privkey") - if !ok { - panic(errors.New("Expected [sr.ht]owner-email in config")) + if !header.Has("Message-Id") { + header.GenerateMessageID() } - privKeyFile, err := os.Open(privKeyPath) - if err != nil { - panic(err) + if !header.Has("Date") { + header.SetDate(time.Now().UTC()) } - defer privKeyFile.Close() - - keyring, err := openpgp.ReadArmoredKeyRing(privKeyFile) - if err != nil { - panic(err) - } - if len(keyring) != 1 { - panic(errors.New("Expected site PGP key to contain one key")) + if !header.Has("From") { + header.SetAddressList("From", []*mail.Address{queue.smtpFrom}) } - entity := keyring[0] - if entity.PrivateKey == nil || entity.PrivateKey.Encrypted { - panic(errors.New("Failed to load private key for email signature")) + if !header.Has("Reply-To") { + header.SetAddressList("Reply-To", []*mail.Address{queue.ownerAddress}) } var ( @@ -161,7 +117,7 @@ func EnqueueStd(ctx context.Context, header mail.Header, ) if rcptKey != nil { - cleartext, err = prepareEncrypted(rcptKey, header, &buf, entity) + cleartext, err = prepareEncrypted(rcptKey, header, &buf, queue.entity) } // Fall back to unencrypted email if encryption did not work // TODO should we add the error message to the email? @@ -172,7 +128,7 @@ func EnqueueStd(ctx context.Context, header mail.Header, strings.Join(rcpts, ", "), err.Error()) } - cleartext, err = prepareSigned(header, &buf, entity) + cleartext, err = prepareSigned(header, &buf, queue.entity) if err != nil { log.Fatal(err) } @@ -192,18 +148,74 @@ func EnqueueStd(ctx context.Context, header mail.Header, log.Fatal(err) } - Enqueue(ctx, &buf, rcpts) + queue.Queue.Enqueue(NewTask(&buf, rcpts)) return nil } +type Queue struct { + *work.Queue + smtpFrom *mail.Address + ownerAddress *mail.Address + entity *openpgp.Entity +} + // Creates a new email processing queue. -func NewQueue() *work.Queue { - return work.NewQueue("email") +func NewQueue(conf ini.File) *Queue { + smtpFrom, ok := conf.Get("mail", "smtp-from") + if !ok { + panic("Expected [mail]smtp-from in config") + } + ownerName, ok := conf.Get("sr.ht", "owner-name") + if !ok { + panic("Expected [sr.ht]owner-name in config") + } + ownerEmail, ok := conf.Get("sr.ht", "owner-email") + if !ok { + panic("Expected [sr.ht]owner-email in config") + } + addr, err := mail.ParseAddress(smtpFrom) + if err != nil { + panic(err) + } + ownerAddr := &mail.Address{ + Name: ownerName, + Address: ownerEmail, + } + + privKeyPath, ok := conf.Get("mail", "pgp-privkey") + if !ok { + panic("Expected [mail]pgp-privkey in config") + } + + privKeyFile, err := os.Open(privKeyPath) + if err != nil { + panic(err) + } + defer privKeyFile.Close() + + keyring, err := openpgp.ReadArmoredKeyRing(privKeyFile) + if err != nil { + panic(err) + } + if len(keyring) != 1 { + panic("Expected site PGP key to contain one key") + } + entity := keyring[0] + if entity.PrivateKey == nil || entity.PrivateKey.Encrypted { + panic("Failed to load private key for email signature") + } + + return &Queue{ + Queue: work.NewQueue("email"), + smtpFrom: addr, + ownerAddress: ownerAddr, + entity: entity, + } } // Returns the email worker for this context. -func ForContext(ctx context.Context) *work.Queue { - q, ok := ctx.Value(emailCtxKey).(*work.Queue) +func ForContext(ctx context.Context) *Queue { + q, ok := ctx.Value(emailCtxKey).(*Queue) if !ok { panic(errors.New("No email worker for this context")) } @@ -211,12 +223,12 @@ func ForContext(ctx context.Context) *work.Queue { } // Returns a context which includes the given mail worker. -func Context(ctx context.Context, queue *work.Queue) context.Context { +func Context(ctx context.Context, queue *Queue) context.Context { return context.WithValue(ctx, emailCtxKey, queue) } // Adds HTTP middleware to provide an email work queue to this context. -func Middleware(queue *work.Queue) func(next http.Handler) http.Handler { +func Middleware(queue *Queue) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { r = r.WithContext(Context(r.Context(), queue)) diff --git a/server/server.go b/server/server.go index 03c96af0d2d3681dd1602f735f6fde430e6d229b..11cf7c82d8506a6de45dde5019cfe60214cbec2b 100644 --- a/server/server.go +++ b/server/server.go @@ -13,14 +13,14 @@ import ( "strconv" "time" - "git.sr.ht/~sircmpwn/dowork" + work "git.sr.ht/~sircmpwn/dowork" "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/playground" "github.com/99designs/gqlgen/handler" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" goRedis "github.com/go-redis/redis/v8" - "github.com/kavu/go_reuseport" + reuseport "github.com/kavu/go_reuseport" _ "github.com/lib/pq" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" @@ -55,7 +55,7 @@ type Server struct { router chi.Router service string queues []*work.Queue - email *work.Queue + email *email.Queue MaxComplexity int } @@ -171,7 +171,7 @@ func (server *Server) WithDefaultMiddleware() *Server { timeout = 3 * time.Second } - server.email = email.NewQueue() + server.email = email.NewQueue(server.conf) server.router.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -199,7 +199,7 @@ func (server *Server) WithDefaultMiddleware() *Server { next.ServeHTTP(w, r) }) }) - server.WithQueues(server.email) + server.WithQueues(server.email.Queue) return server }