summaryrefslogtreecommitdiff
path: root/src/mailinglist/mailer.go
blob: 07d6c3ac6576a7da6140e37e914d637a03ffdc12 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package mailinglist

import (
	"context"
	"errors"
	"strings"

	"github.com/emersion/go-sasl"
	"github.com/emersion/go-smtp"
	"github.com/mediocregopher/blog.mediocregopher.com/srv/cfg"
	"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
	"github.com/mediocregopher/mediocre-go-lib/v2/mlog"
)

// Mailer is used to deliver emails to arbitrary recipients.
type Mailer interface {
	Send(to, subject, body string) error
}

type logMailer struct {
	logger *mlog.Logger
}

// NewLogMailer returns a Mailer instance which will not actually send any
// emails, it will only log to the given Logger when Send is called.
func NewLogMailer(logger *mlog.Logger) Mailer {
	return &logMailer{logger: logger}
}

func (l *logMailer) Send(to, subject, body string) error {
	ctx := mctx.Annotate(context.Background(),
		"to", to,
		"subject", subject,
	)
	l.logger.Info(ctx, "would have sent email")
	return nil
}

// NullMailer acts as a Mailer but actually just does nothing.
var NullMailer = nullMailer{}

type nullMailer struct{}

func (nullMailer) Send(to, subject, body string) error {
	return nil
}

// MailerParams are used to initialize a new Mailer instance.
type MailerParams struct {
	SMTPAddr string

	// Optional, if not given then no auth is attempted.
	SMTPAuth sasl.Client

	// The sending email address to use for all emails being sent.
	SendAs string
}

// SetupCfg implement the cfg.Cfger interface.
func (m *MailerParams) SetupCfg(cfg *cfg.Cfg) {

	cfg.StringVar(&m.SMTPAddr, "ml-smtp-addr", "", "Address of SMTP server to use for sending emails for the mailing list")
	smtpAuthStr := cfg.String("ml-smtp-auth", "", "user:pass to use when authenticating with the mailing list SMTP server. The given user will also be used as the From address.")

	cfg.OnInit(func(ctx context.Context) error {
		if m.SMTPAddr == "" {
			return nil
		}

		smtpAuthParts := strings.SplitN(*smtpAuthStr, ":", 2)
		if len(smtpAuthParts) < 2 {
			return errors.New("invalid -ml-smtp-auth")
		}

		m.SMTPAuth = sasl.NewPlainClient("", smtpAuthParts[0], smtpAuthParts[1])
		m.SendAs = smtpAuthParts[0]

		return nil
	})
}

// Annotate implements mctx.Annotator interface.
func (m *MailerParams) Annotate(a mctx.Annotations) {
	if m.SMTPAddr == "" {
		return
	}

	a["smtpAddr"] = m.SMTPAddr
	a["smtpSendAs"] = m.SendAs
}

type mailer struct {
	params MailerParams
}

// NewMailer initializes and returns a Mailer which will use an external SMTP
// server to deliver email.
func NewMailer(params MailerParams) Mailer {
	return &mailer{
		params: params,
	}
}

func (m *mailer) Send(to, subject, body string) error {

	msg := []byte("From: " + m.params.SendAs + "\r\n" +
		"To: " + to + "\r\n" +
		"Subject: " + subject + "\r\n\r\n" +
		body + "\r\n")

	c, err := smtp.Dial(m.params.SMTPAddr)
	if err != nil {
		return err
	}
	defer c.Close()

	if err = c.Auth(m.params.SMTPAuth); err != nil {
		return err
	}

	if err = c.Mail(m.params.SendAs, nil); err != nil {
		return err
	}

	if err = c.Rcpt(to); err != nil {
		return err
	}

	w, err := c.Data()
	if err != nil {
		return err
	}

	if _, err = w.Write(msg); err != nil {
		return err
	}

	if err = w.Close(); err != nil {
		return err
	}

	return c.Quit()
}