package cli import ( "flag" "fmt" "io" "strconv" "git.dcglab.co.uk/steve/emcli/internal/store" "git.dcglab.co.uk/steve/emcli/internal/tui" ) // runAccount handles `account add|list`. Human-readable output (never JSON). func runAccount(args []string, out, errOut io.Writer) int { if len(args) == 0 { fmt.Fprintln(errOut, "usage: emcli account ") return 2 } sub, rest := args[0], args[1:] st, err := openStore() if err != nil { fmt.Fprintf(errOut, "emcli: %v\n", err) return 1 } defer st.Close() switch sub { case "add": if len(rest) == 0 { // no flags → interactive TUI form return addInteractive(st, tui.Fields{}, out, errOut) } fs := flag.NewFlagSet("account add", flag.ContinueOnError) fs.SetOutput(errOut) name := fs.String("name", "", "account name") mode := fs.String("mode", "RO", "RO|RW") host := fs.String("imap-host", "", "IMAP host") port := fs.Int("imap-port", 993, "IMAP port") sec := fs.String("imap-security", "tls", "tls|starttls") smtpHost := fs.String("smtp-host", "", "SMTP host (RW accounts)") smtpPort := fs.Int("smtp-port", 465, "SMTP port") smtpSec := fs.String("smtp-security", "tls", "tls|starttls") user := fs.String("username", "", "login username") pass := fs.String("password", "", "login password") subj := fs.String("subject-regex", "", "inbound subject filter") wlIn := fs.Bool("whitelist-in", false, "enable inbound whitelist") wlOut := fs.Bool("whitelist-out", false, "enable outbound whitelist") backlog := fs.Bool("process-backlog", false, "treat existing mail as new") if err := fs.Parse(rest); err != nil { return 2 } if *name == "" || *host == "" || *user == "" { fmt.Fprintln(errOut, "name, imap-host, and username are required") return 2 } acc := store.Account{ Name: *name, Mode: *mode, IMAPHost: *host, IMAPPort: *port, IMAPSecurity: *sec, AuthType: "password", Username: *user, Password: *pass, SubjectRegex: *subj, WhitelistInEnabled: *wlIn, WhitelistOutEnabled: *wlOut, ProcessBacklog: *backlog, } if *mode == "RW" { acc.SMTPHost, acc.SMTPPort, acc.SMTPSecurity = *smtpHost, *smtpPort, *smtpSec } _, err := st.AddAccount(acc) if err != nil { fmt.Fprintf(errOut, "add account: %v\n", err) return 1 } fmt.Fprintf(out, "account %q added (%s)\n", *name, *mode) return 0 case "edit": fs := flag.NewFlagSet("account edit", flag.ContinueOnError) fs.SetOutput(errOut) name := fs.String("name", "", "account name (required)") mode := fs.String("mode", "", "RO|RW") host := fs.String("imap-host", "", "IMAP host") port := fs.Int("imap-port", 0, "IMAP port") sec := fs.String("imap-security", "", "tls|starttls") smtpHost := fs.String("smtp-host", "", "SMTP host") smtpPort := fs.Int("smtp-port", 0, "SMTP port") smtpSec := fs.String("smtp-security", "", "tls|starttls") user := fs.String("username", "", "login username") pass := fs.String("password", "", "login password (blank keeps existing)") subj := fs.String("subject-regex", "", "inbound subject filter") if err := fs.Parse(rest); err != nil { return 2 } if *name == "" { fmt.Fprintln(errOut, "--name is required") return 2 } if fs.NFlag() == 1 { // only --name → interactive TUI form, prefilled return editInteractive(st, *name, out, errOut) } acc, err := st.GetAccount(*name) if err != nil { fmt.Fprintf(errOut, "edit: %v\n", err) return 1 } // Overlay only the flags the user actually set. fs.Visit(func(f *flag.Flag) { switch f.Name { case "mode": acc.Mode = *mode case "imap-host": acc.IMAPHost = *host case "imap-port": acc.IMAPPort = *port case "imap-security": acc.IMAPSecurity = *sec case "smtp-host": acc.SMTPHost = *smtpHost case "smtp-port": acc.SMTPPort = *smtpPort case "smtp-security": acc.SMTPSecurity = *smtpSec case "username": acc.Username = *user case "password": acc.Password = *pass case "subject-regex": acc.SubjectRegex = *subj } }) // acc.Password holds the existing (decrypted) password from GetAccount; the // Visit above overwrites it only when --password was passed. UpdateAccount // re-seals whatever non-empty value is present, so the password is preserved. if err := st.UpdateAccount(acc); err != nil { fmt.Fprintf(errOut, "edit: %v\n", err) return 1 } fmt.Fprintf(out, "account %q updated\n", *name) return 0 case "remove": fs := flag.NewFlagSet("account remove", flag.ContinueOnError) fs.SetOutput(errOut) name := fs.String("name", "", "account name (required)") yes := fs.Bool("yes", false, "skip confirmation") if err := fs.Parse(rest); err != nil { return 2 } if *name == "" { fmt.Fprintln(errOut, "--name is required") return 2 } if !*yes { fmt.Fprintf(errOut, "refusing to remove %q without --yes\n", *name) return 2 } if err := st.DeleteAccount(*name); err != nil { fmt.Fprintf(errOut, "remove: %v\n", err) return 1 } fmt.Fprintf(out, "account %q removed\n", *name) return 0 case "list": accs, err := st.ListAccounts() if err != nil { fmt.Fprintf(errOut, "list: %v\n", err) return 1 } fmt.Fprintf(out, "%-16s %-4s %-28s %s\n", "NAME", "MODE", "IMAP", "USER") for _, a := range accs { fmt.Fprintf(out, "%-16s %-4s %-28s %s\n", a.Name, a.Mode, fmt.Sprintf("%s:%d", a.IMAPHost, a.IMAPPort), a.Username) } return 0 default: fmt.Fprintf(errOut, "unknown account subcommand %q\n", sub) return 2 } } // auditList renders recent audit entries (account "" = all) to out. func auditList(st *store.Store, account string, limit int, out io.Writer) error { entries, err := st.RecentAuditFor(account, limit) if err != nil { return err } fmt.Fprintf(out, "%-20s %-12s %-8s %-8s %-20s %s\n", "TS", "ACCOUNT", "ACTION", "RESULT", "TARGET", "REASON") for _, e := range entries { fmt.Fprintf(out, "%-20s %-12s %-8s %-8s %-20s %s\n", e.TS, e.Account, e.Action, e.Result, e.Target, e.Reason) } return nil } // runConfig handles `config set ` and `config get `. func runConfig(args []string, out, errOut io.Writer) int { if len(args) < 2 { fmt.Fprintln(errOut, "usage: emcli config [value]") return 2 } sub, key := args[0], args[1] st, err := openStore() if err != nil { fmt.Fprintf(errOut, "emcli: %v\n", err) return 1 } defer st.Close() switch sub { case "set": if len(args) < 3 { fmt.Fprintln(errOut, "usage: emcli config set ") return 2 } value := args[2] if key == "audit_retention_days" { n, err := strconv.Atoi(value) if err != nil || n < 0 { fmt.Fprintf(errOut, "audit_retention_days must be an integer >= 0, got %q\n", value) return 2 } } if err := st.SetSetting(key, value); err != nil { fmt.Fprintf(errOut, "config set: %v\n", err) return 1 } fmt.Fprintf(out, "%s = %s\n", key, value) return 0 case "get": v, err := st.GetSetting(key) if err != nil { fmt.Fprintf(errOut, "config get: %s not set\n", key) return 1 } fmt.Fprintf(out, "%s = %s\n", key, v) return 0 default: fmt.Fprintf(errOut, "unknown config subcommand %q\n", sub) return 2 } } // runAudit handles `audit list [--account ] [--limit N]`. func runAudit(args []string, out, errOut io.Writer) int { if len(args) == 0 || args[0] != "list" { fmt.Fprintln(errOut, "usage: emcli audit list [--account ] [--limit N]") return 2 } fs := flag.NewFlagSet("audit list", flag.ContinueOnError) fs.SetOutput(errOut) account := fs.String("account", "", "filter by account") limit := fs.Int("limit", 50, "max rows") if err := fs.Parse(args[1:]); err != nil { return 2 } st, err := openStore() if err != nil { fmt.Fprintf(errOut, "emcli: %v\n", err) return 1 } defer st.Close() if err := auditList(st, *account, *limit, out); err != nil { fmt.Fprintf(errOut, "audit list: %v\n", err) return 1 } return 0 } // runWhitelist handles `whitelist add --account NAME --address A`. func runWhitelist(args []string, out, errOut io.Writer) int { if len(args) < 2 { fmt.Fprintln(errOut, "usage: emcli whitelist [flags]") return 2 } dir := store.Direction(args[0]) sub, rest := args[1], args[2:] fs := flag.NewFlagSet("whitelist", flag.ContinueOnError) fs.SetOutput(errOut) account := fs.String("account", "", "account name") address := fs.String("address", "", "email or @domain") if err := fs.Parse(rest); err != nil { return 2 } if *account == "" { fmt.Fprintln(errOut, "--account is required") return 2 } st, err := openStore() if err != nil { fmt.Fprintf(errOut, "emcli: %v\n", err) return 1 } defer st.Close() switch sub { case "add": if err := st.AddWhitelist(*account, dir, *address); err != nil { fmt.Fprintf(errOut, "add: %v\n", err) return 1 } fmt.Fprintf(out, "added %s to %s whitelist of %q\n", *address, dir, *account) case "remove": if err := st.RemoveWhitelist(*account, dir, *address); err != nil { fmt.Fprintf(errOut, "remove: %v\n", err) return 1 } fmt.Fprintf(out, "removed %s\n", *address) case "list": addrs, err := st.ListWhitelist(*account, dir) if err != nil { fmt.Fprintf(errOut, "list: %v\n", err) return 1 } for _, a := range addrs { fmt.Fprintln(out, a) } default: fmt.Fprintf(errOut, "unknown whitelist subcommand %q\n", sub) return 2 } return 0 }