15 "github.com/uber-go/zap"
21 stateNew state
= iota // Before EHLO.
30 func (d delivery
) String() string {
39 panic("Unknown delivery")
43 deliverUnknown delivery
= iota
44 deliverInbound
// Mail is not from one of this server's domains.
45 deliverOutbound
// Mail IS from one of this server's domains.
48 type connection
struct {
57 tls
*tls
.ConnectionState
59 // The authcid from a PLAIN SASL login. Non-empty iff tls is non-nil and
60 // doAUTH() succeeded.
69 // For deliverOutbound, replaces the From and Reply-To values.
73 mailFrom
*mail
.Address
77 func AcceptConnection(netConn net
.Conn
, server Server
, log zap
.Logger
) {
80 tp
: textproto
.NewConn(netConn
),
82 remoteAddr
: netConn
.RemoteAddr(),
83 log
: log
.With(zap
.Stringer("client", netConn
.RemoteAddr())),
87 conn
.log
.Info("accepted connection")
88 conn
.writeReply(220, fmt
.Sprintf("%s ESMTP [%s] (mailpopbox)",
89 server
.Name(), netConn
.LocalAddr()))
93 conn
.line
, err
= conn
.tp
.ReadLine()
95 conn
.log
.Error("ReadLine()", zap
.Error(err
))
100 lineForLog
:= conn
.line
101 const authPlain
= "AUTH PLAIN "
102 if strings
.HasPrefix(conn
.line
, authPlain
) {
103 lineForLog
= authPlain
+ "[redacted]"
105 conn
.log
.Info("ReadLine()", zap
.String("line", lineForLog
))
108 if _
, err
= fmt
.Sscanf(conn
.line
, "%s", &cmd
); err
!= nil {
109 conn
.reply(ReplyBadSyntax
)
113 switch strings
.ToUpper(cmd
) {
115 conn
.writeReply(221, "Goodbye")
137 conn
.writeReply(252, "I'll do my best")
139 conn
.writeReply(550, "access denied")
143 conn
.writeReply(250, "https://tools.ietf.org/html/rfc5321")
145 conn
.writeReply(500, "unrecognized command")
150 func (conn
*connection
) reply(reply ReplyLine
) error
{
151 return conn
.writeReply(reply
.Code
, reply
.Message
)
154 func (conn
*connection
) writeReply(code
int, msg
string) error
{
155 conn
.log
.Info("writeReply", zap
.Int("code", code
))
158 err
= conn
.tp
.PrintfLine("%d %s", code
, msg
)
160 err
= conn
.tp
.PrintfLine("%d", code
)
163 conn
.log
.Error("writeReply",
164 zap
.Int("code", code
),
170 // parsePath parses out either a forward-, reverse-, or return-path from the
171 // current connection line. Returns a (valid-path, ReplyOK) if it was
172 // successfully parsed.
173 func (conn
*connection
) parsePath(command
string) (string, ReplyLine
) {
174 if len(conn
.line
) < len(command
) {
175 return "", ReplyBadSyntax
177 if strings
.ToUpper(command
) != strings
.ToUpper(conn
.line
[:len(command
)]) {
178 return "", ReplyLine
{500, "unrecognized command"}
180 params
:= conn
.line
[len(command
):]
181 idx
:= strings
.Index(params
, ">")
183 return "", ReplyBadSyntax
185 return strings
.ToLower(params
[:idx
+1]), ReplyOK
188 func (conn
*connection
) doEHLO() {
192 _
, err
:= fmt
.Sscanf(conn
.line
, "%s %s", &cmd
, &conn
.ehlo
)
194 conn
.reply(ReplyBadSyntax
)
199 conn
.writeReply(250, fmt
.Sprintf("Hello %s [%s]", conn
.ehlo
, conn
.remoteAddr
))
201 conn
.tp
.PrintfLine("250-Hello %s [%s]", conn
.ehlo
, conn
.remoteAddr
)
202 if conn
.server
.TLSConfig() != nil && conn
.tls
== nil {
203 conn
.tp
.PrintfLine("250-STARTTLS")
206 conn
.tp
.PrintfLine("250-AUTH PLAIN")
208 conn
.tp
.PrintfLine("250 SIZE %d", 40960000)
211 conn
.log
.Info("doEHLO()", zap
.String("ehlo", conn
.ehlo
))
213 conn
.state
= stateInitial
216 func (conn
*connection
) doSTARTTLS() {
217 if conn
.state
!= stateInitial
{
218 conn
.reply(ReplyBadSequence
)
222 tlsConfig
:= conn
.server
.TLSConfig()
223 if !conn
.esmtp || tlsConfig
== nil {
224 conn
.writeReply(500, "unrecognized command")
228 conn
.log
.Info("doSTARTTLS()")
229 conn
.writeReply(220, "initiate TLS connection")
231 tlsConn
:= tls
.Server(conn
.nc
, tlsConfig
)
232 if err
:= tlsConn
.Handshake(); err
!= nil {
233 conn
.log
.Error("failed to do TLS handshake", zap
.Error(err
))
238 conn
.tp
= textproto
.NewConn(tlsConn
)
239 conn
.state
= stateNew
241 connState
:= tlsConn
.ConnectionState()
242 conn
.tls
= &connState
244 conn
.log
.Info("TLS connection done", zap
.String("state", conn
.getTransportString()))
247 func (conn
*connection
) doAUTH() {
248 if conn
.state
!= stateInitial || conn
.tls
== nil {
249 conn
.reply(ReplyBadSequence
)
253 if conn
.authc
!= "" {
254 conn
.writeReply(503, "already authenticated")
258 var cmd
, authType
, authString
string
259 n
, err
:= fmt
.Sscanf(conn
.line
, "%s %s %s", &cmd
, &authType
, &authString
)
261 conn
.reply(ReplyBadSyntax
)
265 if authType
!= "PLAIN" {
266 conn
.writeReply(504, "unrecognized auth type")
270 // If only 2 tokens were scanned, then an initial response was not provided.
271 if n
== 2 && conn
.line
[len(conn
.line
)-1] != ' ' {
272 conn
.reply(ReplyBadSyntax
)
276 conn
.log
.Info("doAUTH()")
278 if authString
== "" {
279 conn
.writeReply(334, " ")
281 authString
, err
= conn
.tp
.ReadLine()
283 conn
.log
.Error("failed to read auth line", zap
.Error(err
))
284 conn
.reply(ReplyBadSyntax
)
289 authBytes
, err
:= base64
.StdEncoding
.DecodeString(authString
)
291 conn
.reply(ReplyBadSyntax
)
295 authParts
:= strings
.Split(string(authBytes
), "\x00")
296 if len(authParts
) != 3 {
297 conn
.log
.Error("bad auth line syntax")
298 conn
.reply(ReplyBadSyntax
)
302 if !conn
.server
.Authenticate(authParts
[0], authParts
[1], authParts
[2]) {
303 conn
.log
.Error("failed to authenticate", zap
.String("authc", authParts
[1]))
304 conn
.writeReply(535, "invalid credentials")
308 conn
.log
.Info("authenticated", zap
.String("authz", authParts
[0]), zap
.String("authc", authParts
[1]))
309 conn
.authc
= authParts
[1]
313 func (conn
*connection
) doMAIL() {
314 if conn
.state
!= stateInitial
{
315 conn
.reply(ReplyBadSequence
)
319 mailFrom
, reply
:= conn
.parsePath("MAIL FROM:")
320 if reply
!= ReplyOK
{
326 conn
.mailFrom
, err
= mail
.ParseAddress(mailFrom
)
327 if err
!= nil || conn
.mailFrom
== nil {
328 conn
.reply(ReplyBadSyntax
)
332 if conn
.server
.VerifyAddress(*conn
.mailFrom
) == ReplyOK
{
333 // Message is being sent from a domain that this is an MTA for. Ultimate
334 // handling of the outbound message requires knowing the recipient.
335 domain
:= DomainForAddress(*conn
.mailFrom
)
336 // TODO: better way to authenticate this?
337 if !strings
.HasSuffix(conn
.authc
, "@"+domain
) {
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
{
371 // Message is addressed to this server. If it's outbound, only support
372 // the special send-as addressing.
373 if conn
.delivery
== deliverOutbound
{
374 if !strings
.HasPrefix(address
.Address
, SendAsAddress
) {
375 conn
.log
.Error("internal relay addressing not supported",
376 zap
.String("address", address
.Address
))
377 conn
.reply(ReplyBadMailbox
)
380 address
.Address
= strings
.TrimPrefix(address
.Address
, SendAsAddress
)
381 if DomainForAddress(*address
) != DomainForAddressString(conn
.authc
) {
382 conn
.log
.Error("not authenticated for send-as",
383 zap
.String("address", address
.Address
),
384 zap
.String("authc", conn
.authc
))
385 conn
.reply(ReplyBadMailbox
)
388 if conn
.sendAs
!= nil {
389 conn
.log
.Error("sendAs already specified",
390 zap
.String("address", address
.Address
),
391 zap
.String("sendAs", conn
.sendAs
.Address
))
392 conn
.reply(ReplyMailboxUnallowed
)
395 conn
.log
.Info("doRCPT()",
396 zap
.String("sendAs", address
.Address
))
397 conn
.sendAs
= address
398 conn
.state
= stateRecipient
403 // Message is not addressed to this server, so the delivery must be outbound.
404 if conn
.delivery
== deliverInbound
{
405 conn
.log
.Warn("invalid address",
406 zap
.String("address", address
.Address
),
407 zap
.Stringer("reply", reply
))
413 conn
.log
.Info("doRCPT()",
414 zap
.String("address", address
.Address
),
415 zap
.String("delivery", conn
.delivery
.String()))
417 conn
.rcptTo
= append(conn
.rcptTo
, *address
)
419 conn
.state
= stateRecipient
423 func (conn
*connection
) doDATA() {
424 if conn
.state
!= stateRecipient
{
425 conn
.reply(ReplyBadSequence
)
429 conn
.writeReply(354, "Start mail input; end with <CRLF>.<CRLF>")
430 conn
.log
.Info("doDATA()")
432 data
, err
:= conn
.tp
.ReadDotBytes()
434 conn
.log
.Error("failed to ReadDotBytes()",
436 zap
.String("bytes", fmt
.Sprintf("%x", data
)))
437 conn
.writeReply(552, "transaction failed")
441 conn
.handleSendAs(&data
)
443 received
:= time
.Now()
445 RemoteAddr
: conn
.remoteAddr
,
447 MailFrom
: *conn
.mailFrom
,
450 ID
: conn
.envelopeID(received
),
453 conn
.log
.Info("received message",
454 zap
.Int("bytes", len(data
)),
455 zap
.Time("date", received
),
456 zap
.String("id", env
.ID
),
457 zap
.String("delivery", conn
.delivery
.String()))
459 trace
:= conn
.getReceivedInfo(env
)
461 env
.Data
= append(trace
, data
...)
463 if conn
.delivery
== deliverInbound
{
464 if reply
:= conn
.server
.OnMessageDelivered(env
); reply
!= nil {
465 conn
.log
.Warn("message was rejected", zap
.String("id", env
.ID
))
469 } else if conn
.delivery
== deliverOutbound
{
470 conn
.server
.RelayMessage(env
)
473 conn
.state
= stateInitial
478 func (conn
*connection
) handleSendAs(data
*[]byte) {
479 if conn
.delivery
!= deliverOutbound || conn
.sendAs
== nil {
483 conn
.mailFrom
= conn
.sendAs
485 // Find the separator between the message header and body.
486 headerIdx
:= bytes
.Index(*data
, []byte("\n\n"))
488 conn
.log
.Error("send-as: could not find headers index")
492 fromPrefix
:= []byte("From: ")
493 fromIdx
:= bytes
.Index(*data
, fromPrefix
)
494 if fromIdx
== -1 || fromIdx
>= headerIdx
{
495 conn
.log
.Error("send-as: could not find From header")
499 if (*data
)[fromIdx
-1] != '\n' {
500 conn
.log
.Error("send-as: could not find From header")
505 fromEndIdx
:= bytes
.IndexByte((*data
)[fromIdx
:], '\n')
507 conn
.log
.Error("send-as: could not find end of From header")
510 fromEndIdx
+= fromIdx
512 newData
:= (*data
)[:fromIdx
]
513 newData
= append(newData
, fromPrefix
...)
514 newData
= append(newData
, []byte(conn
.sendAs
.String())...)
515 newData
= append(newData
, (*data
)[fromEndIdx
:]...)
520 func (conn
*connection
) envelopeID(t time
.Time
) string {
522 rand
.Read(idBytes
[:])
523 return fmt
.Sprintf("m.%d.%x", t
.UnixNano(), idBytes
)
526 func (conn
*connection
) getReceivedInfo(envelope Envelope
) []byte {
527 rhost
, _
, err
:= net
.SplitHostPort(conn
.remoteAddr
.String())
529 rhost
= conn
.remoteAddr
.String()
532 rhosts
, err
:= net
.LookupAddr(rhost
)
534 rhost
= fmt
.Sprintf("%s [%s]", rhosts
[0], rhost
)
537 base
:= fmt
.Sprintf("Received: from %s (%s)\r\n ", conn
.ehlo
, rhost
)
546 base
+= fmt
.Sprintf("by %s (mailpopbox) with %s id %s\r\n ", conn
.server
.Name(), with
, envelope
.ID
)
548 base
+= fmt
.Sprintf("for <%s>\r\n ", envelope
.RcptTo
[0].Address
)
550 transport
:= conn
.getTransportString()
551 date
:= envelope
.Received
.Format(time
.RFC1123Z
) // Same as RFC 5322 ยง 3.3
552 base
+= fmt
.Sprintf("(using %s);\r\n %s\r\n", transport
, date
)
557 func (conn
*connection
) getTransportString() string {
562 ciphers
:= map[uint16]string{
563 tls
.TLS_RSA_WITH_RC4_128_SHA
: "TLS_RSA_WITH_RC4_128_SHA",
564 tls
.TLS_RSA_WITH_3DES_EDE_CBC_SHA
: "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
565 tls
.TLS_RSA_WITH_AES_128_CBC_SHA
: "TLS_RSA_WITH_AES_128_CBC_SHA",
566 tls
.TLS_RSA_WITH_AES_256_CBC_SHA
: "TLS_RSA_WITH_AES_256_CBC_SHA",
567 tls
.TLS_RSA_WITH_AES_128_GCM_SHA256
: "TLS_RSA_WITH_AES_128_GCM_SHA256",
568 tls
.TLS_RSA_WITH_AES_256_GCM_SHA384
: "TLS_RSA_WITH_AES_256_GCM_SHA384",
569 tls
.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA
: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
570 tls
.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
571 tls
.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
572 tls
.TLS_ECDHE_RSA_WITH_RC4_128_SHA
: "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
573 tls
.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA
: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
574 tls
.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
575 tls
.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
576 tls
.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
577 tls
.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
578 tls
.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
579 tls
.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
581 versions
:= map[uint16]string{
582 tls
.VersionSSL30
: "SSLv3.0",
583 tls
.VersionTLS10
: "TLSv1.0",
584 tls
.VersionTLS11
: "TLSv1.1",
585 tls
.VersionTLS12
: "TLSv1.2",
590 version
:= versions
[state
.Version
]
591 cipher
:= ciphers
[state
.CipherSuite
]
594 version
= fmt
.Sprintf("%x", state
.Version
)
597 cipher
= fmt
.Sprintf("%x", state
.CipherSuite
)
601 if state
.ServerName
!= "" {
602 name
= fmt
.Sprintf(" name=%s", state
.ServerName
)
605 return fmt
.Sprintf("%s cipher=%s%s", version
, cipher
, name
)
608 func (conn
*connection
) doRSET() {
609 conn
.log
.Info("doRSET()")
610 conn
.state
= stateInitial
615 func (conn
*connection
) resetBuffers() {
616 conn
.delivery
= deliverUnknown
619 conn
.rcptTo
= make([]mail
.Address
, 0)