Add Gmail --query to mail/sync (default in:inbox) (#4)
* Add --query to Gmail mail/sync instead of always listing in:inbox. Callers keep the search string; default remains in:inbox. * Document Gmail --query on the mail/sync pipeline. * test(mail): assert Gmail --query reaches ListIDs, not only the CLI flag. ParseCLI coverage left a hole: an empty query still has to become in:inbox and a custom q has to be the string the client lists with.
This commit is contained in:
@@ -52,6 +52,7 @@ var/ kb.lbug, var/mail/*, caches (gitignored)
|
||||
|
||||
```bash
|
||||
bin/mail/sync.go --source onlyoffice,gmail --workers 8 --out var/mail # raw message.json + attachments
|
||||
bin/mail/sync.go --source gmail --query 'from:example.com' --out var/mail # Gmail search (default in:inbox)
|
||||
bin/mail/import --from-raw var/mail # message.json → message.md (convert only)
|
||||
bin/mail/index_mail # rebuild brain incl. all mail (fresh DB)
|
||||
```
|
||||
|
||||
@@ -32,6 +32,7 @@ func ParseCLI(args []string) (CLIConfig, int, error) {
|
||||
offset = fs.Int("offset", 0, "skip first N messages per source")
|
||||
force = fs.Bool("force", false, "overwrite existing message.json + attachments")
|
||||
dryRun = fs.Bool("dry-run", false, "list message counts without writing")
|
||||
query = fs.String("query", "in:inbox", "Gmail search query (gmail source only)")
|
||||
srcs = fs.String("source", "onlyoffice", "comma list: onlyoffice,gmail (default onlyoffice)")
|
||||
help = fs.Bool("help", false, "usage")
|
||||
)
|
||||
@@ -60,6 +61,7 @@ func ParseCLI(args []string) (CLIConfig, int, error) {
|
||||
Offset: *offset,
|
||||
Force: *force,
|
||||
DryRun: *dryRun,
|
||||
Query: *query,
|
||||
Policy: RetryPolicy{},
|
||||
}
|
||||
cli := CLIConfig{Sync: cfg, Env: *env, Sources: *srcs}
|
||||
@@ -95,7 +97,7 @@ func Main(args []string) int {
|
||||
return code
|
||||
}
|
||||
if cli.Help {
|
||||
fmt.Fprintln(os.Stderr, "usage: bin/mail/sync.go [--source onlyoffice,gmail] [--limit N] [--offset N] [--workers N] [--force] [--dry-run]")
|
||||
fmt.Fprintln(os.Stderr, "usage: bin/mail/sync.go [--source onlyoffice,gmail] [--query GMAIL_Q] [--limit N] [--offset N] [--workers N] [--force] [--dry-run]")
|
||||
return 0
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Hour)
|
||||
|
||||
+17
-4
@@ -112,6 +112,7 @@ type SyncConfig struct {
|
||||
Offset int // skip first N messages per source
|
||||
Force bool // overwrite existing message.json + attachments
|
||||
DryRun bool // list without writing
|
||||
Query string // Gmail search query; default in:inbox
|
||||
Policy RetryPolicy
|
||||
}
|
||||
|
||||
@@ -136,9 +137,17 @@ type ooSource struct {
|
||||
c *OOClient
|
||||
page int
|
||||
}
|
||||
// gmailAPI is the Gmail client surface gmailSource needs. *GmailClient implements it.
|
||||
type gmailAPI interface {
|
||||
ListIDs(ctx context.Context, q string, maxIDs int, pageToken string) ([]string, string, error)
|
||||
GetMessage(ctx context.Context, id string) (*Message, error)
|
||||
DownloadAttachment(ctx context.Context, msgID, attID string) ([]byte, error)
|
||||
}
|
||||
|
||||
type gmailSource struct {
|
||||
c *GmailClient
|
||||
cur string
|
||||
c gmailAPI
|
||||
cur string
|
||||
query string
|
||||
}
|
||||
|
||||
func (s *ooSource) Folder() string { return "inbox" }
|
||||
@@ -171,7 +180,11 @@ func (s *ooSource) DownloadAttachment(ctx context.Context, msg *Message, att Att
|
||||
}
|
||||
|
||||
func (s *gmailSource) ListIDs(ctx context.Context, limit int, cursor string) ([]string, string, error) {
|
||||
ids, next, err := s.c.ListIDs(ctx, "in:inbox", limit, cursor)
|
||||
q := s.query
|
||||
if q == "" {
|
||||
q = "in:inbox"
|
||||
}
|
||||
ids, next, err := s.c.ListIDs(ctx, q, limit, cursor)
|
||||
return ids, next, err
|
||||
}
|
||||
|
||||
@@ -207,7 +220,7 @@ func Run(ctx context.Context, cfg SyncConfig) (*SyncStats, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gmail init: %w", err)
|
||||
}
|
||||
sources = append(sources, &gmailSource{c: gm})
|
||||
sources = append(sources, &gmailSource{c: gm, query: cfg.Query})
|
||||
}
|
||||
if len(sources) == 0 {
|
||||
return nil, errors.New("sync: no source configured (need OO, Gmail, or both)")
|
||||
|
||||
@@ -221,6 +221,71 @@ func TestCollectParts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type fakeGmailAPI struct {
|
||||
lastQ string
|
||||
lastLimit int
|
||||
ids []string
|
||||
}
|
||||
|
||||
func (f *fakeGmailAPI) ListIDs(_ context.Context, q string, maxIDs int, _ string) ([]string, string, error) {
|
||||
f.lastQ = q
|
||||
f.lastLimit = maxIDs
|
||||
return f.ids, "", nil
|
||||
}
|
||||
func (f *fakeGmailAPI) GetMessage(context.Context, string) (*Message, error) {
|
||||
return nil, errors.New("unused")
|
||||
}
|
||||
func (f *fakeGmailAPI) DownloadAttachment(context.Context, string, string) ([]byte, error) {
|
||||
return nil, errors.New("unused")
|
||||
}
|
||||
|
||||
func TestGmailSourcePassesQueryToListIDs(t *testing.T) {
|
||||
fake := &fakeGmailAPI{ids: []string{"m1"}}
|
||||
src := &gmailSource{c: fake, query: "from:alice@example.com"}
|
||||
ids, _, err := src.ListIDs(context.Background(), 10, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fake.lastQ != "from:alice@example.com" {
|
||||
t.Fatalf("ListIDs q=%q, want from:alice@example.com", fake.lastQ)
|
||||
}
|
||||
if fake.lastLimit != 10 {
|
||||
t.Fatalf("ListIDs limit=%d, want 10", fake.lastLimit)
|
||||
}
|
||||
if len(ids) != 1 || ids[0] != "m1" {
|
||||
t.Fatalf("ids=%v", ids)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGmailSourceEmptyQueryDefaultsToInbox(t *testing.T) {
|
||||
fake := &fakeGmailAPI{}
|
||||
src := &gmailSource{c: fake, query: ""}
|
||||
if _, _, err := src.ListIDs(context.Background(), 5, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fake.lastQ != "in:inbox" {
|
||||
t.Fatalf("empty query q=%q, want in:inbox", fake.lastQ)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCLIGmailQuery(t *testing.T) {
|
||||
cli, code, err := ParseCLI([]string{
|
||||
"--source", "gmail",
|
||||
"--query", "from:letrado@example.com",
|
||||
"--out", t.TempDir(),
|
||||
"--dry-run",
|
||||
})
|
||||
if err != nil || code != 0 {
|
||||
t.Fatalf("ParseCLI: code=%d err=%v", code, err)
|
||||
}
|
||||
if cli.Sync.Query != "from:letrado@example.com" {
|
||||
t.Fatalf("query=%q", cli.Sync.Query)
|
||||
}
|
||||
if cli.Sync.Gmail == nil {
|
||||
t.Fatal("gmail source not configured")
|
||||
}
|
||||
}
|
||||
|
||||
func b64(s string) string {
|
||||
return base64.URLEncoding.EncodeToString([]byte(s))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user