Merge branch 'outbound'
authorRobert Sesek <rsesek@bluestatic.org>
Sun, 7 Jun 2020 04:26:43 +0000 (00:26 -0400)
committerRobert Sesek <rsesek@bluestatic.org>
Sun, 7 Jun 2020 04:26:43 +0000 (00:26 -0400)
 Conflicts:
smtp_test.go

.gitignore
Makefile [new file with mode: 0644]
pop3.go
pop3_test.go
smtp/conn.go
smtp_test.go

index a79c85367166f053ff25e303ece04100b6f5dc53..5000e08088c1c311ed1e5a06ae8e7966371cf34a 100644 (file)
@@ -1,2 +1,6 @@
 config.json
+cover.html
+cover.out
 mailpopbox
+mailpopbox-linux-amd64
+mailpopbox-mac-amd64
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..6620afd
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,12 @@
+.PHONY:
+all: coverage mac linux
+
+coverage:
+       go test -coverprofile ./cover.out ./...
+       go tool cover -html=cover.out -o cover.html
+
+mac:
+       GOOS=darwin GOARCH=amd64 go build -o mailpopbox-mac-amd64
+
+linux:
+       GOOS=linux GOARCH=amd64 go build -o mailpopbox-linux-amd64
diff --git a/pop3.go b/pop3.go
index 085cfdfe09ed46170c0c825a5400ed577a0ee594..20f94bf6a36cfb5c6c618a242962757c89eb9230 100644 (file)
--- a/pop3.go
+++ b/pop3.go
@@ -177,7 +177,7 @@ func (mb *mailbox) ListMessages() ([]pop3.Message, error) {
 }
 
 func (mb *mailbox) GetMessage(id int) pop3.Message {
-       if id > len(mb.messages) {
+       if id == 0 || id > len(mb.messages) {
                return nil
        }
        return &mb.messages[id-1]
index 9613cd1a883b72280f5882beed81e8af99749729..9567748c9add912ed19ea979dd9ee4dd626f5070 100644 (file)
@@ -7,7 +7,13 @@
 package main
 
 import (
+       "io/ioutil"
+       "net/textproto"
+       "os"
+       "path/filepath"
        "testing"
+
+       "go.uber.org/zap"
 )
 
 func TestReset(t *testing.T) {
@@ -41,3 +47,231 @@ func TestReset(t *testing.T) {
                t.Errorf("reset did not un-delete message %v", msg)
        }
 }
+
+func TestOpenMailboxAuth(t *testing.T) {
+       dir, err := ioutil.TempDir("", "maildrop")
+       if err != nil {
+               t.Errorf("Failed to create temp dir: %v", err)
+               return
+       }
+       defer os.RemoveAll(dir)
+
+       s := &pop3Server{
+               config: Config{
+                       Servers: []Server{
+                               {
+                                       Domain:          "example.com",
+                                       MailboxPassword: "letmein",
+                                       MaildropPath:    dir,
+                               },
+                               {
+                                       Domain:          "test.net",
+                                       MailboxPassword: "open-sesame",
+                                       MaildropPath:    dir,
+                               },
+                       },
+               },
+               log: zap.NewNop(),
+       }
+
+       cases := []struct {
+               user, pass string
+               ok         bool
+       }{
+               {"mailbox@example.com", "letmein", true},
+               {"mailbox@test.net", "open-sesame", true},
+               {"mailbox@example.com", "open-sesame", false},
+               {"test@test.net", "open-sesame", false},
+               {"mailbox@an-example.net", "letmein", false},
+       }
+       for i, c := range cases {
+               mb, err := s.OpenMailbox(c.user, c.pass)
+               actual := (mb != nil && err == nil)
+               if actual != c.ok {
+                       t.Errorf("Expected error=%v for case %d (%#v), got %v (error=%v, mb=%v)", c.ok, i, c, actual, err, mb)
+               }
+       }
+}
+
+func TestBasicListener(t *testing.T) {
+       dir, err := ioutil.TempDir("", "maildrop")
+       if err != nil {
+               t.Errorf("Failed to create temp dir: %v", err)
+               return
+       }
+       defer os.RemoveAll(dir)
+
+       s := &pop3Server{
+               config: Config{
+                       POP3Port: 9648,
+                       Hostname: "example.com",
+                       Servers: []Server{
+                               {
+                                       Domain:       "example.com",
+                                       MaildropPath: dir,
+                               },
+                       },
+               },
+               log: zap.NewNop(),
+       }
+
+       go s.run()
+
+       conn, err := textproto.Dial("tcp", "localhost:9648")
+       if err != nil {
+               t.Errorf("Failed to dial test server: %v", err)
+               return
+       }
+
+       _, err = conn.ReadLine()
+       if err != nil {
+               t.Errorf("Failed to read line: %v\n", err)
+               return
+       }
+}
+
+func TestMailbox(t *testing.T) {
+       dir, err := ioutil.TempDir("", "maildrop")
+       if err != nil {
+               t.Errorf("Failed to create temp dir: %v", err)
+               return
+       }
+       defer os.RemoveAll(dir)
+
+       // Create the first message.
+       f, err := os.Create(filepath.Join(dir, "a.msg"))
+       if err != nil {
+               t.Errorf("Failed to create a.msg: %v", err)
+               return
+       }
+       for i := 0; i < 1024*10; i++ {
+               buf := []byte{'a'}
+               _, err = f.Write(buf)
+               if err != nil {
+                       t.Errorf("Failed to write a.msg: %v", err)
+               }
+       }
+       f.Close()
+
+       // Create the second message.
+       f, err = os.Create(filepath.Join(dir, "b.msg"))
+       if err != nil {
+               t.Errorf("Failed to create b.msg: %v", err)
+               return
+       }
+       for i := 0; i < 1024*3; i++ {
+               buf := []byte{'z'}
+               _, err = f.Write(buf)
+               if err != nil {
+                       t.Errorf("Failed to write z.msg: %v", err)
+               }
+       }
+       f.Close()
+
+       s := &pop3Server{
+               config: Config{
+                       Servers: []Server{
+                               {
+                                       Domain:          "example.com",
+                                       MailboxPassword: "letmein",
+                                       MaildropPath:    dir,
+                               },
+                       },
+               },
+               log: zap.NewNop(),
+       }
+
+       // Test message metadata.
+       mb, err := s.OpenMailbox("mailbox@example.com", "letmein")
+       if err != nil {
+               t.Errorf("Failed to open mailbox: %v", err)
+       }
+
+       msgs, err := mb.ListMessages()
+       if err != nil {
+               t.Errorf("Failed to list messages: %v", err)
+       }
+
+       if len(msgs) != 2 {
+               t.Errorf("Expected 2 messages, got %d", len(msgs))
+       }
+
+       if mb.GetMessage(0) != nil {
+               t.Errorf("Messages should be 1-indexed")
+       }
+       if mb.GetMessage(3) != nil {
+               t.Errorf("Retreived unexpected message")
+       }
+
+       if msgs[0] != mb.GetMessage(msgs[0].ID()) {
+               t.Errorf("Failed to look up message by ID")
+       }
+
+       if msgs[0].UniqueID() != "a" {
+               t.Errorf("Expected message #1 unique ID to be a, got %s", msgs[0].UniqueID())
+       }
+       expectedSize := 1024 * 10
+       if msgs[0].Size() != expectedSize {
+               t.Errorf("Expected message #1 size to be %v, got %v", expectedSize, msgs[0].Size())
+       }
+
+       if msgs[1].UniqueID() != "b" {
+               t.Errorf("Expected message #2 unique ID to be b, got %s", msgs[0].UniqueID())
+       }
+       expectedSize = 1024 * 3
+       if msgs[1].Size() != expectedSize {
+               t.Errorf("Expected message #2 size to be %v, got %v", expectedSize, msgs[0].Size())
+       }
+
+       // Test message contents.
+       rc, err := mb.Retrieve(msgs[0])
+       if err != nil {
+               t.Errorf("Failed to retrieve message: %v", err)
+       }
+       rc.Close()
+
+       // Test deletion marking and reset.
+       err = mb.Delete(msgs[1])
+       if err != nil {
+               t.Errorf("Failed to mark message #2 for deletion: %v", err)
+       }
+
+       if !msgs[1].Deleted() {
+               t.Errorf("Message should be marked for deletion and isn't")
+       }
+
+       mb.Reset()
+
+       if msgs[1].Deleted() {
+               t.Errorf("Message is marked for deletion and shouldn't be")
+       }
+
+       // Test deletion for real.
+       err = mb.Delete(msgs[0])
+       if err != nil {
+               t.Errorf("Failed to mark message for deletion: %v", err)
+       }
+
+       err = mb.Close()
+       if err != nil {
+               t.Errorf("Failed to close mailbox: %v", err)
+       }
+
+       mb, err = s.OpenMailbox("mailbox@example.com", "letmein")
+       if err != nil {
+               t.Errorf("Failed to re-open mailbox: %v", err)
+       }
+
+       msgs, err = mb.ListMessages()
+       if err != nil {
+               t.Errorf("Failed to list messages: %v", err)
+       }
+
+       if len(msgs) != 1 {
+               t.Errorf("Number of messages should be 1, got %d", len(msgs))
+       }
+
+       if msgs[0].UniqueID() != "b" {
+               t.Errorf("Message Unique ID should be b, got %s", msgs[0].UniqueID())
+       }
+}
index 9d807d437e9fcf9148928c483c21cb25952b8a8d..9020073086237127febeddcce57375aa1814f009 100644 (file)
@@ -538,29 +538,38 @@ func (conn *connection) getTransportString() string {
        }
 
        ciphers := map[uint16]string{
-               tls.TLS_RSA_WITH_RC4_128_SHA:                "TLS_RSA_WITH_RC4_128_SHA",
-               tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA:           "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
-               tls.TLS_RSA_WITH_AES_128_CBC_SHA:            "TLS_RSA_WITH_AES_128_CBC_SHA",
-               tls.TLS_RSA_WITH_AES_256_CBC_SHA:            "TLS_RSA_WITH_AES_256_CBC_SHA",
-               tls.TLS_RSA_WITH_AES_128_GCM_SHA256:         "TLS_RSA_WITH_AES_128_GCM_SHA256",
-               tls.TLS_RSA_WITH_AES_256_GCM_SHA384:         "TLS_RSA_WITH_AES_256_GCM_SHA384",
-               tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA:        "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
-               tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA:    "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
-               tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA:    "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
-               tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA:          "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
-               tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA:     "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
-               tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:      "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
-               tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:      "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
-               tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:   "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
-               tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
-               tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:   "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
-               tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
+               tls.TLS_RSA_WITH_RC4_128_SHA:                      "TLS_RSA_WITH_RC4_128_SHA",
+               tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA:                 "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
+               tls.TLS_RSA_WITH_AES_128_CBC_SHA:                  "TLS_RSA_WITH_AES_128_CBC_SHA",
+               tls.TLS_RSA_WITH_AES_256_CBC_SHA:                  "TLS_RSA_WITH_AES_256_CBC_SHA",
+               tls.TLS_RSA_WITH_AES_128_CBC_SHA256:               "TLS_RSA_WITH_AES_128_CBC_SHA256",
+               tls.TLS_RSA_WITH_AES_128_GCM_SHA256:               "TLS_RSA_WITH_AES_128_GCM_SHA256",
+               tls.TLS_RSA_WITH_AES_256_GCM_SHA384:               "TLS_RSA_WITH_AES_256_GCM_SHA384",
+               tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA:              "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
+               tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA:          "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
+               tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA:          "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
+               tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA:                "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
+               tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA:           "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
+               tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:            "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
+               tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:            "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
+               tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256:       "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
+               tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256:         "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
+               tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:         "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
+               tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:       "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
+               tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:         "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
+               tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:       "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
+               tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:   "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
+               tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
+               tls.TLS_AES_128_GCM_SHA256:                        "TLS_AES_128_GCM_SHA256",
+               tls.TLS_AES_256_GCM_SHA384:                        "TLS_AES_256_GCM_SHA384",
+               tls.TLS_CHACHA20_POLY1305_SHA256:                  "TLS_CHACHA20_POLY1305_SHA256",
        }
        versions := map[uint16]string{
                tls.VersionSSL30: "SSLv3.0",
                tls.VersionTLS10: "TLSv1.0",
                tls.VersionTLS11: "TLSv1.1",
                tls.VersionTLS12: "TLSv1.2",
+               tls.VersionTLS13: "TLSv1.3",
        }
 
        state := conn.tls
index 2eef868d941de7bfa2b90138c45478c30b8b1a0f..d1239ea99a3bce649d6f04abdfef2f98ed7939c0 100644 (file)
+// mailpopbox
+// Copyright 2020 Blue Static <https://www.bluestatic.org>
+// This program is free software licensed under the GNU General Public License,
+// version 3.0. The full text of the license can be found in LICENSE.txt.
+// SPDX-License-Identifier: GPL-3.0-only
+
 package main
 
 import (
+       "bytes"
+       "io/ioutil"
+       "net/mail"
+       "os"
+       "path/filepath"
        "testing"
+
+       "go.uber.org/zap"
+
+       "src.bluestatic.org/mailpopbox/smtp"
 )
 
-var testConfig = Config{
-       Servers: []Server{
-               Server{
-                       Domain:          "domain1.net",
-                       MailboxPassword: "d1",
+func TestVerifyAddress(t *testing.T) {
+       dir, err := ioutil.TempDir("", "maildrop")
+       if err != nil {
+               t.Errorf("Failed to create temp dir: %v", err)
+               return
+       }
+       defer os.RemoveAll(dir)
+
+       s := smtpServer{
+               config: Config{
+                       Hostname: "mx.example.com",
+                       Servers: []Server{
+                               {
+                                       Domain:       "example.com",
+                                       MaildropPath: dir,
+                               },
+                       },
                },
-               Server{
-                       Domain:          "domain2.xyz",
-                       MailboxPassword: "d2",
+               log: zap.NewNop(),
+       }
+
+       if s.VerifyAddress(mail.Address{Address: "example@example.com"}) != smtp.ReplyOK {
+               t.Errorf("Valid mailbox is not reported to be valid")
+       }
+       if s.VerifyAddress(mail.Address{Address: "mailbox@example.com"}) != smtp.ReplyOK {
+               t.Errorf("Valid mailbox is not reported to be valid")
+       }
+       if s.VerifyAddress(mail.Address{Address: "hello@other.net"}) == smtp.ReplyOK {
+               t.Errorf("Invalid mailbox reports to be valid")
+       }
+       if s.VerifyAddress(mail.Address{Address: "hello@mx.example.com"}) == smtp.ReplyOK {
+               t.Errorf("Invalid mailbox reports to be valid")
+       }
+       if s.VerifyAddress(mail.Address{Address: "unknown"}) == smtp.ReplyOK {
+               t.Errorf("Invalid mailbox reports to be valid")
+       }
+}
+
+func TestMessageDelivery(t *testing.T) {
+       dir, err := ioutil.TempDir("", "maildrop")
+       if err != nil {
+               t.Errorf("Failed to create temp dir: %v", err)
+               return
+       }
+       defer os.RemoveAll(dir)
+
+       s := smtpServer{
+               config: Config{
+                       Hostname: "mx.example.com",
+                       Servers: []Server{
+                               {
+                                       Domain:       "example.com",
+                                       MaildropPath: dir,
+                               },
+                       },
                },
-       },
+               log: zap.NewNop(),
+       }
+
+       env := smtp.Envelope{
+               MailFrom: mail.Address{Address: "sender@mail.net"},
+               RcptTo:   []mail.Address{{Address: "receive@example.com"}},
+               Data:     []byte("Hello, world"),
+               ID:       "msgid",
+       }
+
+       if rl := s.OnMessageDelivered(env); rl != nil {
+               t.Errorf("Failed to deliver message: %v", rl)
+       }
+
+       f, err := os.Open(filepath.Join(dir, "msgid.msg"))
+       if err != nil {
+               t.Errorf("Failed to open delivered message: %v", err)
+       }
+       defer f.Close()
+
+       data, err := ioutil.ReadAll(f)
+       if err != nil {
+               t.Errorf("Failed to read message: %v", err)
+       }
+
+       if !bytes.Contains(data, env.Data) {
+               t.Errorf("Could not find expected data in message")
+       }
 }
 
 func TestAuthenticate(t *testing.T) {
-       server := smtpServer{config: testConfig}
+       server := smtpServer{
+               config: Config{
+                       Servers: []Server{
+                               Server{
+                                       Domain:          "domain1.net",
+                                       MailboxPassword: "d1",
+                               },
+                               Server{
+                                       Domain:          "domain2.xyz",
+                                       MailboxPassword: "d2",
+                               },
+                       },
+               },
+       }
 
        authTests := []struct {
                authz, authc, passwd string