From f3158b36f1f210ff54febbe82b571c1379b30c98 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Tue, 3 Mar 2020 16:20:07 -0500 Subject: [PATCH] Initial support for PGP decryption & signatures --- aerc.go | 4 ++ commands/account/view.go | 8 ++- commands/msg/delete.go | 7 ++- commands/msgview/next.go | 8 ++- go.mod | 6 +- go.sum | 34 +++-------- lib/keystore.go | 81 +++++++++++++++++++++++++ lib/messageview.go | 128 +++++++++++++++++++++++++++++++++++++++ lib/ui/interfaces.go | 4 ++ lib/ui/ui.go | 4 ++ widgets/aerc.go | 48 +++++++++++++++ widgets/getpasswd.go | 61 +++++++++++++++++++ widgets/headerlayout.go | 3 +- widgets/msglist.go | 7 ++- widgets/msgviewer.go | 103 ++++++++++++++++++------------- widgets/pgpinfo.go | 93 ++++++++++++++++++++++++++++ worker/lib/parse.go | 6 +- 17 files changed, 523 insertions(+), 82 deletions(-) create mode 100644 lib/keystore.go create mode 100644 lib/messageview.go create mode 100644 widgets/getpasswd.go create mode 100644 widgets/pgpinfo.go diff --git a/aerc.go b/aerc.go index 028cc6a..1b51d6d 100644 --- a/aerc.go +++ b/aerc.go @@ -166,6 +166,10 @@ func main() { ui.EnableMouse() } + logger.Println("Initializing PGP keyring") + lib.InitKeyring() + defer lib.UnlockKeyring() + logger.Println("Starting Unix server") as, err := lib.StartServer(logger) if err != nil { diff --git a/commands/account/view.go b/commands/account/view.go index aab9052..d1b90ce 100644 --- a/commands/account/view.go +++ b/commands/account/view.go @@ -3,6 +3,7 @@ package account import ( "errors" + "git.sr.ht/~sircmpwn/aerc/lib" "git.sr.ht/~sircmpwn/aerc/widgets" ) @@ -37,7 +38,10 @@ func (ViewMessage) Execute(aerc *widgets.Aerc, args []string) error { if deleted { return nil } - viewer := widgets.NewMessageViewer(acct, aerc.Config(), store, msg) - aerc.NewTab(viewer, msg.Envelope.Subject) + lib.NewMessageStoreView(msg, store, aerc.DecryptKeys, + func(view lib.MessageView) { + viewer := widgets.NewMessageViewer(acct, aerc.Config(), view) + aerc.NewTab(viewer, msg.Envelope.Subject) + }) return nil } diff --git a/commands/msg/delete.go b/commands/msg/delete.go index 3f91a46..fb0d1f8 100644 --- a/commands/msg/delete.go +++ b/commands/msg/delete.go @@ -68,8 +68,11 @@ func (Delete) Execute(aerc *widgets.Aerc, args []string) error { acct.Messages().Scroll() return nil } - nextMv := widgets.NewMessageViewer(acct, aerc.Config(), store, next) - aerc.ReplaceTab(mv, nextMv, next.Envelope.Subject) + lib.NewMessageStoreView(next, store, aerc.DecryptKeys, + func(view lib.MessageView) { + nextMv := widgets.NewMessageViewer(acct, aerc.Config(), view) + aerc.ReplaceTab(mv, nextMv, next.Envelope.Subject) + }) } } acct.Messages().Scroll() diff --git a/commands/msgview/next.go b/commands/msgview/next.go index 4dc504c..c218ad5 100644 --- a/commands/msgview/next.go +++ b/commands/msgview/next.go @@ -2,6 +2,7 @@ package msgview import ( "git.sr.ht/~sircmpwn/aerc/commands/account" + "git.sr.ht/~sircmpwn/aerc/lib" "git.sr.ht/~sircmpwn/aerc/widgets" ) @@ -36,7 +37,10 @@ func (NextPrevMsg) Execute(aerc *widgets.Aerc, args []string) error { aerc.RemoveTab(mv) return nil } - nextMv := widgets.NewMessageViewer(acct, aerc.Config(), store, nextMsg) - aerc.ReplaceTab(mv, nextMv, nextMsg.Envelope.Subject) + lib.NewMessageStoreView(nextMsg, store, aerc.DecryptKeys, + func(view lib.MessageView) { + nextMv := widgets.NewMessageViewer(acct, aerc.Config(), view) + aerc.ReplaceTab(mv, nextMv, nextMsg.Envelope.Subject) + }) return nil } diff --git a/go.mod b/go.mod index a144960..342c020 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,9 @@ require ( github.com/ddevault/go-libvterm v0.0.0-20190526194226-b7d861da3810 github.com/emersion/go-imap v1.0.4 github.com/emersion/go-imap-idle v0.0.0-20190519112320-2704abd7050e - github.com/emersion/go-maildir v0.0.0-20191218233049-14e25d3ea720 + github.com/emersion/go-maildir v0.2.0 github.com/emersion/go-message v0.11.1 + github.com/emersion/go-pgpmail v0.0.0-20200303213726-db035a3a4139 github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b github.com/emersion/go-smtp v0.12.1 github.com/fsnotify/fsnotify v1.4.7 @@ -33,6 +34,7 @@ require ( github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945 // indirect github.com/stretchr/testify v1.3.0 github.com/zenhack/go.notmuch v0.0.0-20190821052706-5a1961965cfb + golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 golang.org/x/net v0.0.0-20200301022130-244492dfa37a // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 // indirect @@ -41,4 +43,6 @@ require ( gopkg.in/yaml.v2 v2.2.8 // indirect ) +replace golang.org/x/crypto => github.com/ProtonMail/crypto v1.0.1-0.20191122234321-e77a1f03baa0 + replace github.com/gdamore/tcell => git.sr.ht/~sircmpwn/tcell v0.0.0-20190807054800-3fdb6bc01a50 diff --git a/go.sum b/go.sum index 4b805c4..93773ca 100644 --- a/go.sum +++ b/go.sum @@ -3,15 +3,14 @@ git.sr.ht/~sircmpwn/getopt v0.0.0-20190808004552-daaf1274538b h1:da5JBQ6dcW14aWn git.sr.ht/~sircmpwn/getopt v0.0.0-20190808004552-daaf1274538b/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw= git.sr.ht/~sircmpwn/tcell v0.0.0-20190807054800-3fdb6bc01a50 h1:GEZXdK3vfsEGlRwlybiAvOnYLA4YKaVWxAQSn/BSkNw= git.sr.ht/~sircmpwn/tcell v0.0.0-20190807054800-3fdb6bc01a50/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= -github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/ProtonMail/crypto v1.0.1-0.20191122234321-e77a1f03baa0 h1:HNSciyt/mdq/xCqb5HSYimGr6Djb5rC8694BJqX2xOo= +github.com/ProtonMail/crypto v1.0.1-0.20191122234321-e77a1f03baa0/go.mod h1:MBriIAodHvZ+YvwvMJWCTmseW/LkeVRPWp/iZKvee4g= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ddevault/go-libvterm v0.0.0-20190526194226-b7d861da3810 h1:VlHKuIrEvuGlED53TkovT4AVUjrqTyeCt3wiqw1OsFc= github.com/ddevault/go-libvterm v0.0.0-20190526194226-b7d861da3810/go.mod h1:Ow1oE1Hr4xE7eWY2/Ih2kbcOyyXDH7G0XKv/I4yiCYs= @@ -19,11 +18,12 @@ github.com/emersion/go-imap v1.0.4 h1:uiCAIHM6Z5Jwkma1zdNDWWXxSCqb+/xHBkHflD7XBr github.com/emersion/go-imap v1.0.4/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU= github.com/emersion/go-imap-idle v0.0.0-20190519112320-2704abd7050e h1:L7bswVJZcf2YHofgom49oFRwVqmBj/qZqDy9/SJpZMY= github.com/emersion/go-imap-idle v0.0.0-20190519112320-2704abd7050e/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78= -github.com/emersion/go-maildir v0.0.0-20191218233049-14e25d3ea720 h1:0Hj2cVnV1NunzYK6Y9fjTdpeqHClF+QNTyiCyhIJ/0E= -github.com/emersion/go-maildir v0.0.0-20191218233049-14e25d3ea720/go.mod h1:I2j27lND/SRLgxROe50Vam81MSaqPFvJ0OHNnDZ7n84= +github.com/emersion/go-maildir v0.2.0 h1:fC4+UVGl8GcQGbFF7AWab2JMf4VbKz+bMNv07xxhzs8= +github.com/emersion/go-maildir v0.2.0/go.mod h1:I2j27lND/SRLgxROe50Vam81MSaqPFvJ0OHNnDZ7n84= github.com/emersion/go-message v0.11.1 h1:0C/S4JIXDTSfXB1vpqdimAYyK4+79fgEAMQ0dSL+Kac= github.com/emersion/go-message v0.11.1/go.mod h1:C4jnca5HOTo4bGN9YdqNQM9sITuT3Y0K6bSUw9RklvY= -github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e h1:ba7YsgX5OV8FjGi5ZWml8Jng6oBrJAb3ahqWMJ5Ce8Q= +github.com/emersion/go-pgpmail v0.0.0-20200303213726-db035a3a4139 h1:JTUbkRuQFtDrl5KHWR2jrh9SUeSDEEEjUcHJkXdAE2Q= +github.com/emersion/go-pgpmail v0.0.0-20200303213726-db035a3a4139/go.mod h1:+Ovy1VQCUKPdjWkOiWvFoiFaWXkqn1PA793VvfEYWQU= github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs= github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= @@ -39,21 +39,16 @@ github.com/go-ini/ini v1.52.0 h1:3UeUAveYUTCYV/G0jNDiIrrtIeAl1oAjshYyU2PaAlQ= github.com/go-ini/ini v1.52.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kyoh86/xdg v1.2.0 h1:CERuT/ShdTDj+A2UaX3hQ3mOV369+Sj+wyn2nIRIIkI= github.com/kyoh86/xdg v1.2.0/go.mod h1:/mg8zwu1+qe76oTFUBnyS7rJzk7LLC0VGEzJyJ19DHs= -github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4= github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= @@ -61,11 +56,9 @@ github.com/martinlindhe/base36 v1.0.0 h1:eYsumTah144C0A8P1T/AVSUk5ZoLnhfYFM3OGQx github.com/martinlindhe/base36 v1.0.0/go.mod h1:+AtEs8xrBpCeYgSLoY/aJ6Wf37jtBuR0s35750M27+8= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-pointer v0.0.0-20180825124634-49522c3f3791 h1:PfHMsLQJwoc0ccjK0sam6J0wQo4s8mOuAo2yQGw+T2U= github.com/mattn/go-pointer v0.0.0-20180825124634-49522c3f3791/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= github.com/mattn/go-pointer v0.0.0-20190911064623-a0a44394634f h1:QTRRO+ozoYgT3CQRIzNVYJRU3DB8HRnkZv6mr4ISmMA= github.com/mattn/go-pointer v0.0.0-20190911064623-a0a44394634f/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= -github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0= github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -75,27 +68,20 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/riywo/loginshell v0.0.0-20190610082906-2ed199a032f6 h1:0QWE8TiOGSB+korydW5z4hPQ5QBVqLos+M2ta4pHaY0= github.com/riywo/loginshell v0.0.0-20190610082906-2ed199a032f6/go.mod h1:/PfPXh0EntGc3QAAyUaviy4S9tzy4Zp0e2ilq4voC6E= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w= github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= -github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945 h1:N8Bg45zpk/UcpNGnfJt2y/3lRWASHNTUET8owPYCgYI= github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/zenhack/go.notmuch v0.0.0-20190821052706-5a1961965cfb h1:eZBIw4TilXSAEYcWKf51bERhwH431YwntDYus0Bgxh0= github.com/zenhack/go.notmuch v0.0.0-20190821052706-5a1961965cfb/go.mod h1:zJtFvR3NinVdmBiLyB4MyXKmqyVfZEb2cK97ISfTgV8= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -103,22 +89,18 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BG golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/ini.v1 v1.44.0 h1:YRJzTUp0kSYWUVFF5XAbDFfyiqwsl0Vb9R8TVP5eRi0= gopkg.in/ini.v1 v1.44.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/lib/keystore.go b/lib/keystore.go new file mode 100644 index 0000000..dcdbd74 --- /dev/null +++ b/lib/keystore.go @@ -0,0 +1,81 @@ +package lib + +import ( + "io" + "os" + "path" + + "github.com/kyoh86/xdg" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/packet" +) + +var ( + Keyring openpgp.EntityList + + locked bool +) + +func InitKeyring() { + os.MkdirAll(path.Join(xdg.DataHome(), "aerc"), 0700) + + lockpath := path.Join(xdg.DataHome(), "aerc", "keyring.lock") + lockfile, err := os.OpenFile(lockpath, os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + // TODO: Consider connecting to main process over IPC socket + locked = false + } else { + locked = true + lockfile.Close() + } + + keypath := path.Join(xdg.DataHome(), "aerc", "keyring.asc") + keyfile, err := os.Open(keypath) + if os.IsNotExist(err) { + return + } else if err != nil { + panic(err) + } + defer keyfile.Close() + + Keyring, err = openpgp.ReadKeyRing(keyfile) + if err != nil { + panic(err) + } +} + +func UnlockKeyring() { + if !locked { + return + } + lockpath := path.Join(xdg.DataHome(), "aerc", "keyring.lock") + os.Remove(lockpath) +} + +func ImportKeys(r io.Reader) error { + keys, err := openpgp.ReadKeyRing(r) + if err != nil { + return err + } + Keyring = append(Keyring, keys...) + if locked { + keypath := path.Join(xdg.DataHome(), "aerc", "keyring.asc") + keyfile, err := os.OpenFile(keypath, os.O_CREATE|os.O_APPEND, 0600) + if err != nil { + return err + } + defer keyfile.Close() + + for _, key := range keys { + if key.PrivateKey != nil { + err = key.SerializePrivate(keyfile, &packet.Config{}) + } else { + err = key.Serialize(keyfile) + } + if err != nil { + return err + } + } + } + return nil +} diff --git a/lib/messageview.go b/lib/messageview.go new file mode 100644 index 0000000..be3b90f --- /dev/null +++ b/lib/messageview.go @@ -0,0 +1,128 @@ +package lib + +import ( + "bytes" + "io" + "io/ioutil" + + "github.com/emersion/go-message" + _ "github.com/emersion/go-message/charset" + "github.com/emersion/go-pgpmail" + "golang.org/x/crypto/openpgp" + + "git.sr.ht/~sircmpwn/aerc/models" + "git.sr.ht/~sircmpwn/aerc/worker/lib" +) + +// This is an abstraction for viewing a message with semi-transparent PGP +// support. +type MessageView interface { + // Returns the MessageInfo for this message + MessageInfo() *models.MessageInfo + + // Returns the BodyStructure for this message + BodyStructure() *models.BodyStructure + + // Returns the message store that this message was originally sourced from + Store() *MessageStore + + // Fetches a specific body part for this message + FetchBodyPart(parent *models.BodyStructure, + part []int, cb func(io.Reader)) + + PGPDetails() *openpgp.MessageDetails +} + +func usePGP(info *models.BodyStructure) bool { + if info.MIMEType == "application" { + if info.MIMESubType == "pgp-encrypted" || + info.MIMESubType == "pgp-signature" { + + return true + } + } + for _, part := range info.Parts { + if usePGP(part) { + return true + } + } + return false +} + +type MessageStoreView struct { + messageInfo *models.MessageInfo + messageStore *MessageStore + message []byte + details *openpgp.MessageDetails + bodyStructure *models.BodyStructure +} + +func NewMessageStoreView(messageInfo *models.MessageInfo, + store *MessageStore, decryptKeys openpgp.PromptFunction, + cb func(MessageView)) { + + msv := &MessageStoreView{messageInfo, store, + nil, nil, messageInfo.BodyStructure} + + if usePGP(messageInfo.BodyStructure) { + store.FetchFull([]uint32{messageInfo.Uid}, func(reader io.Reader) { + pgpReader, err := pgpmail.Read(reader, Keyring, decryptKeys, nil) + if err != nil { + panic(err) + } + msv.message, err = ioutil.ReadAll(pgpReader.MessageDetails.UnverifiedBody) + if err != nil { + panic(err) + } + decrypted, err := message.Read(bytes.NewBuffer(msv.message)) + if err != nil { + panic(err) + } + bs, err := lib.ParseEntityStructure(decrypted) + if err != nil { + panic(err) + } + msv.bodyStructure = bs + msv.details = pgpReader.MessageDetails + cb(msv) + }) + } else { + cb(msv) + } +} + +func (msv *MessageStoreView) MessageInfo() *models.MessageInfo { + return msv.messageInfo +} + +func (msv *MessageStoreView) BodyStructure() *models.BodyStructure { + return msv.bodyStructure +} + +func (msv *MessageStoreView) Store() *MessageStore { + return msv.messageStore +} + +func (msv *MessageStoreView) PGPDetails() *openpgp.MessageDetails { + return msv.details +} + +func (msv *MessageStoreView) FetchBodyPart(parent *models.BodyStructure, + part []int, cb func(io.Reader)) { + + if msv.message == nil { + msv.messageStore.FetchBodyPart(msv.messageInfo.Uid, parent, part, cb) + return + } + + buf := bytes.NewBuffer(msv.message) + msg, err := message.Read(buf) + if err != nil { + panic(err) + } + reader, err := lib.FetchEntityPartReader(msg, part) + if err != nil { + panic(err) + } + cb(reader) +} diff --git a/lib/ui/interfaces.go b/lib/ui/interfaces.go index 9e79571..c12bdb7 100644 --- a/lib/ui/interfaces.go +++ b/lib/ui/interfaces.go @@ -16,6 +16,10 @@ type Drawable interface { Invalidate() } +type RootDrawable interface { + Initialize(ui *UI) +} + type Interactive interface { // Returns true if the event was handled by this component Event(event tcell.Event) bool diff --git a/lib/ui/ui.go b/lib/ui/ui.go index 16b176d..9a9ed14 100644 --- a/lib/ui/ui.go +++ b/lib/ui/ui.go @@ -55,6 +55,10 @@ func Initialize(content DrawableInteractiveBeeper) (*UI, error) { content.OnBeep(screen.Beep) content.Focus(true) + if root, ok := content.(RootDrawable); ok { + root.Initialize(&state) + } + return &state, nil } 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) +} diff --git a/worker/lib/parse.go b/worker/lib/parse.go index fd744a7..ac20ac8 100644 --- a/worker/lib/parse.go +++ b/worker/lib/parse.go @@ -57,7 +57,7 @@ func splitMIME(m string) (string, string) { return parts[0], parts[1] } -func parseEntityStructure(e *message.Entity) (*models.BodyStructure, error) { +func ParseEntityStructure(e *message.Entity) (*models.BodyStructure, error) { var body models.BodyStructure contentType, ctParams, err := e.Header.ContentType() if err != nil { @@ -86,7 +86,7 @@ func parseEntityStructure(e *message.Entity) (*models.BodyStructure, error) { } else if err != nil { return nil, err } - ps, err := parseEntityStructure(part) + ps, err := ParseEntityStructure(part) if err != nil { return nil, fmt.Errorf("could not parse child entity structure: %v", err) } @@ -189,7 +189,7 @@ func MessageInfo(raw RawMessage) (*models.MessageInfo, error) { if err != nil { return nil, fmt.Errorf("could not read message: %v", err) } - bs, err := parseEntityStructure(msg) + bs, err := ParseEntityStructure(msg) if err != nil { return nil, fmt.Errorf("could not get structure: %v", err) }