summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorDrew DeVault <sir@cmpwn.com>2019-03-31 12:14:37 -0400
committerDrew DeVault <sir@cmpwn.com>2019-03-31 12:14:37 -0400
commit27b25174e2f0249a6a1d4ba45b70f8504b63ffb1 (patch)
treeb8626c746e07d9a38f95a5a817a7094c9275bfd1
parent143289bbd0736d72553a3c2a080aa3d125366b38 (diff)
downloadaerc-27b25174e2f0249a6a1d4ba45b70f8504b63ffb1.tar.gz
Make the message viewer real, part one
-rw-r--r--commands/account/pipe.go3
-rw-r--r--commands/account/view-message.go27
-rw-r--r--lib/msgstore.go16
-rw-r--r--widgets/aerc.go2
-rw-r--r--widgets/msgviewer.go172
-rw-r--r--worker/imap/fetch.go32
-rw-r--r--worker/imap/worker.go2
7 files changed, 124 insertions, 130 deletions
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 <cmd> [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:
-
-* 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
-
-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
+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()
+}
 
- 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(store *lib.MessageStore,
+	msg *types.MessageInfo) *MessageViewer {
 
-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 <ivan@habunek.com>",
+			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() {
+	// 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)
+
+	viewer := &MessageViewer{
+		pipe: pipe,
+		grid: grid,
+		term: term,
+	}
+
+	store.FetchBodyPart(msg.Uid, 0, func(reader io.Reader) {
+		viewer.mail = reader
 		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)
+	})
 
-	grid.AddChild(headers).At(0, 0)
-	grid.AddChild(body).At(1, 0)
-	return &MessageViewer{grid, term}
+	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: