aerc

Working clone of aerc-mail.org
git clone git://git.danielmoch.com/aerc.git
Log | Files | Refs | README | LICENSE

commit 77ede6eb5a22a5407541ac587736189fcca0037f
parent 84e9853c1613f43f76bd46f0c1eb143d1db16ac2
Author: Drew DeVault <sir@cmpwn.com>
Date:   Fri, 29 Mar 2019 22:35:53 -0400

Add body fetching support code

Diffstat:
Acommands/account/fetch-msg.go | 29+++++++++++++++++++++++++++++
Mgo.mod | 2++
Mgo.sum | 4++++
Mlib/msgstore.go | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mwidgets/account.go | 3+++
Mwidgets/msglist.go | 2+-
Mworker/imap/fetch.go | 61++++++++++++++++++++++++++++++++++++++++++++++---------------
Mworker/imap/worker.go | 2++
Mworker/types/messages.go | 9+++++++--
9 files changed, 165 insertions(+), 24 deletions(-)

diff --git a/commands/account/fetch-msg.go b/commands/account/fetch-msg.go @@ -0,0 +1,29 @@ +package account + +import ( + "errors" + + "github.com/mohamedattahri/mail" + + "git.sr.ht/~sircmpwn/aerc2/widgets" +) + +func init() { + register("fetch-message", FetchMessage) +} + +func FetchMessage(aerc *widgets.Aerc, args []string) error { + if len(args) != 1 { + return errors.New("Usage: :fetch-message") + } + 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(msg *mail.Message) { + aerc.SetStatus("got message body, woohoo") + }) + return nil +} diff --git a/go.mod b/go.mod @@ -15,8 +15,10 @@ require ( github.com/mattn/go-isatty v0.0.3 github.com/mattn/go-runewidth v0.0.2 github.com/mitchellh/go-homedir v1.1.0 + github.com/mohamedattahri/mail v0.0.0-20150907213728-52bc9c59063f github.com/nsf/termbox-go v0.0.0-20180129072728-88b7b944be8b github.com/riywo/loginshell v0.0.0-20181227004642-c2f4167b2303 github.com/stretchr/testify v1.3.0 golang.org/x/text v0.3.0 + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect ) diff --git a/go.sum b/go.sum @@ -56,6 +56,8 @@ github.com/micromaomao/go-libvterm v0.0.0-20190126085614-2401b10ee7ed h1:SDQJB+u github.com/micromaomao/go-libvterm v0.0.0-20190126085614-2401b10ee7ed/go.mod h1:TEYd4HSsUc2pZan5xJmjJQLA7c3d9dkV9lNsf8Xh3TY= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mohamedattahri/mail v0.0.0-20150907213728-52bc9c59063f h1:eUB6ohYEAv7lbqKAMQXBfPfRxhvOUUQIrHYrs/+1UQs= +github.com/mohamedattahri/mail v0.0.0-20150907213728-52bc9c59063f/go.mod h1:lB0PjFC/A+yHl9ZdreyVugcdsF9KkK3JOHebiPhU1F8= github.com/nsf/termbox-go v0.0.0-20180129072728-88b7b944be8b/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -67,3 +69,5 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= diff --git a/lib/msgstore.go b/lib/msgstore.go @@ -2,6 +2,7 @@ package lib import ( "github.com/emersion/go-imap" + "github.com/mohamedattahri/mail" "git.sr.ht/~sircmpwn/aerc2/worker/types" ) @@ -11,6 +12,10 @@ type MessageStore struct { Messages map[uint32]*types.MessageInfo // Ordered list of known UIDs Uids []uint32 + + bodyCallbacks map[uint32][]func(*mail.Message) + headerCallbacks map[uint32][]func(*types.MessageInfo) + // Map of uids we've asked the worker to fetch onUpdate func(store *MessageStore) // TODO: multiple onUpdate handlers pendingBodies map[uint32]interface{} @@ -24,13 +29,18 @@ func NewMessageStore(worker *types.Worker, return &MessageStore{ DirInfo: *dirInfo, + bodyCallbacks: make(map[uint32][]func(*mail.Message)), + headerCallbacks: make(map[uint32][]func(*types.MessageInfo)), + pendingBodies: make(map[uint32]interface{}), pendingHeaders: make(map[uint32]interface{}), worker: worker, } } -func (store *MessageStore) FetchHeaders(uids []uint32) { +func (store *MessageStore) FetchHeaders(uids []uint32, + cb func(*types.MessageInfo)) { + // 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 @@ -38,12 +48,50 @@ func (store *MessageStore) FetchHeaders(uids []uint32) { if _, ok := store.pendingHeaders[uid]; !ok { toFetch.AddNum(uint32(uid)) store.pendingHeaders[uid] = nil + if cb != nil { + if list, ok := store.headerCallbacks[uid]; ok { + store.headerCallbacks[uid] = append(list, cb) + } else { + store.headerCallbacks[uid] = []func(*types.MessageInfo){cb} + } + } + } + } + if !toFetch.Empty() { + store.worker.PostAction(&types.FetchMessageHeaders{Uids: toFetch}, nil) + } +} + +func (store *MessageStore) FetchBodies(uids []uint32, + cb func(*mail.Message)) { + + // 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 + for _, uid := range uids { + if _, ok := store.pendingBodies[uid]; !ok { + toFetch.AddNum(uint32(uid)) + store.pendingBodies[uid] = nil + if cb != nil { + if list, ok := store.bodyCallbacks[uid]; ok { + store.bodyCallbacks[uid] = append(list, cb) + } else { + store.bodyCallbacks[uid] = []func(*mail.Message){cb} + } + } } } if !toFetch.Empty() { - store.worker.PostAction(&types.FetchMessageHeaders{ - Uids: toFetch, - }, nil) + store.worker.PostAction(&types.FetchMessageBodies{Uids: toFetch}, nil) + } +} + +func (store *MessageStore) merge( + to *types.MessageInfo, from *types.MessageInfo) { + + // TODO: Merge more shit + if from.Envelope != nil { + to.Envelope = from.Envelope } } @@ -66,12 +114,29 @@ func (store *MessageStore) Update(msg types.WorkerMessage) { store.Uids = msg.Uids update = true case *types.MessageInfo: - // TODO: merge message info into existing record, if applicable - store.Messages[msg.Uid] = msg + if existing, ok := store.Messages[msg.Uid]; ok && existing != nil { + store.merge(existing, msg) + } else { + store.Messages[msg.Uid] = msg + } if _, ok := store.pendingHeaders[msg.Uid]; msg.Envelope != nil && ok { delete(store.pendingHeaders, msg.Uid) + if cbs, ok := store.headerCallbacks[msg.Uid]; ok { + for _, cb := range cbs { + cb(msg) + } + } } update = true + case *types.MessageBody: + if _, ok := store.pendingBodies[msg.Uid]; ok { + delete(store.pendingBodies, msg.Uid) + if cbs, ok := store.bodyCallbacks[msg.Uid]; ok { + for _, cb := range cbs { + cb(msg.Mail) + } + } + } case *types.MessagesDeleted: toDelete := make(map[uint32]interface{}) for _, uid := range msg.Uids { diff --git a/widgets/account.go b/widgets/account.go @@ -173,6 +173,9 @@ func (acct *AccountView) onMessage(msg types.WorkerMessage) { case *types.DirectoryContents: store := acct.msgStores[acct.dirlist.selected] store.Update(msg) + case *types.MessageBody: + store := acct.msgStores[acct.dirlist.selected] + store.Update(msg) case *types.MessageInfo: store := acct.msgStores[acct.dirlist.selected] store.Update(msg) diff --git a/widgets/msglist.go b/widgets/msglist.go @@ -88,7 +88,7 @@ func (ml *MessageList) Draw(ctx *ui.Context) { } if len(needsHeaders) != 0 { - ml.store.FetchHeaders(needsHeaders) + ml.store.FetchHeaders(needsHeaders, nil) ml.spinner.Start() } else { ml.spinner.Stop() diff --git a/worker/imap/fetch.go b/worker/imap/fetch.go @@ -2,6 +2,7 @@ package imap import ( "github.com/emersion/go-imap" + "github.com/mohamedattahri/mail" "git.sr.ht/~sircmpwn/aerc2/worker/types" ) @@ -10,28 +11,58 @@ func (imapw *IMAPWorker) handleFetchMessageHeaders( msg *types.FetchMessageHeaders) { imapw.worker.Logger.Printf("Fetching message headers") + items := []imap.FetchItem{ + imap.FetchEnvelope, + imap.FetchInternalDate, + imap.FetchFlags, + imap.FetchUid, + } + + imapw.handleFetchMessages(msg, &msg.Uids, items) +} + +func (imapw *IMAPWorker) handleFetchMessageBodies( + msg *types.FetchMessageBodies) { + + imapw.worker.Logger.Printf("Fetching message bodies") + section := &imap.BodySectionName{} + items := []imap.FetchItem{section.FetchItem()} + imapw.handleFetchMessages(msg, &msg.Uids, items) +} + +func (imapw *IMAPWorker) handleFetchMessages( + msg types.WorkerMessage, uids *imap.SeqSet, items []imap.FetchItem) { go func() { messages := make(chan *imap.Message) done := make(chan error, 1) - items := []imap.FetchItem{ - imap.FetchEnvelope, - imap.FetchInternalDate, - imap.FetchFlags, - imap.FetchUid, - } go func() { - done <- imapw.client.UidFetch(&msg.Uids, items, messages) + done <- imapw.client.UidFetch(uids, items, messages) }() go func() { - for msg := range messages { - imapw.seqMap[msg.SeqNum-1] = msg.Uid - imapw.worker.PostMessage(&types.MessageInfo{ - Envelope: msg.Envelope, - Flags: msg.Flags, - InternalDate: msg.InternalDate, - Uid: msg.Uid, - }, nil) + section := &imap.BodySectionName{} + for _msg := range messages { + imapw.seqMap[_msg.SeqNum-1] = _msg.Uid + if reader := _msg.GetBody(section); reader != nil { + email, err := mail.ReadMessage(reader) + if err != nil { + imapw.worker.PostMessage(&types.Error{ + Message: types.RespondTo(msg), + Error: err, + }, nil) + } + imapw.worker.PostMessage(&types.MessageBody{ + Mail: email, + Uid: _msg.Uid, + }, nil) + } else { + imapw.worker.PostMessage(&types.MessageInfo{ + Envelope: _msg.Envelope, + Flags: _msg.Flags, + InternalDate: _msg.InternalDate, + Uid: _msg.Uid, + }, nil) + } } if err := <-done; err != nil { imapw.worker.PostMessage(&types.Error{ diff --git a/worker/imap/worker.go b/worker/imap/worker.go @@ -158,6 +158,8 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error { w.handleFetchDirectoryContents(msg) case *types.FetchMessageHeaders: w.handleFetchMessageHeaders(msg) + case *types.FetchMessageBodies: + w.handleFetchMessageBodies(msg) case *types.DeleteMessages: w.handleDeleteMessages(msg) default: diff --git a/worker/types/messages.go b/worker/types/messages.go @@ -2,10 +2,10 @@ package types import ( "crypto/x509" - "net/mail" "time" "github.com/emersion/go-imap" + "github.com/mohamedattahri/mail" "git.sr.ht/~sircmpwn/aerc2/config" ) @@ -123,11 +123,16 @@ type MessageInfo struct { Envelope *imap.Envelope Flags []string InternalDate time.Time - Mail *mail.Message Size uint32 Uid uint32 } +type MessageBody struct { + Message + Mail *mail.Message + Uid uint32 +} + type MessagesDeleted struct { Message Uids []uint32