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 if DomainForAddress(*conn
.mailFrom
) != DomainForAddressString(conn
.authc
) {
334 conn
.writeReply(550, "not authenticated")
337 conn
.delivery
= deliverOutbound
339 conn
.delivery
= deliverInbound
342 conn
.log
.Info("doMAIL()", zap
.String("address", conn
.mailFrom
.Address
))
344 conn
.state
= stateMail
348 func (conn
*connection
) doRCPT() {
349 if conn
.state
!= stateMail
&& conn
.state
!= stateRecipient
{
350 conn
.reply(ReplyBadSequence
)
354 rcptTo
, reply
:= conn
.parsePath("RCPT TO:")
355 if reply
!= ReplyOK
{
360 address
, err
:= mail
.ParseAddress(rcptTo
)
362 conn
.reply(ReplyBadSyntax
)
366 if reply
:= conn
.server
.VerifyAddress(*address
); reply
!= ReplyOK
&& conn
.delivery
== deliverInbound
{
367 conn
.log
.Warn("invalid address",
368 zap
.String("address", address
.Address
),
369 zap
.Stringer("reply", reply
))
374 conn
.log
.Info("doRCPT()",
375 zap
.String("address", address
.Address
),
376 zap
.String("delivery", conn
.delivery
.String()))
378 conn
.rcptTo
= append(conn
.rcptTo
, *address
)
380 conn
.state
= stateRecipient
384 func (conn
*connection
) doDATA() {
385 if conn
.state
!= stateRecipient
{
386 conn
.reply(ReplyBadSequence
)
390 conn
.writeReply(354, "Start mail input; end with <CRLF>.<CRLF>")
391 conn
.log
.Info("doDATA()")
393 data
, err
:= conn
.tp
.ReadDotBytes()
395 conn
.log
.Error("failed to ReadDotBytes()",
397 zap
.String("bytes", fmt
.Sprintf("%x", data
)))
398 conn
.writeReply(552, "transaction failed")
402 received
:= time
.Now()
404 RemoteAddr
: conn
.remoteAddr
,
406 MailFrom
: *conn
.mailFrom
,
409 ID
: conn
.envelopeID(received
),
413 conn
.handleSendAs(&env
)
415 conn
.log
.Info("received message",
416 zap
.Int("bytes", len(data
)),
417 zap
.Time("date", received
),
418 zap
.String("id", env
.ID
),
419 zap
.String("delivery", conn
.delivery
.String()))
421 trace
:= conn
.getReceivedInfo(env
)
423 env
.Data
= append(trace
, env
.Data
...)
425 if conn
.delivery
== deliverInbound
{
426 if reply
:= conn
.server
.OnMessageDelivered(env
); reply
!= nil {
427 conn
.log
.Warn("message was rejected", zap
.String("id", env
.ID
))
431 } else if conn
.delivery
== deliverOutbound
{
432 conn
.server
.RelayMessage(env
)
435 conn
.state
= stateInitial
440 func (conn
*connection
) handleSendAs(env
*Envelope
) {
441 if conn
.delivery
!= deliverOutbound
{
445 // Find the separator between the message header and body.
446 headerIdx
:= bytes
.Index(env
.Data
, []byte("\n\n"))
448 conn
.log
.Error("send-as: could not find headers index")
454 headers
:= bytes
.SplitAfter(env
.Data
[:headerIdx
], []byte("\n"))
456 var fromIdx
, subjectIdx
int
457 for i
, header
:= range headers
{
458 if bytes
.HasPrefix(header
, []byte("From:")) {
462 if bytes
.HasPrefix(header
, []byte("Subject:")) {
468 if subjectIdx
== -1 {
469 conn
.log
.Error("send-as: could not find Subject header")
473 conn
.log
.Error("send-as: could not find From header")
477 sendAs
:= SendAsSubject
.FindSubmatchIndex(headers
[subjectIdx
])
479 // No send-as modification.
483 // Submatch 0 is the whole sendas magic. Submatch 1 is the address prefix.
484 sendAsUser
:= headers
[subjectIdx
][sendAs
[2]:sendAs
[3]]
485 sendAsAddress
:= string(sendAsUser
) + "@" + DomainForAddressString(conn
.authc
)
487 for i
, header
:= range headers
{
489 buf
.Write(header
[:sendAs
[0]])
490 buf
.Write(header
[sendAs
[1]:])
491 } else if i
== fromIdx
{
492 addressStart
:= bytes
.LastIndexByte(header
, byte('<'))
493 buf
.Write(header
[:addressStart
+1])
494 buf
.WriteString(sendAsAddress
)
495 buf
.WriteString(">\n")
501 buf
.Write(env
.Data
[headerIdx
:])
503 env
.Data
= buf
.Bytes()
504 env
.MailFrom
.Address
= sendAsAddress
507 func (conn
*connection
) envelopeID(t time
.Time
) string {
509 rand
.Read(idBytes
[:])
510 return fmt
.Sprintf("m.%d.%x", t
.UnixNano(), idBytes
)
513 func (conn
*connection
) getReceivedInfo(envelope Envelope
) []byte {
514 rhost
, _
, err
:= net
.SplitHostPort(conn
.remoteAddr
.String())
516 rhost
= conn
.remoteAddr
.String()
519 rhosts
, err
:= net
.LookupAddr(rhost
)
521 rhost
= fmt
.Sprintf("%s [%s]", rhosts
[0], rhost
)
524 base
:= fmt
.Sprintf("Received: from %s (%s)\r\n ", conn
.ehlo
, rhost
)
533 base
+= fmt
.Sprintf("by %s (mailpopbox) with %s id %s\r\n ", conn
.server
.Name(), with
, envelope
.ID
)
535 if len(envelope
.RcptTo
) > 0 {
536 base
+= fmt
.Sprintf("for <%s>\r\n ", envelope
.RcptTo
[0].Address
)
539 transport
:= conn
.getTransportString()
540 date
:= envelope
.Received
.Format(time
.RFC1123Z
) // Same as RFC 5322 ยง 3.3
541 base
+= fmt
.Sprintf("(using %s);\r\n %s\r\n", transport
, date
)
546 func (conn
*connection
) getTransportString() string {
551 ciphers
:= map[uint16]string{
552 tls
.TLS_RSA_WITH_RC4_128_SHA
: "TLS_RSA_WITH_RC4_128_SHA",
553 tls
.TLS_RSA_WITH_3DES_EDE_CBC_SHA
: "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
554 tls
.TLS_RSA_WITH_AES_128_CBC_SHA
: "TLS_RSA_WITH_AES_128_CBC_SHA",
555 tls
.TLS_RSA_WITH_AES_256_CBC_SHA
: "TLS_RSA_WITH_AES_256_CBC_SHA",
556 tls
.TLS_RSA_WITH_AES_128_GCM_SHA256
: "TLS_RSA_WITH_AES_128_GCM_SHA256",
557 tls
.TLS_RSA_WITH_AES_256_GCM_SHA384
: "TLS_RSA_WITH_AES_256_GCM_SHA384",
558 tls
.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA
: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
559 tls
.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
560 tls
.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
561 tls
.TLS_ECDHE_RSA_WITH_RC4_128_SHA
: "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
562 tls
.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA
: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
563 tls
.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
564 tls
.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
565 tls
.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
566 tls
.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
567 tls
.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
568 tls
.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
570 versions
:= map[uint16]string{
571 tls
.VersionSSL30
: "SSLv3.0",
572 tls
.VersionTLS10
: "TLSv1.0",
573 tls
.VersionTLS11
: "TLSv1.1",
574 tls
.VersionTLS12
: "TLSv1.2",
579 version
:= versions
[state
.Version
]
580 cipher
:= ciphers
[state
.CipherSuite
]
583 version
= fmt
.Sprintf("%x", state
.Version
)
586 cipher
= fmt
.Sprintf("%x", state
.CipherSuite
)
590 if state
.ServerName
!= "" {
591 name
= fmt
.Sprintf(" name=%s", state
.ServerName
)
594 return fmt
.Sprintf("%s cipher=%s%s", version
, cipher
, name
)
597 func (conn
*connection
) doRSET() {
598 conn
.log
.Info("doRSET()")
599 conn
.state
= stateInitial
604 func (conn
*connection
) resetBuffers() {
605 conn
.delivery
= deliverUnknown
608 conn
.rcptTo
= make([]mail
.Address
, 0)