diff options
Diffstat (limited to 'widgets')
-rw-r--r-- | widgets/aerc.go | 48 | ||||
-rw-r--r-- | widgets/getpasswd.go | 61 | ||||
-rw-r--r-- | widgets/headerlayout.go | 3 | ||||
-rw-r--r-- | widgets/msglist.go | 7 | ||||
-rw-r--r-- | widgets/msgviewer.go | 103 | ||||
-rw-r--r-- | widgets/pgpinfo.go | 93 |
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) +} |