Add Gmail --query to mail/sync (default in:inbox) (#4)
Tests / Test (push) Skipped
Tests / Release (semver) (push) Skipped
Tests / Test (push) Skipped
Tests / Release (semver) (push) Skipped
* 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:
committed by
GitHub
co-authored by
GitHub
parent
c96c393a4a
commit
140d86a4b9
@@ -52,6 +52,7 @@ var/ kb.lbug, var/mail/*, caches (gitignored)
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
bin/mail/sync.go --source onlyoffice,gmail --workers 8 --out var/mail # raw message.json + attachments
|
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/import --from-raw var/mail # message.json → message.md (convert only)
|
||||||
bin/mail/index_mail # rebuild brain incl. all mail (fresh DB)
|
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")
|
offset = fs.Int("offset", 0, "skip first N messages per source")
|
||||||
force = fs.Bool("force", false, "overwrite existing message.json + attachments")
|
force = fs.Bool("force", false, "overwrite existing message.json + attachments")
|
||||||
dryRun = fs.Bool("dry-run", false, "list message counts without writing")
|
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)")
|
srcs = fs.String("source", "onlyoffice", "comma list: onlyoffice,gmail (default onlyoffice)")
|
||||||
help = fs.Bool("help", false, "usage")
|
help = fs.Bool("help", false, "usage")
|
||||||
)
|
)
|
||||||
@@ -60,6 +61,7 @@ func ParseCLI(args []string) (CLIConfig, int, error) {
|
|||||||
Offset: *offset,
|
Offset: *offset,
|
||||||
Force: *force,
|
Force: *force,
|
||||||
DryRun: *dryRun,
|
DryRun: *dryRun,
|
||||||
|
Query: *query,
|
||||||
Policy: RetryPolicy{},
|
Policy: RetryPolicy{},
|
||||||
}
|
}
|
||||||
cli := CLIConfig{Sync: cfg, Env: *env, Sources: *srcs}
|
cli := CLIConfig{Sync: cfg, Env: *env, Sources: *srcs}
|
||||||
@@ -95,7 +97,7 @@ func Main(args []string) int {
|
|||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
if cli.Help {
|
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
|
return 0
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Hour)
|
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
|
Offset int // skip first N messages per source
|
||||||
Force bool // overwrite existing message.json + attachments
|
Force bool // overwrite existing message.json + attachments
|
||||||
DryRun bool // list without writing
|
DryRun bool // list without writing
|
||||||
|
Query string // Gmail search query; default in:inbox
|
||||||
Policy RetryPolicy
|
Policy RetryPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,9 +137,17 @@ type ooSource struct {
|
|||||||
c *OOClient
|
c *OOClient
|
||||||
page int
|
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 {
|
type gmailSource struct {
|
||||||
c *GmailClient
|
c gmailAPI
|
||||||
cur string
|
cur string
|
||||||
|
query string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ooSource) Folder() string { return "inbox" }
|
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) {
|
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
|
return ids, next, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +220,7 @@ func Run(ctx context.Context, cfg SyncConfig) (*SyncStats, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("gmail init: %w", err)
|
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 {
|
if len(sources) == 0 {
|
||||||
return nil, errors.New("sync: no source configured (need OO, Gmail, or both)")
|
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 {
|
func b64(s string) string {
|
||||||
return base64.URLEncoding.EncodeToString([]byte(s))
|
return base64.URLEncoding.EncodeToString([]byte(s))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user