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
26 stateNew state
= iota // Before EHLO.
35 func (d delivery
) String() string {
44 panic("Unknown delivery")
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.
53 type connection
struct {
62 tls
*tls
.ConnectionState
66 // The authcid from a PLAIN SASL login. Non-empty iff tls is non-nil and
67 // doAUTH() succeeded.
74 // For deliverOutbound, replaces the From and Reply-To values.
78 mailFrom
*mail
.Address
82 func AcceptConnection(netConn net
.Conn
, server Server
, log
*zap
.Logger
) {
85 tp
: textproto
.NewConn(netConn
),
87 remoteAddr
: netConn
.RemoteAddr(),
88 log
: log
.With(zap
.Stringer("client", netConn
.RemoteAddr())),
92 conn
.log
.Info("accepted connection")
93 conn
.writeReply(220, fmt
.Sprintf("%s ESMTP [%s] (mailpopbox)",
94 server
.Name(), netConn
.LocalAddr()))
98 conn
.line
, err
= conn
.tp
.ReadLine()
100 conn
.log
.Error("ReadLine()", zap
.Error(err
))
105 lineForLog
:= conn
.line
106 const authPlain
= "AUTH PLAIN "
107 if strings
.HasPrefix(conn
.line
, authPlain
) {
108 lineForLog
= authPlain
+ "[redacted]"
110 conn
.log
.Info("ReadLine()", zap
.String("line", lineForLog
))
113 if _
, err
= fmt
.Sscanf(conn
.line
, "%s", &cmd
); err
!= nil {
114 conn
.reply(ReplyBadSyntax
)
118 switch strings
.ToUpper(cmd
) {
120 conn
.writeReply(221, "Goodbye")
142 conn
.writeReply(252, "I'll do my best")
144 conn
.writeReply(550, "access denied")
148 conn
.writeReply(250, "https://tools.ietf.org/html/rfc5321")
150 conn
.writeReply(500, "unrecognized command")
155 func (conn
*connection
) reply(reply ReplyLine
) error
{
156 return conn
.writeReply(reply
.Code
, reply
.Message
)
159 func (conn
*connection
) writeReply(code
int, msg
string) error
{
160 conn
.log
.Info("writeReply", zap
.Int("code", code
))
163 err
= conn
.tp
.PrintfLine("%d %s", code
, msg
)
165 err
= conn
.tp
.PrintfLine("%d", code
)
168 conn
.log
.Error("writeReply",
169 zap
.Int("code", code
),
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
182 if strings
.ToUpper(command
) != strings
.ToUpper(conn
.line
[:len(command
)]) {
183 return "", ReplyLine
{500, "unrecognized command"}
185 params
:= conn
.line
[len(command
):]
186 idx
:= strings
.Index(params
, ">")
188 return "", ReplyBadSyntax
190 return strings
.ToLower(params
[:idx
+1]), ReplyOK
193 func (conn
*connection
) doEHLO() {
197 _
, err
:= fmt
.Sscanf(conn
.line
, "%s %s", &cmd
, &conn
.ehlo
)
199 conn
.reply(ReplyBadSyntax
)
204 conn
.writeReply(250, fmt
.Sprintf("Hello %s [%s]", conn
.ehlo
, conn
.remoteAddr
))
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")
211 conn
.tp
.PrintfLine("250-AUTH PLAIN")
213 conn
.tp
.PrintfLine("250 SIZE %d", 40960000)
216 conn
.log
.Info("doEHLO()", zap
.String("ehlo", conn
.ehlo
))
218 conn
.state
= stateInitial
221 func (conn
*connection
) doSTARTTLS() {
222 if conn
.state
!= stateInitial
{
223 conn
.reply(ReplyBadSequence
)
227 tlsConfig
:= conn
.server
.TLSConfig()
228 if !conn
.esmtp || tlsConfig
== nil {
229 conn
.writeReply(500, "unrecognized command")
233 conn
.log
.Info("doSTARTTLS()")
234 conn
.writeReply(220, "initiate TLS connection")
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
))
243 conn
.tp
= textproto
.NewConn(tlsConn
)
244 conn
.state
= stateNew
246 connState
:= tlsConn
.ConnectionState()
247 conn
.tls
= &connState
249 conn
.log
.Info("TLS connection done", zap
.String("state", conn
.getTransportString()))
252 func (conn
*connection
) doAUTH() {
253 if conn
.state
!= stateInitial || conn
.tls
== nil {
254 conn
.reply(ReplyBadSequence
)
258 if conn
.authc
!= "" {
259 conn
.writeReply(503, "already authenticated")
263 var cmd
, authType
, authString
string
264 n
, err
:= fmt
.Sscanf(conn
.line
, "%s %s %s", &cmd
, &authType
, &authString
)
266 conn
.reply(ReplyBadSyntax
)
270 if authType
!= "PLAIN" {
271 conn
.writeReply(504, "unrecognized auth type")
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
)
281 conn
.log
.Info("doAUTH()")
283 if authString
== "" {
284 conn
.writeReply(334, " ")
286 authString
, err
= conn
.tp
.ReadLine()
288 conn
.log
.Error("failed to read auth line", zap
.Error(err
))
289 conn
.reply(ReplyBadSyntax
)
294 authBytes
, err
:= base64
.StdEncoding
.DecodeString(authString
)
296 conn
.reply(ReplyBadSyntax
)
300 authParts
:= strings
.Split(string(authBytes
), "\x00")
301 if len(authParts
) != 3 {
302 conn
.log
.Error("bad auth line syntax")
303 conn
.reply(ReplyBadSyntax
)
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")
313 conn
.log
.Info("authenticated", zap
.String("authz", authParts
[0]), zap
.String("authc", authParts
[1]))
314 conn
.authc
= authParts
[1]
318 func (conn
*connection
) doMAIL() {
319 if conn
.state
!= stateInitial
{
320 conn
.reply(ReplyBadSequence
)
324 mailFrom
, reply
:= conn
.parsePath("MAIL FROM:")
325 if reply
!= ReplyOK
{
331 conn
.mailFrom
, err
= mail
.ParseAddress(mailFrom
)
332 if err
!= nil || conn
.mailFrom
== nil {
333 conn
.reply(ReplyBadSyntax
)
337 if conn
.server
.VerifyAddress(*conn
.mailFrom
) == ReplyOK
{
338 if DomainForAddress(*conn
.mailFrom
) != DomainForAddressString(conn
.authc
) {
339 conn
.writeReply(550, "not authenticated")
342 conn
.delivery
= deliverOutbound
344 conn
.delivery
= deliverInbound
347 conn
.log
.Info("doMAIL()", zap
.String("address", conn
.mailFrom
.Address
))
349 conn
.state
= stateMail
353 func (conn
*connection
) doRCPT() {
354 if conn
.state
!= stateMail
&& conn
.state
!= stateRecipient
{
355 conn
.reply(ReplyBadSequence
)
359 rcptTo
, reply
:= conn
.parsePath("RCPT TO:")
360 if reply
!= ReplyOK
{
365 address
, err
:= mail
.ParseAddress(rcptTo
)
367 conn
.reply(ReplyBadSyntax
)
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
))
379 conn
.log
.Info("doRCPT()",
380 zap
.String("address", address
.Address
),
381 zap
.String("delivery", conn
.delivery
.String()))
383 conn
.rcptTo
= append(conn
.rcptTo
, *address
)
385 conn
.state
= stateRecipient
389 func (conn
*connection
) doDATA() {
390 if conn
.state
!= stateRecipient
{
391 conn
.reply(ReplyBadSequence
)
395 conn
.writeReply(354, "Start mail input; end with <CRLF>.<CRLF>")
396 conn
.log
.Info("doDATA()")
398 data
, err
:= conn
.tp
.ReadDotBytes()
400 conn
.log
.Error("failed to ReadDotBytes()",
402 zap
.String("bytes", fmt
.Sprintf("%x", data
)))
403 conn
.writeReply(552, "transaction failed")
407 received
:= time
.Now()
409 RemoteAddr
: conn
.remoteAddr
,
411 MailFrom
: *conn
.mailFrom
,
414 ID
: generateEnvelopeId("m", received
),
418 conn
.handleSendAs(&env
)
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()))
426 trace
:= conn
.getReceivedInfo(env
)
428 env
.Data
= append(trace
, env
.Data
...)
430 if conn
.delivery
== deliverInbound
{
431 if reply
:= conn
.server
.OnMessageDelivered(env
); reply
!= nil {
432 conn
.log
.Warn("message was rejected", zap
.String("id", env
.ID
))
436 } else if conn
.delivery
== deliverOutbound
{
437 conn
.server
.RelayMessage(env
)
440 conn
.state
= stateInitial
445 func (conn
*connection
) handleSendAs(env
*Envelope
) {
446 if conn
.delivery
!= deliverOutbound
{
450 // Find the separator between the message header and body.
451 headerIdx
:= bytes
.Index(env
.Data
, []byte("\n\n"))
453 conn
.log
.Error("send-as: could not find headers index")
459 headers
:= bytes
.SplitAfter(env
.Data
[:headerIdx
], []byte("\n"))
461 var fromIdx
, subjectIdx
int
462 for i
, header
:= range headers
{
463 if bytes
.HasPrefix(header
, []byte("From:")) {
467 if bytes
.HasPrefix(header
, []byte("Subject:")) {
473 if subjectIdx
== -1 {
474 conn
.log
.Error("send-as: could not find Subject header")
478 conn
.log
.Error("send-as: could not find From header")
482 sendAs
:= SendAsSubject
.FindSubmatchIndex(headers
[subjectIdx
])
484 // No send-as modification.
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
)
492 for i
, header
:= range headers
{
494 buf
.Write(header
[:sendAs
[0]])
495 buf
.Write(header
[sendAs
[1]:])
496 } else if i
== fromIdx
{
497 addressStart
:= bytes
.LastIndexByte(header
, byte('<'))
498 buf
.Write(header
[:addressStart
+1])
499 buf
.WriteString(sendAsAddress
)
500 buf
.WriteString(">\n")
506 buf
.Write(env
.Data
[headerIdx
:])
508 env
.Data
= buf
.Bytes()
509 env
.MailFrom
.Address
= sendAsAddress
512 func (conn
*connection
) getReceivedInfo(envelope Envelope
) []byte {
513 base
:= fmt
.Sprintf("Received: from %s (%s)\r\n ", conn
.ehlo
, lookupRemoteHost(conn
.remoteAddr
))
522 base
+= fmt
.Sprintf("by %s (mailpopbox) with %s id %s\r\n ", conn
.server
.Name(), with
, envelope
.ID
)
524 if len(envelope
.RcptTo
) > 0 {
525 base
+= fmt
.Sprintf("for <%s>\r\n ", envelope
.RcptTo
[0].Address
)
528 transport
:= conn
.getTransportString()
529 date
:= envelope
.Received
.Format(time
.RFC1123Z
) // Same as RFC 5322 ยง 3.3
530 base
+= fmt
.Sprintf("(using %s);\r\n %s\r\n", transport
, date
)
535 func (conn
*connection
) getTransportString() string {
540 ciphers
:= map[uint16]string{
541 tls
.TLS_RSA_WITH_RC4_128_SHA
: "TLS_RSA_WITH_RC4_128_SHA",
542 tls
.TLS_RSA_WITH_3DES_EDE_CBC_SHA
: "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
543 tls
.TLS_RSA_WITH_AES_128_CBC_SHA
: "TLS_RSA_WITH_AES_128_CBC_SHA",
544 tls
.TLS_RSA_WITH_AES_256_CBC_SHA
: "TLS_RSA_WITH_AES_256_CBC_SHA",
545 tls
.TLS_RSA_WITH_AES_128_CBC_SHA256
: "TLS_RSA_WITH_AES_128_CBC_SHA256",
546 tls
.TLS_RSA_WITH_AES_128_GCM_SHA256
: "TLS_RSA_WITH_AES_128_GCM_SHA256",
547 tls
.TLS_RSA_WITH_AES_256_GCM_SHA384
: "TLS_RSA_WITH_AES_256_GCM_SHA384",
548 tls
.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA
: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
549 tls
.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
550 tls
.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
551 tls
.TLS_ECDHE_RSA_WITH_RC4_128_SHA
: "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
552 tls
.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA
: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
553 tls
.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
554 tls
.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
555 tls
.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
556 tls
.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
557 tls
.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
558 tls
.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
559 tls
.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
560 tls
.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
561 tls
.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
562 tls
.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
563 tls
.TLS_AES_128_GCM_SHA256
: "TLS_AES_128_GCM_SHA256",
564 tls
.TLS_AES_256_GCM_SHA384
: "TLS_AES_256_GCM_SHA384",
565 tls
.TLS_CHACHA20_POLY1305_SHA256
: "TLS_CHACHA20_POLY1305_SHA256",
567 versions
:= map[uint16]string{
568 tls
.VersionSSL30
: "SSLv3.0",
569 tls
.VersionTLS10
: "TLSv1.0",
570 tls
.VersionTLS11
: "TLSv1.1",
571 tls
.VersionTLS12
: "TLSv1.2",
572 tls
.VersionTLS13
: "TLSv1.3",
577 version
:= versions
[state
.Version
]
578 cipher
:= ciphers
[state
.CipherSuite
]
581 version
= fmt
.Sprintf("%x", state
.Version
)
584 cipher
= fmt
.Sprintf("%x", state
.CipherSuite
)
588 if state
.ServerName
!= "" {
589 name
= fmt
.Sprintf(" name=%s", state
.ServerName
)
592 return fmt
.Sprintf("%s cipher=%s%s", version
, cipher
, name
)
595 func (conn
*connection
) doRSET() {
596 conn
.log
.Info("doRSET()")
597 conn
.state
= stateInitial
602 func (conn
*connection
) resetBuffers() {
603 conn
.delivery
= deliverUnknown
606 conn
.rcptTo
= make([]mail
.Address
, 0)