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)