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)