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.
This commit is contained in:
Adnan Maolood 2022-05-04 11:00:26 -04:00 committed by Drew DeVault
parent 3f80f677f5
commit 8c2729f421
2 changed files with 83 additions and 71 deletions

View File

@ -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 !header.Has("From") {
header.SetAddressList("From", []*mail.Address{queue.smtpFrom})
}
if len(keyring) != 1 {
panic(errors.New("Expected site PGP key to contain one key"))
}
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))

View File

@ -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
}