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