ee7bec11e3
The extraction seam landed on the previous branch but nothing fed it. This adds the daemon that does: every interval it opens one mailbox read-only (EXAMINE + BODY.PEEK, so reading leaves no \Seen behind), fetches the UIDs it has not handed over yet, and posts each message to core over ingest_mail. Core runs the model and writes task candidates; this daemon writes nothing and cannot create a reminder. It is a separate daemon because of the credential. mavpoll set the precedent with the zenmoney token (#125): the module talking to the third party holds the secret, reads it from a file so it never lands in argv, in docker-compose.yml or in shell history, and core never sees it. There is deliberately no -password flag, and a test asserts that. Off unless configured at both ends: without -password-file the daemon refuses to start, and if core has no email block the first ingest returns ErrUnknownMethod, which disables the reader instead of hammering a socket that will keep refusing. A seen-UID state file (0600, atomic write) keeps a restart from re-extracting the whole lookback window; correctness does not depend on it, since capture dedupes on normalised text. Logs are counts and UIDs — no subject, sender or body. Verified with an in-process IMAP server and a fake core: bulk mail is filtered before core is asked, seen UIDs are not re-fetched, a failed ingest is retried next poll, ErrUnknownMethod stops at the first message, and state survives a restart. The live half is untested by design — no IMAP credential exists on this box; setup is written up as QA steps. Vikunja #246
50 lines
1.4 KiB
Go
50 lines
1.4 KiB
Go
package email
|
|
|
|
import (
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestFetchSinceRun(t *testing.T) {
|
|
mk := func(subject string) string {
|
|
return "Subject: " + subject + "\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nbody\r\n"
|
|
}
|
|
f := &fakeIMAP{
|
|
uids: []uint32{1, 2, 3},
|
|
msgs: map[uint32]string{1: mk("one"), 2: mk("two"), 3: mk("three")},
|
|
}
|
|
fs := FetchSince{
|
|
Addr: "mail.example:993", User: "kami", Mailbox: "INBOX",
|
|
Timeout: 5 * time.Second,
|
|
Since: time.Date(2026, 7, 30, 0, 0, 0, 0, time.UTC),
|
|
Max: 2,
|
|
Skip: func(uid uint32) bool { return uid == 3 },
|
|
}
|
|
msgs, err := fs.RunWith("secret", func(addr string, timeout time.Duration) (*Conn, error) {
|
|
cli, srv := net.Pipe()
|
|
go f.serve(t, srv)
|
|
return NewConn(cli, timeout)
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("run: %v", err)
|
|
}
|
|
// Newest first, the already-seen UID skipped, Max respected.
|
|
if len(msgs) != 2 {
|
|
t.Fatalf("got %d messages, want 2: %+v", len(msgs), msgs)
|
|
}
|
|
if msgs[0].Subject != "two" || msgs[1].Subject != "one" {
|
|
t.Errorf("subjects = %q,%q, want two,one (newest first)", msgs[0].Subject, msgs[1].Subject)
|
|
}
|
|
if strings.Contains(strings.Join(f.cmds, " "), "UID FETCH 3") {
|
|
t.Error("a skipped UID must not be fetched again")
|
|
}
|
|
}
|
|
|
|
func TestFetchSinceRequiresConfig(t *testing.T) {
|
|
if _, err := (FetchSince{}).Run("secret"); err == nil {
|
|
t.Fatal("an unconfigured mailbox must not be read")
|
|
}
|
|
}
|