mirror of https://git.sr.ht/~sircmpwn/core-go
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:
parent
3f80f677f5
commit
8c2729f421
144
email/worker.go
144
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 !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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue