IMP-01: imap-postfach-abruf-scheduler
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
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
0d3779d03e
commit
e9947b1e28
@@ -28,9 +28,9 @@ type fakeMailboxStore struct {
|
||||
mailboxes map[string][]Message
|
||||
}
|
||||
|
||||
func (f fakeMailboxStore) Select(_ context.Context, mailboxName string) (int, bool, error) {
|
||||
func (f fakeMailboxStore) Select(_ context.Context, mailboxName string) (int, uint64, bool, error) {
|
||||
msgs, ok := f.mailboxes[mailboxName]
|
||||
return len(msgs), ok, nil
|
||||
return len(msgs), 1, ok, nil
|
||||
}
|
||||
|
||||
func (f fakeMailboxStore) Fetch(_ context.Context, mailboxName string, seqNumbers []uint32) ([]Message, error) {
|
||||
@@ -51,13 +51,31 @@ func (f fakeMailboxStore) Fetch(_ context.Context, mailboxName string, seqNumber
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (f fakeMailboxStore) FetchByUID(_ context.Context, mailboxName string, uids []uint32) ([]Message, error) {
|
||||
msgs, ok := f.mailboxes[mailboxName]
|
||||
if !ok {
|
||||
return nil, errors.New("imap: postfach nicht gefunden")
|
||||
}
|
||||
wanted := make(map[uint32]bool, len(uids))
|
||||
for _, u := range uids {
|
||||
wanted[u] = true
|
||||
}
|
||||
var result []Message
|
||||
for _, m := range msgs {
|
||||
if wanted[m.UID] {
|
||||
result = append(result, m)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func startTestServer(t *testing.T) (addr string, stop func()) {
|
||||
t.Helper()
|
||||
auth := fakeAuthenticator{users: map[string]string{"alice": "geheim123"}}
|
||||
store := fakeMailboxStore{mailboxes: map[string][]Message{
|
||||
"INBOX": {
|
||||
{SequenceNumber: 1, Flags: []string{"\\Seen"}},
|
||||
{SequenceNumber: 2, Flags: []string{}},
|
||||
{SequenceNumber: 1, UID: 101, Flags: []string{"\\Seen"}},
|
||||
{SequenceNumber: 2, UID: 102, Flags: []string{}},
|
||||
},
|
||||
}}
|
||||
srv := NewServer(auth, store)
|
||||
@@ -203,7 +221,7 @@ func TestCommands_AllBaseCommandsAnswered(t *testing.T) {
|
||||
}
|
||||
|
||||
_, lines = c.sendTagged(t, "FETCH 1 (FLAGS)")
|
||||
if !containsSubstring(lines, "FETCH (FLAGS") {
|
||||
if !containsSubstring(lines, "FETCH (UID") {
|
||||
t.Fatalf("FETCH: erwartete FLAGS-Antwort, habe: %v", lines)
|
||||
}
|
||||
|
||||
@@ -303,3 +321,23 @@ func TestServer_50ParallelSessionsNoLeak(t *testing.T) {
|
||||
t.Errorf("parallele sitzung fehlgeschlagen: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCommands_UIDFetchReturnsUID belegt die für IMP-01 nötige
|
||||
// UID-FETCH-Erweiterung: reale UID-basierte Abfrage über echtes TCP.
|
||||
func TestCommands_UIDFetchReturnsUID(t *testing.T) {
|
||||
addr, stop := startTestServer(t)
|
||||
defer stop()
|
||||
c := dial(t, addr)
|
||||
defer c.close()
|
||||
|
||||
c.sendTagged(t, "LOGIN alice geheim123")
|
||||
c.sendTagged(t, "SELECT INBOX")
|
||||
|
||||
_, lines := c.sendTagged(t, "UID FETCH 101:102 (FLAGS)")
|
||||
if !containsSubstring(lines, "UID 101") || !containsSubstring(lines, "UID 102") {
|
||||
t.Fatalf("erwartete beide UIDs in der antwort, habe: %v", lines)
|
||||
}
|
||||
if !strings.Contains(lines[len(lines)-1], "OK") {
|
||||
t.Fatalf("erwartete OK-abschluss, habe: %v", lines)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user