Files

221 lines
6.3 KiB
Go

package imapx
import (
"context"
"fmt"
"testing"
"time"
"github.com/emersion/go-imap/v2"
)
// seedInbox logs in as login/pass and APPENDs n minimal messages with unique
// Message-IDs into INBOX via a dedicated connection.
func seedInbox(t *testing.T, ep Endpoint, login, pass string, n int) {
t.Helper()
ctx := context.Background()
c, err := Connect(ctx, ep)
if err != nil {
t.Fatalf("seedInbox connect: %v", err)
}
defer func() { _ = c.Logout().Wait() }()
if err := c.Login(login, pass).Wait(); err != nil {
t.Fatalf("seedInbox login: %v", err)
}
for i := 0; i < n; i++ {
msg := fmt.Sprintf(
"From: sender@localhost\r\nTo: %s\r\nSubject: seed %d\r\nMessage-Id: <seed-%d-%p@localhost>\r\n\r\nBody %d\r\n",
login, i, i, &i, i,
)
buf := []byte(msg)
appendCmd := c.Append("INBOX", int64(len(buf)), nil)
if _, err := appendCmd.Write(buf); err != nil {
t.Fatalf("seedInbox write %d: %v", i, err)
}
if err := appendCmd.Close(); err != nil {
t.Fatalf("seedInbox close %d: %v", i, err)
}
if _, err := appendCmd.Wait(); err != nil {
t.Fatalf("seedInbox append %d: %v", i, err)
}
}
}
// seedInboxWithDate APPENDs a single message with a given subject and a
// KNOWN IMAP internal date (via AppendOptions.Time), so the test can assert
// the date survives the copy instead of silently becoming "now" on dst.
func seedInboxWithDate(t *testing.T, ep Endpoint, login, pass, subject string, when time.Time) {
t.Helper()
ctx := context.Background()
c, err := Connect(ctx, ep)
if err != nil {
t.Fatalf("seedInboxWithDate connect: %v", err)
}
defer func() { _ = c.Logout().Wait() }()
if err := c.Login(login, pass).Wait(); err != nil {
t.Fatalf("seedInboxWithDate login: %v", err)
}
msg := fmt.Sprintf(
"From: sender@localhost\r\nTo: %s\r\nSubject: %s\r\nMessage-Id: <%s@localhost>\r\n\r\nBody\r\n",
login, subject, subject,
)
buf := []byte(msg)
appendCmd := c.Append("INBOX", int64(len(buf)), &imap.AppendOptions{Time: when})
if _, err := appendCmd.Write(buf); err != nil {
t.Fatalf("seedInboxWithDate write: %v", err)
}
if err := appendCmd.Close(); err != nil {
t.Fatalf("seedInboxWithDate close: %v", err)
}
if _, err := appendCmd.Wait(); err != nil {
t.Fatalf("seedInboxWithDate append: %v", err)
}
}
// fetchInternalDateBySubject connects, selects INBOX and returns the
// InternalDate of the first message whose Envelope.Subject matches.
func fetchInternalDateBySubject(t *testing.T, ep Endpoint, login, pass, subject string) time.Time {
t.Helper()
ctx := context.Background()
c, err := Connect(ctx, ep)
if err != nil {
t.Fatalf("fetchInternalDateBySubject connect: %v", err)
}
defer func() { _ = c.Logout().Wait() }()
if err := c.Login(login, pass).Wait(); err != nil {
t.Fatalf("fetchInternalDateBySubject login: %v", err)
}
sel, err := c.Select("INBOX", &imap.SelectOptions{ReadOnly: true}).Wait()
if err != nil {
t.Fatalf("fetchInternalDateBySubject select: %v", err)
}
if sel.NumMessages == 0 {
t.Fatalf("fetchInternalDateBySubject: INBOX empty")
}
set := imap.SeqSet{imap.SeqRange{Start: 1, Stop: sel.NumMessages}}
msgs, err := c.Fetch(set, &imap.FetchOptions{
Envelope: true, InternalDate: true,
}).Collect()
if err != nil {
t.Fatalf("fetchInternalDateBySubject fetch: %v", err)
}
for _, m := range msgs {
if m.Envelope != nil && m.Envelope.Subject == subject {
return m.InternalDate
}
}
t.Fatalf("fetchInternalDateBySubject: subject %q not found among %d messages", subject, len(msgs))
return time.Time{}
}
// TestCopyFolderPreservesInternalDate proves CopyFolder threads the source
// message's IMAP internal date through to APPEND on dst, instead of letting
// dst stamp it with "now" at APPEND time.
func TestCopyFolderPreservesInternalDate(t *testing.T) {
ep := testEP(t)
ctx := context.Background()
const subject = "datecheck"
knownTime := time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC)
seedInboxWithDate(t, ep, "datesrc@localhost", "p", subject, knownTime)
src, err := Connect(ctx, ep)
if err != nil {
t.Fatal(err)
}
defer func() { _ = src.Logout().Wait() }()
if err := src.Login("datesrc@localhost", "p").Wait(); err != nil {
t.Fatal(err)
}
dst, err := Connect(ctx, ep)
if err != nil {
t.Fatal(err)
}
defer func() { _ = dst.Logout().Wait() }()
if err := dst.Login("datedst@localhost", "p").Wait(); err != nil {
t.Fatal(err)
}
deps := CopyDeps{
IsMigrated: func(string) (bool, error) { return false, nil },
MarkMigrated: func(_, _ string) error { return nil },
OnProgress: func(_, _ int) {},
}
r, err := CopyFolder(ctx, src, dst, "INBOX", "INBOX", deps)
if err != nil {
t.Fatalf("CopyFolder: %v", err)
}
if r.Copied != 1 {
t.Fatalf("copied=%d want 1", r.Copied)
}
got := fetchInternalDateBySubject(t, ep, "datedst@localhost", "p", subject).UTC().Truncate(time.Second)
want := knownTime.Truncate(time.Second)
if !got.Equal(want) {
t.Fatalf("internal date not preserved: got %v want %v", got, want)
}
}
// Требует два ящика на greenmail. Первый запуск копирует N, второй — 0 (все skipped).
func TestCopyFolderIdempotent(t *testing.T) {
ep := testEP(t) // plain greenmail
ctx := context.Background()
// подготовка: APPEND 2 письма в INBOX источника через отдельное соединение
seedInbox(t, ep, "src@localhost", "p", 2)
src, err := Connect(ctx, ep)
if err != nil {
t.Fatal(err)
}
defer func() { _ = src.Logout().Wait() }()
if err := src.Login("src@localhost", "p").Wait(); err != nil {
t.Fatal(err)
}
dst, err := Connect(ctx, ep)
if err != nil {
t.Fatal(err)
}
defer func() { _ = dst.Logout().Wait() }()
if err := dst.Login("dst@localhost", "p").Wait(); err != nil {
t.Fatal(err)
}
seen := map[string]bool{}
deps := CopyDeps{
IsMigrated: func(k string) (bool, error) { return seen[k], nil },
MarkMigrated: func(_, k string) error { seen[k] = true; return nil },
OnProgress: func(_, _ int) {},
}
r1, err := CopyFolder(ctx, src, dst, "INBOX", "INBOX", deps)
if err != nil {
t.Fatalf("run1: %v", err)
}
if r1.Copied != 2 {
t.Fatalf("run1 copied=%d want 2", r1.Copied)
}
r2, err := CopyFolder(ctx, src, dst, "INBOX", "INBOX", deps)
if err != nil {
t.Fatalf("run2: %v", err)
}
if r2.Copied != 0 || r2.Skipped != 2 {
t.Fatalf("run2 copied=%d skipped=%d want 0/2", r2.Copied, r2.Skipped)
}
}