summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--commands/account/reply.go4
-rw-r--r--lib/msgstore.go2
-rw-r--r--widgets/msgviewer.go206
-rw-r--r--worker/imap/fetch.go2
-rw-r--r--worker/types/messages.go2
5 files changed, 146 insertions, 70 deletions
diff --git a/commands/account/reply.go b/commands/account/reply.go
index f55d448..cd07ec9 100644
--- a/commands/account/reply.go
+++ b/commands/account/reply.go
@@ -123,8 +123,8 @@ func Reply(aerc *widgets.Aerc, args []string) error {
 	}
 
 	if quote {
-		// TODO: something more intelligent than fetching the 0th part
-		store.FetchBodyPart(msg.Uid, 0, func(reader io.Reader) {
+		// TODO: something more intelligent than fetching the 1st part
+		store.FetchBodyPart(msg.Uid, []int{1}, func(reader io.Reader) {
 			header := message.Header{}
 			header.SetText(
 				"Content-Transfer-Encoding", msg.BodyStructure.Encoding)
diff --git a/lib/msgstore.go b/lib/msgstore.go
index 827d7cb..6ab7fc2 100644
--- a/lib/msgstore.go
+++ b/lib/msgstore.go
@@ -90,7 +90,7 @@ func (store *MessageStore) FetchFull(uids []uint32, cb func(io.Reader)) {
 }
 
 func (store *MessageStore) FetchBodyPart(
-	uid uint32, part int, cb func(io.Reader)) {
+	uid uint32, part []int, cb func(io.Reader)) {
 
 	store.worker.PostAction(&types.FetchMessageBodyPart{
 		Uid:  uid,
diff --git a/widgets/msgviewer.go b/widgets/msgviewer.go
index 60e1d23..ab79b5e 100644
--- a/widgets/msgviewer.go
+++ b/widgets/msgviewer.go
@@ -23,10 +23,17 @@ import (
 )
 
 type MessageViewer struct {
+	ui.Invalidatable
 	conf     *config.AercConfig
 	err      error
-	msg      *types.MessageInfo
 	grid     *ui.Grid
+	msg      *types.MessageInfo
+	switcher *PartSwitcher
+	store    *lib.MessageStore
+}
+
+type PartSwitcher struct {
+	ui.Invalidatable
 	parts    []*PartViewer
 	selected int
 }
@@ -48,8 +55,8 @@ func formatAddresses(addrs []*imap.Address) string {
 	return val.String()
 }
 
-func NewMessageViewer(conf *config.AercConfig, store *lib.MessageStore,
-	msg *types.MessageInfo) *MessageViewer {
+func NewMessageViewer(conf *config.AercConfig,
+	store *lib.MessageStore, msg *types.MessageInfo) *MessageViewer {
 
 	grid := ui.NewGrid().Rows([]ui.GridSpec{
 		{ui.SIZE_EXACT, 3}, // TODO: Based on number of header rows
@@ -91,28 +98,51 @@ func NewMessageViewer(conf *config.AercConfig, store *lib.MessageStore,
 		{ui.SIZE_EXACT, 20},
 	})
 
-	for i, part := range msg.BodyStructure.Parts {
-		fmt.Println(i, part.MIMEType, part.MIMESubType)
-	}
+	var (
+		err error
+		mv  *MessageViewer
+	)
 
-	// TODO: add multipart switcher and configure additional parts
-	pv, err := NewPartViewer(conf, msg, 0)
-	if err != nil {
-		goto handle_error
+	switcher := &PartSwitcher{}
+	if len(msg.BodyStructure.Parts) == 0 {
+		pv, err := NewPartViewer(conf, store, msg, msg.BodyStructure, []int{1})
+		if err != nil {
+			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
+		}
+		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 == 0 && pv.part.MIMEType != "multipart" {
+				switcher.selected = i
+			}
+		}
 	}
-	body.AddChild(pv).At(0, 0).Span(1, 2)
 
 	grid.AddChild(headers).At(0, 0)
 	grid.AddChild(body).At(1, 0)
 
-	store.FetchBodyPart(msg.Uid, 0, pv.SetSource)
-
-	return &MessageViewer{
-		grid:  grid,
-		msg:   msg,
-		parts: []*PartViewer{pv},
+	mv = &MessageViewer{
+		grid:     grid,
+		msg:      msg,
+		store:    store,
+		switcher: switcher,
 	}
 
+	body.AddChild(mv.switcher).At(0, 0).Span(1, 2)
+	return mv
+
 handle_error:
 	return &MessageViewer{
 		err:  err,
@@ -121,6 +151,34 @@ handle_error:
 	}
 }
 
+func enumerateParts(conf *config.AercConfig, store *lib.MessageStore,
+	msg *types.MessageInfo, body *imap.BodyStructure,
+	index []int) ([]*PartViewer, error) {
+
+	var parts []*PartViewer
+	for i, part := range body.Parts {
+		curindex := append(index, i+1)
+		if part.MIMEType == "multipart" {
+			// Multipart meta-parts are faked
+			pv := &PartViewer{part: part}
+			parts = append(parts, pv)
+			subParts, err := enumerateParts(
+				conf, store, msg, part, curindex)
+			if err != nil {
+				return nil, err
+			}
+			parts = append(parts, subParts...)
+			continue
+		}
+		pv, err := NewPartViewer(conf, store, msg, part, curindex)
+		if err != nil {
+			return nil, err
+		}
+		parts = append(parts, pv)
+	}
+	return parts, nil
+}
+
 func (mv *MessageViewer) Draw(ctx *ui.Context) {
 	if mv.err != nil {
 		ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
@@ -140,44 +198,69 @@ func (mv *MessageViewer) OnInvalidate(fn func(d ui.Drawable)) {
 	})
 }
 
-func (mv *MessageViewer) Event(event tcell.Event) bool {
-	// What is encapsulation even
-	if mv.parts[mv.selected].term != nil {
-		return mv.parts[mv.selected].term.Event(event)
+func (ps *PartSwitcher) Invalidate() {
+	ps.DoInvalidate(ps)
+}
+
+func (ps *PartSwitcher) Focus(focus bool) {
+	if ps.parts[ps.selected].term != nil {
+		ps.parts[ps.selected].term.Focus(focus)
+	}
+}
+
+func (ps *PartSwitcher) Event(event tcell.Event) bool {
+	if ps.parts[ps.selected].term != nil {
+		return ps.parts[ps.selected].term.Event(event)
 	}
 	return false
 }
 
-func (mv *MessageViewer) Focus(focus bool) {
-	if mv.parts[mv.selected].term != nil {
-		mv.parts[mv.selected].term.Focus(focus)
+func (ps *PartSwitcher) Draw(ctx *ui.Context) {
+	height := len(ps.parts)
+	if height == 1 {
+		ps.parts[ps.selected].Draw(ctx)
+		return
 	}
+	// TODO: cap height and add scrolling for messages with many parts
+	y := ctx.Height() - height
+	for i, part := range ps.parts {
+		style := tcell.StyleDefault.Reverse(ps.selected == i)
+		ctx.Fill(0, y+i, ctx.Width(), 1, ' ', style)
+		ctx.Printf(len(part.index)*2, y+i, style, "%s/%s",
+			strings.ToLower(part.part.MIMEType),
+			strings.ToLower(part.part.MIMESubType))
+	}
+	ps.parts[ps.selected].Draw(ctx.Subcontext(
+		0, 0, ctx.Width(), ctx.Height()-height))
+}
+
+func (mv *MessageViewer) Event(event tcell.Event) bool {
+	return mv.switcher.Event(event)
+}
+
+func (mv *MessageViewer) Focus(focus bool) {
+	mv.switcher.Focus(focus)
 }
 
 type PartViewer struct {
+	ui.Invalidatable
 	err     error
+	fetched bool
 	filter  *exec.Cmd
-	index   string
+	index   []int
 	msg     *types.MessageInfo
 	pager   *exec.Cmd
 	pagerin io.WriteCloser
 	part    *imap.BodyStructure
 	sink    io.WriteCloser
 	source  io.Reader
+	store   *lib.MessageStore
 	term    *Terminal
 }
 
 func NewPartViewer(conf *config.AercConfig,
-	msg *types.MessageInfo, index int) (*PartViewer, error) {
-	var (
-		part *imap.BodyStructure
-	)
-	// TODO: Find IMAP index, which may differ
-	if len(msg.BodyStructure.Parts) != 0 {
-		part = msg.BodyStructure.Parts[index]
-	} else {
-		part = msg.BodyStructure
-	}
+	store *lib.MessageStore, msg *types.MessageInfo,
+	part *imap.BodyStructure, index []int) (*PartViewer, error) {
 
 	var (
 		filter  *exec.Cmd
@@ -228,26 +311,30 @@ func NewPartViewer(conf *config.AercConfig,
 		if pagerin, _ = pager.StdinPipe(); err != nil {
 			return nil, err
 		}
-	} else {
-		if pipe, err = pager.StdinPipe(); err != nil {
+		if term, err = NewTerminal(pager); err != nil {
 			return nil, err
 		}
 	}
-	if term, err = NewTerminal(pager); err != nil {
-		return nil, err
-	}
 
 	pv := &PartViewer{
 		filter:  filter,
+		index:   index, // TODO: Nested multipart does indicies differently
+		msg:     msg,
 		pager:   pager,
 		pagerin: pagerin,
 		part:    part,
 		sink:    pipe,
+		store:   store,
 		term:    term,
 	}
 
-	term.OnStart = func() {
-		pv.attemptCopy()
+	if term != nil {
+		term.OnStart = func() {
+			pv.attemptCopy()
+		}
+		term.OnInvalidate(func(_ ui.Drawable) {
+			pv.Invalidate()
+		})
 	}
 
 	return pv, nil
@@ -297,17 +384,22 @@ func (pv *PartViewer) attemptCopy() {
 	}
 }
 
-func (pv *PartViewer) OnInvalidate(fn func(ui.Drawable)) {
-	pv.term.OnInvalidate(func(_ ui.Drawable) {
-		fn(pv)
-	})
-}
-
 func (pv *PartViewer) Invalidate() {
-	pv.term.Invalidate()
+	pv.DoInvalidate(pv)
 }
 
 func (pv *PartViewer) Draw(ctx *ui.Context) {
+	if pv.filter == nil {
+		// TODO: Let them download it directly or something
+		ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
+		ctx.Printf(0, 0, tcell.StyleDefault,
+			"No filter configured for this mimetype")
+		return
+	}
+	if !pv.fetched {
+		pv.store.FetchBodyPart(pv.msg.Uid, pv.index, pv.SetSource)
+		pv.fetched = true
+	}
 	if pv.err != nil {
 		ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ', tcell.StyleDefault)
 		ctx.Printf(0, 0, tcell.StyleDefault, "%s", pv.err.Error())
@@ -347,19 +439,3 @@ func (hv *HeaderView) Draw(ctx *ui.Context) {
 func (hv *HeaderView) Invalidate() {
 	hv.DoInvalidate(hv)
 }
-
-type MultipartView struct {
-	ui.Invalidatable
-}
-
-func (mpv *MultipartView) 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(0, 0, tcell.StyleDefault.Reverse(true), "text/plain")
-	ctx.Printf(0, 1, tcell.StyleDefault, "text/html")
-	ctx.Printf(0, 2, tcell.StyleDefault, "application/pgp-si…")
-}
-
-func (mpv *MultipartView) Invalidate() {
-	mpv.DoInvalidate(mpv)
-}
diff --git a/worker/imap/fetch.go b/worker/imap/fetch.go
index e3d8fa3..c5e7cd6 100644
--- a/worker/imap/fetch.go
+++ b/worker/imap/fetch.go
@@ -26,7 +26,7 @@ func (imapw *IMAPWorker) handleFetchMessageBodyPart(
 
 	imapw.worker.Logger.Printf("Fetching message part")
 	section := &imap.BodySectionName{}
-	section.Path = []int{msg.Part + 1}
+	section.Path = msg.Part
 	items := []imap.FetchItem{section.FetchItem()}
 	uids := imap.SeqSet{}
 	uids.AddNum(msg.Uid)
diff --git a/worker/types/messages.go b/worker/types/messages.go
index 555e3ce..0169e05 100644
--- a/worker/types/messages.go
+++ b/worker/types/messages.go
@@ -94,7 +94,7 @@ type FetchFullMessages struct {
 type FetchMessageBodyPart struct {
 	Message
 	Uid  uint32
-	Part int
+	Part []int
 }
 
 type DeleteMessages struct {