Files

265 lines
7.5 KiB
Go

package imapserver_test
import (
"net"
"path/filepath"
"testing"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/imapclient"
"mailgoserver/internal/db"
"mailgoserver/internal/imapserver"
"mailgoserver/internal/mailstore"
)
// newTestMailboxWithAppPassword seeds a domain + mailbox + app password (returning the
// plaintext, since only its bcrypt hash is stored) and one stored message, and starts
// a plain IMAP listener against it.
func newTestMailboxWithAppPassword(t *testing.T) (client *imapclient.Client, mailboxEmail, appPassword string, mailboxID int64) {
t.Helper()
dir := t.TempDir()
database, err := db.Open(filepath.Join(dir, "test.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { database.Close() })
domainID, err := database.CreateDomain("example.com")
if err != nil {
t.Fatal(err)
}
store := mailstore.New(database, mailstore.GenerateDEK(), t.TempDir())
dek := mailstore.GenerateDEK()
wrapped, nonce, err := store.WrapDEK(dek)
if err != nil {
t.Fatal(err)
}
portalHash, err := db.HashPassword("portal-password-unused")
if err != nil {
t.Fatal(err)
}
mailboxID, err = database.CreateMailbox("inbox@example.com", portalHash, domainID, 5*1024*1024*1024, wrapped, nonce)
if err != nil {
t.Fatal(err)
}
appPassword = db.GenerateAppPassword(25)
appHash, err := db.HashPassword(appPassword)
if err != nil {
t.Fatal(err)
}
if _, err := database.CreateAppPassword(mailboxID, "test client", appHash, nil); err != nil {
t.Fatal(err)
}
raw := []byte("From: Alice <alice@example.com>\r\nSubject: Hello there\r\n\r\nBody text.")
if _, err := store.StoreMessage(mailboxID, "INBOX", raw, "<abc@example.com>", "Alice <alice@example.com>", "Hello there"); err != nil {
t.Fatal(err)
}
backend := &imapserver.Backend{DB: database, Mailstore: store}
srv := imapserver.NewPlainServer(backend)
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
go srv.Serve(ln)
t.Cleanup(func() { srv.Close() })
conn, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
t.Fatal(err)
}
client = imapclient.New(conn, nil)
t.Cleanup(func() { client.Close() })
return client, "inbox@example.com", appPassword, mailboxID
}
func TestIMAPLoginSelectFetch(t *testing.T) {
client, email, appPassword, _ := newTestMailboxWithAppPassword(t)
if err := client.Login(email, appPassword).Wait(); err != nil {
t.Fatalf("login with app password: %v", err)
}
selectData, err := client.Select("INBOX", nil).Wait()
if err != nil {
t.Fatalf("select INBOX: %v", err)
}
if selectData.NumMessages != 1 {
t.Fatalf("NumMessages = %d, want 1", selectData.NumMessages)
}
msgs, err := client.Fetch(imap.SeqSetNum(1), &imap.FetchOptions{
Envelope: true,
Flags: true,
BodySection: []*imap.FetchItemBodySection{{}},
}).Collect()
if err != nil {
t.Fatalf("fetch: %v", err)
}
if len(msgs) != 1 {
t.Fatalf("got %d messages, want 1", len(msgs))
}
msg := msgs[0]
if msg.Envelope == nil || msg.Envelope.Subject != "Hello there" {
t.Fatalf("envelope subject = %+v, want %q", msg.Envelope, "Hello there")
}
if len(msg.Envelope.From) != 1 || msg.Envelope.From[0].Addr() != "alice@example.com" {
t.Fatalf("envelope from = %+v, want alice@example.com", msg.Envelope.From)
}
if len(msg.BodySection) != 1 {
t.Fatalf("expected one body section, got %d", len(msg.BodySection))
}
got := string(msg.BodySection[0].Bytes)
want := "From: Alice <alice@example.com>\r\nSubject: Hello there\r\n\r\nBody text."
if got != want {
t.Fatalf("body section = %q, want %q", got, want)
}
if err := client.Logout().Wait(); err != nil {
t.Fatalf("logout: %v", err)
}
}
func TestIMAPLoginRejectsPortalPassword(t *testing.T) {
client, email, _, _ := newTestMailboxWithAppPassword(t)
// The mailbox's own portal password must never work for IMAP login — only an
// app password does (see Session.Login).
err := client.Login(email, "portal-password-unused").Wait()
if err == nil {
t.Fatal("expected login with the portal password to fail")
}
}
func TestIMAPListAndSelectAdditionalFolder(t *testing.T) {
dir := t.TempDir()
database, err := db.Open(filepath.Join(dir, "test.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { database.Close() })
domainID, err := database.CreateDomain("example.com")
if err != nil {
t.Fatal(err)
}
store := mailstore.New(database, mailstore.GenerateDEK(), t.TempDir())
dek := mailstore.GenerateDEK()
wrapped, nonce, err := store.WrapDEK(dek)
if err != nil {
t.Fatal(err)
}
portalHash, err := db.HashPassword("portal-password-unused")
if err != nil {
t.Fatal(err)
}
mailboxID, err := database.CreateMailbox("inbox@example.com", portalHash, domainID, 5*1024*1024*1024, wrapped, nonce)
if err != nil {
t.Fatal(err)
}
appPassword := db.GenerateAppPassword(25)
appHash, err := db.HashPassword(appPassword)
if err != nil {
t.Fatal(err)
}
if _, err := database.CreateAppPassword(mailboxID, "test client", appHash, nil); err != nil {
t.Fatal(err)
}
if _, err := store.StoreMessage(mailboxID, "INBOX", []byte("Subject: normal\r\n\r\nhi"), "<a@example.com>", "a@example.com", "normal"); err != nil {
t.Fatal(err)
}
if _, err := store.StoreMessage(mailboxID, "Spam", []byte("Subject: junk\r\n\r\nspam"), "<b@example.com>", "b@example.com", "junk"); err != nil {
t.Fatal(err)
}
backend := &imapserver.Backend{DB: database, Mailstore: store}
srv := imapserver.NewPlainServer(backend)
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
go srv.Serve(ln)
t.Cleanup(func() { srv.Close() })
conn, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
t.Fatal(err)
}
client := imapclient.New(conn, nil)
t.Cleanup(func() { client.Close() })
if err := client.Login("inbox@example.com", appPassword).Wait(); err != nil {
t.Fatalf("login: %v", err)
}
mailboxes, err := client.List("", "%", nil).Collect()
if err != nil {
t.Fatalf("list: %v", err)
}
var names []string
for _, m := range mailboxes {
names = append(names, m.Mailbox)
}
if len(names) != 2 {
t.Fatalf("expected 2 folders (INBOX, Spam), got %v", names)
}
inboxData, err := client.Select("INBOX", nil).Wait()
if err != nil {
t.Fatalf("select INBOX: %v", err)
}
if inboxData.NumMessages != 1 {
t.Fatalf("INBOX NumMessages = %d, want 1", inboxData.NumMessages)
}
spamData, err := client.Select("Spam", nil).Wait()
if err != nil {
t.Fatalf("select Spam: %v", err)
}
if spamData.NumMessages != 1 {
t.Fatalf("Spam NumMessages = %d, want 1", spamData.NumMessages)
}
msgs, err := client.Fetch(imap.SeqSetNum(1), &imap.FetchOptions{Envelope: true}).Collect()
if err != nil {
t.Fatalf("fetch in Spam: %v", err)
}
if len(msgs) != 1 || msgs[0].Envelope.Subject != "junk" {
t.Fatalf("expected the Spam-folder message (subject %q), got %+v", "junk", msgs)
}
}
func TestIMAPStoreSeenFlag(t *testing.T) {
client, email, appPassword, _ := newTestMailboxWithAppPassword(t)
if err := client.Login(email, appPassword).Wait(); err != nil {
t.Fatalf("login: %v", err)
}
if _, err := client.Select("INBOX", nil).Wait(); err != nil {
t.Fatalf("select: %v", err)
}
storeFlags := &imap.StoreFlags{Op: imap.StoreFlagsAdd, Flags: []imap.Flag{imap.FlagSeen}}
msgs, err := client.Store(imap.SeqSetNum(1), storeFlags, nil).Collect()
if err != nil {
t.Fatalf("store: %v", err)
}
if len(msgs) != 1 {
t.Fatalf("got %d fetch responses from STORE, want 1", len(msgs))
}
found := false
for _, f := range msgs[0].Flags {
if f == imap.FlagSeen {
found = true
}
}
if !found {
t.Fatalf("expected \\Seen in returned flags, got %v", msgs[0].Flags)
}
}