aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2022-12-06 19:56:41 -0800
committerMike Crute <mike@crute.us>2022-12-06 19:56:41 -0800
commit3bbe51cbec9a974339f3166356c20bbdc8d2d1c8 (patch)
tree6d262bc2935c7142380aaacb21ee4e8185c49ede
parentadb1ae5d378ecb2619b6ccba57fa142b0a60f075 (diff)
downloadses-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.go2
-rw-r--r--smtpd/smtpd.go530
2 files changed, 531 insertions, 1 deletions
diff --git a/main.go b/main.go
index d633368..a1a2625 100644
--- a/main.go
+++ b/main.go
@@ -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
14const ( 14const (
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.
9package smtpd
10
11// TODO:
12// -- send 421 to connected clients on graceful server shutdown (s3.8)
13//
14
15import (
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
31var (
32 rcptToRE = regexp.MustCompile(`[Tt][Oo]:\s*<(.+)>`)
33 mailFromRE = regexp.MustCompile(`[Ff][Rr][Oo][Mm]:\s*<(.*)>`)
34)
35
36// Server is an SMTP server.
37type 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
57type 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.
64type Connection interface {
65 IsAuthenticated() bool
66 Addr() net.Addr
67 Close() error // to force-close a connection
68}
69
70type Envelope interface {
71 AddRecipient(rcpt MailAddress) error
72 BeginData() error
73 Write(line []byte) error
74 Close() error
75}
76
77type BasicEnvelope struct {
78 rcpts []MailAddress
79}
80
81func (e *BasicEnvelope) AddRecipient(rcpt MailAddress) error {
82 e.rcpts = append(e.rcpts, rcpt)
83 return nil
84}
85
86func (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
93func (e *BasicEnvelope) Write(line []byte) error {
94 log.Printf("Line: %q", string(line))
95 return nil
96}
97
98func (e *BasicEnvelope) Close() error {
99 return nil
100}
101
102func (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.
116func (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
128func (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
148type 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
161func (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
171func (s *session) IsAuthenticated() bool {
172 return s.authenticated != ""
173}
174
175func (s *session) errorf(format string, args ...interface{}) {
176 log.Printf("Client error: "+format, args...)
177}
178
179func (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
187func (s *session) sendlinef(format string, args ...interface{}) {
188 s.sendf(format+"\r\n", args...)
189}
190
191func (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
199func (s *session) Addr() net.Addr {
200 return s.rwc.RemoteAddr()
201}
202
203func (s *session) Close() error { return s.rwc.Close() }
204
205func (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
281func (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
293func (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
315func (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
362func (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
374func (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
405func (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
430func (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
466func (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
475type addrString string
476
477func (a addrString) Email() string {
478 return string(a)
479}
480
481func (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
489type cmdLine string
490
491func (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
506func (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
514func (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
522func (cl cmdLine) String() string {
523 return string(cl)
524}
525
526type SMTPError string
527
528func (e SMTPError) Error() string {
529 return string(e)
530}