diff --git a/commands/compose/encrypt.go b/commands/compose/encrypt.go index d63940b..3bd8ca4 100644 --- a/commands/compose/encrypt.go +++ b/commands/compose/encrypt.go @@ -2,7 +2,6 @@ package compose import ( "errors" - "time" "git.sr.ht/~rjarry/aerc/widgets" ) @@ -29,16 +28,5 @@ func (Encrypt) Execute(aerc *widgets.Aerc, args []string) error { composer, _ := aerc.SelectedTab().(*widgets.Composer) composer.SetEncrypt(!composer.Encrypt()) - - var statusline string - - if composer.Encrypt() { - statusline = "Message will be encrypted." - } else { - statusline = "Message will not be encrypted." - } - - aerc.PushStatus(statusline, 10*time.Second) - return nil } diff --git a/lib/crypto/crypto.go b/lib/crypto/crypto.go index cab9346..54a20e6 100644 --- a/lib/crypto/crypto.go +++ b/lib/crypto/crypto.go @@ -20,6 +20,7 @@ type Provider interface { Init(*log.Logger) error Close() GetSignerKeyId(string) (string, error) + GetKeyId(string) (string, error) } func New(s string) Provider { diff --git a/lib/crypto/gpg/gpg.go b/lib/crypto/gpg/gpg.go index 457788d..fe32468 100644 --- a/lib/crypto/gpg/gpg.go +++ b/lib/crypto/gpg/gpg.go @@ -55,6 +55,10 @@ func (m *Mail) GetSignerKeyId(s string) (string, error) { return gpgbin.GetPrivateKeyId(s) } +func (m *Mail) GetKeyId(s string) (string, error) { + return gpgbin.GetKeyId(s) +} + func handleSignatureError(e string) models.SignatureValidity { if e == "gpg: missing public key" { return models.UnknownEntity diff --git a/lib/crypto/gpg/gpgbin/keys.go b/lib/crypto/gpg/gpgbin/keys.go index 660ce82..9c8b233 100644 --- a/lib/crypto/gpg/gpgbin/keys.go +++ b/lib/crypto/gpg/gpgbin/keys.go @@ -11,3 +11,13 @@ func GetPrivateKeyId(s string) (string, error) { } return id, nil } + +// GetKeyId runs gpg --list-keys s +func GetKeyId(s string) (string, error) { + private := false + id := getKeyId(s, private) + if id == "" { + return "", fmt.Errorf("no public key found") + } + return id, nil +} diff --git a/lib/crypto/pgp/pgp.go b/lib/crypto/pgp/pgp.go index e0c5671..f0f3f65 100644 --- a/lib/crypto/pgp/pgp.go +++ b/lib/crypto/pgp/pgp.go @@ -263,6 +263,14 @@ func (m *Mail) GetSignerKeyId(s string) (string, error) { return signerEntity.PrimaryKey.KeyIdString(), nil } +func (m *Mail) GetKeyId(s string) (string, error) { + entity, err := m.getEntityByEmail(s) + if err != nil { + return "", err + } + return entity.PrimaryKey.KeyIdString(), nil +} + func handleSignatureError(e string) models.SignatureValidity { if e == "openpgp: signature made by unknown entity" { return models.UnknownEntity diff --git a/lib/ui/textinput.go b/lib/ui/textinput.go index aa15300..0a331dc 100644 --- a/lib/ui/textinput.go +++ b/lib/ui/textinput.go @@ -26,6 +26,7 @@ type TextInput struct { scroll int text []rune change []func(ti *TextInput) + focusLost []func(ti *TextInput) tabcomplete func(s string) ([]string, string) completions []string prefix string @@ -157,6 +158,9 @@ func (ti *TextInput) MouseEvent(localX int, localY int, event tcell.Event) { } func (ti *TextInput) Focus(focus bool) { + if ti.focus && !focus { + ti.onFocusLost() + } ti.focus = focus if focus && ti.ctx != nil { cells := runewidth.StringWidth(string(ti.text[:ti.index])) @@ -274,6 +278,12 @@ func (ti *TextInput) onChange() { } } +func (ti *TextInput) onFocusLost() { + for _, focusLost := range ti.focusLost { + focusLost(ti) + } +} + func (ti *TextInput) updateCompletions() { if ti.tabcomplete == nil { // no completer @@ -304,6 +314,10 @@ func (ti *TextInput) OnChange(onChange func(ti *TextInput)) { ti.change = append(ti.change, onChange) } +func (ti *TextInput) OnFocusLost(onFocusLost func(ti *TextInput)) { + ti.focusLost = append(ti.focusLost, onFocusLost) +} + func (ti *TextInput) Event(event tcell.Event) bool { switch event := event.(type) { case *tcell.EventKey: diff --git a/widgets/compose.go b/widgets/compose.go index 5dab429..49627fc 100644 --- a/widgets/compose.go +++ b/widgets/compose.go @@ -198,8 +198,21 @@ func (c *Composer) Sign() bool { } func (c *Composer) SetEncrypt(encrypt bool) *Composer { - c.encrypt = encrypt - c.updateCrypto() + if !encrypt { + c.encrypt = encrypt + c.updateCrypto() + return c + } + // Check on any attempt to encrypt, and any lost focus of "to", "cc", or + // "bcc" field. Use OnFocusLost instead of OnChange to limit keyring checks + c.encrypt = c.checkEncryptionKeys("") + if c.crypto.setEncOneShot { + // Prevent registering a lot of callbacks + c.OnFocusLost("to", c.checkEncryptionKeys) + c.OnFocusLost("cc", c.checkEncryptionKeys) + c.OnFocusLost("bcc", c.checkEncryptionKeys) + c.crypto.setEncOneShot = false + } return c } @@ -365,6 +378,15 @@ func (c *Composer) OnHeaderChange(header string, fn func(subject string)) { } } +// OnFocusLost registers an OnFocusLost callback for the specified header. +func (c *Composer) OnFocusLost(header string, fn func(input string) bool) { + if editor, ok := c.editors[strings.ToLower(header)]; ok { + editor.OnFocusLost(func() { + fn(editor.input.String()) + }) + } +} + func (c *Composer) OnClose(fn func(composer *Composer)) { c.onClose = append(c.onClose, fn) } @@ -984,6 +1006,12 @@ func (he *headerEditor) OnChange(fn func()) { }) } +func (he *headerEditor) OnFocusLost(fn func()) { + he.input.OnFocusLost(func(_ *ui.TextInput) { + fn() + }) +} + type reviewMessage struct { composer *Composer grid *ui.Grid @@ -1090,18 +1118,21 @@ func (rm *reviewMessage) Draw(ctx *ui.Context) { } type cryptoStatus struct { - title string - status *ui.Text - uiConfig *config.UIConfig - signKey string + title string + status *ui.Text + uiConfig *config.UIConfig + signKey string + setEncOneShot bool } func newCryptoStatus(uiConfig *config.UIConfig) *cryptoStatus { defaultStyle := uiConfig.GetStyle(config.STYLE_DEFAULT) return &cryptoStatus{ - title: "Security", - status: ui.NewText("", defaultStyle), - uiConfig: uiConfig, + title: "Security", + status: ui.NewText("", defaultStyle), + uiConfig: uiConfig, + signKey: "", + setEncOneShot: true, } } @@ -1124,3 +1155,33 @@ func (cs *cryptoStatus) OnInvalidate(fn func(ui.Drawable)) { fn(cs) }) } + +func (c *Composer) checkEncryptionKeys(_ string) bool { + rcpts, err := getRecipientsEmail(c) + if err != nil { + // checkEncryptionKeys gets registered as a callback and must + // explicitly call c.SetEncrypt(false) when encryption is not possible + c.SetEncrypt(false) + st := fmt.Sprintf("Cannot encrypt: %v", err) + c.aerc.statusline.PushError(st) + return false + } + var mk []string + for _, rcpt := range rcpts { + key, err := c.aerc.Crypto.GetKeyId(rcpt) + if err != nil || key == "" { + mk = append(mk, rcpt) + } + } + if len(mk) > 0 { + c.SetEncrypt(false) + st := fmt.Sprintf("Cannot encrypt, missing keys: %s", strings.Join(mk, ", ")) + c.aerc.statusline.PushError(st) + return false + } + // If callbacks were registered, encrypt will be set when user removes + // recipients with missing keys + c.encrypt = true + c.updateCrypto() + return true +}