Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
140d86a4b9 | ||
|
|
c96c393a4a | ||
|
|
3d0d95cf00 |
@@ -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)
|
||||
```
|
||||
@@ -89,7 +90,7 @@ parameter. Search before reading whole files.
|
||||
Test data must be synthetic (Alice, Bob, Charlie, Diana, example.com).
|
||||
3. **No credentials/secrets in commits.** API keys, tokens, passwords, session
|
||||
strings, phone numbers only in gitignored `.env` files, referenced by path.
|
||||
4. **Curasoft — no files, no mentions.** Remove all traces if found.
|
||||
4. **Curasoft, edelweiss — no files, no mentions.** Remove all traces if found.
|
||||
5. **Check git history before push.** If any commit contains leaks, rewrite
|
||||
history (rebase + force push) AND delete affected GitHub releases/tags.
|
||||
6. **`docs/chat-import-plan.md`** — reference Gitea issue, never embed secrets.
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LinkedInMCPSource struct {
|
||||
userDataDir string
|
||||
limit int
|
||||
}
|
||||
|
||||
type lnInboxItem struct {
|
||||
ThreadID string `json:"thread_id"`
|
||||
Participants string `json:"participants"`
|
||||
LastMessage string `json:"last_message"`
|
||||
LastMessageDate string `json:"last_message_date"`
|
||||
Unread bool `json:"unread"`
|
||||
}
|
||||
|
||||
type lnInboxEnvelope struct {
|
||||
Results []lnInboxItem `json:"results"`
|
||||
HasMore bool `json:"hasMore"`
|
||||
}
|
||||
|
||||
type lnMessage struct {
|
||||
From string `json:"from"`
|
||||
Date string `json:"date"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type lnConvEnvelope struct {
|
||||
Results []lnMessage `json:"results"`
|
||||
HasMore bool `json:"hasMore"`
|
||||
TotalCount int `json:"total_count"`
|
||||
}
|
||||
|
||||
func NewLinkedInMCPSource(userDataDir string) *LinkedInMCPSource {
|
||||
return &LinkedInMCPSource{userDataDir: userDataDir}
|
||||
}
|
||||
|
||||
func (s *LinkedInMCPSource) Name() string { return "linkedin" }
|
||||
|
||||
func (s *LinkedInMCPSource) Sync(ctx context.Context, outDir string, limit int) error {
|
||||
if limit > 0 {
|
||||
s.limit = limit
|
||||
}
|
||||
|
||||
client, err := newLinkedInMCP(ctx, s.userDataDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("linkedin mcp: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
inbox, err := client.GetInbox(ctx, 50)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get_inbox: %w", err)
|
||||
}
|
||||
if len(inbox) == 0 {
|
||||
fmt.Println("chats: no LinkedIn conversations found")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("chats: found %d LinkedIn conversations\n", len(inbox))
|
||||
|
||||
for _, conv := range inbox {
|
||||
convID := sanitizeDir(conv.ThreadID)
|
||||
if convID == "" {
|
||||
convID = fmt.Sprintf("conv_%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
parts := strings.SplitN(conv.Participants, ",", 2)
|
||||
chatName := strings.TrimSpace(parts[0])
|
||||
if chatName == "" {
|
||||
chatName = convID
|
||||
}
|
||||
|
||||
chatDir := filepath.Join(outDir, "linkedin", convID)
|
||||
if err := os.MkdirAll(chatDir, 0755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "chats: mkdir %s: %v\n", chatDir, err)
|
||||
continue
|
||||
}
|
||||
|
||||
msgLimit := 100
|
||||
if s.limit > 0 {
|
||||
msgLimit = s.limit
|
||||
}
|
||||
|
||||
msgs, err := client.GetConversation(ctx, "", conv.ThreadID, msgLimit)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "chats: get_conversation %s: %v\n", convID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
jsonlPath := filepath.Join(chatDir, "messages.jsonl")
|
||||
f, err := os.Create(jsonlPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "chats: create %s: %v\n", jsonlPath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(f)
|
||||
written := 0
|
||||
for i, m := range msgs {
|
||||
text := m.Text
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ts := m.Date
|
||||
if t, err := time.Parse("2006-01-02T15:04:05Z07:00", m.Date); err == nil {
|
||||
ts = t.UTC().Format(time.RFC3339)
|
||||
} else if t, err := time.Parse(time.RFC3339, m.Date); err == nil {
|
||||
ts = t.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
chatMsg := Message{
|
||||
ID: fmt.Sprintf("li_%s_%d", convID, i),
|
||||
Timestamp: ts,
|
||||
From: m.From,
|
||||
Text: text,
|
||||
Platform: "linkedin",
|
||||
}
|
||||
if err := enc.Encode(chatMsg); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "chats: encode: %v\n", err)
|
||||
continue
|
||||
}
|
||||
written++
|
||||
}
|
||||
f.Close()
|
||||
|
||||
fmt.Printf("chats: synced %s (%s) — %d messages\n", chatName, convID, written)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type linkedInMCPClient struct {
|
||||
cmd *exec.Cmd
|
||||
stdin *bufio.Writer
|
||||
stdout *bufio.Scanner
|
||||
msgID int
|
||||
}
|
||||
|
||||
func newLinkedInMCP(ctx context.Context, userDataDir string) (*linkedInMCPClient, error) {
|
||||
args := []string{
|
||||
"mcp-server-linkedin@latest",
|
||||
"--user-data-dir", userDataDir,
|
||||
"--no-auto-import",
|
||||
"--transport", "stdio",
|
||||
"--login-timeout", "10",
|
||||
"--browser-wait", "1",
|
||||
"--browser-idle-timeout", "10",
|
||||
"--log-level", "ERROR",
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "uvx", args...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stdin pipe: %w", err)
|
||||
}
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stdout pipe: %w", err)
|
||||
}
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, fmt.Errorf("start: %w", err)
|
||||
}
|
||||
|
||||
c := &linkedInMCPClient{
|
||||
cmd: cmd,
|
||||
stdin: bufio.NewWriter(stdin),
|
||||
stdout: bufio.NewScanner(stdout),
|
||||
msgID: 0,
|
||||
}
|
||||
c.stdout.Buffer(make([]byte, 1<<20), 1<<20)
|
||||
|
||||
if err := c.initialize(ctx); err != nil {
|
||||
c.Close()
|
||||
return nil, fmt.Errorf("init: %w", err)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *linkedInMCPClient) nextID() int {
|
||||
c.msgID++
|
||||
return c.msgID
|
||||
}
|
||||
|
||||
func (c *linkedInMCPClient) initialize(ctx context.Context) error {
|
||||
params := map[string]interface{}{
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": map[string]interface{}{},
|
||||
"clientInfo": map[string]string{
|
||||
"name": "chats-sync",
|
||||
"version": "0.1.0",
|
||||
},
|
||||
}
|
||||
_, err := c.send(ctx, "initialize", params)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *linkedInMCPClient) send(ctx context.Context, method string, params interface{}) (json.RawMessage, error) {
|
||||
id := c.nextID()
|
||||
req := map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
"method": method,
|
||||
}
|
||||
if params != nil {
|
||||
req["params"] = params
|
||||
}
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal: %w", err)
|
||||
}
|
||||
|
||||
if _, err := c.stdin.Write(body); err != nil {
|
||||
return nil, fmt.Errorf("write: %w", err)
|
||||
}
|
||||
if err := c.stdin.WriteByte('\n'); err != nil {
|
||||
return nil, fmt.Errorf("newline: %w", err)
|
||||
}
|
||||
if err := c.stdin.Flush(); err != nil {
|
||||
return nil, fmt.Errorf("flush: %w", err)
|
||||
}
|
||||
|
||||
for c.stdout.Scan() {
|
||||
line := c.stdout.Text()
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
var resp struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID int `json:"id"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
Error *struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(line), &resp); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal: %w\nline: %s", err, line[:min(len(line), 500)])
|
||||
}
|
||||
if resp.Error != nil {
|
||||
return nil, fmt.Errorf("rpc error %d: %s", resp.Error.Code, resp.Error.Message)
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no response: %w", c.stdout.Err())
|
||||
}
|
||||
|
||||
func (c *linkedInMCPClient) GetInbox(ctx context.Context, limit int) ([]lnInboxItem, error) {
|
||||
params := map[string]interface{}{
|
||||
"limit": limit,
|
||||
}
|
||||
result, err := c.send(ctx, "tools/call", map[string]interface{}{
|
||||
"name": "get_inbox",
|
||||
"arguments": params,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var toolRes struct {
|
||||
Content []struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
IsError bool `json:"isError"`
|
||||
}
|
||||
if err := json.Unmarshal(result, &toolRes); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal tool: %w", err)
|
||||
}
|
||||
if toolRes.IsError {
|
||||
return nil, fmt.Errorf("get_inbox error")
|
||||
}
|
||||
if len(toolRes.Content) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
text := toolRes.Content[0].Text
|
||||
var env lnInboxEnvelope
|
||||
if err := json.Unmarshal([]byte(text), &env); err != nil {
|
||||
var arr []lnInboxItem
|
||||
if err2 := json.Unmarshal([]byte(text), &arr); err2 == nil {
|
||||
return arr, nil
|
||||
}
|
||||
return nil, fmt.Errorf("parse inbox: %w", err)
|
||||
}
|
||||
return env.Results, nil
|
||||
}
|
||||
|
||||
func (c *linkedInMCPClient) GetConversation(ctx context.Context, username, threadID string, limit int) ([]lnMessage, error) {
|
||||
params := map[string]interface{}{
|
||||
"linkedin_username": username,
|
||||
"thread_id": threadID,
|
||||
"index": limit,
|
||||
}
|
||||
result, err := c.send(ctx, "tools/call", map[string]interface{}{
|
||||
"name": "get_conversation",
|
||||
"arguments": params,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var toolRes struct {
|
||||
Content []struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
IsError bool `json:"isError"`
|
||||
}
|
||||
if err := json.Unmarshal(result, &toolRes); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal tool: %w", err)
|
||||
}
|
||||
if toolRes.IsError {
|
||||
return nil, nil
|
||||
}
|
||||
if len(toolRes.Content) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
text := toolRes.Content[0].Text
|
||||
var env lnConvEnvelope
|
||||
if err := json.Unmarshal([]byte(text), &env); err != nil {
|
||||
var arr []lnMessage
|
||||
if err2 := json.Unmarshal([]byte(text), &arr); err2 == nil {
|
||||
return arr, nil
|
||||
}
|
||||
return nil, fmt.Errorf("parse conv: %w", err)
|
||||
}
|
||||
return env.Results, nil
|
||||
}
|
||||
|
||||
func (c *linkedInMCPClient) Close() error {
|
||||
if c.stdin != nil {
|
||||
c.stdin.Flush()
|
||||
}
|
||||
if c.cmd != nil && c.cmd.Process != nil {
|
||||
c.cmd.Process.Kill()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+1
-2
@@ -40,8 +40,7 @@ func main() {
|
||||
fmt.Fprintf(os.Stderr, "chats: WhatsApp not implemented yet\n")
|
||||
os.Exit(1)
|
||||
case "linkedin":
|
||||
fmt.Fprintf(os.Stderr, "chats: LinkedIn not implemented yet\n")
|
||||
os.Exit(1)
|
||||
os.Exit(runSyncLinkedIn(platformArgs))
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "chats: unknown platform %q\n", platform)
|
||||
os.Exit(2)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func checkLinkedInSession(userDataDir string) (bool, error) {
|
||||
cmd := exec.Command("uvx", "mcp-server-linkedin@latest",
|
||||
"--user-data-dir", userDataDir,
|
||||
"--no-auto-import",
|
||||
"--status",
|
||||
)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("status check: %w\n%s", err, string(out))
|
||||
}
|
||||
return !strings.Contains(string(out), "✅"), nil
|
||||
}
|
||||
|
||||
func runSyncLinkedIn(args []string) int {
|
||||
fs := flag.NewFlagSet("chats sync linkedin", flag.ContinueOnError)
|
||||
limit := fs.Int("limit", 0, "max messages per conversation (0 = all)")
|
||||
help := fs.Bool("help", false, "")
|
||||
fs.SetOutput(os.Stderr)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 2
|
||||
}
|
||||
if *help {
|
||||
fmt.Fprintln(os.Stderr, "usage: chats sync linkedin [--limit N]")
|
||||
return 0
|
||||
}
|
||||
|
||||
userDataDir := envVar("LINKEDIN_USER_DATA_DIR", "")
|
||||
if userDataDir == "" {
|
||||
home, _ := os.UserHomeDir()
|
||||
userDataDir = home + "/.linkedin-mcp/profile"
|
||||
}
|
||||
|
||||
// Check session first
|
||||
loginNeeded, err := checkLinkedInSession(userDataDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "chats: linkedin status check: %v\n", err)
|
||||
}
|
||||
if loginNeeded {
|
||||
fmt.Fprintf(os.Stderr, "chats: LinkedIn session expired. Run:\n")
|
||||
fmt.Fprintf(os.Stderr, " uvx mcp-server-linkedin@latest --user-data-dir %s --login\n", userDataDir)
|
||||
fmt.Fprintf(os.Stderr, "Then retry 'chats sync linkedin'\n")
|
||||
return 1
|
||||
}
|
||||
|
||||
src := NewLinkedInMCPSource(userDataDir)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
start := time.Now()
|
||||
if err := src.Sync(ctx, chatsDir(), *limit); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "chats sync linkedin: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
fmt.Printf("chats sync linkedin: completed in %s\n", time.Since(start).Round(time.Millisecond))
|
||||
return 0
|
||||
}
|
||||
@@ -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