feat(cli): configurable send-as From address (flags, TUI, validation)
- tui.ValidFromAddress: exported validator; blank passes, malformed rejects - Fields.FromAddress: new field, round-trips through ToAccount/FieldsFromAccount - Fields.Validate: calls ValidFromAddress before returning nil - TUI form: from_address fieldDef between username and password - send.go: From set via acc.SendFrom() instead of acc.Username - admin.go account add: --from flag with pre-parse validation - admin.go account edit: --from flag; validate before Visit, apply in Visit - USER-MANUAL.md: --from flag added to account add flags table Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,7 @@ func runAccount(args []string, role store.Role, out, errOut io.Writer) int {
|
||||
smtpSec := fs.String("smtp-security", "tls", "tls|starttls")
|
||||
user := fs.String("username", "", "login username")
|
||||
pass := fs.String("password", "", "login password")
|
||||
from := fs.String("from", "", "send-as address (blank = use username)")
|
||||
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")
|
||||
@@ -56,9 +57,14 @@ func runAccount(args []string, role store.Role, out, errOut io.Writer) int {
|
||||
fmt.Fprintln(errOut, "name, imap-host, and username are required")
|
||||
return 2
|
||||
}
|
||||
if err := tui.ValidFromAddress(*from); err != nil {
|
||||
fmt.Fprintln(errOut, err)
|
||||
return 2
|
||||
}
|
||||
acc := store.Account{
|
||||
Name: *name, Mode: *mode, IMAPHost: *host, IMAPPort: *port, IMAPSecurity: *sec,
|
||||
AuthType: "password", Username: *user, Password: *pass,
|
||||
FromAddress: *from,
|
||||
SubjectRegex: *subj, WhitelistInEnabled: *wlIn, WhitelistOutEnabled: *wlOut,
|
||||
ProcessBacklog: *backlog,
|
||||
}
|
||||
@@ -85,6 +91,7 @@ func runAccount(args []string, role store.Role, out, errOut io.Writer) int {
|
||||
smtpSec := fs.String("smtp-security", "", "tls|starttls")
|
||||
user := fs.String("username", "", "login username")
|
||||
pass := fs.String("password", "", "login password (blank keeps existing)")
|
||||
from := fs.String("from", "", "send-as address (blank keeps existing)")
|
||||
subj := fs.String("subject-regex", "", "inbound subject filter")
|
||||
if err := fs.Parse(rest); err != nil {
|
||||
return 2
|
||||
@@ -96,6 +103,10 @@ func runAccount(args []string, role store.Role, out, errOut io.Writer) int {
|
||||
if fs.NFlag() == 1 { // only --name → interactive TUI form, prefilled
|
||||
return editInteractive(st, *name, out, errOut)
|
||||
}
|
||||
if err := tui.ValidFromAddress(*from); err != nil {
|
||||
fmt.Fprintln(errOut, err)
|
||||
return 2
|
||||
}
|
||||
acc, err := st.GetAccount(*name)
|
||||
if err != nil {
|
||||
fmt.Fprintf(errOut, "edit: %v\n", err)
|
||||
@@ -122,6 +133,8 @@ func runAccount(args []string, role store.Role, out, errOut io.Writer) int {
|
||||
acc.Username = *user
|
||||
case "password":
|
||||
acc.Password = *pass
|
||||
case "from":
|
||||
acc.FromAddress = *from
|
||||
case "subject-regex":
|
||||
acc.SubjectRegex = *subj
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func SendCmd(d Deps, account string, to, cc, bcc []string, subject, body string,
|
||||
}
|
||||
|
||||
msg := mail.OutgoingMessage{
|
||||
From: acc.Username, To: to, Cc: cc, Bcc: bcc,
|
||||
From: acc.SendFrom(), To: to, Cc: cc, Bcc: bcc,
|
||||
Subject: subject, BodyText: body,
|
||||
}
|
||||
recipients := msg.Recipients()
|
||||
|
||||
@@ -139,6 +139,32 @@ func TestSendReplyToThreadsHeaders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendUsesConfiguredFromAddress(t *testing.T) {
|
||||
acc := rwAccount()
|
||||
acc.FromAddress = "Steve Cliff <me@stevecliff.com>"
|
||||
d, sent, _ := sendDeps(t, acc, nil)
|
||||
if err := SendCmd(d, "send", []string{"me@stevecliff.com"}, nil, nil, "hi", "body", nil, 0, "INBOX"); err != nil {
|
||||
t.Fatalf("SendCmd: %v", err)
|
||||
}
|
||||
if len(*sent) != 1 {
|
||||
t.Fatalf("want 1 send, got %d", len(*sent))
|
||||
}
|
||||
if got := (*sent)[0].From; got != "Steve Cliff <me@stevecliff.com>" {
|
||||
t.Fatalf("From = %q, want configured from-address", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendFallsBackToUsernameAsFrom(t *testing.T) {
|
||||
// rwAccount has no FromAddress, so From must be the login username.
|
||||
d, sent, _ := sendDeps(t, rwAccount(), nil)
|
||||
if err := SendCmd(d, "send", []string{"me@stevecliff.com"}, nil, nil, "hi", "body", nil, 0, "INBOX"); err != nil {
|
||||
t.Fatalf("SendCmd: %v", err)
|
||||
}
|
||||
if got := (*sent)[0].From; got != "emcli@stevecliff.com" {
|
||||
t.Fatalf("From = %q, want username fallback", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendReplyToFilteredSourceNotFound(t *testing.T) {
|
||||
acc := rwAccount()
|
||||
acc.WhitelistInEnabled = true // inbound filter active
|
||||
|
||||
Reference in New Issue
Block a user