package app import ( "archive/zip" "bytes" "encoding/json" "io" "mime/multipart" "net/http" "net/http/httptest" "strings" "testing" ) func TestParseMBOXMultipleMessages(t *testing.T) { raw := strings.Join([]string{ "From sender@example.com Mon Jan 1 00:00:00 2024", "From: sender@example.com", "To: first@example.com", "Subject: first", "", "first body", ">From escaped body line", "From sender@example.com Tue Jan 2 00:00:00 2024", "From: sender@example.com", "To: second@example.com", "Subject: second", "", "second body", }, "\n") messages, err := parseMBOX(strings.NewReader(raw), 1<<20) if err != nil { t.Fatal(err) } if len(messages) != 2 { t.Fatalf("messages=%d", len(messages)) } if !bytes.Contains(messages[0], []byte("Subject: first")) || !bytes.Contains(messages[0], []byte("From escaped body line")) { t.Fatalf("first message=%q", messages[0]) } if !bytes.Contains(messages[1], []byte("Subject: second")) { t.Fatalf("second message=%q", messages[1]) } } func TestMailImportExportAndOwnership(t *testing.T) { a := newTestApp(t) ts := httptest.NewServer(a.Router()) defer ts.Close() admin := &testClient{t: t, server: ts} var login map[string]any if code := admin.do("POST", "/api/auth/login", map[string]string{"email": "admin@lanqin.local", "password": "ChangeMe123!"}, &login); code != http.StatusOK { t.Fatalf("admin login=%d", code) } var domains struct { Items []Domain `json:"items"` } if code := admin.do("GET", "/api/admin/domains", nil, &domains); code != http.StatusOK || len(domains.Items) == 0 { t.Fatalf("domains code=%d items=%d", code, len(domains.Items)) } ownerMailbox := createTestMailbox(t, admin, domains.Items[0].ID, "transfer-owner", "Transfer Owner", "Password123!", nil) otherMailbox := createTestMailbox(t, admin, domains.Items[0].ID, "transfer-other", "Transfer Other", "Password123!", nil) owner := &testClient{t: t, server: ts} if code := owner.do("POST", "/api/auth/login", map[string]string{"email": ownerMailbox.Address, "password": "Password123!"}, &login); code != http.StatusOK { t.Fatalf("owner login=%d", code) } eml := []byte("From: sender@example.com\r\nTo: " + ownerMailbox.Address + "\r\nSubject: imported message\r\nMessage-ID: \r\nContent-Type: text/plain; charset=utf-8\r\n\r\nhello import") var imported struct { Imported int `json:"imported"` Skipped int `json:"skipped"` Errors []string `json:"errors"` } if code := doMailImport(t, owner, ownerMailbox.ID, "Inbox", map[string][]byte{"message.eml": eml}, &imported); code != http.StatusOK || imported.Imported != 1 || imported.Skipped != 0 { t.Fatalf("import code=%d response=%+v", code, imported) } var list struct { Items []MailMessage `json:"items"` } if code := owner.do("GET", "/api/mail/messages?folder=Inbox&mailboxId="+ownerMailbox.ID, nil, &list); code != http.StatusOK || len(list.Items) != 1 || list.Items[0].Subject != "imported message" { t.Fatalf("list code=%d items=%+v", code, list.Items) } status, archive := getMailExport(t, owner, "/api/mail/export?view=folder&folder=Inbox&mailboxId="+ownerMailbox.ID) if status != http.StatusOK { t.Fatalf("export status=%d body=%q", status, archive) } zr, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive))) if err != nil { t.Fatal(err) } if len(zr.File) != 1 { t.Fatalf("zip entries=%d", len(zr.File)) } entry, err := zr.File[0].Open() if err != nil { t.Fatal(err) } exported, err := io.ReadAll(entry) entry.Close() if err != nil || !bytes.Contains(exported, []byte("Subject: imported message")) { t.Fatalf("exported message err=%v raw=%q", err, exported) } var denied map[string]any if code := doMailImport(t, owner, otherMailbox.ID, "Inbox", map[string][]byte{"message.eml": eml}, &denied); code != http.StatusNotFound { t.Fatalf("cross-mailbox import code=%d", code) } status, _ = getMailExport(t, owner, "/api/mail/export?view=unknown") if status != http.StatusForbidden { t.Fatalf("unknown export status=%d", status) } } func doMailImport(t *testing.T, client *testClient, mailboxID, folder string, files map[string][]byte, out any) int { t.Helper() var body bytes.Buffer writer := multipart.NewWriter(&body) _ = writer.WriteField("mailboxId", mailboxID) _ = writer.WriteField("folder", folder) for name, content := range files { part, err := writer.CreateFormFile("files", name) if err != nil { t.Fatal(err) } if _, err := part.Write(content); err != nil { t.Fatal(err) } } if err := writer.Close(); err != nil { t.Fatal(err) } req, err := http.NewRequest(http.MethodPost, client.server.URL+"/api/mail/import", &body) if err != nil { t.Fatal(err) } req.Header.Set("Content-Type", writer.FormDataContentType()) if client.cookie != nil { req.AddCookie(client.cookie) } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if out != nil { if err := json.NewDecoder(resp.Body).Decode(out); err != nil { t.Fatalf("decode import response: %v", err) } } return resp.StatusCode } func getMailExport(t *testing.T, client *testClient, path string) (int, []byte) { t.Helper() req, err := http.NewRequest(http.MethodGet, client.server.URL+path, nil) if err != nil { t.Fatal(err) } if client.cookie != nil { req.AddCookie(client.cookie) } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) } return resp.StatusCode, body }