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