feat(send): Phase 2 send path — SMTP, MIME, reply threading, outbound policy
Adds the `send` agent command and everything behind it: - store: Account carries SMTP host/port/security (NULL-safe scan/insert/select); admin `account add` gains --smtp-* flags (applied for RW accounts). - policy: OutboundRule.Check(recipients) → (ok, reason); RO ⇒ ro_mode, whitelist-out blocks the whole send if any recipient fails (no partial send). - mail: Header.References; OutgoingMessage + BuildMIME (plain text + attachments, In-Reply-To/References threading, Bcc envelope-only); SendSMTP (tls/starttls, SASL PLAIN, envelope send) via emersion/go-smtp. - cli: SendCmd gates outbound, resolves --reply-to under the inbound filter (filtered/absent source ⇒ not_found), reads attachments, audits, emits the JSON envelope; repeatable --to/--cc/--bcc/--attach flags wired into the router. Implemented test-first; full suite passes incl -race. Validated live against friday.mxlogin.com: real send to me@stevecliff.com, RO + whitelist-out blocks, and --reply-to threading off a live INBOX message. test-creds.md gitignored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,9 @@ func runAccount(args []string, out, errOut io.Writer) int {
|
||||
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")
|
||||
@@ -44,12 +47,16 @@ func runAccount(args []string, out, errOut io.Writer) int {
|
||||
fmt.Fprintln(errOut, "name, imap-host, and username are required")
|
||||
return 2
|
||||
}
|
||||
_, err := st.AddAccount(store.Account{
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user