279 lines
8.6 KiB
Go
279 lines
8.6 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"io/fs"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Fixture additions (testdata/mail/produktor.io):
|
|
//
|
|
// ano/cur/2.multipart — multipart/mixed: alternative (text/plain+text/html)
|
|
// + image/png attachment (cid logo-cid-1), RFC 2047 encoded subject.
|
|
// ano/cur/3.searchme — simple message with subject/from search targets.
|
|
//
|
|
// Combined with the pre-existing fixtures the unified inbox contains:
|
|
// info 3 (cur 1.fixture1, 2.fixture2 + new 3.fixture3), ano 3, postmaster 1.
|
|
|
|
func TestListMessagesCounts(t *testing.T) {
|
|
msgs, err := listMessages("testdata/mail", "", "", 500)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(msgs) != 7 {
|
|
t.Fatalf("got %d messages, want 7 (info 3 + ano 3 + postmaster 1)", len(msgs))
|
|
}
|
|
byMailbox := map[string]int{}
|
|
for _, m := range msgs {
|
|
byMailbox[m.Mailbox]++
|
|
if m.Size <= 0 {
|
|
t.Errorf("%s/%s: size = %d, want > 0", m.Mailbox, m.Filename, m.Size)
|
|
}
|
|
if m.Filename == "" {
|
|
t.Error("empty filename in listing")
|
|
}
|
|
}
|
|
want := map[string]int{
|
|
"info@produktor.io": 3,
|
|
"ano@produktor.io": 3,
|
|
"postmaster@produktor.io": 1,
|
|
}
|
|
for mb, n := range want {
|
|
if byMailbox[mb] != n {
|
|
t.Errorf("mailbox %s: got %d messages, want %d", mb, byMailbox[mb], n)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListMessagesSortNewestFirst(t *testing.T) {
|
|
msgs, err := listMessages("testdata/mail", "", "", 500)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// 3.searchme (Date 11:00 +0100) is newer than 2.multipart (10:00 +0100);
|
|
// messages without a parseable Date fall back to file mtime.
|
|
if msgs[0].Filename != "3.searchme" {
|
|
t.Errorf("first = %s/%s, want ano/3.searchme", msgs[0].Mailbox, msgs[0].Filename)
|
|
}
|
|
if msgs[1].Filename != "2.multipart" {
|
|
t.Errorf("second = %s/%s, want ano/2.multipart", msgs[1].Mailbox, msgs[1].Filename)
|
|
}
|
|
}
|
|
|
|
func TestListMessagesMailboxFilter(t *testing.T) {
|
|
msgs, err := listMessages("testdata/mail", "info@produktor.io", "", 500)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(msgs) != 3 {
|
|
t.Fatalf("got %d, want 3 for info@produktor.io", len(msgs))
|
|
}
|
|
for _, m := range msgs {
|
|
if m.Mailbox != "info@produktor.io" {
|
|
t.Errorf("mailbox = %q, want info@produktor.io", m.Mailbox)
|
|
}
|
|
}
|
|
// localpart-only filter is accepted too
|
|
msgs, err = listMessages("testdata/mail", "postmaster", "", 500)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(msgs) != 1 {
|
|
t.Fatalf("localpart filter: got %d, want 1", len(msgs))
|
|
}
|
|
}
|
|
|
|
func TestListMessagesQuery(t *testing.T) {
|
|
msgs, err := listMessages("testdata/mail", "", "acme", 500)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(msgs) != 1 || msgs[0].Filename != "3.searchme" {
|
|
t.Fatalf("q=acme: got %+v, want 1 hit (3.searchme)", msgs)
|
|
}
|
|
// case-insensitive on subject
|
|
msgs, err = listMessages("testdata/mail", "", "INVOICE", 500)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(msgs) != 1 || msgs[0].Subject != "Invoice #42 from Acme Corp" {
|
|
t.Fatalf("q=INVOICE: got %+v, want 3.searchme", msgs)
|
|
}
|
|
msgs, err = listMessages("testdata/mail", "", "no such term", 500)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(msgs) != 0 {
|
|
t.Fatalf("q=no-match: got %d, want 0", len(msgs))
|
|
}
|
|
}
|
|
|
|
func TestListMessagesLimit(t *testing.T) {
|
|
msgs, err := listMessages("testdata/mail", "", "", 2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(msgs) != 2 {
|
|
t.Fatalf("limit 2: got %d, want 2", len(msgs))
|
|
}
|
|
}
|
|
|
|
func TestReadMessageMultipart(t *testing.T) {
|
|
d, err := readMessage("testdata/mail", "ano@produktor.io", "2.multipart")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := d.Headers.Get("Subject"); !strings.Contains(got, "Multipart fixture with") {
|
|
t.Errorf("Subject header = %q, want RFC 2047 decoded", got)
|
|
}
|
|
if !strings.Contains(d.Text, "hello plain body") {
|
|
t.Errorf("Text = %q, want plain body", d.Text)
|
|
}
|
|
if !strings.Contains(d.HTML, "<b>html</b>") {
|
|
t.Errorf("HTML = %q, want html body", d.HTML)
|
|
}
|
|
if len(d.Attachments) != 1 {
|
|
t.Fatalf("attachments = %d, want 1", len(d.Attachments))
|
|
}
|
|
a := d.Attachments[0]
|
|
if a.Filename != "logo.png" || a.CID != "logo-cid-1" || a.Size != 8 {
|
|
t.Errorf("attachment = %+v, want logo.png cid=logo-cid-1 size=8", a)
|
|
}
|
|
if d.Headers.Get("From") == "" || d.Headers.Get("To") == "" || d.Headers.Get("Date") == "" {
|
|
t.Error("expected from/to/date headers")
|
|
}
|
|
}
|
|
|
|
func TestReadMessageSimple(t *testing.T) {
|
|
d, err := readMessage("testdata/mail", "ano@produktor.io", "3.searchme")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if d.Subject() != "Invoice #42 from Acme Corp" {
|
|
t.Errorf("subject = %q", d.Subject())
|
|
}
|
|
if !strings.Contains(d.Text, "Please pay") {
|
|
t.Errorf("Text = %q, want body", d.Text)
|
|
}
|
|
if d.HTML != "" {
|
|
t.Errorf("HTML = %q, want empty for plain message", d.HTML)
|
|
}
|
|
if len(d.Attachments) != 0 {
|
|
t.Errorf("attachments = %d, want 0", len(d.Attachments))
|
|
}
|
|
}
|
|
|
|
func TestReadMessagePathSafety(t *testing.T) {
|
|
cases := []struct{ mailbox, filename string }{
|
|
{"../../etc", "passwd"},
|
|
{"ano@produktor.io", "../../etc/passwd"},
|
|
{"ano@produktor.io", "..%2F1.fixtureano"},
|
|
{"ano@produktor.io", "nonexistent"},
|
|
{"ANO@produktor.io", "1.fixtureano"}, // uppercase rejected
|
|
{"ano..@produktor.io", "1.fixtureano"},
|
|
{"ano@produktor.io/../info", "1.fixture1"},
|
|
}
|
|
for _, c := range cases {
|
|
if _, err := readMessage("testdata/mail", c.mailbox, c.filename); err == nil {
|
|
t.Errorf("readMessage(%q, %q): expected error, got nil", c.mailbox, c.filename)
|
|
} else if !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, errBadMailbox) {
|
|
t.Errorf("readMessage(%q, %q): err = %v, want errBadMailbox or fs.ErrNotExist", c.mailbox, c.filename, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAPIMessages(t *testing.T) {
|
|
h := testServer(t).handler()
|
|
rec := doAuth(t, h, "/admin/api/messages", "bot", "s3cret")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("code = %d, want 200: %s", rec.Code, rec.Body.String())
|
|
}
|
|
var resp messagesResponse
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Count != 7 || len(resp.Messages) != 7 {
|
|
t.Fatalf("count = %d len = %d, want 7", resp.Count, len(resp.Messages))
|
|
}
|
|
if resp.Messages[0].Filename != "3.searchme" {
|
|
t.Errorf("first = %+v, want 3.searchme (newest first)", resp.Messages[0])
|
|
}
|
|
|
|
rec = doAuth(t, h, "/admin/api/messages?mailbox=postmaster@produktor.io", "bot", "s3cret")
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Count != 1 || resp.Messages[0].Mailbox != "postmaster@produktor.io" {
|
|
t.Errorf("mailbox filter: count = %d, want 1 postmaster", resp.Count)
|
|
}
|
|
|
|
rec = doAuth(t, h, "/admin/api/messages?q=acme", "bot", "s3cret")
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Count != 1 {
|
|
t.Errorf("q filter: count = %d, want 1", resp.Count)
|
|
}
|
|
|
|
rec = doAuth(t, h, "/admin/api/messages?limit=2", "bot", "s3cret")
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(resp.Messages) != 2 {
|
|
t.Errorf("limit=2: got %d, want 2", len(resp.Messages))
|
|
}
|
|
}
|
|
|
|
func TestAPIMessageDetail(t *testing.T) {
|
|
h := testServer(t).handler()
|
|
rec := doAuth(t, h, "/admin/api/messages/ano@produktor.io/2.multipart", "bot", "s3cret")
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("code = %d, want 200: %s", rec.Code, rec.Body.String())
|
|
}
|
|
var d messageDetail
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &d); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(d.Text, "hello plain body") || !strings.Contains(d.HTML, "<b>html</b>") {
|
|
t.Errorf("detail body: text=%q html=%q", d.Text, d.HTML)
|
|
}
|
|
if len(d.Attachments) != 1 || d.Attachments[0].Filename != "logo.png" {
|
|
t.Errorf("attachments = %+v, want logo.png", d.Attachments)
|
|
}
|
|
}
|
|
|
|
func TestAPIMessageDetailUnsafe(t *testing.T) {
|
|
h := testServer(t).handler()
|
|
for _, p := range []string{
|
|
"/admin/api/messages/..%2F..%2Fetc/passwd",
|
|
"/admin/api/messages/ano@produktor.io/..%2F..%2Fetc/passwd",
|
|
"/admin/api/messages/ANO@produktor.io/1.fixtureano",
|
|
"/admin/api/messages/ano@produktor.io/nonexistent",
|
|
"/admin/api/messages/ano@produktor.io/1.fixtureano/extra",
|
|
} {
|
|
rec := doAuth(t, h, p, "bot", "s3cret")
|
|
if rec.Code == http.StatusOK {
|
|
t.Errorf("%s: code = %d, want 4xx", p, rec.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMessagesPagesRender(t *testing.T) {
|
|
h := testServer(t).handler()
|
|
rec := doAuth(t, h, "/admin/messages", "bot", "s3cret")
|
|
body := rec.Body.String()
|
|
if !strings.Contains(body, "All messages") || !strings.Contains(body, "ano@produktor.io") {
|
|
t.Error("messages page missing title/mailbox grouping")
|
|
}
|
|
|
|
rec = doAuth(t, h, "/admin/messages", "bot", "s3cret")
|
|
rec = doAuth(t, h, "/admin/message/ano@produktor.io/2.multipart", "bot", "s3cret")
|
|
body = rec.Body.String()
|
|
if rec.Code != http.StatusOK || !strings.Contains(body, "hello plain body") {
|
|
t.Errorf("message page: code = %d, body missing text: %s", rec.Code, body[:min(len(body), 200)])
|
|
}
|
|
}
|