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
25 stateNew state = iota // Before EHLO.
34 func (d delivery) String() string {
43 panic("Unknown delivery")
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.
52 type connection struct {
61 tls *tls.ConnectionState
65 // The authcid from a PLAIN SASL login. Non-empty iff tls is non-nil and
66 // doAUTH() succeeded.
73 // For deliverOutbound, replaces the From and Reply-To values.
77 mailFrom *mail.Address
81 func AcceptConnection(netConn net.Conn, server Server, log *zap.Logger) {
84 tp: textproto.NewConn(netConn),
86 remoteAddr: netConn.RemoteAddr(),
87 log: log.With(zap.Stringer("client", netConn.RemoteAddr())),
91 conn.log.Info("accepted connection")
92 conn.writeReply(220, fmt.Sprintf("%s ESMTP [%s] (mailpopbox)",
93 server.Name(), netConn.LocalAddr()))
97 conn.line, err = conn.tp.ReadLine()
99 conn.log.Error("ReadLine()", zap.Error(err))
104 lineForLog := conn.line
105 const authPlain = "AUTH PLAIN "
106 if strings.HasPrefix(conn.line, authPlain) {
107 lineForLog = authPlain + "[redacted]"
109 conn.log.Info("ReadLine()", zap.String("line", lineForLog))
112 if _, err = fmt.Sscanf(conn.line, "%s", &cmd); err != nil {
113 conn.reply(ReplyBadSyntax)
117 switch strings.ToUpper(cmd) {
119 conn.writeReply(221, "Goodbye")
141 conn.writeReply(252, "I'll do my best")
143 conn.writeReply(550, "access denied")
147 conn.writeReply(250, "https://tools.ietf.org/html/rfc5321")
149 conn.writeReply(500, "unrecognized command")
154 func (conn *connection) reply(reply ReplyLine) error {
155 return conn.writeReply(reply.Code, reply.Message)
158 func (conn *connection) writeReply(code int, msg string) error {
159 conn.log.Info("writeReply", zap.Int("code", code))
162 err = conn.tp.PrintfLine("%d %s", code, msg)
164 err = conn.tp.PrintfLine("%d", code)
167 conn.log.Error("writeReply",
168 zap.Int("code", code),
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
181 if strings.ToUpper(command) != strings.ToUpper(conn.line[:len(command)]) {
182 return "", ReplyLine{500, "unrecognized command"}
184 params := conn.line[len(command):]
185 idx := strings.Index(params, ">")
187 return "", ReplyBadSyntax
189 return strings.ToLower(params[:idx+1]), ReplyOK
192 func (conn *connection) doEHLO() {
196 _, err := fmt.Sscanf(conn.line, "%s %s", &cmd, &conn.ehlo)
198 conn.reply(ReplyBadSyntax)
203 conn.writeReply(250, fmt.Sprintf("Hello %s [%s]", conn.ehlo, conn.remoteAddr))
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")
210 conn.tp.PrintfLine("250-AUTH PLAIN")
212 conn.tp.PrintfLine("250 SIZE %d", 40960000)
215 conn.log.Info("doEHLO()", zap.String("ehlo", conn.ehlo))
217 conn.state = stateInitial
220 func (conn *connection) doSTARTTLS() {
221 if conn.state != stateInitial {
222 conn.reply(ReplyBadSequence)
226 tlsConfig := conn.server.TLSConfig()
227 if !conn.esmtp || tlsConfig == nil {
228 conn.writeReply(500, "unrecognized command")
232 conn.log.Info("doSTARTTLS()")
233 conn.writeReply(220, "initiate TLS connection")
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))
242 conn.tp = textproto.NewConn(tlsConn)
243 conn.state = stateNew
245 connState := tlsConn.ConnectionState()
246 conn.tls = &connState
248 conn.log.Info("TLS connection done", zap.String("state", conn.getTransportString()))
251 func (conn *connection) doAUTH() {
252 if conn.state != stateInitial || conn.tls == nil {
253 conn.reply(ReplyBadSequence)
257 if conn.authc != "" {
258 conn.writeReply(503, "already authenticated")
262 var cmd, authType, authString string
263 n, err := fmt.Sscanf(conn.line, "%s %s %s", &cmd, &authType, &authString)
265 conn.reply(ReplyBadSyntax)
269 if authType != "PLAIN" {
270 conn.writeReply(504, "unrecognized auth type")
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)
280 conn.log.Info("doAUTH()")
282 if authString == "" {
283 conn.writeReply(334, " ")
285 authString, err = conn.tp.ReadLine()
287 conn.log.Error("failed to read auth line", zap.Error(err))
288 conn.reply(ReplyBadSyntax)
293 authBytes, err := base64.StdEncoding.DecodeString(authString)
295 conn.reply(ReplyBadSyntax)
299 authParts := strings.Split(string(authBytes), "\x00")
300 if len(authParts) != 3 {
301 conn.log.Error("bad auth line syntax")
302 conn.reply(ReplyBadSyntax)
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")
312 conn.log.Info("authenticated", zap.String("authz", authParts[0]), zap.String("authc", authParts[1]))
313 conn.authc = authParts[1]
314 conn.reply(ReplyAuthOK)
317 func (conn *connection) doMAIL() {
318 if conn.state != stateInitial {
319 conn.reply(ReplyBadSequence)
323 mailFrom, reply := conn.parsePath("MAIL FROM:")
324 if reply != ReplyOK {
330 conn.mailFrom, err = mail.ParseAddress(mailFrom)
331 if err != nil || conn.mailFrom == nil {
332 conn.reply(ReplyBadSyntax)
336 if conn.server.VerifyAddress(*conn.mailFrom) == ReplyOK {
337 if DomainForAddress(*conn.mailFrom) != DomainForAddressString(conn.authc) {
338 conn.writeReply(550, "not authenticated")
341 conn.delivery = deliverOutbound
343 conn.delivery = deliverInbound
346 conn.log.Info("doMAIL()", zap.String("address", conn.mailFrom.Address))
348 conn.state = stateMail
352 func (conn *connection) doRCPT() {
353 if conn.state != stateMail && conn.state != stateRecipient {
354 conn.reply(ReplyBadSequence)
358 rcptTo, reply := conn.parsePath("RCPT TO:")
359 if reply != ReplyOK {
364 address, err := mail.ParseAddress(rcptTo)
366 conn.reply(ReplyBadSyntax)
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))
378 conn.log.Info("doRCPT()",
379 zap.String("address", address.Address),
380 zap.String("delivery", conn.delivery.String()))
382 conn.rcptTo = append(conn.rcptTo, *address)
384 conn.state = stateRecipient
388 func (conn *connection) doDATA() {
389 if conn.state != stateRecipient {
390 conn.reply(ReplyBadSequence)
394 conn.writeReply(354, "Start mail input; end with <CRLF>.<CRLF>")
395 conn.log.Info("doDATA()")
397 data, err := conn.tp.ReadDotBytes()
399 conn.log.Error("failed to ReadDotBytes()",
401 zap.String("bytes", fmt.Sprintf("%x", data)))
402 conn.writeReply(552, "transaction failed")
406 received := time.Now()
408 RemoteAddr: conn.remoteAddr,
410 MailFrom: *conn.mailFrom,
413 ID: generateEnvelopeId("m", received),
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()))
423 trace := conn.getReceivedInfo(env)
425 env.Data = append(trace, env.Data...)
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))
433 } else if conn.delivery == deliverOutbound {
434 conn.server.RelayMessage(env, conn.authc)
437 conn.state = stateInitial
442 func (conn *connection) getReceivedInfo(envelope Envelope) []byte {
443 base := fmt.Sprintf("Received: from %s (%s)\r\n ", conn.ehlo, lookupRemoteHost(conn.remoteAddr))
452 base += fmt.Sprintf("by %s (mailpopbox) with %s id %s\r\n ", conn.server.Name(), with, envelope.ID)
454 if len(envelope.RcptTo) > 0 {
455 base += fmt.Sprintf("for <%s>\r\n ", envelope.RcptTo[0].Address)
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)
465 func (conn *connection) getTransportString() string {
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",
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",
507 version := versions[state.Version]
508 cipher := ciphers[state.CipherSuite]
511 version = fmt.Sprintf("%x", state.Version)
514 cipher = fmt.Sprintf("%x", state.CipherSuite)
518 if state.ServerName != "" {
519 name = fmt.Sprintf(" name=%s", state.ServerName)
522 return fmt.Sprintf("%s cipher=%s%s", version, cipher, name)
525 func (conn *connection) doRSET() {
526 conn.log.Info("doRSET()")
527 conn.state = stateInitial
532 func (conn *connection) resetBuffers() {
533 conn.delivery = deliverUnknown
536 conn.rcptTo = make([]mail.Address, 0)