package cli import ( "flag" "fmt" "io" "strconv" "strings" "time" "git.dcglab.co.uk/steve/emcli/internal/crypto" "git.dcglab.co.uk/steve/emcli/internal/mail" "git.dcglab.co.uk/steve/emcli/internal/store" ) func realMailer(acc store.Account) (Mailer, error) { c, err := mail.Dial(mail.IMAPConfig{ Host: acc.IMAPHost, Port: acc.IMAPPort, Security: acc.IMAPSecurity, Username: acc.Username, Password: acc.Password, }) if err != nil { return nil, err } return c, nil } // openStore loads the key and opens the DB, returning a human-readable error string. func openStore() (*store.Store, error) { key, err := crypto.KeyFromEnv() if err != nil { return nil, err } path, err := store.DefaultDBPath() if err != nil { return nil, err } return store.Open(path, key) } func newDepsLive(st *store.Store, out io.Writer) Deps { return Deps{Store: st, Dial: realMailer, Now: time.Now, Out: out} } // Run routes a command line and returns an exit code. func Run(args []string, out, errOut io.Writer) int { if len(args) == 0 { fmt.Fprintln(errOut, "emcli: no command given") return 2 } cmd, rest := args[0], args[1:] switch cmd { case "list", "get", "search", "ack": return runAgent(cmd, rest, out, errOut) case "account": return runAccount(rest, out, errOut) case "whitelist": return runWhitelist(rest, out, errOut) default: fmt.Fprintf(errOut, "emcli: unknown command %q\n", cmd) return 2 } } // runAgent handles JSON-emitting commands. Errors are emitted as JSON envelopes. func runAgent(cmd string, args []string, out, errOut io.Writer) int { fs := flag.NewFlagSet(cmd, flag.ContinueOnError) fs.SetOutput(errOut) account := fs.String("account", "", "account name") folder := fs.String("folder", "INBOX", "folder/mailbox") onlyNew := fs.Bool("new", false, "only new (unacked) messages") limit := fs.Int("limit", 50, "max results (cap 500)") uid := fs.Uint("uid", 0, "message UID (get)") before := fs.Uint("before", 0, "list: UID cursor, older than") since := fs.Uint("since", 0, "list: UID cursor, newer than") from := fs.String("from", "", "search: sender") subjectContains := fs.String("subject-contains", "", "search: subject substring") text := fs.String("text", "", "search: full-text") sinceDate := fs.String("since-date", "", "search: RFC3339 date lower bound") beforeDate := fs.String("before-date", "", "search: RFC3339 date upper bound") ackUIDs := fs.String("uid-list", "", "ack: comma-separated UIDs") if err := fs.Parse(args); err != nil { _ = Failure(CodeUsage, err.Error()).Write(out) return 2 } if *limit > 500 { *limit = 500 } if *account == "" { _ = Failure(CodeUsage, "--account is required").Write(out) return 2 } st, err := openStore() if err != nil { _ = Failure(CodeConfig, err.Error()).Write(out) return 1 } defer st.Close() _, _ = st.PurgeAudit(time.Now()) d := newDepsLive(st, out) switch cmd { case "list": if err := ListCmd(d, *account, *folder, *onlyNew, u32(*before), u32(*since), *limit); err != nil { return 1 } case "get": if *uid == 0 { _ = Failure(CodeUsage, "--uid is required").Write(out) return 2 } if err := GetCmd(d, *account, *folder, u32(*uid)); err != nil { return 1 } case "search": sc := mail.SearchCriteria{From: *from, SubjectContains: *subjectContains, Text: *text} if *sinceDate != "" { if tm, err := time.Parse(time.RFC3339, *sinceDate); err == nil { sc.Since = tm } } if *beforeDate != "" { if tm, err := time.Parse(time.RFC3339, *beforeDate); err == nil { sc.Before = tm } } if err := SearchCmd(d, *account, *folder, sc, *limit); err != nil { return 1 } case "ack": uids, err := parseUIDList(*ackUIDs) if err != nil { _ = Failure(CodeUsage, err.Error()).Write(out) return 2 } if err := AckCmd(d, *account, *folder, uids); err != nil { return 1 } } return 0 } func u32(u uint) uint32 { return uint32(u) } func parseUIDList(s string) ([]uint32, error) { if strings.TrimSpace(s) == "" { return nil, fmt.Errorf("--uid-list is required") } var out []uint32 for _, part := range strings.Split(s, ",") { n, err := strconv.ParseUint(strings.TrimSpace(part), 10, 32) if err != nil { return nil, fmt.Errorf("invalid uid %q", part) } out = append(out, uint32(n)) } return out, nil }