From 27b25174e2f0249a6a1d4ba45b70f8504b63ffb1 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Sun, 31 Mar 2019 12:14:37 -0400 Subject: [PATCH] Make the message viewer real, part one --- commands/account/pipe.go | 3 - commands/account/view-message.go | 27 +++++ lib/msgstore.go | 16 ++- widgets/aerc.go | 2 - widgets/msgviewer.go | 176 ++++++++++--------------------- worker/imap/fetch.go | 32 ++++-- worker/imap/worker.go | 2 + 7 files changed, 126 insertions(+), 132 deletions(-) create mode 100644 commands/account/view-message.go diff --git a/commands/account/pipe.go b/commands/account/pipe.go index b07a860..18130eb 100644 --- a/commands/account/pipe.go +++ b/commands/account/pipe.go @@ -20,9 +20,6 @@ func Pipe(aerc *widgets.Aerc, args []string) error { return errors.New("Usage: :pipe [args...]") } acct := aerc.SelectedAccount() - if acct == nil { - return errors.New("No account selected") - } store := acct.Messages().Store() msg := acct.Messages().Selected() store.FetchBodies([]uint32{msg.Uid}, func(reader io.Reader) { diff --git a/commands/account/view-message.go b/commands/account/view-message.go new file mode 100644 index 0000000..bef2740 --- /dev/null +++ b/commands/account/view-message.go @@ -0,0 +1,27 @@ +package account + +import ( + "errors" + + "github.com/mattn/go-runewidth" + + "git.sr.ht/~sircmpwn/aerc2/widgets" +) + +func init() { + register("view-message", ViewMessage) +} + +func ViewMessage(aerc *widgets.Aerc, args []string) error { + if len(args) != 1 { + return errors.New("Usage: view-message") + } + acct := aerc.SelectedAccount() + store := acct.Messages().Store() + msg := acct.Messages().Selected() + viewer := widgets.NewMessageViewer(store, msg) + aerc.NewTab(viewer, runewidth.Truncate( + msg.Envelope.Subject, 32, "…")) + return nil +} + diff --git a/lib/msgstore.go b/lib/msgstore.go index 64b1638..2169b2e 100644 --- a/lib/msgstore.go +++ b/lib/msgstore.go @@ -67,7 +67,6 @@ func (store *MessageStore) FetchHeaders(uids []uint32, } func (store *MessageStore) FetchBodies(uids []uint32, cb func(io.Reader)) { - // TODO: this could be optimized by pre-allocating toFetch and trimming it // at the end. In practice we expect to get most messages back in one frame. var toFetch imap.SeqSet @@ -89,6 +88,21 @@ func (store *MessageStore) FetchBodies(uids []uint32, cb func(io.Reader)) { } } +func (store *MessageStore) FetchBodyPart( + uid uint32, part int, cb func(io.Reader)) { + + store.worker.PostAction(&types.FetchMessageBodyPart{ + Uid: uid, + Part: part, + }, func(resp types.WorkerMessage) { + msg, ok := resp.(*types.MessageBodyPart) + if !ok { + return + } + cb(msg.Reader) + }) +} + func (store *MessageStore) merge( to *types.MessageInfo, from *types.MessageInfo) { diff --git a/widgets/aerc.go b/widgets/aerc.go index 781cb2b..773848d 100644 --- a/widgets/aerc.go +++ b/widgets/aerc.go @@ -62,8 +62,6 @@ func NewAerc(conf *config.AercConfig, logger *log.Logger, tabs.Add(view, acct.Name) } - tabs.Add(NewMessageViewer(), "[PATCH todo.sr.ht v2 …") - return aerc } diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go index ecfac3b..2c77aeb 100644 --- a/widgets/msgviewer.go +++ b/widgets/msgviewer.go @@ -2,124 +2,58 @@ package widgets import ( "bytes" + "fmt" "io" "os/exec" + "github.com/emersion/go-imap" "github.com/gdamore/tcell" "github.com/mattn/go-runewidth" + "git.sr.ht/~sircmpwn/aerc2/lib" "git.sr.ht/~sircmpwn/aerc2/lib/ui" + "git.sr.ht/~sircmpwn/aerc2/worker/types" ) type MessageViewer struct { + mail io.Reader + pipe io.Writer grid *ui.Grid term *Terminal } -var testMsg = `Makes the following changes to the Event type: +func formatAddresses(addrs []*imap.Address) string { + val := bytes.Buffer{} + for i, addr := range addrs { + if addr.PersonalName != "" { + val.WriteString(fmt.Sprintf("%s <%s@%s>", + addr.PersonalName, addr.MailboxName, addr.HostName)) + } else { + val.WriteString(fmt.Sprintf("%s@%s", + addr.MailboxName, addr.HostName)) + } + if i != len(addrs)-1 { + val.WriteString(", ") + } + } + return val.String() +} -* make 'user' and 'ticket' nullable since some events require it -* add 'by_user' and 'from_ticket' to enable mentions -* remove 'assinged_user' which is no longer used +func NewMessageViewer(store *lib.MessageStore, + msg *types.MessageInfo) *MessageViewer { -Ticket: https://todo.sr.ht/~sircmpwn/todo.sr.ht/156 ---- - tests/test_comments.py | 23 ++- - .../versions/75ff2f7624fd_new_event_fields.py | 142 ++++++++++++++++++ - todosrht/templates/events.html | 18 ++- - todosrht/templates/ticket.html | 31 +++- - todosrht/tickets.py | 14 +- - todosrht/types/event.py | 16 +- - 6 files changed, 207 insertions(+), 37 deletions(-) - create mode 100644 todosrht/alembic/versions/75ff2f7624fd_new_event_fields.py - -diff --git a/tests/test_comments.py b/tests/test_comments.py -index 4b3161d..b85d751 100644 ---- a/tests/test_comments.py -+++ b/tests/test_comments.py -@@ -253,20 +253,25 @@ def test_notifications_and_events(mailbox): - # Check correct events are generated - comment_events = {e for e in ticket.events - if e.event_type == EventType.comment} -- user_events = {e for e in ticket.events -+ u1_events = {e for e in u1.events -+ if e.event_type == EventType.user_mentioned} -+ u2_events = {e for e in u2.events - if e.event_type == EventType.user_mentioned} - - assert len(comment_events) == 1 -- assert len(user_events) == 2 -+ assert len(u1_events) == 1 -+ assert len(u2_events) == 1 - -- u1_mention = next(e for e in user_events if e.user == u1) -- u2_mention = next(e for e in user_events if e.user == u2) -+ u1_mention = u1_events.pop() -+ u2_mention = u2_events.pop() - - assert u1_mention.comment == comment -- assert u1_mention.ticket == ticket -+ assert u1_mention.from_ticket == ticket -+ assert u1_mention.by_user == commenter - - assert u2_mention.comment == comment -- assert u2_mention.ticket == ticket -+ assert u2_mention.from_ticket == ticket -+ assert u2_mention.by_user == commenter - - assert len(t1.events) == 1 - assert len(t2.events) == 1 -@@ -276,10 +281,12 @@ def test_notifications_and_events(mailbox): - t2_mention = t2.events[0] - - assert t1_mention.comment == comment -- assert t1_mention.user == commenter -+ assert t1_mention.from_ticket == ticket -+ assert t1_mention.by_user == commenter - - assert t2_mention.comment == comment -- assert t2_mention.user == commenter -+ assert t2_mention.from_ticket == ticket -+ assert t2_mention.by_user == commenter - - def test_ticket_mention_pattern(): - def match(text): -diff --git a/todosrht/alembic/versions/75ff2f7624fd_new_event_fields.py -b/todosrht/alembic/versions/75ff2f7624fd_new_event_fields.py -new file mode 100644 -index 0000000..1c55bfe ---- /dev/null -+++ b/todosrht/alembic/versions/75ff2f7624fd_new_event_fields.py -@@ -0,0 +1,142 @@ -+"""Add new event fields and migrate data. -+ -+Also makes Event.ticket_id and Event.user_id nullable since some these fields -+can be empty for mention events. -+ -+Revision ID: 75ff2f7624fd -+Revises: c7146cb70d6b -+Create Date: 2019-03-28 16:26:18.714300 -+ -+""" -+ -+# revision identifiers, used by Alembic. -+revision = "75ff2f7624fd" -+down_revision = "c7146cb70d6b" -` - -func NewMessageViewer() *MessageViewer { grid := ui.NewGrid().Rows([]ui.GridSpec{ - {ui.SIZE_EXACT, 4}, + {ui.SIZE_EXACT, 3}, // TODO: Based on number of header rows {ui.SIZE_WEIGHT, 1}, }).Columns([]ui.GridSpec{ {ui.SIZE_WEIGHT, 1}, }) + // TODO: let user specify additional headers to show by default headers := ui.NewGrid().Rows([]ui.GridSpec{ {ui.SIZE_EXACT, 1}, {ui.SIZE_EXACT, 1}, {ui.SIZE_EXACT, 1}, - {ui.SIZE_EXACT, 1}, }).Columns([]ui.GridSpec{ {ui.SIZE_WEIGHT, 1}, {ui.SIZE_WEIGHT, 1}, @@ -127,25 +61,19 @@ func NewMessageViewer() *MessageViewer { headers.AddChild( &HeaderView{ Name: "From", - Value: "Ivan Habunek ", + Value: formatAddresses(msg.Envelope.From), }).At(0, 0) headers.AddChild( &HeaderView{ Name: "To", - Value: "~sircmpwn/sr.ht-dev@lists.sr.ht", + Value: formatAddresses(msg.Envelope.To), }).At(0, 1) headers.AddChild( &HeaderView{ - Name: "Subject", - Value: "[PATCH todo.sr.ht v2 1/3 Alter Event fields " + - "and migrate data]", + Name: "Subject", + Value: msg.Envelope.Subject, }).At(1, 0).Span(1, 2) - headers.AddChild( - &HeaderView{ - Name: "PGP", - Value: "✓ Valid PGP signature from Ivan Habunek", - }).At(2, 0).Span(1, 2) - headers.AddChild(ui.NewFill(' ')).At(3, 0).Span(1, 2) + headers.AddChild(ui.NewFill(' ')).At(2, 0).Span(1, 2) body := ui.NewGrid().Rows([]ui.GridSpec{ {ui.SIZE_WEIGHT, 1}, @@ -154,25 +82,30 @@ func NewMessageViewer() *MessageViewer { {ui.SIZE_EXACT, 20}, }) - cmd := exec.Command("sh", "-c", "./contrib/hldiff.py | less -R") + cmd := exec.Command("less") pipe, _ := cmd.StdinPipe() term, _ := NewTerminal(cmd) - term.OnStart = func() { - go func() { - reader := bytes.NewBufferString(testMsg) - io.Copy(pipe, reader) - pipe.Close() - }() - } - term.Focus(true) - body.AddChild(term).At(0, 0) - - body.AddChild(ui.NewBordered( - &MultipartView{}, ui.BORDER_LEFT)).At(0, 1) + // TODO: configure multipart view. I left a spot for it in the grid + body.AddChild(term).At(0, 0).Span(1, 2) grid.AddChild(headers).At(0, 0) grid.AddChild(body).At(1, 0) - return &MessageViewer{grid, term} + + viewer := &MessageViewer{ + pipe: pipe, + grid: grid, + term: term, + } + + store.FetchBodyPart(msg.Uid, 0, func(reader io.Reader) { + viewer.mail = reader + go func() { + io.Copy(pipe, reader) + pipe.Close() + }() + }) + + return viewer } func (mv *MessageViewer) Draw(ctx *ui.Context) { @@ -205,7 +138,10 @@ type HeaderView struct { } func (hv *HeaderView) Draw(ctx *ui.Context) { - size := runewidth.StringWidth(hv.Name) + name := hv.Name + size := runewidth.StringWidth(name) + lim := ctx.Width() - size - 1 + value := runewidth.Truncate(" "+hv.Value, lim, "…") var ( hstyle tcell.Style vstyle tcell.Style @@ -219,8 +155,8 @@ func (hv *HeaderView) Draw(ctx *ui.Context) { hstyle = tcell.StyleDefault.Bold(true) } ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', vstyle) - ctx.Printf(0, 0, hstyle, hv.Name) - ctx.Printf(size, 0, vstyle, " "+hv.Value) + ctx.Printf(0, 0, hstyle, name) + ctx.Printf(size, 0, vstyle, value) } func (hv *HeaderView) Invalidate() { diff --git a/worker/imap/fetch.go b/worker/imap/fetch.go index 8052e13..feae462 100644 --- a/worker/imap/fetch.go +++ b/worker/imap/fetch.go @@ -30,6 +30,18 @@ func (imapw *IMAPWorker) handleFetchMessageBodies( imapw.handleFetchMessages(msg, &msg.Uids, items) } +func (imapw *IMAPWorker) handleFetchMessageBodyPart( + msg *types.FetchMessageBodyPart) { + + imapw.worker.Logger.Printf("Fetching message part") + section := &imap.BodySectionName{} + section.Path = []int{msg.Part} + items := []imap.FetchItem{section.FetchItem()} + uids := imap.SeqSet{} + uids.AddNum(msg.Uid) + imapw.handleFetchMessages(msg, &uids, items) +} + func (imapw *IMAPWorker) handleFetchMessages( msg types.WorkerMessage, uids *imap.SeqSet, items []imap.FetchItem) { @@ -43,12 +55,8 @@ func (imapw *IMAPWorker) handleFetchMessages( section := &imap.BodySectionName{} for _msg := range messages { imapw.seqMap[_msg.SeqNum-1] = _msg.Uid - if reader := _msg.GetBody(section); reader != nil { - imapw.worker.PostMessage(&types.MessageBody{ - Reader: reader, - Uid: _msg.Uid, - }, nil) - } else { + switch msg.(type) { + case *types.FetchMessageHeaders: imapw.worker.PostMessage(&types.MessageInfo{ BodyStructure: _msg.BodyStructure, Envelope: _msg.Envelope, @@ -56,6 +64,18 @@ func (imapw *IMAPWorker) handleFetchMessages( InternalDate: _msg.InternalDate, Uid: _msg.Uid, }, nil) + case *types.FetchMessageBodies: + reader := _msg.GetBody(section) + imapw.worker.PostMessage(&types.MessageBody{ + Reader: reader, + Uid: _msg.Uid, + }, nil) + case *types.FetchMessageBodyPart: + reader := _msg.GetBody(section) + imapw.worker.PostMessage(&types.MessageBodyPart{ + Reader: reader, + Uid: _msg.Uid, + }, nil) } } if err := <-done; err != nil { diff --git a/worker/imap/worker.go b/worker/imap/worker.go index 2f98595..a11d82b 100644 --- a/worker/imap/worker.go +++ b/worker/imap/worker.go @@ -160,6 +160,8 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error { w.handleFetchMessageHeaders(msg) case *types.FetchMessageBodies: w.handleFetchMessageBodies(msg) + case *types.FetchMessageBodyPart: + w.handleFetchMessageBodyPart(msg) case *types.DeleteMessages: w.handleDeleteMessages(msg) default: