From ca90343850290c256430ad64daa3d809a7016299 Mon Sep 17 00:00:00 2001 From: Stas Rudakou Date: Sat, 30 Jul 2022 22:42:49 +0200 Subject: [PATCH] outgoing-cred-cmd: delay execution until an email needs to be sent This can be useful in cases when: 1. outgoing-cred-cmd requires a user action or confirmation (e.g. when using pass with a Yubikey or similar smart card that requires a user to enter a pin or touch the device when decrypting the password) 2. A user starts aerc frequently, but not all the sessions end up with sending emails 3. So the user only wants to execute outgoing-cred-cmd when the password is really used, so the user doesn't have to enter pin or touch their Yubikey each time aerc starts Signed-off-by: Stas Rudakou Acked-by: Robin Jarry --- commands/compose/send.go | 8 +++- config/config.go | 95 ++++++++++++++++++++++----------------- widgets/account-wizard.go | 2 +- 3 files changed, 60 insertions(+), 45 deletions(-) diff --git a/commands/compose/send.go b/commands/compose/send.go index cca1540..5448b72 100644 --- a/commands/compose/send.go +++ b/commands/compose/send.go @@ -51,7 +51,11 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error { tabName := tab.Name config := composer.Config() - if config.Outgoing == "" { + outgoing, err := config.Outgoing.ConnectionString() + if err != nil { + return errors.Wrap(err, "ReadCredentials(outgoing)") + } + if outgoing == "" { return errors.New( "No outgoing mail transport configured for this account") } @@ -74,7 +78,7 @@ func (Send) Execute(aerc *widgets.Aerc, args []string) error { return errors.Wrap(err, "ParseAddress(config.From)") } - uri, err := url.Parse(config.Outgoing) + uri, err := url.Parse(outgoing) if err != nil { return errors.Wrap(err, "url.Parse(outgoing)") } diff --git a/config/config.go b/config/config.go index 233f2e0..75519ab 100644 --- a/config/config.go +++ b/config/config.go @@ -95,6 +95,50 @@ const ( FILTER_HEADER ) +type RemoteConfig struct { + Value string + PasswordCmd string +} + +func (c RemoteConfig) parseValue() (*url.URL, error) { + return url.Parse(c.Value) +} + +func (c RemoteConfig) ConnectionString() (string, error) { + if c.Value == "" || c.PasswordCmd == "" { + return c.Value, nil + } + + u, err := c.parseValue() + if err != nil { + return "", err + } + + // ignore the command if a password is specified + if _, exists := u.User.Password(); exists { + return c.Value, nil + } + + // don't attempt to parse the command if the url is a path (ie /usr/bin/sendmail) + if !u.IsAbs() { + return c.Value, nil + } + + cmd := exec.Command("sh", "-c", c.PasswordCmd) + cmd.Stdin = os.Stdin + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to read password: %s", err) + } + + pw := strings.TrimSpace(string(output)) + u.User = url.UserPassword(u.User.Username(), pw) + c.Value = u.String() + c.PasswordCmd = "" + + return c.Value, nil +} + type AccountConfig struct { Archive string CopyTo string @@ -104,12 +148,10 @@ type AccountConfig struct { Aliases string Name string Source string - SourceCredCmd string Folders []string FoldersExclude []string Params map[string]string - Outgoing string - OutgoingCredCmd string + Outgoing RemoteConfig SignatureFile string SignatureCmd string EnableFoldersSort bool `ini:"enable-folders-sort"` @@ -239,6 +281,7 @@ func loadAccountConfig(path string) ([]AccountConfig, error) { continue } sec := file.Section(_sec) + sourceRemoteConfig := RemoteConfig{} account := AccountConfig{ Archive: "Archive", Default: "INBOX", @@ -260,12 +303,14 @@ func loadAccountConfig(path string) ([]AccountConfig, error) { folders := strings.Split(val, ",") sort.Strings(folders) account.FoldersExclude = folders + } else if key == "source" { + sourceRemoteConfig.Value = val } else if key == "source-cred-cmd" { - account.SourceCredCmd = val + sourceRemoteConfig.PasswordCmd = val } else if key == "outgoing" { - account.Outgoing = val + account.Outgoing.Value = val } else if key == "outgoing-cred-cmd" { - account.OutgoingCredCmd = val + account.Outgoing.PasswordCmd = val } else if key == "from" { account.From = val } else if key == "aliases" { @@ -295,56 +340,22 @@ func loadAccountConfig(path string) ([]AccountConfig, error) { return nil, fmt.Errorf("Expected from for account %s", _sec) } - source, err := parseCredential(account.Source, account.SourceCredCmd) + source, err := sourceRemoteConfig.ConnectionString() if err != nil { return nil, fmt.Errorf("Invalid source credentials for %s: %s", _sec, err) } account.Source = source - outgoing, err := parseCredential(account.Outgoing, account.OutgoingCredCmd) + _, err = account.Outgoing.parseValue() if err != nil { return nil, fmt.Errorf("Invalid outgoing credentials for %s: %s", _sec, err) } - account.Outgoing = outgoing accounts = append(accounts, account) } return accounts, nil } -func parseCredential(cred, command string) (string, error) { - if cred == "" || command == "" { - return cred, nil - } - - u, err := url.Parse(cred) - if err != nil { - return "", err - } - - // ignore the command if a password is specified - if _, exists := u.User.Password(); exists { - return cred, nil - } - - // don't attempt to parse the command if the url is a path (ie /usr/bin/sendmail) - if !u.IsAbs() { - return cred, nil - } - - cmd := exec.Command("sh", "-c", command) - cmd.Stdin = os.Stdin - output, err := cmd.Output() - if err != nil { - return "", fmt.Errorf("failed to read password: %s", err) - } - - pw := strings.TrimSpace(string(output)) - u.User = url.UserPassword(u.User.Username(), pw) - - return u.String(), nil -} - // Set at build time var shareDir string diff --git a/widgets/account-wizard.go b/widgets/account-wizard.go index ab69139..8b78dcd 100644 --- a/widgets/account-wizard.go +++ b/widgets/account-wizard.go @@ -522,7 +522,7 @@ func (wizard *AccountWizard) finish(tutorial bool) { Default: "INBOX", From: sec.Key("from").String(), Source: sec.Key("source").String(), - Outgoing: sec.Key("outgoing").String(), + Outgoing: config.RemoteConfig{Value: sec.Key("outgoing").String()}, } if wizard.smtpMode == SMTP_STARTTLS { account.Params = map[string]string{