When receiving a message, the SMTP server must add its trace information.
authorRobert Sesek <rsesek@bluestatic.org>
Sat, 17 Dec 2016 22:27:32 +0000 (17:27 -0500)
committerRobert Sesek <rsesek@bluestatic.org>
Sat, 17 Dec 2016 22:27:32 +0000 (17:27 -0500)
This also computes a Message-ID for the Envelope.

smtp/conn.go
smtp/conn_test.go
smtp/server.go

index 34858942c70259b4092116595417d608d3d53917..bd9132613e12c1279031e90b75ddd6a936f8e59a 100644 (file)
@@ -1,11 +1,13 @@
 package smtp
 
 import (
+       "crypto/rand"
        "fmt"
        "net"
        "net/mail"
        "net/textproto"
        "strings"
+       "time"
 )
 
 type state int
@@ -24,6 +26,9 @@ type connection struct {
        tp         *textproto.Conn
        remoteAddr net.Addr
 
+       esmtp bool
+       tls   bool
+
        state
        line string
 
@@ -63,8 +68,10 @@ func AcceptConnection(netConn net.Conn, server Server) error {
                        conn.tp.Close()
                        break
                case "HELO":
+                       conn.esmtp = false
                        fallthrough
                case "EHLO":
+                       conn.esmtp = true
                        conn.doEHLO()
                case "MAIL":
                        conn.doMAIL()
@@ -204,14 +211,20 @@ func (conn *connection) doDATA() {
                return
        }
 
+       received := time.Now()
        env := Envelope{
                RemoteAddr: conn.remoteAddr,
                EHLO:       conn.ehlo,
                MailFrom:   *conn.mailFrom,
                RcptTo:     conn.rcptTo,
-               Data:       data,
+               Received:   received,
+               ID:         conn.envelopeID(received),
        }
 
+       trace := conn.getReceivedInfo(env)
+
+       env.Data = append(trace, data...)
+
        if reply := conn.server.OnMessageDelivered(env); reply != nil {
                conn.reply(*reply)
                return
@@ -221,6 +234,46 @@ func (conn *connection) doDATA() {
        conn.reply(ReplyOK)
 }
 
+func (conn *connection) envelopeID(t time.Time) string {
+       var idBytes [4]byte
+       rand.Read(idBytes[:])
+       return fmt.Sprintf("m.%d.%x", t.UnixNano(), idBytes)
+}
+
+func (conn *connection) getReceivedInfo(envelope Envelope) []byte {
+       rhost, _, err := net.SplitHostPort(conn.remoteAddr.String())
+       if err != nil {
+               rhost = conn.remoteAddr.String()
+       }
+
+       rhosts, err := net.LookupAddr(rhost)
+       if err == nil {
+               rhost = fmt.Sprintf("%s [%s]", rhosts[0], rhost)
+       }
+
+       base := fmt.Sprintf("Received: from %s (%s)\r\n        ", conn.ehlo, rhost)
+
+       with := "SMTP"
+       if conn.esmtp {
+               with = "E" + with
+       }
+       if conn.tls {
+               with += "S"
+       }
+       base += fmt.Sprintf("by %s (mailpopbox) with %s id %s\r\n        ", conn.server.Name(), with, envelope.ID)
+
+       base += fmt.Sprintf("for <%s>\r\n        ", envelope.RcptTo[0].Address)
+
+       transport := "PLAINTEXT"
+       if conn.tls {
+               // TODO: TLS version, cipher, bits
+       }
+       date := envelope.Received.Format(time.RFC1123Z) // Same as RFC 5322 ยง 3.3
+       base += fmt.Sprintf("(using %s);\r\n        %s\r\n", transport, date)
+
+       return []byte(base)
+}
+
 func (conn *connection) doRSET() {
        conn.state = stateInitial
        conn.resetBuffers()
index 32ca4849291e98e60a53ffcee5e9a05dcb410c9e..cd75299dda4a0d9c44a7d64abbd8fb4b4449c3a3 100644 (file)
@@ -9,6 +9,7 @@ import (
        "runtime"
        "strings"
        "testing"
+       "time"
 )
 
 func _fl(depth int) string {
@@ -188,3 +189,68 @@ func TestCaseSensitivty(t *testing.T) {
                {"QUiT", 221, nil},
        })
 }
+
+func TestGetReceivedInfo(t *testing.T) {
+       conn := connection{
+               server:     &testServer{},
+               remoteAddr: &net.IPAddr{net.IPv4(127, 0, 0, 1), ""},
+       }
+
+       now := time.Now()
+
+       const crlf = "\r\n"
+       const line1 = "Received: from remote.test. (localhost [127.0.0.1])" + crlf
+       const line2 = "by Test-Server (mailpopbox) with "
+       const msgId = "abcdef.hijk"
+       lineLast := now.Format(time.RFC1123Z) + crlf
+
+       type params struct {
+               ehlo    string
+               esmtp   bool
+               tls     bool
+               address string
+       }
+
+       tests := []struct {
+               params params
+
+               expect []string
+       }{
+               {params{"remote.test.", true, false, "foo@bar.com"},
+                       []string{line1,
+                               line2 + "ESMTP id " + msgId + crlf,
+                               "for <foo@bar.com>" + crlf,
+                               "(using PLAINTEXT);" + crlf,
+                               lineLast, ""}},
+       }
+
+       for _, test := range tests {
+               t.Logf("%#v", test.params)
+
+               conn.ehlo = test.params.ehlo
+               conn.esmtp = test.params.esmtp
+               conn.tls = test.params.tls
+
+               envelope := Envelope{
+                       RcptTo:   []mail.Address{{"", test.params.address}},
+                       Received: now,
+                       ID:       msgId,
+               }
+
+               actual := conn.getReceivedInfo(envelope)
+               actualLines := strings.SplitAfter(string(actual), crlf)
+
+               if len(actualLines) != len(test.expect) {
+                       t.Errorf("wrong numbber of lines, expected %d, got %d", len(test.expect), len(actualLines))
+                       continue
+               }
+
+               for i, line := range actualLines {
+                       expect := test.expect[i]
+                       if expect != strings.TrimLeft(line, " ") {
+                               t.Errorf("Expected equal string %q, got %q", expect, line)
+                       }
+               }
+       }
+
+}
index e992cda6a4710978997707da8f1461b09f840a62..a574ef9906639c6e263c987ccf5d236dc0074001 100644 (file)
@@ -4,6 +4,7 @@ import (
        "crypto/tls"
        "net"
        "net/mail"
+       "time"
 )
 
 type ReplyLine struct {
@@ -24,6 +25,8 @@ type Envelope struct {
        MailFrom   mail.Address
        RcptTo     []mail.Address
        Data       []byte
+       Received   time.Time
+       ID         string
 }
 
 type Server interface {
@@ -34,7 +37,7 @@ type Server interface {
        OnMessageDelivered(Envelope) *ReplyLine
 }
 
-type EmptyServerCallbacks struct {}
+type EmptyServerCallbacks struct{}
 
 func (*EmptyServerCallbacks) TLSConfig() *tls.Config {
        return nil