/
mailer.go
232 lines (214 loc) · 5.74 KB
/
mailer.go
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
package mailer
import (
"context"
"fmt"
"io"
"net/textproto"
log "github.com/blitznwl/GoPhishTest/logger"
"github.com/gophish/gomail"
"github.com/sirupsen/logrus"
)
// MaxReconnectAttempts is the maximum number of times we should reconnect to a server
var MaxReconnectAttempts = 10
// ErrMaxConnectAttempts is thrown when the maximum number of reconnect attempts
// is reached.
type ErrMaxConnectAttempts struct {
underlyingError error
}
// Error returns the wrapped error response
func (e *ErrMaxConnectAttempts) Error() string {
errString := "Max connection attempts exceeded"
if e.underlyingError != nil {
errString = fmt.Sprintf("%s - %s", errString, e.underlyingError.Error())
}
return errString
}
// Mailer is an interface that defines an object used to queue and
// send mailer.Mail instances.
type Mailer interface {
Start(ctx context.Context)
Queue([]Mail)
}
// Sender exposes the common operations required for sending email.
type Sender interface {
Send(from string, to []string, msg io.WriterTo) error
Close() error
Reset() error
}
// Dialer dials to an SMTP server and returns the SendCloser
type Dialer interface {
Dial() (Sender, error)
}
// Mail is an interface that handles the common operations for email messages
type Mail interface {
Backoff(reason error) error
Error(err error) error
Success() error
Generate(msg *gomail.Message) error
GetDialer() (Dialer, error)
GetSmtpFrom() (string, error)
}
// MailWorker is the worker that receives slices of emails
// on a channel to send. It's assumed that every slice of emails received is meant
// to be sent to the same server.
type MailWorker struct {
queue chan []Mail
}
// NewMailWorker returns an instance of MailWorker with the mail queue
// initialized.
func NewMailWorker() *MailWorker {
return &MailWorker{
queue: make(chan []Mail),
}
}
// Start launches the mail worker to begin listening on the Queue channel
// for new slices of Mail instances to process.
func (mw *MailWorker) Start(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case ms := <-mw.queue:
go func(ctx context.Context, ms []Mail) {
dialer, err := ms[0].GetDialer()
if err != nil {
errorMail(err, ms)
return
}
sendMail(ctx, dialer, ms)
}(ctx, ms)
}
}
}
// Queue sends the provided mail to the internal queue for processing.
func (mw *MailWorker) Queue(ms []Mail) {
mw.queue <- ms
}
// errorMail is a helper to handle erroring out a slice of Mail instances
// in the case that an unrecoverable error occurs.
func errorMail(err error, ms []Mail) {
for _, m := range ms {
m.Error(err)
}
}
// dialHost attempts to make a connection to the host specified by the Dialer.
// It returns MaxReconnectAttempts if the number of connection attempts has been
// exceeded.
func dialHost(ctx context.Context, dialer Dialer) (Sender, error) {
sendAttempt := 0
var sender Sender
var err error
for {
select {
case <-ctx.Done():
return nil, nil
default:
break
}
sender, err = dialer.Dial()
if err == nil {
break
}
sendAttempt++
if sendAttempt == MaxReconnectAttempts {
err = &ErrMaxConnectAttempts{
underlyingError: err,
}
break
}
}
return sender, err
}
// sendMail attempts to send the provided Mail instances.
// If the context is cancelled before all of the mail are sent,
// sendMail just returns and does not modify those emails.
func sendMail(ctx context.Context, dialer Dialer, ms []Mail) {
sender, err := dialHost(ctx, dialer)
if err != nil {
log.Warn(err)
errorMail(err, ms)
return
}
defer sender.Close()
message := gomail.NewMessage()
for i, m := range ms {
select {
case <-ctx.Done():
return
default:
break
}
message.Reset()
err = m.Generate(message)
if err != nil {
log.Warn(err)
m.Error(err)
continue
}
smtp_from, err := m.GetSmtpFrom()
if err != nil {
m.Error(err)
continue
}
err = gomail.SendCustomFrom(sender, smtp_from, message)
if err != nil {
if te, ok := err.(*textproto.Error); ok {
switch {
// If it's a temporary error, we should backoff and try again later.
// We'll reset the connection so future messages don't incur a
// different error (see https://github.com/blitznwl/GoPhishTest/issues/787).
case te.Code >= 400 && te.Code <= 499:
log.WithFields(logrus.Fields{
"code": te.Code,
"email": message.GetHeader("To")[0],
}).Warn(err)
m.Backoff(err)
sender.Reset()
continue
// Otherwise, if it's a permanent error, we shouldn't backoff this message,
// since the RFC specifies that running the same commands won't work next time.
// We should reset our sender and error this message out.
case te.Code >= 500 && te.Code <= 599:
log.WithFields(logrus.Fields{
"code": te.Code,
"email": message.GetHeader("To")[0],
}).Warn(err)
m.Error(err)
sender.Reset()
continue
// If something else happened, let's just error out and reset the
// sender
default:
log.WithFields(logrus.Fields{
"code": "unknown",
"email": message.GetHeader("To")[0],
}).Warn(err)
m.Error(err)
sender.Reset()
continue
}
} else {
// This likely indicates that something happened to the underlying
// connection. We'll try to reconnect and, if that fails, we'll
// error out the remaining emails.
log.WithFields(logrus.Fields{
"email": message.GetHeader("To")[0],
}).Warn(err)
origErr := err
sender, err = dialHost(ctx, dialer)
if err != nil {
errorMail(err, ms[i:])
break
}
m.Backoff(origErr)
continue
}
}
log.WithFields(logrus.Fields{
"smtp_from": smtp_from,
"envelope_from": message.GetHeader("From")[0],
"email": message.GetHeader("To")[0],
}).Info("Email sent")
m.Success()
}
}