summaryrefslogtreecommitdiff
path: root/widgets
diff options
context:
space:
mode:
authorDrew DeVault <sir@cmpwn.com>2020-03-03 16:20:07 -0500
committerDrew DeVault <sir@cmpwn.com>2020-03-03 16:49:52 -0500
commitf3158b36f1f210ff54febbe82b571c1379b30c98 (patch)
tree10cde839c9517609f55b8f1057b1cf84ac592632 /widgets
parent89f1684ea4b5e680db7ff06a54b2d4e78212cd12 (diff)
downloadaerc-f3158b36f1f210ff54febbe82b571c1379b30c98.zip
Initial support for PGP decryption & signatures
Diffstat (limited to 'widgets')
-rw-r--r--widgets/aerc.go48
-rw-r--r--widgets/getpasswd.go61
-rw-r--r--widgets/headerlayout.go3
-rw-r--r--widgets/msglist.go7
-rw-r--r--widgets/msgviewer.go103
-rw-r--r--widgets/pgpinfo.go93
6 files changed, 269 insertions, 46 deletions
diff --git a/widgets/aerc.go b/widgets/aerc.go
index a9be47e..e6d2525 100644
--- a/widgets/aerc.go
+++ b/widgets/aerc.go
@@ -2,6 +2,7 @@ package widgets
import (
"errors"
+ "fmt"
"io"
"log"
"net/url"
@@ -10,6 +11,7 @@ import (
"github.com/gdamore/tcell"
"github.com/google/shlex"
+ "golang.org/x/crypto/openpgp"
"git.sr.ht/~sircmpwn/aerc/config"
"git.sr.ht/~sircmpwn/aerc/lib"
@@ -32,7 +34,9 @@ type Aerc struct {
pendingKeys []config.KeyStroke
prompts *ui.Stack
tabs *ui.Tabs
+ ui *ui.UI
beep func() error
+ getpasswd *GetPasswd
}
func NewAerc(conf *config.AercConfig, logger *log.Logger,
@@ -160,6 +164,10 @@ func (aerc *Aerc) Focus(focus bool) {
func (aerc *Aerc) Draw(ctx *ui.Context) {
aerc.grid.Draw(ctx)
+ if aerc.getpasswd != nil {
+ aerc.getpasswd.Draw(ctx.Subcontext(4, 4,
+ ctx.Width()-8, ctx.Height()-8))
+ }
}
func (aerc *Aerc) getBindings() *config.KeyBindings {
@@ -198,6 +206,10 @@ func (aerc *Aerc) simulate(strokes []config.KeyStroke) {
}
func (aerc *Aerc) Event(event tcell.Event) bool {
+ if aerc.getpasswd != nil {
+ return aerc.getpasswd.Event(event)
+ }
+
if aerc.focused != nil {
return aerc.focused.Event(event)
}
@@ -484,3 +496,39 @@ func (aerc *Aerc) CloseBackends() error {
}
return returnErr
}
+
+func (aerc *Aerc) GetPassword(title string, prompt string, cb func(string)) {
+ aerc.getpasswd = NewGetPasswd(title, prompt, func(pw string) {
+ aerc.getpasswd = nil
+ aerc.Invalidate()
+ cb(pw)
+ })
+ aerc.getpasswd.OnInvalidate(func(_ ui.Drawable) {
+ aerc.Invalidate()
+ })
+ aerc.Invalidate()
+}
+
+func (aerc *Aerc) Initialize(ui *ui.UI) {
+ aerc.ui = ui
+}
+
+func (aerc *Aerc) DecryptKeys(keys []openpgp.Key, symmetric bool) ([]byte, error) {
+ // HACK HACK HACK
+ for _, key := range keys {
+ var ident *openpgp.Identity
+ for _, ident = range key.Entity.Identities {
+ break
+ }
+ aerc.GetPassword("Decrypt PGP private key",
+ fmt.Sprintf("Enter password for %s (%8X)",
+ ident.Name, key.PublicKey.KeyId),
+ func(pass string) {
+ key.PrivateKey.Decrypt([]byte(pass))
+ })
+ for aerc.getpasswd != nil {
+ aerc.ui.Tick()
+ }
+ }
+ return nil, nil
+}
diff --git a/widgets/getpasswd.go b/widgets/getpasswd.go
new file mode 100644
index 0000000..08702c5
--- /dev/null
+++ b/widgets/getpasswd.go
@@ -0,0 +1,61 @@
+package widgets
+
+import (
+ "github.com/gdamore/tcell"
+
+ "git.sr.ht/~sircmpwn/aerc/lib/ui"
+)
+
+type GetPasswd struct {
+ ui.Invalidatable
+ callback func(string)
+ title string
+ prompt string
+ input *ui.TextInput
+}
+
+func NewGetPasswd(title string, prompt string, cb func(string)) *GetPasswd {
+ getpasswd := &GetPasswd{
+ callback: cb,
+ title: title,
+ prompt: prompt,
+ input: ui.NewTextInput("").Password(true).Prompt("Password: "),
+ }
+ getpasswd.input.OnInvalidate(func(_ ui.Drawable) {
+ getpasswd.Invalidate()
+ })
+ getpasswd.input.Focus(true)
+ return getpasswd
+}
+
+func (gp *GetPasswd) Draw(ctx *ui.Context) {
+ ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
+ ctx.Fill(0, 0, ctx.Width(), 1, ' ', tcell.StyleDefault.Reverse(true))
+ ctx.Printf(1, 0, tcell.StyleDefault.Reverse(true), "%s", gp.title)
+ ctx.Printf(1, 1, tcell.StyleDefault, gp.prompt)
+ gp.input.Draw(ctx.Subcontext(1, 3, ctx.Width()-2, 1))
+}
+
+func (gp *GetPasswd) Invalidate() {
+ gp.DoInvalidate(gp)
+}
+
+func (gp *GetPasswd) Event(event tcell.Event) bool {
+ switch event := event.(type) {
+ case *tcell.EventKey:
+ switch event.Key() {
+ case tcell.KeyEnter:
+ gp.input.Focus(false)
+ gp.callback(gp.input.String())
+ default:
+ gp.input.Event(event)
+ }
+ default:
+ gp.input.Event(event)
+ }
+ return true
+}
+
+func (gp *GetPasswd) Focus(f bool) {
+ // Who cares
+}
diff --git a/widgets/headerlayout.go b/widgets/headerlayout.go
index 7f6b93d..904b079 100644
--- a/widgets/headerlayout.go
+++ b/widgets/headerlayout.go
@@ -31,7 +31,7 @@ func (filter HeaderLayoutFilter) forMessage(msg *models.MessageInfo) HeaderLayou
// grid builds a ui grid, populating each cell by calling a callback function
// with the current header string.
func (layout HeaderLayout) grid(cb func(string) ui.Drawable) (grid *ui.Grid, height int) {
- rowCount := len(layout) + 1 // extra row for spacer
+ rowCount := len(layout)
grid = ui.MakeGrid(rowCount, 1, ui.SIZE_EXACT, ui.SIZE_WEIGHT)
for i, cols := range layout {
r := ui.MakeGrid(1, len(cols), ui.SIZE_EXACT, ui.SIZE_WEIGHT)
@@ -40,6 +40,5 @@ func (layout HeaderLayout) grid(cb func(string) ui.Drawable) (grid *ui.Grid, hei
}
grid.AddChild(r).At(i, 0)
}
- grid.AddChild(ui.NewFill(' ')).At(rowCount-1, 0)
return grid, rowCount
}
diff --git a/widgets/msglist.go b/widgets/msglist.go
index 7c1a03b..f36901f 100644
--- a/widgets/msglist.go
+++ b/widgets/msglist.go
@@ -165,8 +165,11 @@ func (ml *MessageList) MouseEvent(localX int, localY int, event tcell.Event) {
if msg == nil {
return
}
- viewer := NewMessageViewer(acct, ml.aerc.Config(), store, msg)
- ml.aerc.NewTab(viewer, msg.Envelope.Subject)
+ lib.NewMessageStoreView(msg, store, ml.aerc.DecryptKeys,
+ func(view lib.MessageView) {
+ viewer := NewMessageViewer(acct, ml.aerc.Config(), view)
+ ml.aerc.NewTab(viewer, msg.Envelope.Subject)
+ })
}
case tcell.WheelDown:
if ml.store != nil {
diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go
index 19a2380..36e7997 100644
--- a/widgets/msgviewer.go
+++ b/widgets/msgviewer.go
@@ -30,9 +30,8 @@ type MessageViewer struct {
conf *config.AercConfig
err error
grid *ui.Grid
- msg *models.MessageInfo
switcher *PartSwitcher
- store *lib.MessageStore
+ msg lib.MessageView
}
type PartSwitcher struct {
@@ -46,8 +45,8 @@ type PartSwitcher struct {
mv *MessageViewer
}
-func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
- store *lib.MessageStore, msg *models.MessageInfo) *MessageViewer {
+func NewMessageViewer(acct *AccountView,
+ conf *config.AercConfig, msg lib.MessageView) *MessageViewer {
hf := HeaderLayoutFilter{
layout: HeaderLayout(conf.Viewer.HeaderLayout),
@@ -58,25 +57,40 @@ func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
return false
},
}
- layout := hf.forMessage(msg)
+ layout := hf.forMessage(msg.MessageInfo())
header, headerHeight := layout.grid(
func(header string) ui.Drawable {
return &HeaderView{
- Name: header,
- Value: fmtHeader(msg, header, acct.UiConfig().TimestampFormat),
+ Name: header,
+ Value: fmtHeader(msg.MessageInfo(), header,
+ acct.UiConfig().TimestampFormat),
}
},
)
- grid := ui.NewGrid().Rows([]ui.GridSpec{
+ rows := []ui.GridSpec{
{ui.SIZE_EXACT, headerHeight},
+ }
+
+ if msg.PGPDetails() != nil {
+ height := 1
+ if msg.PGPDetails().IsSigned && msg.PGPDetails().IsEncrypted {
+ height = 2
+ }
+ rows = append(rows, ui.GridSpec{ui.SIZE_EXACT, height})
+ }
+
+ rows = append(rows, []ui.GridSpec{
+ {ui.SIZE_EXACT, 1},
{ui.SIZE_WEIGHT, 1},
- }).Columns([]ui.GridSpec{
+ }...)
+
+ grid := ui.NewGrid().Rows(rows).Columns([]ui.GridSpec{
{ui.SIZE_WEIGHT, 1},
})
switcher := &PartSwitcher{}
- err := createSwitcher(acct, switcher, conf, store, msg)
+ err := createSwitcher(acct, switcher, conf, msg)
if err != nil {
return &MessageViewer{
err: err,
@@ -86,14 +100,20 @@ func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
}
grid.AddChild(header).At(0, 0)
- grid.AddChild(switcher).At(1, 0)
+ if msg.PGPDetails() != nil {
+ grid.AddChild(NewPGPInfo(msg.PGPDetails())).At(1, 0)
+ grid.AddChild(ui.NewFill(' ')).At(2, 0)
+ grid.AddChild(switcher).At(3, 0)
+ } else {
+ grid.AddChild(ui.NewFill(' ')).At(1, 0)
+ grid.AddChild(switcher).At(2, 0)
+ }
mv := &MessageViewer{
acct: acct,
conf: conf,
grid: grid,
msg: msg,
- store: store,
switcher: switcher,
}
switcher.mv = mv
@@ -122,8 +142,8 @@ func fmtHeader(msg *models.MessageInfo, header string, timefmt string) string {
}
}
-func enumerateParts(acct *AccountView, conf *config.AercConfig, store *lib.MessageStore,
- msg *models.MessageInfo, body *models.BodyStructure,
+func enumerateParts(acct *AccountView, conf *config.AercConfig,
+ msg lib.MessageView, body *models.BodyStructure,
index []int) ([]*PartViewer, error) {
var parts []*PartViewer
@@ -134,14 +154,14 @@ func enumerateParts(acct *AccountView, conf *config.AercConfig, store *lib.Messa
pv := &PartViewer{part: part}
parts = append(parts, pv)
subParts, err := enumerateParts(
- acct, conf, store, msg, part, curindex)
+ acct, conf, msg, part, curindex)
if err != nil {
return nil, err
}
parts = append(parts, subParts...)
continue
}
- pv, err := NewPartViewer(acct, conf, store, msg, part, curindex)
+ pv, err := NewPartViewer(acct, conf, msg, part, curindex)
if err != nil {
return nil, err
}
@@ -150,17 +170,17 @@ func enumerateParts(acct *AccountView, conf *config.AercConfig, store *lib.Messa
return parts, nil
}
-func createSwitcher(acct *AccountView, switcher *PartSwitcher, conf *config.AercConfig,
- store *lib.MessageStore, msg *models.MessageInfo) error {
+func createSwitcher(acct *AccountView, switcher *PartSwitcher,
+ conf *config.AercConfig, msg lib.MessageView) error {
var err error
switcher.selected = -1
switcher.showHeaders = conf.Viewer.ShowHeaders
switcher.alwaysShowMime = conf.Viewer.AlwaysShowMime
- if len(msg.BodyStructure.Parts) == 0 {
+ if len(msg.BodyStructure().Parts) == 0 {
switcher.selected = 0
- pv, err := NewPartViewer(acct, conf, store, msg, msg.BodyStructure, []int{1})
+ pv, err := NewPartViewer(acct, conf, msg, msg.BodyStructure(), []int{1})
if err != nil {
return err
}
@@ -169,8 +189,8 @@ func createSwitcher(acct *AccountView, switcher *PartSwitcher, conf *config.Aerc
switcher.Invalidate()
})
} else {
- switcher.parts, err = enumerateParts(acct, conf, store,
- msg, msg.BodyStructure, []int{})
+ switcher.parts, err = enumerateParts(acct, conf, msg,
+ msg.BodyStructure(), []int{})
if err != nil {
return err
}
@@ -228,7 +248,7 @@ func (mv *MessageViewer) OnInvalidate(fn func(d ui.Drawable)) {
}
func (mv *MessageViewer) Store() *lib.MessageStore {
- return mv.store
+ return mv.msg.Store()
}
func (mv *MessageViewer) SelectedAccount() *AccountView {
@@ -239,7 +259,7 @@ func (mv *MessageViewer) SelectedMessage() (*models.MessageInfo, error) {
if mv.msg == nil {
return nil, errors.New("no message selected")
}
- return mv.msg, nil
+ return mv.msg.MessageInfo(), nil
}
func (mv *MessageViewer) MarkedMessages() ([]*models.MessageInfo, error) {
@@ -250,8 +270,7 @@ func (mv *MessageViewer) MarkedMessages() ([]*models.MessageInfo, error) {
func (mv *MessageViewer) ToggleHeaders() {
switcher := mv.switcher
mv.conf.Viewer.ShowHeaders = !mv.conf.Viewer.ShowHeaders
- err := createSwitcher(
- mv.acct, switcher, mv.conf, mv.store, mv.msg)
+ err := createSwitcher(mv.acct, switcher, mv.conf, mv.msg)
if err != nil {
mv.acct.Logger().Printf(
"warning: error during create switcher - %v", err)
@@ -265,9 +284,9 @@ func (mv *MessageViewer) SelectedMessagePart() *PartInfo {
return &PartInfo{
Index: part.index,
- Msg: part.msg,
+ Msg: part.msg.MessageInfo(),
Part: part.part,
- Store: part.store,
+ Store: mv.Store(),
}
}
@@ -420,22 +439,20 @@ type PartViewer struct {
fetched bool
filter *exec.Cmd
index []int
- msg *models.MessageInfo
+ msg lib.MessageView
pager *exec.Cmd
pagerin io.WriteCloser
part *models.BodyStructure
showHeaders bool
sink io.WriteCloser
source io.Reader
- store *lib.MessageStore
term *Terminal
selecter *Selecter
grid *ui.Grid
}
func NewPartViewer(acct *AccountView, conf *config.AercConfig,
- store *lib.MessageStore, msg *models.MessageInfo,
- part *models.BodyStructure,
+ msg lib.MessageView, part *models.BodyStructure,
index []int) (*PartViewer, error) {
var (
@@ -452,6 +469,7 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
pager = exec.Command(cmd[0], cmd[1:]...)
+ info := msg.MessageInfo()
for _, f := range conf.Filters {
mime := strings.ToLower(part.MIMEType) +
"/" + strings.ToLower(part.MIMESubType)
@@ -464,13 +482,13 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
var header string
switch f.Header {
case "subject":
- header = msg.Envelope.Subject
+ header = info.Envelope.Subject
case "from":
- header = models.FormatAddresses(msg.Envelope.From)
+ header = models.FormatAddresses(info.Envelope.From)
case "to":
- header = models.FormatAddresses(msg.Envelope.To)
+ header = models.FormatAddresses(info.Envelope.To)
case "cc":
- header = models.FormatAddresses(msg.Envelope.Cc)
+ header = models.FormatAddresses(info.Envelope.Cc)
}
if f.Regex.Match([]byte(header)) {
filter = exec.Command("sh", "-c", f.Command)
@@ -521,7 +539,6 @@ func NewPartViewer(acct *AccountView, conf *config.AercConfig,
part: part,
showHeaders: conf.Viewer.ShowHeaders,
sink: pipe,
- store: store,
term: term,
selecter: selecter,
grid: grid,
@@ -577,11 +594,12 @@ func (pv *PartViewer) attemptCopy() {
}()
}
go func() {
- if pv.showHeaders && pv.msg.RFC822Headers != nil {
+ info := pv.msg.MessageInfo()
+ if pv.showHeaders && info.RFC822Headers != nil {
// header need to bypass the filter, else we run into issues
// with the filter messing with newlines etc.
// hence all writes in this block go directly to the pager
- fields := pv.msg.RFC822Headers.Fields()
+ fields := info.RFC822Headers.Fields()
for fields.Next() {
var value string
var err error
@@ -594,8 +612,8 @@ func (pv *PartViewer) attemptCopy() {
pv.pagerin.Write([]byte(field))
}
// virtual header
- if len(pv.msg.Labels) != 0 {
- labels := fmtHeader(pv.msg, "Labels", "")
+ if len(info.Labels) != 0 {
+ labels := fmtHeader(info, "Labels", "")
pv.pagerin.Write([]byte(fmt.Sprintf("Labels: %s\n", labels)))
}
pv.pagerin.Write([]byte{'\n'})
@@ -635,7 +653,8 @@ func (pv *PartViewer) Draw(ctx *ui.Context) {
return
}
if !pv.fetched {
- pv.store.FetchBodyPart(pv.msg.Uid, pv.msg.BodyStructure, pv.index, pv.SetSource)
+ pv.msg.FetchBodyPart(pv.msg.BodyStructure(),
+ pv.index, pv.SetSource)
pv.fetched = true
}
if pv.err != nil {
diff --git a/widgets/pgpinfo.go b/widgets/pgpinfo.go
new file mode 100644
index 0000000..b6a7a16
--- /dev/null
+++ b/widgets/pgpinfo.go
@@ -0,0 +1,93 @@
+package widgets
+
+import (
+ "errors"
+
+ "git.sr.ht/~sircmpwn/aerc/lib/ui"
+
+ "github.com/gdamore/tcell"
+ "golang.org/x/crypto/openpgp"
+ pgperrors "golang.org/x/crypto/openpgp/errors"
+)
+
+type PGPInfo struct {
+ ui.Invalidatable
+ details *openpgp.MessageDetails
+}
+
+func NewPGPInfo(details *openpgp.MessageDetails) *PGPInfo {
+ return &PGPInfo{details: details}
+}
+
+func (p *PGPInfo) DrawSignature(ctx *ui.Context, offs bool) {
+ errorStyle := tcell.StyleDefault.Background(tcell.ColorRed).
+ Foreground(tcell.ColorWhite).Bold(true)
+ softErrorStyle := tcell.StyleDefault.Foreground(tcell.ColorYellow).
+ Reverse(true).Bold(true)
+ validStyle := tcell.StyleDefault.Foreground(tcell.ColorGreen).Bold(true)
+ header := "Signature "
+ if offs {
+ header += " "
+ }
+
+ // TODO: Nicer prompt for TOFU, fetch from keyserver, etc
+ if errors.Is(p.details.SignatureError, pgperrors.ErrUnknownIssuer) ||
+ p.details.SignedBy == nil {
+
+ x := ctx.Printf(0, 0, tcell.StyleDefault.Bold(true), "%s", header)
+ x += ctx.Printf(x, 0, softErrorStyle, " Unknown ")
+ x += ctx.Printf(x, 0, tcell.StyleDefault,
+ " Signed with unknown key (%8X); authenticity unknown",
+ p.details.SignedByKeyId)
+ } else if p.details.SignatureError != nil {
+ x := ctx.Printf(0, 0, tcell.StyleDefault.Bold(true), "%s", header)
+ x += ctx.Printf(x, 0, errorStyle, " ✗ Invalid! ")
+ x += ctx.Printf(x, 0, tcell.StyleDefault.
+ Foreground(tcell.ColorRed).Bold(true),
+ " This message may have been tampered with! (%s)",
+ p.details.SignatureError.Error())
+ } else {
+ entity := p.details.SignedBy.Entity
+ var ident *openpgp.Identity
+ // TODO: Pick identity more intelligently
+ for _, ident = range entity.Identities {
+ break
+ }
+ ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', validStyle)
+ x := ctx.Printf(0, 0, tcell.StyleDefault.Bold(true), "%s", header)
+ x += ctx.Printf(x, 0, validStyle, "✓ Signed ")
+ x += ctx.Printf(x, 0, tcell.StyleDefault,
+ "by %s (%8X)", ident.Name, p.details.SignedByKeyId)
+ }
+}
+
+func (p *PGPInfo) DrawEncryption(ctx *ui.Context, y int) {
+ validStyle := tcell.StyleDefault.Foreground(tcell.ColorGreen).Bold(true)
+ entity := p.details.DecryptedWith.Entity
+ var ident *openpgp.Identity
+ // TODO: Pick identity more intelligently
+ for _, ident = range entity.Identities {
+ break
+ }
+
+ x := ctx.Printf(0, y, tcell.StyleDefault.Bold(true), "Encryption ")
+ x += ctx.Printf(x, y, validStyle, "✓ Encrypted ")
+ x += ctx.Printf(x, y, tcell.StyleDefault,
+ "for %s (%8X) ", ident.Name, p.details.DecryptedWith.PublicKey.KeyId)
+}
+
+func (p *PGPInfo) Draw(ctx *ui.Context) {
+ ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
+ if p.details.IsSigned && p.details.IsEncrypted {
+ p.DrawSignature(ctx, true)
+ p.DrawEncryption(ctx, 1)
+ } else if p.details.IsSigned {
+ p.DrawSignature(ctx, false)
+ } else if p.details.IsEncrypted {
+ p.DrawEncryption(ctx, 0)
+ }
+}
+
+func (p *PGPInfo) Invalidate() {
+ p.DoInvalidate(p)
+}