summary refs log tree commit diff stats
path: root/widgets
diff options
context:
space:
mode:
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)
+}