From a173f807b75348b6b472cdaed1b83febfc9786c7 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sat, 17 Dec 2016 17:27:32 -0500 Subject: [PATCH] When receiving a message, the SMTP server must add its trace information. This also computes a Message-ID for the Envelope. --- smtp/conn.go | 55 ++++++++++++++++++++++++++++++++++++++- smtp/conn_test.go | 66 +++++++++++++++++++++++++++++++++++++++++++++++ smtp/server.go | 5 +++- 3 files changed, 124 insertions(+), 2 deletions(-) diff --git a/smtp/conn.go b/smtp/conn.go index 3485894..bd91326 100644 --- a/smtp/conn.go +++ b/smtp/conn.go @@ -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() diff --git a/smtp/conn_test.go b/smtp/conn_test.go index 32ca484..cd75299 100644 --- a/smtp/conn_test.go +++ b/smtp/conn_test.go @@ -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 " + 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) + } + } + } + +} diff --git a/smtp/server.go b/smtp/server.go index e992cda..a574ef9 100644 --- a/smtp/server.go +++ b/smtp/server.go @@ -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 -- 2.22.5