diff options
author | Mike Crute <mike@crute.us> | 2022-12-06 19:56:41 -0800 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2022-12-06 19:56:41 -0800 |
commit | 3bbe51cbec9a974339f3166356c20bbdc8d2d1c8 (patch) | |
tree | 6d262bc2935c7142380aaacb21ee4e8185c49ede | |
parent | adb1ae5d378ecb2619b6ccba57fa142b0a60f075 (diff) | |
download | ses-smtpd-proxy-3bbe51cbec9a974339f3166356c20bbdc8d2d1c8.tar.bz2 ses-smtpd-proxy-3bbe51cbec9a974339f3166356c20bbdc8d2d1c8.tar.xz ses-smtpd-proxy-3bbe51cbec9a974339f3166356c20bbdc8d2d1c8.zip |
Import smtpd code since it's tiny
-rw-r--r-- | main.go | 2 | ||||
-rw-r--r-- | smtpd/smtpd.go | 530 |
2 files changed, 531 insertions, 1 deletions
@@ -6,9 +6,9 @@ import ( | |||
6 | "log" | 6 | "log" |
7 | "os" | 7 | "os" |
8 | 8 | ||
9 | "code.crute.us/mcrute/ses-smtpd-proxy/smtpd" | ||
9 | "github.com/aws/aws-sdk-go/aws/session" | 10 | "github.com/aws/aws-sdk-go/aws/session" |
10 | "github.com/aws/aws-sdk-go/service/ses" | 11 | "github.com/aws/aws-sdk-go/service/ses" |
11 | "github.com/mcrute/go-smtpd/smtpd" | ||
12 | ) | 12 | ) |
13 | 13 | ||
14 | const ( | 14 | const ( |
diff --git a/smtpd/smtpd.go b/smtpd/smtpd.go new file mode 100644 index 0000000..83a37f8 --- /dev/null +++ b/smtpd/smtpd.go | |||
@@ -0,0 +1,530 @@ | |||
1 | // Copyright 2011 The go-smtpd Authors. All rights reserved. | ||
2 | // Use of this source code is governed by a BSD-style | ||
3 | // license that can be found in the LICENSE file. | ||
4 | // | ||
5 | // Originally from https://github.com/bradfitz/go-smtpd | ||
6 | |||
7 | // Package smtpd implements an SMTP server. Hooks are provided to customize | ||
8 | // its behavior. | ||
9 | package smtpd | ||
10 | |||
11 | // TODO: | ||
12 | // -- send 421 to connected clients on graceful server shutdown (s3.8) | ||
13 | // | ||
14 | |||
15 | import ( | ||
16 | "bufio" | ||
17 | "bytes" | ||
18 | "crypto/tls" | ||
19 | "encoding/base64" | ||
20 | "errors" | ||
21 | "fmt" | ||
22 | "log" | ||
23 | "net" | ||
24 | "os" | ||
25 | "regexp" | ||
26 | "strings" | ||
27 | "time" | ||
28 | "unicode" | ||
29 | ) | ||
30 | |||
31 | var ( | ||
32 | rcptToRE = regexp.MustCompile(`[Tt][Oo]:\s*<(.+)>`) | ||
33 | mailFromRE = regexp.MustCompile(`[Ff][Rr][Oo][Mm]:\s*<(.*)>`) | ||
34 | ) | ||
35 | |||
36 | // Server is an SMTP server. | ||
37 | type Server struct { | ||
38 | Addr string // TCP address to listen on, ":25" if empty | ||
39 | Hostname string // optional Hostname to announce; "" to use system hostname | ||
40 | ReadTimeout time.Duration // optional read timeout | ||
41 | WriteTimeout time.Duration // optional write timeout | ||
42 | |||
43 | StartTLS *tls.Config // advertise STARTTLS and use the given config to upgrade the connection with | ||
44 | |||
45 | // OnNewConnection, if non-nil, is called on new connections. | ||
46 | // If it returns non-nil, the connection is closed. | ||
47 | OnNewConnection func(c Connection) error | ||
48 | |||
49 | // OnNewMail must be defined and is called when a new message beings. | ||
50 | // (when a MAIL FROM line arrives) | ||
51 | OnNewMail func(c Connection, from MailAddress) (Envelope, error) | ||
52 | |||
53 | OnAuthentication func(c Connection, user string, password string) error | ||
54 | } | ||
55 | |||
56 | // MailAddress is defined by | ||
57 | type MailAddress interface { | ||
58 | Email() string // email address, as provided | ||
59 | Hostname() string // canonical hostname, lowercase | ||
60 | } | ||
61 | |||
62 | // Connection is implemented by the SMTP library and provided to callers | ||
63 | // customizing their own Servers. | ||
64 | type Connection interface { | ||
65 | IsAuthenticated() bool | ||
66 | Addr() net.Addr | ||
67 | Close() error // to force-close a connection | ||
68 | } | ||
69 | |||
70 | type Envelope interface { | ||
71 | AddRecipient(rcpt MailAddress) error | ||
72 | BeginData() error | ||
73 | Write(line []byte) error | ||
74 | Close() error | ||
75 | } | ||
76 | |||
77 | type BasicEnvelope struct { | ||
78 | rcpts []MailAddress | ||
79 | } | ||
80 | |||
81 | func (e *BasicEnvelope) AddRecipient(rcpt MailAddress) error { | ||
82 | e.rcpts = append(e.rcpts, rcpt) | ||
83 | return nil | ||
84 | } | ||
85 | |||
86 | func (e *BasicEnvelope) BeginData() error { | ||
87 | if len(e.rcpts) == 0 { | ||
88 | return SMTPError("554 5.5.1 Error: no valid recipients") | ||
89 | } | ||
90 | return nil | ||
91 | } | ||
92 | |||
93 | func (e *BasicEnvelope) Write(line []byte) error { | ||
94 | log.Printf("Line: %q", string(line)) | ||
95 | return nil | ||
96 | } | ||
97 | |||
98 | func (e *BasicEnvelope) Close() error { | ||
99 | return nil | ||
100 | } | ||
101 | |||
102 | func (srv *Server) hostname() string { | ||
103 | if srv.Hostname != "" { | ||
104 | return srv.Hostname | ||
105 | } | ||
106 | out, err := os.Hostname() | ||
107 | if err != nil { | ||
108 | return "" | ||
109 | } | ||
110 | return strings.TrimSpace(string(out)) | ||
111 | } | ||
112 | |||
113 | // ListenAndServe listens on the TCP network address srv.Addr and then | ||
114 | // calls Serve to handle requests on incoming connections. If | ||
115 | // srv.Addr is blank, ":25" is used. | ||
116 | func (srv *Server) ListenAndServe() error { | ||
117 | addr := srv.Addr | ||
118 | if addr == "" { | ||
119 | addr = ":25" | ||
120 | } | ||
121 | ln, e := net.Listen("tcp", addr) | ||
122 | if e != nil { | ||
123 | return e | ||
124 | } | ||
125 | return srv.Serve(ln) | ||
126 | } | ||
127 | |||
128 | func (srv *Server) Serve(ln net.Listener) error { | ||
129 | defer ln.Close() | ||
130 | for { | ||
131 | rw, e := ln.Accept() | ||
132 | if e != nil { | ||
133 | if ne, ok := e.(net.Error); ok && ne.Temporary() { | ||
134 | log.Printf("smtpd: Accept error: %v", e) | ||
135 | continue | ||
136 | } | ||
137 | return e | ||
138 | } | ||
139 | sess, err := srv.newSession(rw) | ||
140 | if err != nil { | ||
141 | continue | ||
142 | } | ||
143 | go sess.serve() | ||
144 | } | ||
145 | panic("not reached") | ||
146 | } | ||
147 | |||
148 | type session struct { | ||
149 | srv *Server | ||
150 | rwc net.Conn | ||
151 | br *bufio.Reader | ||
152 | bw *bufio.Writer | ||
153 | |||
154 | env Envelope // current envelope, or nil | ||
155 | |||
156 | helloType string | ||
157 | helloHost string | ||
158 | authenticated string | ||
159 | } | ||
160 | |||
161 | func (srv *Server) newSession(rwc net.Conn) (s *session, err error) { | ||
162 | s = &session{ | ||
163 | srv: srv, | ||
164 | rwc: rwc, | ||
165 | br: bufio.NewReader(rwc), | ||
166 | bw: bufio.NewWriter(rwc), | ||
167 | } | ||
168 | return | ||
169 | } | ||
170 | |||
171 | func (s *session) IsAuthenticated() bool { | ||
172 | return s.authenticated != "" | ||
173 | } | ||
174 | |||
175 | func (s *session) errorf(format string, args ...interface{}) { | ||
176 | log.Printf("Client error: "+format, args...) | ||
177 | } | ||
178 | |||
179 | func (s *session) sendf(format string, args ...interface{}) { | ||
180 | if s.srv.WriteTimeout != 0 { | ||
181 | s.rwc.SetWriteDeadline(time.Now().Add(s.srv.WriteTimeout)) | ||
182 | } | ||
183 | fmt.Fprintf(s.bw, format, args...) | ||
184 | s.bw.Flush() | ||
185 | } | ||
186 | |||
187 | func (s *session) sendlinef(format string, args ...interface{}) { | ||
188 | s.sendf(format+"\r\n", args...) | ||
189 | } | ||
190 | |||
191 | func (s *session) sendSMTPErrorOrLinef(err error, format string, args ...interface{}) { | ||
192 | if se, ok := err.(SMTPError); ok { | ||
193 | s.sendlinef("%s", se.Error()) | ||
194 | return | ||
195 | } | ||
196 | s.sendlinef(format, args...) | ||
197 | } | ||
198 | |||
199 | func (s *session) Addr() net.Addr { | ||
200 | return s.rwc.RemoteAddr() | ||
201 | } | ||
202 | |||
203 | func (s *session) Close() error { return s.rwc.Close() } | ||
204 | |||
205 | func (s *session) serve() { | ||
206 | defer s.rwc.Close() | ||
207 | if onc := s.srv.OnNewConnection; onc != nil { | ||
208 | if err := onc(s); err != nil { | ||
209 | s.sendSMTPErrorOrLinef(err, "554 connection rejected") | ||
210 | return | ||
211 | } | ||
212 | } | ||
213 | s.sendf("220 %s ESMTP gosmtpd\r\n", s.srv.hostname()) | ||
214 | for { | ||
215 | if s.srv.ReadTimeout != 0 { | ||
216 | s.rwc.SetReadDeadline(time.Now().Add(s.srv.ReadTimeout)) | ||
217 | } | ||
218 | sl, err := s.br.ReadSlice('\n') | ||
219 | if err != nil { | ||
220 | s.errorf("read error: %v", err) | ||
221 | return | ||
222 | } | ||
223 | line := cmdLine(string(sl)) | ||
224 | if err := line.checkValid(); err != nil { | ||
225 | s.sendlinef("500 %v", err) | ||
226 | continue | ||
227 | } | ||
228 | |||
229 | switch line.Verb() { | ||
230 | case "HELO", "EHLO": | ||
231 | s.handleHello(line.Verb(), line.Arg()) | ||
232 | case "STARTTLS": | ||
233 | if s.srv.StartTLS == nil { | ||
234 | s.sendlinef("502 5.5.2 Error: command not recognized") | ||
235 | continue | ||
236 | } | ||
237 | s.sendlinef("220 Ready to start TLS") | ||
238 | if err := s.handleStartTLS(); err != nil { | ||
239 | s.errorf("failed to start tls: %s", err) | ||
240 | s.sendSMTPErrorOrLinef(err, "550 ??? failed") | ||
241 | } | ||
242 | case "QUIT": | ||
243 | s.sendlinef("221 2.0.0 Bye") | ||
244 | return | ||
245 | case "RSET": | ||
246 | s.env = nil | ||
247 | s.sendlinef("250 2.0.0 OK") | ||
248 | case "NOOP": | ||
249 | s.sendlinef("250 2.0.0 OK") | ||
250 | case "MAIL": | ||
251 | if !s.validateAuth() { | ||
252 | return | ||
253 | } | ||
254 | arg := line.Arg() // "From:<foo@bar.com>" | ||
255 | m := mailFromRE.FindStringSubmatch(arg) | ||
256 | if m == nil { | ||
257 | log.Printf("invalid MAIL arg: %q", arg) | ||
258 | s.sendlinef("501 5.1.7 Bad sender address syntax") | ||
259 | continue | ||
260 | } | ||
261 | s.handleMailFrom(m[1]) | ||
262 | case "RCPT": | ||
263 | if !s.validateAuth() { | ||
264 | return | ||
265 | } | ||
266 | s.handleRcpt(line) | ||
267 | case "AUTH": | ||
268 | s.handleAuth(line) | ||
269 | case "DATA": | ||
270 | if !s.validateAuth() { | ||
271 | return | ||
272 | } | ||
273 | s.handleData() | ||
274 | default: | ||
275 | log.Printf("Client: %q, verhb: %q", line, line.Verb()) | ||
276 | s.sendlinef("502 5.5.2 Error: command not recognized") | ||
277 | } | ||
278 | } | ||
279 | } | ||
280 | |||
281 | func (s *session) handleStartTLS() error { | ||
282 | tlsConn := tls.Server(s.rwc, s.srv.StartTLS) | ||
283 | err := tlsConn.Handshake() | ||
284 | if err != nil { | ||
285 | return err | ||
286 | } | ||
287 | s.rwc = net.Conn(tlsConn) | ||
288 | s.bw.Reset(s.rwc) | ||
289 | s.br.Reset(s.rwc) | ||
290 | return nil | ||
291 | } | ||
292 | |||
293 | func (s *session) handleHello(greeting, host string) { | ||
294 | s.helloType = greeting | ||
295 | s.helloHost = host | ||
296 | fmt.Fprintf(s.bw, "250-%s\r\n", s.srv.hostname()) | ||
297 | extensions := []string{} | ||
298 | if s.srv.OnAuthentication != nil { | ||
299 | extensions = append(extensions, "250-AUTH PLAIN") | ||
300 | } | ||
301 | if s.srv.StartTLS != nil { | ||
302 | extensions = append(extensions, "250-STARTTLS") | ||
303 | } | ||
304 | extensions = append(extensions, "250-PIPELINING", | ||
305 | "250-SIZE 10240000", | ||
306 | "250-ENHANCEDSTATUSCODES", | ||
307 | "250-8BITMIME", | ||
308 | "250 DSN") | ||
309 | for _, ext := range extensions { | ||
310 | fmt.Fprintf(s.bw, "%s\r\n", ext) | ||
311 | } | ||
312 | s.bw.Flush() | ||
313 | } | ||
314 | |||
315 | func (s *session) handleAuth(line cmdLine) { | ||
316 | ah := s.srv.OnAuthentication | ||
317 | if ah == nil { | ||
318 | log.Printf("smtp: Server.OnAuthentication is nil; rejecting AUTH") | ||
319 | s.sendlinef("502 5.5.2 Error: command not recognized") | ||
320 | return | ||
321 | } | ||
322 | |||
323 | if ah != nil && s.IsAuthenticated() { | ||
324 | log.Printf("smtp: invalid second AUTH on connection") | ||
325 | s.sendlinef("503 5.5.1 Error: unable to AUTH more than once") | ||
326 | return | ||
327 | } | ||
328 | |||
329 | p := strings.Split(line.Arg(), " ") | ||
330 | if len(p) != 2 && p[0] != "PLAIN" { | ||
331 | log.Printf("smtp: invalid AUTH argument format") | ||
332 | s.sendlinef("502 5.5.2 Error: command not recognized") | ||
333 | return | ||
334 | } | ||
335 | |||
336 | c, err := base64.StdEncoding.DecodeString(p[1]) | ||
337 | if err != nil { | ||
338 | log.Printf("smtp: error decoding credentials %v", err) | ||
339 | s.sendlinef("535 5.7.8 Authentication credentials invalid") | ||
340 | return | ||
341 | } | ||
342 | |||
343 | cp := bytes.Split(c, []byte{0}) | ||
344 | if len(cp) != 3 { | ||
345 | log.Printf("smtp: invalid decoded username and password") | ||
346 | s.sendlinef("535 5.7.8 Authentication credentials invalid") | ||
347 | return | ||
348 | } | ||
349 | |||
350 | user := string(cp[1]) | ||
351 | if err := ah(s, user, string(cp[2])); err != nil { | ||
352 | log.Printf("smtp: authentication failed: %v", err) | ||
353 | s.sendlinef("535 5.7.8 Authentication credentials invalid") | ||
354 | return | ||
355 | } | ||
356 | |||
357 | s.authenticated = user | ||
358 | log.Printf("smtp: successfully authenticated %s", user) | ||
359 | s.sendlinef("235 2.7.0 Authentication Succeeded") | ||
360 | } | ||
361 | |||
362 | func (s *session) validateAuth() bool { | ||
363 | if s.srv.OnAuthentication == nil { | ||
364 | return true | ||
365 | } | ||
366 | if s.srv.OnAuthentication != nil && !s.IsAuthenticated() { | ||
367 | log.Printf("smtp: authentication required but session not authenticated; rejecting") | ||
368 | s.sendlinef("530 5.7.0 Authentication required") | ||
369 | return false | ||
370 | } | ||
371 | return true | ||
372 | } | ||
373 | |||
374 | func (s *session) handleMailFrom(email string) { | ||
375 | // TODO: 4.1.1.11. If the server SMTP does not recognize or | ||
376 | // cannot implement one or more of the parameters associated | ||
377 | // qwith a particular MAIL FROM or RCPT TO command, it will return | ||
378 | // code 555. | ||
379 | |||
380 | if s.env != nil { | ||
381 | s.sendlinef("503 5.5.1 Error: nested MAIL command") | ||
382 | return | ||
383 | } | ||
384 | cb := s.srv.OnNewMail | ||
385 | if cb == nil { | ||
386 | log.Printf("smtp: Server.OnNewMail is nil; rejecting MAIL FROM") | ||
387 | s.sendf("451 Server.OnNewMail not configured\r\n") | ||
388 | return | ||
389 | } | ||
390 | s.env = nil | ||
391 | env, err := cb(s, addrString(email)) | ||
392 | if err != nil { | ||
393 | log.Printf("rejecting MAIL FROM %q: %v", email, err) | ||
394 | s.sendf("451 denied\r\n") | ||
395 | |||
396 | s.bw.Flush() | ||
397 | time.Sleep(100 * time.Millisecond) | ||
398 | s.rwc.Close() | ||
399 | return | ||
400 | } | ||
401 | s.env = env | ||
402 | s.sendlinef("250 2.1.0 Ok") | ||
403 | } | ||
404 | |||
405 | func (s *session) handleRcpt(line cmdLine) { | ||
406 | // TODO: 4.1.1.11. If the server SMTP does not recognize or | ||
407 | // cannot implement one or more of the parameters associated | ||
408 | // qwith a particular MAIL FROM or RCPT TO command, it will return | ||
409 | // code 555. | ||
410 | |||
411 | if s.env == nil { | ||
412 | s.sendlinef("503 5.5.1 Error: need MAIL command") | ||
413 | return | ||
414 | } | ||
415 | arg := line.Arg() // "To:<foo@bar.com>" | ||
416 | m := rcptToRE.FindStringSubmatch(arg) | ||
417 | if m == nil { | ||
418 | log.Printf("bad RCPT address: %q", arg) | ||
419 | s.sendlinef("501 5.1.7 Bad sender address syntax") | ||
420 | return | ||
421 | } | ||
422 | err := s.env.AddRecipient(addrString(m[1])) | ||
423 | if err != nil { | ||
424 | s.sendSMTPErrorOrLinef(err, "550 bad recipient") | ||
425 | return | ||
426 | } | ||
427 | s.sendlinef("250 2.1.0 Ok") | ||
428 | } | ||
429 | |||
430 | func (s *session) handleData() { | ||
431 | if s.env == nil { | ||
432 | s.sendlinef("503 5.5.1 Error: need RCPT command") | ||
433 | return | ||
434 | } | ||
435 | if err := s.env.BeginData(); err != nil { | ||
436 | s.handleError(err) | ||
437 | return | ||
438 | } | ||
439 | s.sendlinef("354 Go ahead") | ||
440 | for { | ||
441 | sl, err := s.br.ReadSlice('\n') | ||
442 | if err != nil { | ||
443 | s.errorf("read error: %v", err) | ||
444 | return | ||
445 | } | ||
446 | if bytes.Equal(sl, []byte(".\r\n")) { | ||
447 | break | ||
448 | } | ||
449 | if sl[0] == '.' { | ||
450 | sl = sl[1:] | ||
451 | } | ||
452 | err = s.env.Write(sl) | ||
453 | if err != nil { | ||
454 | s.sendSMTPErrorOrLinef(err, "550 ??? failed") | ||
455 | return | ||
456 | } | ||
457 | } | ||
458 | if err := s.env.Close(); err != nil { | ||
459 | s.handleError(err) | ||
460 | return | ||
461 | } | ||
462 | s.sendlinef("250 2.0.0 Ok: queued") | ||
463 | s.env = nil | ||
464 | } | ||
465 | |||
466 | func (s *session) handleError(err error) { | ||
467 | if se, ok := err.(SMTPError); ok { | ||
468 | s.sendlinef("%s", se) | ||
469 | return | ||
470 | } | ||
471 | log.Printf("Error: %s", err) | ||
472 | s.env = nil | ||
473 | } | ||
474 | |||
475 | type addrString string | ||
476 | |||
477 | func (a addrString) Email() string { | ||
478 | return string(a) | ||
479 | } | ||
480 | |||
481 | func (a addrString) Hostname() string { | ||
482 | e := string(a) | ||
483 | if idx := strings.Index(e, "@"); idx != -1 { | ||
484 | return strings.ToLower(e[idx+1:]) | ||
485 | } | ||
486 | return "" | ||
487 | } | ||
488 | |||
489 | type cmdLine string | ||
490 | |||
491 | func (cl cmdLine) checkValid() error { | ||
492 | if !strings.HasSuffix(string(cl), "\r\n") { | ||
493 | return errors.New(`line doesn't end in \r\n`) | ||
494 | } | ||
495 | // Check for verbs defined not to have an argument | ||
496 | // (RFC 5321 s4.1.1) | ||
497 | switch cl.Verb() { | ||
498 | case "RSET", "DATA", "QUIT": | ||
499 | if cl.Arg() != "" { | ||
500 | return errors.New("unexpected argument") | ||
501 | } | ||
502 | } | ||
503 | return nil | ||
504 | } | ||
505 | |||
506 | func (cl cmdLine) Verb() string { | ||
507 | s := string(cl) | ||
508 | if idx := strings.Index(s, " "); idx != -1 { | ||
509 | return strings.ToUpper(s[:idx]) | ||
510 | } | ||
511 | return strings.ToUpper(s[:len(s)-2]) | ||
512 | } | ||
513 | |||
514 | func (cl cmdLine) Arg() string { | ||
515 | s := string(cl) | ||
516 | if idx := strings.Index(s, " "); idx != -1 { | ||
517 | return strings.TrimRightFunc(s[idx+1:len(s)-2], unicode.IsSpace) | ||
518 | } | ||
519 | return "" | ||
520 | } | ||
521 | |||
522 | func (cl cmdLine) String() string { | ||
523 | return string(cl) | ||
524 | } | ||
525 | |||
526 | type SMTPError string | ||
527 | |||
528 | func (e SMTPError) Error() string { | ||
529 | return string(e) | ||
530 | } | ||