From 4ba7fea939f8646503eceba32bc17d59865f1985 Mon Sep 17 00:00:00 2001 From: Christian Joergensen Date: Mon, 21 Jul 2014 00:06:56 +0200 Subject: [PATCH] Added TLS param to Peer. Added option to prepend Received header to envelope data. --- envelope.go | 54 +++++++++++++++++++++++++++++++++++++++++++++ protocol.go | 4 ++++ smtpd.go | 18 +++++---------- smtpd_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++- wrap.go | 22 +++++++++++++++++++ wrap_test.go | 24 ++++++++++++++++++++ 6 files changed, 170 insertions(+), 13 deletions(-) create mode 100644 envelope.go create mode 100644 wrap.go create mode 100644 wrap_test.go diff --git a/envelope.go b/envelope.go new file mode 100644 index 0000000..29dd009 --- /dev/null +++ b/envelope.go @@ -0,0 +1,54 @@ +package smtpd + +import ( + "crypto/tls" + "fmt" + "strings" + "time" +) + +// Envelope holds a message +type Envelope struct { + Sender string + Recipients []string + Data []byte +} + +// AddReceivedLine prepends a Received header to the Data +func (env *Envelope) AddReceivedLine(peer Peer, serverName string) { + + tlsDetails := "" + + tlsVersions := map[uint16]string{ + tls.VersionSSL30: "SSL3.0", + tls.VersionTLS10: "TLS1.0", + tls.VersionTLS11: "TLS1.1", + tls.VersionTLS12: "TLS1.2", + } + + if peer.TLS != nil { + tlsDetails = fmt.Sprintf( + "\r\n\t(version=%s cipher=0x%x);", + tlsVersions[peer.TLS.Version], + peer.TLS.CipherSuite, + ) + } + + line := wrap([]byte(fmt.Sprintf( + "Received: from %s [%s] by %s with %s;%s\r\n\t%s\r\n", + peer.HeloName, + strings.Split(peer.Addr.String(), ":")[0], + serverName, + peer.Protocol, + tlsDetails, + time.Now().Format("Mon Jan 2 15:04:05 -0700 2006"), + ))) + + env.Data = append(env.Data, line...) + + // Move the new Received line up front + + copy(env.Data[len(line):], env.Data[0:len(env.Data)-len(line)]) + copy(env.Data, line) + +} diff --git a/protocol.go b/protocol.go index 6caaf36..5382040 100644 --- a/protocol.go +++ b/protocol.go @@ -272,6 +272,10 @@ func (session *session) handleSTARTTLS(cmd command) { session.scanner = bufio.NewScanner(session.reader) session.tls = true + // Save connection state on peer + state := tlsConn.ConnectionState() + session.peer.TLS = &state + // Flush the connection to set new timeout deadlines session.flush() diff --git a/smtpd.go b/smtpd.go index b6a07c7..460c416 100644 --- a/smtpd.go +++ b/smtpd.go @@ -58,18 +58,12 @@ const ( // Peer represents the client connecting to the server type Peer struct { - HeloName string // Server name used in HELO/EHLO command - Username string // Username from authentication, if authenticated - Password string // Password from authentication, if authenticated - Protocol Protocol // Protocol used, SMTP or ESMTP - Addr net.Addr // Network address -} - -// Envelope holds a message -type Envelope struct { - Sender string - Recipients []string - Data []byte + HeloName string // Server name used in HELO/EHLO command + Username string // Username from authentication, if authenticated + Password string // Password from authentication, if authenticated + Protocol Protocol // Protocol used, SMTP or ESMTP + Addr net.Addr // Network address + TLS *tls.ConnectionState // TLS Connection details, if on TLS } // Error represents an Error reported in the SMTP session. diff --git a/smtpd_test.go b/smtpd_test.go index 3770744..2d817fd 100644 --- a/smtpd_test.go +++ b/smtpd_test.go @@ -1,7 +1,7 @@ package smtpd_test import ( - "bitbucket.org/chrj/smtpd" + "bytes" "crypto/tls" "errors" "fmt" @@ -10,6 +10,8 @@ import ( "strings" "testing" "time" + + "bitbucket.org/chrj/smtpd" ) var localhostCert = []byte(`-----BEGIN CERTIFICATE----- @@ -1145,3 +1147,60 @@ func TestXCLIENT(t *testing.T) { } } + +func TestEnvelopeReceived(t *testing.T) { + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen failed: %v", err) + } + + defer ln.Close() + + server := &smtpd.Server{ + Handler: func(peer smtpd.Peer, env smtpd.Envelope) error { + env.AddReceivedLine(peer, "foobar.example.net") + if !bytes.HasPrefix(env.Data, []byte("Received: from localhost [127.0.0.1] by foobar.example.net with ESMTP;")) { + t.Fatal("Wrong received line.") + } + return nil + }, + } + + go func() { + server.Serve(ln) + }() + + c, err := smtp.Dial(ln.Addr().String()) + if err != nil { + t.Fatalf("Dial failed: %v", err) + } + + if err := c.Mail("sender@example.org"); err != nil { + t.Fatalf("MAIL failed: %v", err) + } + + if err := c.Rcpt("recipient@example.net"); err != nil { + t.Fatalf("RCPT failed: %v", err) + } + + wc, err := c.Data() + if err != nil { + t.Fatalf("Data failed: %v", err) + } + + _, err = fmt.Fprintf(wc, "This is the email body") + if err != nil { + t.Fatalf("Data body failed: %v", err) + } + + err = wc.Close() + if err != nil { + t.Fatalf("Data close failed: %v", err) + } + + if err := c.Quit(); err != nil { + t.Fatalf("QUIT failed: %v", err) + } + +} diff --git a/wrap.go b/wrap.go new file mode 100644 index 0000000..91a6cce --- /dev/null +++ b/wrap.go @@ -0,0 +1,22 @@ +package smtpd + +// Wrap a byte slice paragraph for use in SMTP header +func wrap(sl []byte) []byte { + length := 0 + for i := 0; i < len(sl); i++ { + if length > 76 && sl[i] == ' ' { + sl = append(sl, 0, 0) + copy(sl[i+2:], sl[i:]) + sl[i] = '\r' + sl[i+1] = '\n' + sl[i+2] = '\t' + i += 2 + length = 0 + } + if sl[i] == '\n' { + length = 0 + } + length++ + } + return sl +} diff --git a/wrap_test.go b/wrap_test.go new file mode 100644 index 0000000..a8b65de --- /dev/null +++ b/wrap_test.go @@ -0,0 +1,24 @@ +package smtpd + +import ( + "testing" +) + +func TestWrap(t *testing.T) { + + cases := map[string]string{ + "foobar": "foobar", + "foobar quux": "foobar quux", + "foobar\r\n": "foobar\r\n", + "foobar\r\nquux": "foobar\r\nquux", + "foobar quux foobar quux foobar quux foobar quux foobar quux foobar quux foobar quux foobar quux": "foobar quux foobar quux foobar quux foobar quux foobar quux foobar quux foobar\r\n\tquux foobar quux", + "foobar quux foobar quux foobar quux foobar quux foobar quux foobar\r\n\tquux foobar quux foobar quux": "foobar quux foobar quux foobar quux foobar quux foobar quux foobar\r\n\tquux foobar quux foobar quux", + } + + for k, v := range cases { + if string(wrap([]byte(k))) != v { + t.Fatal("Didn't wrap correctly.") + } + } + +}