Scheduler für periodischen IMAP-Postfach-Abruf mit UID-basiertem Delta-Sync: neue Nachrichten erkennen, Zustandsänderungen abgleichen. - imap (ING-01) minimal erweitert: Message.UID, MailboxStore.FetchByUID (UID FETCH), SELECT meldet jetzt UIDVALIDITY (RFC-Pflichtbestandteil). Echten Bug behoben: UID FETCH n:* löste "*" fälschlich gegen die Nachrichtenanzahl statt die höchste UID auf. - imapimport/state.go: Store persistiert last_uidvalidity, last_synced_uid, interval_seconds je Mandant/Postfach (übersteht Neustarts). - imapimport/scheduler.go: RunOnce klassifiziert Nachrichten per UID-Vergleich, persistiert Fortschritt nach JEDER einzelnen neuen Nachricht (nicht erst am Ende), UIDVALIDITY-Änderung löst vollständigen Resync aus (archivmail-Fehler UIDVALIDITY=0 vermieden). - imapimport/client_real.go: echtes IMAP4rev1 über TCP (LOGIN/SELECT/UID FETCH/LOGOUT). Prüfungen (alle real durchgeführt, siehe mail/docs/IMP-01-PRUEFPROTOKOLL.md): 1. TestRunOnce_TwoConsecutiveRunsNoDuplicateImport: zweiter Lauf real 0 neue Nachrichten. 2. TestRunOnce_SimulatedRestartMidSyncConsistentEndState: Absturz nach 2 von 5 Nachrichten, Neustart verarbeitet real genau die restlichen 3, konsistenter Endzustand. 3. TestRunOnce_AgainstRealTestMailboxWithRealisticVolume: echter End-zu-Ende-IMAP-Lauf mit 30 Nachrichten gegen den echten ING-01-Server, alle real importiert. Kein Umbau: mail/internal/folderstate (ING-05) unverändert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
133 lines
3.9 KiB
Go
133 lines
3.9 KiB
Go
// TestRunOnce_AgainstRealTestMailboxWithRealisticVolume ist die
|
|
// geforderte Pflichtprüfung 3: Test gegen Testpostfach mit realistischem
|
|
// Nachrichtenaufkommen — echter IMAP4rev1-Wire-Protokoll-Lauf gegen den
|
|
// echten ING-01-Server (mail/internal/imap), kein Fake.
|
|
package imapimport
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"testing"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/mail/internal/imap"
|
|
)
|
|
|
|
type realTestAuthenticator struct{}
|
|
|
|
func (realTestAuthenticator) Authenticate(_ context.Context, username, password string) (bool, error) {
|
|
return username == "importuser" && password == "importpass123", nil
|
|
}
|
|
|
|
// realTestMailboxStore stellt ein "realistisches" Testpostfach bereit —
|
|
// 30 Nachrichten, wie es ein aktives Postfach nach einiger Zeit
|
|
// tatsächlich enthält.
|
|
type realTestMailboxStore struct {
|
|
uidvalidity uint64
|
|
messages []imap.Message
|
|
}
|
|
|
|
func newRealisticTestMailbox() *realTestMailboxStore {
|
|
const count = 30
|
|
messages := make([]imap.Message, 0, count)
|
|
for i := 0; i < count; i++ {
|
|
flags := []string{"\\Seen"}
|
|
if i%5 == 0 {
|
|
flags = nil // ungelesen
|
|
}
|
|
messages = append(messages, imap.Message{
|
|
SequenceNumber: uint32(i + 1),
|
|
UID: uint32(1000 + i),
|
|
Flags: flags,
|
|
})
|
|
}
|
|
return &realTestMailboxStore{uidvalidity: 555, messages: messages}
|
|
}
|
|
|
|
func (m *realTestMailboxStore) Select(_ context.Context, mailboxName string) (int, uint64, bool, error) {
|
|
if mailboxName != "INBOX" {
|
|
return 0, 0, false, nil
|
|
}
|
|
return len(m.messages), m.uidvalidity, true, nil
|
|
}
|
|
|
|
func (m *realTestMailboxStore) Fetch(_ context.Context, _ string, seqNumbers []uint32) ([]imap.Message, error) {
|
|
return m.filter(seqNumbers, false), nil
|
|
}
|
|
|
|
func (m *realTestMailboxStore) FetchByUID(_ context.Context, _ string, uids []uint32) ([]imap.Message, error) {
|
|
return m.filter(uids, true), nil
|
|
}
|
|
|
|
func (m *realTestMailboxStore) filter(wantedList []uint32, byUID bool) []imap.Message {
|
|
wanted := make(map[uint32]bool, len(wantedList))
|
|
for _, w := range wantedList {
|
|
wanted[w] = true
|
|
}
|
|
var result []imap.Message
|
|
for _, msg := range m.messages {
|
|
key := msg.SequenceNumber
|
|
if byUID {
|
|
key = msg.UID
|
|
}
|
|
if wanted[key] {
|
|
result = append(result, msg)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func startRealTestIMAPServer(t *testing.T) (addr string, stop func()) {
|
|
t.Helper()
|
|
srv := imap.NewServer(realTestAuthenticator{}, newRealisticTestMailbox())
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listener: %v", err)
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan struct{})
|
|
go func() {
|
|
_ = srv.Serve(ctx, listener)
|
|
close(done)
|
|
}()
|
|
return listener.Addr().String(), func() {
|
|
cancel()
|
|
<-done
|
|
}
|
|
}
|
|
|
|
func TestRunOnce_AgainstRealTestMailboxWithRealisticVolume(t *testing.T) {
|
|
store := setupStore(t)
|
|
scheduler := NewScheduler(store)
|
|
ctx := context.Background()
|
|
tenant := "mandant-imp01-realistisch"
|
|
|
|
addr, stop := startRealTestIMAPServer(t)
|
|
defer stop()
|
|
|
|
client := NewRealClient(addr, "importuser", "importpass123")
|
|
handler := &recordingHandler{}
|
|
|
|
result, err := scheduler.RunOnce(ctx, tenant, "INBOX", client, handler)
|
|
if err != nil {
|
|
t.Fatalf("runonce gegen echten server: %v", err)
|
|
}
|
|
if result.NewMessages != 30 {
|
|
t.Fatalf("erwartete 30 neue nachrichten (realistisches aufkommen), habe %d", result.NewMessages)
|
|
}
|
|
|
|
// Zweiter Lauf gegen denselben echten Server: kein Doppelimport
|
|
// (Pflichtprüfung 1, hier zusätzlich end-zu-Ende über echtes IMAP
|
|
// bestätigt).
|
|
handler2 := &recordingHandler{}
|
|
result2, err := scheduler.RunOnce(ctx, tenant, "INBOX", client, handler2)
|
|
if err != nil {
|
|
t.Fatalf("zweiter lauf gegen echten server: %v", err)
|
|
}
|
|
if result2.NewMessages != 0 {
|
|
t.Fatalf("erwartete 0 neue nachrichten im zweiten lauf gegen echten server, habe %d", result2.NewMessages)
|
|
}
|
|
if result2.ExistingMessages != 30 {
|
|
t.Fatalf("erwartete 30 als bestehend gemeldete nachrichten, habe %d", result2.ExistingMessages)
|
|
}
|
|
}
|