Add support for outbound SMTP delivery.
[mailpopbox.git] / smtp / conn.go
1 package smtp
2
3 import (
4 "bytes"
5 "crypto/rand"
6 "crypto/tls"
7 "encoding/base64"
8 "fmt"
9 "net"
10 "net/mail"
11 "net/textproto"
12 "strings"
13 "time"
14
15 "github.com/uber-go/zap"
16 )
17
18 type state int
19
20 const (
21 stateNew state = iota // Before EHLO.
22 stateInitial
23 stateMail
24 stateRecipient
25 stateData
26 )
27
28 type delivery int
29
30 func (d delivery) String() string {
31 switch d {
32 case deliverUnknown:
33 return "unknown"
34 case deliverInbound:
35 return "inbound"
36 case deliverOutbound:
37 return "outbound"
38 }
39 panic("Unknown delivery")
40 }
41
42 const (
43 deliverUnknown delivery = iota
44 deliverInbound // Mail is not from one of this server's domains.
45 deliverOutbound // Mail IS from one of this server's domains.
46 )
47
48 type connection struct {
49 server Server
50
51 tp *textproto.Conn
52
53 nc net.Conn
54 remoteAddr net.Addr
55
56 esmtp bool
57 tls *tls.ConnectionState
58
59 // The authcid from a PLAIN SASL login. Non-empty iff tls is non-nil and
60 // doAUTH() succeeded.
61 authc string
62
63 log zap.Logger
64
65 state
66 line string
67
68 delivery
69 // For deliverOutbound, replaces the From and Reply-To values.
70 sendAs *mail.Address
71
72 ehlo string
73 mailFrom *mail.Address
74 rcptTo []mail.Address
75 }
76
77 func AcceptConnection(netConn net.Conn, server Server, log zap.Logger) {
78 conn := connection{
79 server: server,
80 tp: textproto.NewConn(netConn),
81 nc: netConn,
82 remoteAddr: netConn.RemoteAddr(),
83 log: log.With(zap.Stringer("client", netConn.RemoteAddr())),
84 state: stateNew,
85 }
86
87 conn.log.Info("accepted connection")
88 conn.writeReply(220, fmt.Sprintf("%s ESMTP [%s] (mailpopbox)",
89 server.Name(), netConn.LocalAddr()))
90
91 for {
92 var err error
93 conn.line, err = conn.tp.ReadLine()
94 if err != nil {
95 conn.log.Error("ReadLine()", zap.Error(err))
96 conn.tp.Close()
97 return
98 }
99
100 conn.log.Info("ReadLine()", zap.String("line", conn.line))
101
102 var cmd string
103 if _, err = fmt.Sscanf(conn.line, "%s", &cmd); err != nil {
104 conn.reply(ReplyBadSyntax)
105 continue
106 }
107
108 switch strings.ToUpper(cmd) {
109 case "QUIT":
110 conn.writeReply(221, "Goodbye")
111 conn.tp.Close()
112 return
113 case "HELO":
114 conn.esmtp = false
115 fallthrough
116 case "EHLO":
117 conn.esmtp = true
118 conn.doEHLO()
119 case "STARTTLS":
120 conn.doSTARTTLS()
121 case "AUTH":
122 conn.doAUTH()
123 case "MAIL":
124 conn.doMAIL()
125 case "RCPT":
126 conn.doRCPT()
127 case "DATA":
128 conn.doDATA()
129 case "RSET":
130 conn.doRSET()
131 case "VRFY":
132 conn.writeReply(252, "I'll do my best")
133 case "EXPN":
134 conn.writeReply(550, "access denied")
135 case "NOOP":
136 conn.reply(ReplyOK)
137 case "HELP":
138 conn.writeReply(250, "https://tools.ietf.org/html/rfc5321")
139 default:
140 conn.writeReply(500, "unrecognized command")
141 }
142 }
143 }
144
145 func (conn *connection) reply(reply ReplyLine) error {
146 return conn.writeReply(reply.Code, reply.Message)
147 }
148
149 func (conn *connection) writeReply(code int, msg string) error {
150 conn.log.Info("writeReply", zap.Int("code", code))
151 var err error
152 if len(msg) > 0 {
153 err = conn.tp.PrintfLine("%d %s", code, msg)
154 } else {
155 err = conn.tp.PrintfLine("%d", code)
156 }
157 if err != nil {
158 conn.log.Error("writeReply",
159 zap.Int("code", code),
160 zap.Error(err))
161 }
162 return err
163 }
164
165 // parsePath parses out either a forward-, reverse-, or return-path from the
166 // current connection line. Returns a (valid-path, ReplyOK) if it was
167 // successfully parsed.
168 func (conn *connection) parsePath(command string) (string, ReplyLine) {
169 if len(conn.line) < len(command) {
170 return "", ReplyBadSyntax
171 }
172 if strings.ToUpper(command) != strings.ToUpper(conn.line[:len(command)]) {
173 return "", ReplyLine{500, "unrecognized command"}
174 }
175 params := conn.line[len(command):]
176 idx := strings.Index(params, ">")
177 if idx == -1 {
178 return "", ReplyBadSyntax
179 }
180 return strings.ToLower(params[:idx+1]), ReplyOK
181 }
182
183 func (conn *connection) doEHLO() {
184 conn.resetBuffers()
185
186 var cmd string
187 _, err := fmt.Sscanf(conn.line, "%s %s", &cmd, &conn.ehlo)
188 if err != nil {
189 conn.reply(ReplyBadSyntax)
190 return
191 }
192
193 if cmd == "HELO" {
194 conn.writeReply(250, fmt.Sprintf("Hello %s [%s]", conn.ehlo, conn.remoteAddr))
195 } else {
196 conn.tp.PrintfLine("250-Hello %s [%s]", conn.ehlo, conn.remoteAddr)
197 if conn.server.TLSConfig() != nil && conn.tls == nil {
198 conn.tp.PrintfLine("250-STARTTLS")
199 }
200 if conn.tls != nil {
201 conn.tp.PrintfLine("250-AUTH PLAIN")
202 }
203 conn.tp.PrintfLine("250 SIZE %d", 40960000)
204 }
205
206 conn.log.Info("doEHLO()", zap.String("ehlo", conn.ehlo))
207
208 conn.state = stateInitial
209 }
210
211 func (conn *connection) doSTARTTLS() {
212 if conn.state != stateInitial {
213 conn.reply(ReplyBadSequence)
214 return
215 }
216
217 tlsConfig := conn.server.TLSConfig()
218 if !conn.esmtp || tlsConfig == nil {
219 conn.writeReply(500, "unrecognized command")
220 return
221 }
222
223 conn.log.Info("doSTARTTLS()")
224 conn.writeReply(220, "initiate TLS connection")
225
226 tlsConn := tls.Server(conn.nc, tlsConfig)
227 if err := tlsConn.Handshake(); err != nil {
228 conn.log.Error("failed to do TLS handshake", zap.Error(err))
229 return
230 }
231
232 conn.nc = tlsConn
233 conn.tp = textproto.NewConn(tlsConn)
234 conn.state = stateNew
235
236 connState := tlsConn.ConnectionState()
237 conn.tls = &connState
238
239 conn.log.Info("TLS connection done", zap.String("state", conn.getTransportString()))
240 }
241
242 func (conn *connection) doAUTH() {
243 if conn.state != stateInitial || conn.tls == nil {
244 conn.reply(ReplyBadSequence)
245 return
246 }
247
248 if conn.authc != "" {
249 conn.writeReply(503, "already authenticated")
250 return
251 }
252
253 var cmd, authType string
254 _, err := fmt.Sscanf(conn.line, "%s %s", &cmd, &authType)
255 if err != nil {
256 conn.reply(ReplyBadSyntax)
257 return
258 }
259
260 if authType != "PLAIN" {
261 conn.writeReply(504, "unrecognized auth type")
262 return
263 }
264
265 conn.log.Info("doAUTH()")
266
267 conn.writeReply(334, " ")
268
269 authLine, err := conn.tp.ReadLine()
270 if err != nil {
271 conn.log.Error("failed to read auth line", zap.Error(err))
272 conn.reply(ReplyBadSyntax)
273 return
274 }
275
276 authBytes, err := base64.StdEncoding.DecodeString(authLine)
277 if err != nil {
278 conn.reply(ReplyBadSyntax)
279 return
280 }
281
282 authParts := strings.Split(string(authBytes), "\x00")
283 if len(authParts) != 3 {
284 conn.log.Error("bad auth line syntax")
285 conn.reply(ReplyBadSyntax)
286 return
287 }
288
289 if !conn.server.Authenticate(authParts[0], authParts[1], authParts[2]) {
290 conn.log.Error("failed to authenticate", zap.String("authc", authParts[1]))
291 conn.writeReply(535, "invalid credentials")
292 return
293 }
294
295 conn.log.Info("authenticated", zap.String("authz", authParts[0]), zap.String("authc", authParts[1]))
296 conn.authc = authParts[1]
297 conn.reply(ReplyOK)
298 }
299
300 func (conn *connection) doMAIL() {
301 if conn.state != stateInitial {
302 conn.reply(ReplyBadSequence)
303 return
304 }
305
306 mailFrom, reply := conn.parsePath("MAIL FROM:")
307 if reply != ReplyOK {
308 conn.reply(reply)
309 return
310 }
311
312 var err error
313 conn.mailFrom, err = mail.ParseAddress(mailFrom)
314 if err != nil || conn.mailFrom == nil {
315 conn.reply(ReplyBadSyntax)
316 return
317 }
318
319 if conn.server.VerifyAddress(*conn.mailFrom) == ReplyOK {
320 // Message is being sent from a domain that this is an MTA for. Ultimate
321 // handling of the outbound message requires knowing the recipient.
322 domain := DomainForAddress(*conn.mailFrom)
323 // TODO: better way to authenticate this?
324 if !strings.HasSuffix(conn.authc, "@"+domain) {
325 conn.writeReply(550, "not authenticated")
326 return
327 }
328 conn.delivery = deliverOutbound
329 } else {
330 conn.delivery = deliverInbound
331 }
332
333 conn.log.Info("doMAIL()", zap.String("address", conn.mailFrom.Address))
334
335 conn.state = stateMail
336 conn.reply(ReplyOK)
337 }
338
339 func (conn *connection) doRCPT() {
340 if conn.state != stateMail && conn.state != stateRecipient {
341 conn.reply(ReplyBadSequence)
342 return
343 }
344
345 rcptTo, reply := conn.parsePath("RCPT TO:")
346 if reply != ReplyOK {
347 conn.reply(reply)
348 return
349 }
350
351 address, err := mail.ParseAddress(rcptTo)
352 if err != nil {
353 conn.reply(ReplyBadSyntax)
354 return
355 }
356
357 if reply := conn.server.VerifyAddress(*address); reply == ReplyOK {
358 // Message is addressed to this server. If it's outbound, only support
359 // the special send-as addressing.
360 if conn.delivery == deliverOutbound {
361 if !strings.HasPrefix(address.Address, SendAsAddress) {
362 conn.log.Error("internal relay addressing not supported",
363 zap.String("address", address.Address))
364 conn.reply(ReplyBadMailbox)
365 return
366 }
367 address.Address = strings.TrimPrefix(address.Address, SendAsAddress)
368 if DomainForAddress(*address) != DomainForAddressString(conn.authc) {
369 conn.log.Error("not authenticated for send-as",
370 zap.String("address", address.Address),
371 zap.String("authc", conn.authc))
372 conn.reply(ReplyBadMailbox)
373 return
374 }
375 if conn.sendAs != nil {
376 conn.log.Error("sendAs already specified",
377 zap.String("address", address.Address),
378 zap.String("sendAs", conn.sendAs.Address))
379 conn.reply(ReplyMailboxUnallowed)
380 return
381 }
382 conn.log.Info("doRCPT()",
383 zap.String("sendAs", address.Address))
384 conn.sendAs = address
385 conn.state = stateRecipient
386 conn.reply(ReplyOK)
387 return
388 }
389 } else {
390 // Message is not addressed to this server, so the delivery must be outbound.
391 if conn.delivery == deliverInbound {
392 conn.log.Warn("invalid address",
393 zap.String("address", address.Address),
394 zap.Stringer("reply", reply))
395 conn.reply(reply)
396 return
397 }
398 }
399
400 conn.log.Info("doRCPT()",
401 zap.String("address", address.Address),
402 zap.String("delivery", conn.delivery.String()))
403
404 conn.rcptTo = append(conn.rcptTo, *address)
405
406 conn.state = stateRecipient
407 conn.reply(ReplyOK)
408 }
409
410 func (conn *connection) doDATA() {
411 if conn.state != stateRecipient {
412 conn.reply(ReplyBadSequence)
413 return
414 }
415
416 conn.writeReply(354, "Start mail input; end with <CRLF>.<CRLF>")
417 conn.log.Info("doDATA()")
418
419 data, err := conn.tp.ReadDotBytes()
420 if err != nil {
421 conn.log.Error("failed to ReadDotBytes()",
422 zap.Error(err),
423 zap.String("bytes", fmt.Sprintf("%x", data)))
424 conn.writeReply(552, "transaction failed")
425 return
426 }
427
428 conn.handleSendAs(&data)
429
430 received := time.Now()
431 env := Envelope{
432 RemoteAddr: conn.remoteAddr,
433 EHLO: conn.ehlo,
434 MailFrom: *conn.mailFrom,
435 RcptTo: conn.rcptTo,
436 Received: received,
437 ID: conn.envelopeID(received),
438 }
439
440 conn.log.Info("received message",
441 zap.Int("bytes", len(data)),
442 zap.Time("date", received),
443 zap.String("id", env.ID),
444 zap.String("delivery", conn.delivery.String()))
445
446 trace := conn.getReceivedInfo(env)
447
448 env.Data = append(trace, data...)
449
450 if conn.delivery == deliverInbound {
451 if reply := conn.server.OnMessageDelivered(env); reply != nil {
452 conn.log.Warn("message was rejected", zap.String("id", env.ID))
453 conn.reply(*reply)
454 return
455 }
456 } else if conn.delivery == deliverOutbound {
457 conn.server.RelayMessage(env)
458 }
459
460 conn.state = stateInitial
461 conn.resetBuffers()
462 conn.reply(ReplyOK)
463 }
464
465 func (conn *connection) handleSendAs(data *[]byte) {
466 if conn.delivery != deliverOutbound || conn.sendAs == nil {
467 return
468 }
469
470 conn.mailFrom = conn.sendAs
471
472 // Find the separator between the message header and body.
473 headerIdx := bytes.Index(*data, []byte("\n\n"))
474 if headerIdx == -1 {
475 conn.log.Error("send-as: could not find headers index")
476 return
477 }
478
479 fromPrefix := []byte("From: ")
480 fromIdx := bytes.Index(*data, fromPrefix)
481 if fromIdx == -1 || fromIdx >= headerIdx {
482 conn.log.Error("send-as: could not find From header")
483 return
484 }
485 if fromIdx != 0 {
486 if (*data)[fromIdx-1] != '\n' {
487 conn.log.Error("send-as: could not find From header")
488 return
489 }
490 }
491
492 fromEndIdx := bytes.IndexByte((*data)[fromIdx:], '\n')
493 if fromIdx == -1 {
494 conn.log.Error("send-as: could not find end of From header")
495 return
496 }
497 fromEndIdx += fromIdx
498
499 newData := (*data)[:fromIdx]
500 newData = append(newData, fromPrefix...)
501 newData = append(newData, []byte(conn.sendAs.String())...)
502 newData = append(newData, (*data)[fromEndIdx:]...)
503
504 *data = newData
505 }
506
507 func (conn *connection) envelopeID(t time.Time) string {
508 var idBytes [4]byte
509 rand.Read(idBytes[:])
510 return fmt.Sprintf("m.%d.%x", t.UnixNano(), idBytes)
511 }
512
513 func (conn *connection) getReceivedInfo(envelope Envelope) []byte {
514 rhost, _, err := net.SplitHostPort(conn.remoteAddr.String())
515 if err != nil {
516 rhost = conn.remoteAddr.String()
517 }
518
519 rhosts, err := net.LookupAddr(rhost)
520 if err == nil {
521 rhost = fmt.Sprintf("%s [%s]", rhosts[0], rhost)
522 }
523
524 base := fmt.Sprintf("Received: from %s (%s)\r\n ", conn.ehlo, rhost)
525
526 with := "SMTP"
527 if conn.esmtp {
528 with = "E" + with
529 }
530 if conn.tls != nil {
531 with += "S"
532 }
533 base += fmt.Sprintf("by %s (mailpopbox) with %s id %s\r\n ", conn.server.Name(), with, envelope.ID)
534
535 base += fmt.Sprintf("for <%s>\r\n ", envelope.RcptTo[0].Address)
536
537 transport := conn.getTransportString()
538 date := envelope.Received.Format(time.RFC1123Z) // Same as RFC 5322 ยง 3.3
539 base += fmt.Sprintf("(using %s);\r\n %s\r\n", transport, date)
540
541 return []byte(base)
542 }
543
544 func (conn *connection) getTransportString() string {
545 if conn.tls == nil {
546 return "PLAINTEXT"
547 }
548
549 ciphers := map[uint16]string{
550 tls.TLS_RSA_WITH_RC4_128_SHA: "TLS_RSA_WITH_RC4_128_SHA",
551 tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA: "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
552 tls.TLS_RSA_WITH_AES_128_CBC_SHA: "TLS_RSA_WITH_AES_128_CBC_SHA",
553 tls.TLS_RSA_WITH_AES_256_CBC_SHA: "TLS_RSA_WITH_AES_256_CBC_SHA",
554 tls.TLS_RSA_WITH_AES_128_GCM_SHA256: "TLS_RSA_WITH_AES_128_GCM_SHA256",
555 tls.TLS_RSA_WITH_AES_256_GCM_SHA384: "TLS_RSA_WITH_AES_256_GCM_SHA384",
556 tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
557 tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
558 tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
559 tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA: "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
560 tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
561 tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
562 tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
563 tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
564 tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
565 tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
566 tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
567 }
568 versions := map[uint16]string{
569 tls.VersionSSL30: "SSLv3.0",
570 tls.VersionTLS10: "TLSv1.0",
571 tls.VersionTLS11: "TLSv1.1",
572 tls.VersionTLS12: "TLSv1.2",
573 }
574
575 state := conn.tls
576
577 version := versions[state.Version]
578 cipher := ciphers[state.CipherSuite]
579
580 if version == "" {
581 version = fmt.Sprintf("%x", state.Version)
582 }
583 if cipher == "" {
584 cipher = fmt.Sprintf("%x", state.CipherSuite)
585 }
586
587 name := ""
588 if state.ServerName != "" {
589 name = fmt.Sprintf(" name=%s", state.ServerName)
590 }
591
592 return fmt.Sprintf("%s cipher=%s%s", version, cipher, name)
593 }
594
595 func (conn *connection) doRSET() {
596 conn.log.Info("doRSET()")
597 conn.state = stateInitial
598 conn.resetBuffers()
599 conn.reply(ReplyOK)
600 }
601
602 func (conn *connection) resetBuffers() {
603 conn.delivery = deliverUnknown
604 conn.sendAs = nil
605 conn.mailFrom = nil
606 conn.rcptTo = make([]mail.Address, 0)
607 }