Files
emcli/internal/store/account_test.go
T
steve c99eaedafd 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>
2026-06-22 17:39:07 +01:00

87 lines
2.1 KiB
Go

package store
import (
"errors"
"testing"
)
func sampleAccount() Account {
return Account{
Name: "work", Mode: "RO",
IMAPHost: "imap.example.com", IMAPPort: 993, IMAPSecurity: "tls",
AuthType: "password", Username: "me@example.com",
Password: "s3cr3t", SubjectRegex: "",
}
}
func TestAddGetAccountDecryptsSecret(t *testing.T) {
s := openTemp(t)
id, err := s.AddAccount(sampleAccount())
if err != nil {
t.Fatalf("AddAccount: %v", err)
}
if id == 0 {
t.Fatal("want non-zero id")
}
got, err := s.GetAccount("work")
if err != nil {
t.Fatalf("GetAccount: %v", err)
}
if got.Password != "s3cr3t" {
t.Fatalf("password not decrypted: %q", got.Password)
}
if got.Mode != "RO" || got.IMAPPort != 993 {
t.Fatalf("fields wrong: %+v", got)
}
}
func TestAddGetAccountRoundTripsSMTP(t *testing.T) {
s := openTemp(t)
a := sampleAccount()
a.Mode = "RW"
a.SMTPHost = "smtp.example.com"
a.SMTPPort = 465
a.SMTPSecurity = "tls"
if _, err := s.AddAccount(a); err != nil {
t.Fatalf("AddAccount: %v", err)
}
got, err := s.GetAccount("work")
if err != nil {
t.Fatalf("GetAccount: %v", err)
}
if got.SMTPHost != "smtp.example.com" || got.SMTPPort != 465 || got.SMTPSecurity != "tls" {
t.Fatalf("SMTP fields not round-tripped: %+v", got)
}
}
func TestPasswordStoredEncrypted(t *testing.T) {
s := openTemp(t)
_, _ = s.AddAccount(sampleAccount())
var blob []byte
if err := s.db.QueryRow("SELECT enc_password FROM accounts WHERE name='work'").Scan(&blob); err != nil {
t.Fatalf("query: %v", err)
}
if string(blob) == "s3cr3t" || len(blob) == 0 {
t.Fatalf("password not encrypted at rest: %q", blob)
}
}
func TestGetAccountNotFound(t *testing.T) {
s := openTemp(t)
if _, err := s.GetAccount("nope"); !errors.Is(err, ErrAccountNotFound) {
t.Fatalf("want ErrAccountNotFound, got %v", err)
}
}
func TestListAccountsOmitsSecrets(t *testing.T) {
s := openTemp(t)
_, _ = s.AddAccount(sampleAccount())
list, err := s.ListAccounts()
if err != nil || len(list) != 1 {
t.Fatalf("list: %v len=%d", err, len(list))
}
if list[0].Password != "" {
t.Fatal("ListAccounts must not return secrets")
}
}