author | Alberto Bertogli
<albertito@blitiri.com.ar> 2018-11-22 03:00:55 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2018-11-30 10:03:48 UTC |
parent | 328008061d0e782e0fc6537a23c59b5c976d1884 |
internal/smtpsrv/conn.go | +56 | -10 |
internal/smtpsrv/conn_test.go | +39 | -0 |
test/t-05-null_address/expected_dsr | +3 | -3 |
diff --git a/internal/smtpsrv/conn.go b/internal/smtpsrv/conn.go index 14aee1c..31c205b 100644 --- a/internal/smtpsrv/conn.go +++ b/internal/smtpsrv/conn.go @@ -119,6 +119,9 @@ type Conn struct { // Are we using TLS? onTLS bool + // Have we used EHLO? + isESMTP bool + // Authenticator, aliases and local domains, taken from the server at // creation time. authr *auth.Authenticator @@ -292,6 +295,7 @@ func (c *Conn) EHLO(params string) (code int, msg string) { return 501, "Invisible customers are not welcome!" } c.ehloAddress = strings.Fields(params)[0] + c.isESMTP = true buf := bytes.NewBuffer(nil) fmt.Fprintf(buf, c.hostname+" - Your hour of destiny has come.\n") @@ -628,26 +632,47 @@ func (c *Conn) addReceivedHeader() { // https://tools.ietf.org/html/rfc5321#section-4.4 if c.completedAuth { - v += fmt.Sprintf("from %s (authenticated as %s@%s)\n", - c.ehloAddress, c.authUser, c.authDomain) + // For authenticated users, only show the EHLO address they gave; + // explicitly hide their network address. + v += fmt.Sprintf("from %s\n", c.ehloAddress) } else { - v += fmt.Sprintf("from %s (%s)\n", - c.ehloAddress, c.conn.RemoteAddr().String()) + // For non-authenticated users we show the real address as canonical, + // and then the given EHLO address for convenience and + // troubleshooting. + v += fmt.Sprintf("from [%s] (%s)\n", + addrLiteral(c.conn.RemoteAddr()), c.ehloAddress) } - v += fmt.Sprintf("by %s (chasquid)\n", c.hostname) + v += fmt.Sprintf("by %s (chasquid) ", c.hostname) + + // https://www.iana.org/assignments/mail-parameters/mail-parameters.xhtml#mail-parameters-7 + with := "SMTP" + if c.isESMTP { + with = "ESMTP" + } + if c.onTLS { + with += "S" + } + if c.completedAuth { + with += "A" + } + v += fmt.Sprintf("with %s\n", with) - v += fmt.Sprintf("(over %s ", c.mode) if c.tlsConnState != nil { - v += fmt.Sprintf("%s-%s)\n", - tlsconst.VersionName(c.tlsConnState.Version), + // https://tools.ietf.org/html/rfc8314#section-4.3 + v += fmt.Sprintf("tls %s\n", tlsconst.CipherSuiteName(c.tlsConnState.CipherSuite)) + } + + v += fmt.Sprintf("(over %s, ", c.mode) + if c.tlsConnState != nil { + v += fmt.Sprintf("%s, ", tlsconst.VersionName(c.tlsConnState.Version)) } else { - v += "plain text!)\n" + v += "plain text!, " } // Note we must NOT include c.rcptTo, that would leak BCCs. - v += fmt.Sprintf("(envelope from %q)\n", c.mailFrom) + v += fmt.Sprintf("envelope from %q)\n", c.mailFrom) // This should be the last part in the Received header, by RFC. // The ";" is a mandatory separator. The date format is not standard but @@ -663,6 +688,27 @@ func (c *Conn) addReceivedHeader() { } } +// addrLiteral converts a net.Addr (must be TCP) into a string for use as +// address literal, compliant with +// https://tools.ietf.org/html/rfc5321#section-4.1.3. +func addrLiteral(addr net.Addr) string { + tcp, ok := addr.(*net.TCPAddr) + if !ok { + // Fall back to Go's string representation; non-compliant but + // better than anything for our purposes. + return addr.String() + } + + // IPv6 addresses take the "IPv6:" prefix. + // IPv4 addresses are used literally. + s := tcp.IP.String() + if strings.Contains(s, ":") { + return "IPv6:" + s + } + + return s +} + // checkData performs very basic checks on the body of the email, to help // detect very broad problems like email loops. It does not fully check the // sanity of the headers or the structure of the payload. diff --git a/internal/smtpsrv/conn_test.go b/internal/smtpsrv/conn_test.go index 47a97ba..9da4664 100644 --- a/internal/smtpsrv/conn_test.go +++ b/internal/smtpsrv/conn_test.go @@ -1,6 +1,7 @@ package smtpsrv import ( + "net" "testing" "blitiri.com.ar/go/chasquid/internal/domaininfo" @@ -77,3 +78,41 @@ func TestIsHeader(t *testing.T) { } } } + +func TestAddrLiteral(t *testing.T) { + // TCP addresses. + casesTCP := []struct { + addr net.IP + expected string + }{ + {net.IPv4(1, 2, 3, 4), "1.2.3.4"}, + {net.IPv4(0, 0, 0, 0), "0.0.0.0"}, + {net.ParseIP("1.2.3.4"), "1.2.3.4"}, + {net.ParseIP("2001:db8::68"), "IPv6:2001:db8::68"}, + {net.ParseIP("::1"), "IPv6:::1"}, + } + for _, c := range casesTCP { + tcp := &net.TCPAddr{ + IP: c.addr, + Port: 12345, + } + s := addrLiteral(tcp) + if s != c.expected { + t.Errorf("%v: expected %q, got %q", tcp, c.expected, s) + } + } + + // Non-TCP addresses. We expect these to match addr.String(). + casesOther := []net.Addr{ + &net.UDPAddr{ + IP: net.ParseIP("1.2.3.4"), + Port: 12345, + }, + } + for _, addr := range casesOther { + s := addrLiteral(addr) + if s != addr.String() { + t.Errorf("%v: expected %q, got %q", addr, addr.String(), s) + } + } +} diff --git a/test/t-05-null_address/expected_dsr b/test/t-05-null_address/expected_dsr index 64b198c..c5e83c2 100644 --- a/test/t-05-null_address/expected_dsr +++ b/test/t-05-null_address/expected_dsr @@ -20,10 +20,10 @@ Delivery to the following recipient(s) failed permanently: ----- Original message ----- -Received: from localhost (authenticated as user@testserver) - by testserver (chasquid) +Received: from localhost + by testserver (chasquid) with ESMTPSA + tls * (over * - (envelope from "user@testserver") ; * Date: * From: Mailer daemon <somewhere@horns.com>