~bigbes/core-go

8c2729f421a1ebb923db7120d569465dbb5238fc — Adnan Maolood 4 years ago 3f80f67
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.
2 files changed, 83 insertions(+), 71 deletions(-)

M email/worker.go
M server/server.go
M email/worker.go => email/worker.go +78 -66
@@ 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))

M server/server.go => server/server.go +5 -5
@@ 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
}