git » chasquid » commit d05b8ef

chasquid: Add "Received" email headers

author Alberto Bertogli
2016-09-22 23:16:07 UTC
committer Alberto Bertogli
2016-10-09 23:51:04 UTC
parent 5f15b4e0409bacfba2d843bd25ad71f093432287

chasquid: Add "Received" email headers

This patch makes chasquid add "Received" email headers, after DATA completes
and before queueing.

We only use this for debugging purposes.

chasquid.go +50 -8
internal/envelope/envelope.go +12 -0

diff --git a/chasquid.go b/chasquid.go
index d629438..f7205c3 100644
--- a/chasquid.go
+++ b/chasquid.go
@@ -376,9 +376,10 @@ type Conn struct {
 	maxDataSize int64
 
 	// Connection information.
-	netconn net.Conn
-	tc      *textproto.Conn
-	mode    SocketMode
+	netconn      net.Conn
+	tc           *textproto.Conn
+	mode         SocketMode
+	tlsConnState *tls.ConnectionState
 
 	// System configuration.
 	config *config.Config
@@ -675,6 +676,8 @@ func (c *Conn) DATA(params string, tr *trace.Trace) (code int, msg string) {
 
 	tr.LazyPrintf("-> ... %d bytes of data", len(c.data))
 
+	c.addReceivedHeader()
+
 	// There are no partial failures here: we put it in the queue, and then if
 	// individual deliveries fail, we report via email.
 	msgID, err := c.queue.Put(c.mailFrom, c.rcptTo, c.data)
@@ -700,6 +703,34 @@ func (c *Conn) DATA(params string, tr *trace.Trace) (code int, msg string) {
 	return 250, msgs[rand.Int()%len(msgs)]
 }
 
+func (c *Conn) addReceivedHeader() {
+	var v string
+
+	if c.completedAuth {
+		v += fmt.Sprintf("from user %s@%s\n", c.authUser, c.authDomain)
+	} else {
+		v += fmt.Sprintf("from %s\n", c.netconn.RemoteAddr().String())
+	}
+
+	v += fmt.Sprintf("by %s (chasquid SMTP) over ", c.hostname)
+	if c.tlsConnState != nil {
+		v += fmt.Sprintf("TLS (%#x-%#x)\n",
+			c.tlsConnState.Version, c.tlsConnState.CipherSuite)
+	} else {
+		v += "plain text!\n"
+	}
+
+	// Note we must NOT include c.rcptTo, that would leak BCCs.
+	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
+	// this one seems to be widely used.
+	// https://tools.ietf.org/html/rfc5322#section-3.6.7
+	v += fmt.Sprintf("on ; %s\n", time.Now().Format(time.RFC1123Z))
+	c.data = envelope.AddHeader(c.data, "Received", v)
+}
+
 func (c *Conn) STARTTLS(params string, tr *trace.Trace) (code int, msg string) {
 	if c.onTLS {
 		return 503, "You are already wearing that!"
@@ -712,23 +743,34 @@ func (c *Conn) STARTTLS(params string, tr *trace.Trace) (code int, msg string) {
 
 	tr.LazyPrintf("<- 220  You experience a strange sense of peace")
 
-	client := tls.Server(c.netconn, c.tlsConfig)
-	err = client.Handshake()
+	server := tls.Server(c.netconn, c.tlsConfig)
+	err = server.Handshake()
 	if err != nil {
-		return 554, fmt.Sprintf("error in client handshake: %v", err)
+		return 554, fmt.Sprintf("error in TLS handshake: %v", err)
 	}
 
 	tr.LazyPrintf("<> ...  jump to TLS was successful")
 
 	// Override the connections. We don't need the older ones anymore.
-	c.netconn = client
-	c.tc = textproto.NewConn(client)
+	c.netconn = server
+	c.tc = textproto.NewConn(server)
+
+	// Take the connection state, so we can use it later for logging and
+	// tracing purposes.
+	cstate := server.ConnectionState()
+	c.tlsConnState = &cstate
 
 	// Reset the envelope; clients must start over after switching to TLS.
 	c.resetEnvelope()
 
 	c.onTLS = true
 
+	// If the client requested a specific server and we complied, that's our
+	// identity from now on.
+	if name := c.tlsConnState.ServerName; name != "" {
+		c.hostname = name
+	}
+
 	// 0 indicates not to send back a reply.
 	return 0, ""
 }
diff --git a/internal/envelope/envelope.go b/internal/envelope/envelope.go
index 4c31214..d0eb1d2 100644
--- a/internal/envelope/envelope.go
+++ b/internal/envelope/envelope.go
@@ -3,6 +3,7 @@
 package envelope
 
 import (
+	"fmt"
 	"strings"
 
 	"blitiri.com.ar/go/chasquid/internal/set"
@@ -36,3 +37,14 @@ func DomainIn(addr string, locals *set.String) bool {
 
 	return locals.Has(domain)
 }
+
+func AddHeader(data []byte, k, v string) []byte {
+	// If the value contains newlines, indent them properly.
+	if v[len(v)-1] == '\n' {
+		v = v[:len(v)-1]
+	}
+	v = strings.Replace(v, "\n", "\n\t", -1)
+
+	header := []byte(fmt.Sprintf("%s: %s\n", k, v))
+	return append(header, data...)
+}