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