config.json
+cover.html
+cover.out
mailpopbox
+mailpopbox-linux-amd64
+mailpopbox-mac-amd64
--- /dev/null
+.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
}
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]
package main
import (
+ "io/ioutil"
+ "net/textproto"
+ "os"
+ "path/filepath"
"testing"
+
+ "go.uber.org/zap"
)
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())
+ }
+}
}
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
+// 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