265 lines
7.5 KiB
Go
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)
|
|
}
|
|
}
|