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
27 stateNew state
= iota // Before EHLO.
36 func (d delivery
) String() string {
45 panic("Unknown delivery")
49 deliverUnknown delivery
= iota
50 deliverInbound
// Mail is not from one of this server's domains.
51 deliverOutbound
// Mail IS from one of this server's domains.
54 type connection
struct {
63 tls
*tls
.ConnectionState
67 // The authcid from a PLAIN SASL login. Non-empty iff tls is non-nil and
68 // doAUTH() succeeded.
75 // For deliverOutbound, replaces the From and Reply-To values.
79 mailFrom
*mail
.Address
83 func AcceptConnection(netConn net
.Conn
, server Server
, log
*zap
.Logger
) {
86 tp
: textproto
.NewConn(netConn
),
88 remoteAddr
: netConn
.RemoteAddr(),
89 log
: log
.With(zap
.Stringer("client", netConn
.RemoteAddr())),
93 conn
.log
.Info("accepted connection")
94 conn
.writeReply(220, fmt
.Sprintf("%s ESMTP [%s] (mailpopbox)",
95 server
.Name(), netConn
.LocalAddr()))
99 conn
.line
, err
= conn
.tp
.ReadLine()
101 conn
.log
.Error("ReadLine()", zap
.Error(err
))
106 lineForLog
:= conn
.line
107 const authPlain
= "AUTH PLAIN "
108 if strings
.HasPrefix(conn
.line
, authPlain
) {
109 lineForLog
= authPlain
+ "[redacted]"
111 conn
.log
.Info("ReadLine()", zap
.String("line", lineForLog
))
114 if _
, err
= fmt
.Sscanf(conn
.line
, "%s", &cmd
); err
!= nil {
115 conn
.reply(ReplyBadSyntax
)
119 switch strings
.ToUpper(cmd
) {
121 conn
.writeReply(221, "Goodbye")
143 conn
.writeReply(252, "I'll do my best")
145 conn
.writeReply(550, "access denied")
149 conn
.writeReply(250, "https://tools.ietf.org/html/rfc5321")
151 conn
.writeReply(500, "unrecognized command")
156 func (conn
*connection
) reply(reply ReplyLine
) error
{
157 return conn
.writeReply(reply
.Code
, reply
.Message
)
160 func (conn
*connection
) writeReply(code
int, msg
string) error
{
161 conn
.log
.Info("writeReply", zap
.Int("code", code
))
164 err
= conn
.tp
.PrintfLine("%d %s", code
, msg
)
166 err
= conn
.tp
.PrintfLine("%d", code
)
169 conn
.log
.Error("writeReply",
170 zap
.Int("code", code
),
176 // parsePath parses out either a forward-, reverse-, or return-path from the
177 // current connection line. Returns a (valid-path, ReplyOK) if it was
178 // successfully parsed.
179 func (conn
*connection
) parsePath(command
string) (string, ReplyLine
) {
180 if len(conn
.line
) < len(command
) {
181 return "", ReplyBadSyntax
183 if strings
.ToUpper(command
) != strings
.ToUpper(conn
.line
[:len(command
)]) {
184 return "", ReplyLine
{500, "unrecognized command"}
186 params
:= conn
.line
[len(command
):]
187 idx
:= strings
.Index(params
, ">")
189 return "", ReplyBadSyntax
191 return strings
.ToLower(params
[:idx
+1]), ReplyOK
194 func (conn
*connection
) doEHLO() {
198 _
, err
:= fmt
.Sscanf(conn
.line
, "%s %s", &cmd
, &conn
.ehlo
)
200 conn
.reply(ReplyBadSyntax
)
205 conn
.writeReply(250, fmt
.Sprintf("Hello %s [%s]", conn
.ehlo
, conn
.remoteAddr
))
207 conn
.tp
.PrintfLine("250-Hello %s [%s]", conn
.ehlo
, conn
.remoteAddr
)
208 if conn
.server
.TLSConfig() != nil && conn
.tls
== nil {
209 conn
.tp
.PrintfLine("250-STARTTLS")
212 conn
.tp
.PrintfLine("250-AUTH PLAIN")
214 conn
.tp
.PrintfLine("250 SIZE %d", 40960000)
217 conn
.log
.Info("doEHLO()", zap
.String("ehlo", conn
.ehlo
))
219 conn
.state
= stateInitial
222 func (conn
*connection
) doSTARTTLS() {
223 if conn
.state
!= stateInitial
{
224 conn
.reply(ReplyBadSequence
)
228 tlsConfig
:= conn
.server
.TLSConfig()
229 if !conn
.esmtp || tlsConfig
== nil {
230 conn
.writeReply(500, "unrecognized command")
234 conn
.log
.Info("doSTARTTLS()")
235 conn
.writeReply(220, "initiate TLS connection")
237 tlsConn
:= tls
.Server(conn
.nc
, tlsConfig
)
238 if err
:= tlsConn
.Handshake(); err
!= nil {
239 conn
.log
.Error("failed to do TLS handshake", zap
.Error(err
))
244 conn
.tp
= textproto
.NewConn(tlsConn
)
245 conn
.state
= stateNew
247 connState
:= tlsConn
.ConnectionState()
248 conn
.tls
= &connState
250 conn
.log
.Info("TLS connection done", zap
.String("state", conn
.getTransportString()))
253 func (conn
*connection
) doAUTH() {
254 if conn
.state
!= stateInitial || conn
.tls
== nil {
255 conn
.reply(ReplyBadSequence
)
259 if conn
.authc
!= "" {
260 conn
.writeReply(503, "already authenticated")
264 var cmd
, authType
, authString
string
265 n
, err
:= fmt
.Sscanf(conn
.line
, "%s %s %s", &cmd
, &authType
, &authString
)
267 conn
.reply(ReplyBadSyntax
)
271 if authType
!= "PLAIN" {
272 conn
.writeReply(504, "unrecognized auth type")
276 // If only 2 tokens were scanned, then an initial response was not provided.
277 if n
== 2 && conn
.line
[len(conn
.line
)-1] != ' ' {
278 conn
.reply(ReplyBadSyntax
)
282 conn
.log
.Info("doAUTH()")
284 if authString
== "" {
285 conn
.writeReply(334, " ")
287 authString
, err
= conn
.tp
.ReadLine()
289 conn
.log
.Error("failed to read auth line", zap
.Error(err
))
290 conn
.reply(ReplyBadSyntax
)
295 authBytes
, err
:= base64
.StdEncoding
.DecodeString(authString
)
297 conn
.reply(ReplyBadSyntax
)
301 authParts
:= strings
.Split(string(authBytes
), "\x00")
302 if len(authParts
) != 3 {
303 conn
.log
.Error("bad auth line syntax")
304 conn
.reply(ReplyBadSyntax
)
308 if !conn
.server
.Authenticate(authParts
[0], authParts
[1], authParts
[2]) {
309 conn
.log
.Error("failed to authenticate", zap
.String("authc", authParts
[1]))
310 conn
.writeReply(535, "invalid credentials")
314 conn
.log
.Info("authenticated", zap
.String("authz", authParts
[0]), zap
.String("authc", authParts
[1]))
315 conn
.authc
= authParts
[1]
319 func (conn
*connection
) doMAIL() {
320 if conn
.state
!= stateInitial
{
321 conn
.reply(ReplyBadSequence
)
325 mailFrom
, reply
:= conn
.parsePath("MAIL FROM:")
326 if reply
!= ReplyOK
{
332 conn
.mailFrom
, err
= mail
.ParseAddress(mailFrom
)
333 if err
!= nil || conn
.mailFrom
== nil {
334 conn
.reply(ReplyBadSyntax
)
338 if conn
.server
.VerifyAddress(*conn
.mailFrom
) == ReplyOK
{
339 if DomainForAddress(*conn
.mailFrom
) != DomainForAddressString(conn
.authc
) {
340 conn
.writeReply(550, "not authenticated")
343 conn
.delivery
= deliverOutbound
345 conn
.delivery
= deliverInbound
348 conn
.log
.Info("doMAIL()", zap
.String("address", conn
.mailFrom
.Address
))
350 conn
.state
= stateMail
354 func (conn
*connection
) doRCPT() {
355 if conn
.state
!= stateMail
&& conn
.state
!= stateRecipient
{
356 conn
.reply(ReplyBadSequence
)
360 rcptTo
, reply
:= conn
.parsePath("RCPT TO:")
361 if reply
!= ReplyOK
{
366 address
, err
:= mail
.ParseAddress(rcptTo
)
368 conn
.reply(ReplyBadSyntax
)
372 if reply
:= conn
.server
.VerifyAddress(*address
); reply
!= ReplyOK
&& conn
.delivery
== deliverInbound
{
373 conn
.log
.Warn("invalid address",
374 zap
.String("address", address
.Address
),
375 zap
.Stringer("reply", reply
))
380 conn
.log
.Info("doRCPT()",
381 zap
.String("address", address
.Address
),
382 zap
.String("delivery", conn
.delivery
.String()))
384 conn
.rcptTo
= append(conn
.rcptTo
, *address
)
386 conn
.state
= stateRecipient
390 func (conn
*connection
) doDATA() {
391 if conn
.state
!= stateRecipient
{
392 conn
.reply(ReplyBadSequence
)
396 conn
.writeReply(354, "Start mail input; end with <CRLF>.<CRLF>")
397 conn
.log
.Info("doDATA()")
399 data
, err
:= conn
.tp
.ReadDotBytes()
401 conn
.log
.Error("failed to ReadDotBytes()",
403 zap
.String("bytes", fmt
.Sprintf("%x", data
)))
404 conn
.writeReply(552, "transaction failed")
408 received
:= time
.Now()
410 RemoteAddr
: conn
.remoteAddr
,
412 MailFrom
: *conn
.mailFrom
,
415 ID
: conn
.envelopeID(received
),
419 conn
.handleSendAs(&env
)
421 conn
.log
.Info("received message",
422 zap
.Int("bytes", len(data
)),
423 zap
.Time("date", received
),
424 zap
.String("id", env
.ID
),
425 zap
.String("delivery", conn
.delivery
.String()))
427 trace
:= conn
.getReceivedInfo(env
)
429 env
.Data
= append(trace
, env
.Data
...)
431 if conn
.delivery
== deliverInbound
{
432 if reply
:= conn
.server
.OnMessageDelivered(env
); reply
!= nil {
433 conn
.log
.Warn("message was rejected", zap
.String("id", env
.ID
))
437 } else if conn
.delivery
== deliverOutbound
{
438 conn
.server
.RelayMessage(env
)
441 conn
.state
= stateInitial
446 func (conn
*connection
) handleSendAs(env
*Envelope
) {
447 if conn
.delivery
!= deliverOutbound
{
451 // Find the separator between the message header and body.
452 headerIdx
:= bytes
.Index(env
.Data
, []byte("\n\n"))
454 conn
.log
.Error("send-as: could not find headers index")
460 headers
:= bytes
.SplitAfter(env
.Data
[:headerIdx
], []byte("\n"))
462 var fromIdx
, subjectIdx
int
463 for i
, header
:= range headers
{
464 if bytes
.HasPrefix(header
, []byte("From:")) {
468 if bytes
.HasPrefix(header
, []byte("Subject:")) {
474 if subjectIdx
== -1 {
475 conn
.log
.Error("send-as: could not find Subject header")
479 conn
.log
.Error("send-as: could not find From header")
483 sendAs
:= SendAsSubject
.FindSubmatchIndex(headers
[subjectIdx
])
485 // No send-as modification.
489 // Submatch 0 is the whole sendas magic. Submatch 1 is the address prefix.
490 sendAsUser
:= headers
[subjectIdx
][sendAs
[2]:sendAs
[3]]
491 sendAsAddress
:= string(sendAsUser
) + "@" + DomainForAddressString(conn
.authc
)
493 for i
, header
:= range headers
{
495 buf
.Write(header
[:sendAs
[0]])
496 buf
.Write(header
[sendAs
[1]:])
497 } else if i
== fromIdx
{
498 addressStart
:= bytes
.LastIndexByte(header
, byte('<'))
499 buf
.Write(header
[:addressStart
+1])
500 buf
.WriteString(sendAsAddress
)
501 buf
.WriteString(">\n")
507 buf
.Write(env
.Data
[headerIdx
:])
509 env
.Data
= buf
.Bytes()
510 env
.MailFrom
.Address
= sendAsAddress
513 func (conn
*connection
) envelopeID(t time
.Time
) string {
515 rand
.Read(idBytes
[:])
516 return fmt
.Sprintf("m.%d.%x", t
.UnixNano(), idBytes
)
519 func (conn
*connection
) getReceivedInfo(envelope Envelope
) []byte {
520 rhost
, _
, err
:= net
.SplitHostPort(conn
.remoteAddr
.String())
522 rhost
= conn
.remoteAddr
.String()
525 rhosts
, err
:= net
.LookupAddr(rhost
)
527 rhost
= fmt
.Sprintf("%s [%s]", rhosts
[0], rhost
)
530 base
:= fmt
.Sprintf("Received: from %s (%s)\r\n ", conn
.ehlo
, rhost
)
539 base
+= fmt
.Sprintf("by %s (mailpopbox) with %s id %s\r\n ", conn
.server
.Name(), with
, envelope
.ID
)
541 if len(envelope
.RcptTo
) > 0 {
542 base
+= fmt
.Sprintf("for <%s>\r\n ", envelope
.RcptTo
[0].Address
)
545 transport
:= conn
.getTransportString()
546 date
:= envelope
.Received
.Format(time
.RFC1123Z
) // Same as RFC 5322 ยง 3.3
547 base
+= fmt
.Sprintf("(using %s);\r\n %s\r\n", transport
, date
)
552 func (conn
*connection
) getTransportString() string {
557 ciphers
:= map[uint16]string{
558 tls
.TLS_RSA_WITH_RC4_128_SHA
: "TLS_RSA_WITH_RC4_128_SHA",
559 tls
.TLS_RSA_WITH_3DES_EDE_CBC_SHA
: "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
560 tls
.TLS_RSA_WITH_AES_128_CBC_SHA
: "TLS_RSA_WITH_AES_128_CBC_SHA",
561 tls
.TLS_RSA_WITH_AES_256_CBC_SHA
: "TLS_RSA_WITH_AES_256_CBC_SHA",
562 tls
.TLS_RSA_WITH_AES_128_GCM_SHA256
: "TLS_RSA_WITH_AES_128_GCM_SHA256",
563 tls
.TLS_RSA_WITH_AES_256_GCM_SHA384
: "TLS_RSA_WITH_AES_256_GCM_SHA384",
564 tls
.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA
: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
565 tls
.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
566 tls
.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
567 tls
.TLS_ECDHE_RSA_WITH_RC4_128_SHA
: "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
568 tls
.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA
: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
569 tls
.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
570 tls
.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
571 tls
.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
572 tls
.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
573 tls
.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
574 tls
.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
576 versions
:= map[uint16]string{
577 tls
.VersionSSL30
: "SSLv3.0",
578 tls
.VersionTLS10
: "TLSv1.0",
579 tls
.VersionTLS11
: "TLSv1.1",
580 tls
.VersionTLS12
: "TLSv1.2",
585 version
:= versions
[state
.Version
]
586 cipher
:= ciphers
[state
.CipherSuite
]
589 version
= fmt
.Sprintf("%x", state
.Version
)
592 cipher
= fmt
.Sprintf("%x", state
.CipherSuite
)
596 if state
.ServerName
!= "" {
597 name
= fmt
.Sprintf(" name=%s", state
.ServerName
)
600 return fmt
.Sprintf("%s cipher=%s%s", version
, cipher
, name
)
603 func (conn
*connection
) doRSET() {
604 conn
.log
.Info("doRSET()")
605 conn
.state
= stateInitial
610 func (conn
*connection
) resetBuffers() {
611 conn
.delivery
= deliverUnknown
614 conn
.rcptTo
= make([]mail
.Address
, 0)