implements ability to view headers in message view
This commit is contained in:
parent
2279ac3ab3
commit
b83e7c9fa6
6 changed files with 146 additions and 54 deletions
25
commands/msgview/toggle-headers.go
Normal file
25
commands/msgview/toggle-headers.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package msgview
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.sr.ht/~sircmpwn/aerc/widgets"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
register("toggle-headers", ToggleHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggleHeadersUsage(cmd string) error {
|
||||||
|
return errors.New(fmt.Sprintf("Usage: %s", cmd))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToggleHeaders(aerc *widgets.Aerc, args []string) error {
|
||||||
|
if len(args) > 1 {
|
||||||
|
return toggleHeadersUsage(args[0])
|
||||||
|
}
|
||||||
|
mv, _ := aerc.SelectedTab().(*widgets.MessageViewer)
|
||||||
|
mv.ToggleHeaders()
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -44,6 +44,13 @@ pager=less -R
|
||||||
# Default: text/plain,text/html
|
# Default: text/plain,text/html
|
||||||
alternatives=text/plain,text/html
|
alternatives=text/plain,text/html
|
||||||
|
|
||||||
|
#
|
||||||
|
# Default setting to determine whether to show full headers or only parsed
|
||||||
|
# ones in message viewer.
|
||||||
|
#
|
||||||
|
# Default: false
|
||||||
|
show-headers=false
|
||||||
|
|
||||||
[compose]
|
[compose]
|
||||||
#
|
#
|
||||||
# Specifies the command to run the editor with. It will be shown in an embedded
|
# Specifies the command to run the editor with. It will be shown in an embedded
|
||||||
|
|
|
@ -72,6 +72,7 @@ type FilterConfig struct {
|
||||||
type ViewerConfig struct {
|
type ViewerConfig struct {
|
||||||
Pager string
|
Pager string
|
||||||
Alternatives []string
|
Alternatives []string
|
||||||
|
ShowHeaders bool `ini:"show-headers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AercConfig struct {
|
type AercConfig struct {
|
||||||
|
|
|
@ -35,8 +35,9 @@ type MessageViewer struct {
|
||||||
|
|
||||||
type PartSwitcher struct {
|
type PartSwitcher struct {
|
||||||
ui.Invalidatable
|
ui.Invalidatable
|
||||||
parts []*PartViewer
|
parts []*PartViewer
|
||||||
selected int
|
selected int
|
||||||
|
showHeaders bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatAddresses(addrs []*imap.Address) string {
|
func formatAddresses(addrs []*imap.Address) string {
|
||||||
|
@ -98,33 +99,10 @@ func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
|
||||||
}).At(2, 0).Span(1, 2)
|
}).At(2, 0).Span(1, 2)
|
||||||
headers.AddChild(ui.NewFill(' ')).At(3, 0).Span(1, 2)
|
headers.AddChild(ui.NewFill(' ')).At(3, 0).Span(1, 2)
|
||||||
|
|
||||||
var err error
|
|
||||||
switcher := &PartSwitcher{}
|
switcher := &PartSwitcher{}
|
||||||
if len(msg.BodyStructure.Parts) == 0 {
|
err := createSwitcher(switcher, conf, store, msg, conf.Viewer.ShowHeaders)
|
||||||
pv, err := NewPartViewer(conf, store, msg, msg.BodyStructure, []int{1})
|
if err != nil {
|
||||||
if err != nil {
|
goto handle_error
|
||||||
goto handle_error
|
|
||||||
}
|
|
||||||
switcher.parts = []*PartViewer{pv}
|
|
||||||
pv.OnInvalidate(func(_ ui.Drawable) {
|
|
||||||
switcher.Invalidate()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
switcher.parts, err = enumerateParts(conf, store,
|
|
||||||
msg, msg.BodyStructure, []int{})
|
|
||||||
if err != nil {
|
|
||||||
goto handle_error
|
|
||||||
}
|
|
||||||
switcher.selected = -1
|
|
||||||
for i, pv := range switcher.parts {
|
|
||||||
pv.OnInvalidate(func(_ ui.Drawable) {
|
|
||||||
switcher.Invalidate()
|
|
||||||
})
|
|
||||||
// TODO: switch to user's preferred mimetype, if configured
|
|
||||||
if switcher.selected == -1 && pv.part.MIMEType != "multipart" {
|
|
||||||
switcher.selected = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
grid.AddChild(headers).At(0, 0)
|
grid.AddChild(headers).At(0, 0)
|
||||||
|
@ -132,6 +110,7 @@ func NewMessageViewer(acct *AccountView, conf *config.AercConfig,
|
||||||
|
|
||||||
return &MessageViewer{
|
return &MessageViewer{
|
||||||
acct: acct,
|
acct: acct,
|
||||||
|
conf: conf,
|
||||||
grid: grid,
|
grid: grid,
|
||||||
msg: msg,
|
msg: msg,
|
||||||
store: store,
|
store: store,
|
||||||
|
@ -148,7 +127,7 @@ handle_error:
|
||||||
|
|
||||||
func enumerateParts(conf *config.AercConfig, store *lib.MessageStore,
|
func enumerateParts(conf *config.AercConfig, store *lib.MessageStore,
|
||||||
msg *types.MessageInfo, body *imap.BodyStructure,
|
msg *types.MessageInfo, body *imap.BodyStructure,
|
||||||
index []int) ([]*PartViewer, error) {
|
showHeaders bool, index []int) ([]*PartViewer, error) {
|
||||||
|
|
||||||
var parts []*PartViewer
|
var parts []*PartViewer
|
||||||
for i, part := range body.Parts {
|
for i, part := range body.Parts {
|
||||||
|
@ -158,14 +137,14 @@ func enumerateParts(conf *config.AercConfig, store *lib.MessageStore,
|
||||||
pv := &PartViewer{part: part}
|
pv := &PartViewer{part: part}
|
||||||
parts = append(parts, pv)
|
parts = append(parts, pv)
|
||||||
subParts, err := enumerateParts(
|
subParts, err := enumerateParts(
|
||||||
conf, store, msg, part, curindex)
|
conf, store, msg, part, showHeaders, curindex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
parts = append(parts, subParts...)
|
parts = append(parts, subParts...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
pv, err := NewPartViewer(conf, store, msg, part, curindex)
|
pv, err := NewPartViewer(conf, store, msg, part, showHeaders, curindex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -174,6 +153,44 @@ func enumerateParts(conf *config.AercConfig, store *lib.MessageStore,
|
||||||
return parts, nil
|
return parts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createSwitcher(switcher *PartSwitcher, conf *config.AercConfig,
|
||||||
|
store *lib.MessageStore, msg *types.MessageInfo, showHeaders bool) error {
|
||||||
|
var err error
|
||||||
|
switcher.showHeaders = showHeaders
|
||||||
|
|
||||||
|
if showHeaders {
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(msg.BodyStructure.Parts) == 0 {
|
||||||
|
pv, err := NewPartViewer(conf, store, msg, msg.BodyStructure,
|
||||||
|
showHeaders, []int{1})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switcher.parts = []*PartViewer{pv}
|
||||||
|
pv.OnInvalidate(func(_ ui.Drawable) {
|
||||||
|
switcher.Invalidate()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
switcher.parts, err = enumerateParts(conf, store,
|
||||||
|
msg, msg.BodyStructure, showHeaders, []int{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switcher.selected = -1
|
||||||
|
for i, pv := range switcher.parts {
|
||||||
|
pv.OnInvalidate(func(_ ui.Drawable) {
|
||||||
|
switcher.Invalidate()
|
||||||
|
})
|
||||||
|
// TODO: switch to user's preferred mimetype, if configured
|
||||||
|
if switcher.selected == -1 && pv.part.MIMEType != "multipart" {
|
||||||
|
switcher.selected = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (mv *MessageViewer) Draw(ctx *ui.Context) {
|
func (mv *MessageViewer) Draw(ctx *ui.Context) {
|
||||||
if mv.err != nil {
|
if mv.err != nil {
|
||||||
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
|
||||||
|
@ -205,6 +222,15 @@ func (mv *MessageViewer) SelectedMessage() *types.MessageInfo {
|
||||||
return mv.msg
|
return mv.msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mv *MessageViewer) ToggleHeaders() {
|
||||||
|
switcher := mv.switcher
|
||||||
|
err := createSwitcher(switcher, mv.conf, mv.store, mv.msg, !switcher.showHeaders)
|
||||||
|
if err != nil {
|
||||||
|
mv.acct.Logger().Printf("warning: error during create switcher - %v", err)
|
||||||
|
}
|
||||||
|
switcher.Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
func (mv *MessageViewer) CurrentPart() *PartInfo {
|
func (mv *MessageViewer) CurrentPart() *PartInfo {
|
||||||
switcher := mv.switcher
|
switcher := mv.switcher
|
||||||
part := switcher.parts[switcher.selected]
|
part := switcher.parts[switcher.selected]
|
||||||
|
@ -295,18 +321,19 @@ func (mv *MessageViewer) Focus(focus bool) {
|
||||||
|
|
||||||
type PartViewer struct {
|
type PartViewer struct {
|
||||||
ui.Invalidatable
|
ui.Invalidatable
|
||||||
err error
|
err error
|
||||||
fetched bool
|
fetched bool
|
||||||
filter *exec.Cmd
|
filter *exec.Cmd
|
||||||
index []int
|
index []int
|
||||||
msg *types.MessageInfo
|
msg *types.MessageInfo
|
||||||
pager *exec.Cmd
|
pager *exec.Cmd
|
||||||
pagerin io.WriteCloser
|
pagerin io.WriteCloser
|
||||||
part *imap.BodyStructure
|
part *imap.BodyStructure
|
||||||
sink io.WriteCloser
|
showHeaders bool
|
||||||
source io.Reader
|
sink io.WriteCloser
|
||||||
store *lib.MessageStore
|
source io.Reader
|
||||||
term *Terminal
|
store *lib.MessageStore
|
||||||
|
term *Terminal
|
||||||
}
|
}
|
||||||
|
|
||||||
type PartInfo struct {
|
type PartInfo struct {
|
||||||
|
@ -318,7 +345,8 @@ type PartInfo struct {
|
||||||
|
|
||||||
func NewPartViewer(conf *config.AercConfig,
|
func NewPartViewer(conf *config.AercConfig,
|
||||||
store *lib.MessageStore, msg *types.MessageInfo,
|
store *lib.MessageStore, msg *types.MessageInfo,
|
||||||
part *imap.BodyStructure, index []int) (*PartViewer, error) {
|
part *imap.BodyStructure, showHeaders bool,
|
||||||
|
index []int) (*PartViewer, error) {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
filter *exec.Cmd
|
filter *exec.Cmd
|
||||||
|
@ -375,15 +403,16 @@ func NewPartViewer(conf *config.AercConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
pv := &PartViewer{
|
pv := &PartViewer{
|
||||||
filter: filter,
|
filter: filter,
|
||||||
index: index,
|
index: index,
|
||||||
msg: msg,
|
msg: msg,
|
||||||
pager: pager,
|
pager: pager,
|
||||||
pagerin: pagerin,
|
pagerin: pagerin,
|
||||||
part: part,
|
part: part,
|
||||||
sink: pipe,
|
showHeaders: showHeaders,
|
||||||
store: store,
|
sink: pipe,
|
||||||
term: term,
|
store: store,
|
||||||
|
term: term,
|
||||||
}
|
}
|
||||||
|
|
||||||
if term != nil {
|
if term != nil {
|
||||||
|
@ -439,6 +468,15 @@ func (pv *PartViewer) attemptCopy() {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
|
if pv.showHeaders && pv.msg.RFC822Headers != nil {
|
||||||
|
fields := pv.msg.RFC822Headers.Fields()
|
||||||
|
for fields.Next() {
|
||||||
|
field := fmt.Sprintf("%s: %s\n", fields.Key(), fields.Value())
|
||||||
|
pv.sink.Write([]byte(field))
|
||||||
|
}
|
||||||
|
pv.sink.Write([]byte{'\n'})
|
||||||
|
}
|
||||||
|
|
||||||
entity, err := message.New(header, pv.source)
|
entity, err := message.New(header, pv.source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pv.err = err
|
pv.err = err
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
package imap
|
package imap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
|
||||||
"github.com/emersion/go-imap"
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-message"
|
||||||
|
"github.com/emersion/go-message/mail"
|
||||||
|
"github.com/emersion/go-message/textproto"
|
||||||
|
|
||||||
"git.sr.ht/~sircmpwn/aerc/worker/types"
|
"git.sr.ht/~sircmpwn/aerc/worker/types"
|
||||||
)
|
)
|
||||||
|
@ -10,15 +15,22 @@ func (imapw *IMAPWorker) handleFetchMessageHeaders(
|
||||||
msg *types.FetchMessageHeaders) {
|
msg *types.FetchMessageHeaders) {
|
||||||
|
|
||||||
imapw.worker.Logger.Printf("Fetching message headers")
|
imapw.worker.Logger.Printf("Fetching message headers")
|
||||||
|
section := &imap.BodySectionName{
|
||||||
|
BodyPartName: imap.BodyPartName{
|
||||||
|
Specifier: imap.HeaderSpecifier,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
items := []imap.FetchItem{
|
items := []imap.FetchItem{
|
||||||
imap.FetchBodyStructure,
|
imap.FetchBodyStructure,
|
||||||
imap.FetchEnvelope,
|
imap.FetchEnvelope,
|
||||||
imap.FetchInternalDate,
|
imap.FetchInternalDate,
|
||||||
imap.FetchFlags,
|
imap.FetchFlags,
|
||||||
imap.FetchUid,
|
imap.FetchUid,
|
||||||
|
section.FetchItem(),
|
||||||
}
|
}
|
||||||
|
|
||||||
imapw.handleFetchMessages(msg, &msg.Uids, items, nil)
|
imapw.handleFetchMessages(msg, &msg.Uids, items, section)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (imapw *IMAPWorker) handleFetchMessageBodyPart(
|
func (imapw *IMAPWorker) handleFetchMessageBodyPart(
|
||||||
|
@ -54,12 +66,19 @@ func (imapw *IMAPWorker) handleFetchMessages(
|
||||||
imapw.seqMap[_msg.SeqNum-1] = _msg.Uid
|
imapw.seqMap[_msg.SeqNum-1] = _msg.Uid
|
||||||
switch msg.(type) {
|
switch msg.(type) {
|
||||||
case *types.FetchMessageHeaders:
|
case *types.FetchMessageHeaders:
|
||||||
|
reader := _msg.GetBody(section)
|
||||||
|
textprotoHeader, err := textproto.ReadHeader(bufio.NewReader(reader))
|
||||||
|
var header *mail.Header
|
||||||
|
if err == nil {
|
||||||
|
header = &mail.Header{message.Header{textprotoHeader}}
|
||||||
|
}
|
||||||
imapw.worker.PostMessage(&types.MessageInfo{
|
imapw.worker.PostMessage(&types.MessageInfo{
|
||||||
Message: types.RespondTo(msg),
|
Message: types.RespondTo(msg),
|
||||||
BodyStructure: _msg.BodyStructure,
|
BodyStructure: _msg.BodyStructure,
|
||||||
Envelope: _msg.Envelope,
|
Envelope: _msg.Envelope,
|
||||||
Flags: _msg.Flags,
|
Flags: _msg.Flags,
|
||||||
InternalDate: _msg.InternalDate,
|
InternalDate: _msg.InternalDate,
|
||||||
|
RFC822Headers: header,
|
||||||
Uid: _msg.Uid,
|
Uid: _msg.Uid,
|
||||||
}, nil)
|
}, nil)
|
||||||
case *types.FetchFullMessages:
|
case *types.FetchFullMessages:
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/emersion/go-imap"
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-message/mail"
|
||||||
|
|
||||||
"git.sr.ht/~sircmpwn/aerc/config"
|
"git.sr.ht/~sircmpwn/aerc/config"
|
||||||
)
|
)
|
||||||
|
@ -145,6 +146,7 @@ type MessageInfo struct {
|
||||||
Envelope *imap.Envelope
|
Envelope *imap.Envelope
|
||||||
Flags []string
|
Flags []string
|
||||||
InternalDate time.Time
|
InternalDate time.Time
|
||||||
|
RFC822Headers *mail.Header
|
||||||
Size uint32
|
Size uint32
|
||||||
Uid uint32
|
Uid uint32
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue