Commit 29709a0d authored by Alberto Bertogli's avatar Alberto Bertogli

smtpsrv: Improve "Received" header standard compliance

Despite its loose appearance, the "Received" header has a reasonably
standarized format.

We were not following the standard format as closely as we should; this
rarely causes problems in this particular case, but there's no need to
deviate from it.

This patch changes the Received header generation as follows:

 - The "from" section now uses the remote address as canonical (for
   non-authenticated users) which provides more valuable information
   than the user-supplied EHLO address (which is also included).
 - The remote authenticated user is now hidden, for additional privacy.
 - Use the "with" optional clause.
 - Use the standard way of printing TLS cipher suite.
 - Use the standard way of printing address literals.
parent 32800806
......@@ -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.
......
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)
}
}
}
......@@ -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>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment