aerc

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

commit b0bf09b98fc038c1bc9921d568c06260b7448a15
parent 52b318127fe7ec001ca824947193b2cb7b0ebda6
Author: Drew DeVault <sir@cmpwn.com>
Date:   Wed, 15 May 2019 19:41:21 -0400

Copy sent emails to the Sent folder

Or rather, to a user-specified folder

Diffstat:
Mcommands/account/compose.go | 3++-
Mcommands/compose/send.go | 65+++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mconfig/accounts.conf | 1+
Mconfig/config.go | 3+++
Mgo.mod | 1+
Mgo.sum | 2++
Mwidgets/compose.go | 10+++++++++-
Mworker/imap/movecopy.go | 27+++++++++++++++++++++++++++
Mworker/imap/worker.go | 4++++
Mworker/types/messages.go | 20++++++++++++++++++++
Mworker/types/worker.go | 16++++++++++++----
11 files changed, 132 insertions(+), 20 deletions(-)

diff --git a/commands/account/compose.go b/commands/account/compose.go @@ -16,7 +16,8 @@ func Compose(aerc *widgets.Aerc, args []string) error { return errors.New("Usage: compose") } acct := aerc.SelectedAccount() - composer := widgets.NewComposer(aerc.Config(), acct.AccountConfig()) + composer := widgets.NewComposer( + aerc.Config(), acct.AccountConfig(), acct.Worker()) tab := aerc.NewTab(composer, "New email") composer.OnSubjectChange(func(subject string) { if subject == "" { diff --git a/commands/compose/send.go b/commands/compose/send.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "errors" "fmt" + "io" "net/mail" "net/url" "strings" @@ -12,8 +13,10 @@ import ( "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" "github.com/gdamore/tcell" + "github.com/miolini/datacounter" "git.sr.ht/~sircmpwn/aerc2/widgets" + "git.sr.ht/~sircmpwn/aerc2/worker/types" ) func init() { @@ -79,10 +82,9 @@ func SendMessage(aerc *widgets.Aerc, args []string) error { return fmt.Errorf("Unsupported auth mechanism %s", auth) } - aerc.SetStatus("Sending...") aerc.RemoveTab(composer) - sendAsync := func() { + sendAsync := func() (int, error) { tlsConfig := &tls.Config{ // TODO: ask user first InsecureSkipVerify: true, @@ -97,7 +99,7 @@ func SendMessage(aerc *widgets.Aerc, args []string) error { if err != nil { aerc.PushStatus(" "+err.Error(), 10*time.Second). Color(tcell.ColorDefault, tcell.ColorRed) - return + return 0, nil } defer conn.Close() if sup, _ := conn.Extension("STARTTLS"); sup { @@ -105,7 +107,7 @@ func SendMessage(aerc *widgets.Aerc, args []string) error { if err = conn.StartTLS(tlsConfig); err != nil { aerc.PushStatus(" "+err.Error(), 10*time.Second). Color(tcell.ColorDefault, tcell.ColorRed) - return + return 0, nil } } case "smtps": @@ -117,7 +119,7 @@ func SendMessage(aerc *widgets.Aerc, args []string) error { if err != nil { aerc.PushStatus(" "+err.Error(), 10*time.Second). Color(tcell.ColorDefault, tcell.ColorRed) - return + return 0, nil } defer conn.Close() } @@ -127,37 +129,72 @@ func SendMessage(aerc *widgets.Aerc, args []string) error { if err = conn.Auth(saslClient); err != nil { aerc.PushStatus(" "+err.Error(), 10*time.Second). Color(tcell.ColorDefault, tcell.ColorRed) - return + return 0, nil } } // TODO: the user could conceivably want to use a different From and sender if err = conn.Mail(from.Address); err != nil { aerc.PushStatus(" "+err.Error(), 10*time.Second). Color(tcell.ColorDefault, tcell.ColorRed) - return + return 0, nil } for _, rcpt := range rcpts { if err = conn.Rcpt(rcpt); err != nil { aerc.PushStatus(" "+err.Error(), 10*time.Second). Color(tcell.ColorDefault, tcell.ColorRed) - return + return 0, nil } } wc, err := conn.Data() if err != nil { aerc.PushStatus(" "+err.Error(), 10*time.Second). Color(tcell.ColorDefault, tcell.ColorRed) - return + return 0, nil } defer wc.Close() - composer.WriteMessage(header, wc) - composer.Close() + ctr := datacounter.NewWriterCounter(wc) + composer.WriteMessage(header, ctr) + return int(ctr.Count()), nil } go func() { - sendAsync() - // TODO: Use a stack - aerc.SetStatus("Sent.") + aerc.SetStatus("Sending...") + nbytes, err := sendAsync() + if err != nil { + aerc.PushStatus(" "+err.Error(), 10*time.Second). + Color(tcell.ColorDefault, tcell.ColorRed) + return + } + if config.CopyTo != "" { + aerc.SetStatus("Copying to " + config.CopyTo) + worker := composer.Worker() + r, w := io.Pipe() + worker.PostAction(&types.AppendMessage{ + Destination: config.CopyTo, + Flags: []string{}, + Date: time.Now(), + Reader: r, + Length: nbytes, + }, func(msg types.WorkerMessage) { + switch msg := msg.(type) { + case *types.Done: + aerc.SetStatus("Sent.") + r.Close() + composer.Close() + case *types.Error: + aerc.PushStatus(" "+msg.Error.Error(), 10*time.Second). + Color(tcell.ColorDefault, tcell.ColorRed) + r.Close() + composer.Close() + } + }) + header, _, _ := composer.Header() + composer.WriteMessage(header, w) + w.Close() + } else { + aerc.SetStatus("Sent.") + composer.Close() + } }() return nil } diff --git a/config/accounts.conf b/config/accounts.conf @@ -8,6 +8,7 @@ # [Personal] # source=imaps://username[:password]@hostname[:port] # outgoing=smtps+plain://username[:password]@hostname[:port] +# copy-to=Sent # from=Joe Bloe <joe@example.org> # # [Work] diff --git a/config/config.go b/config/config.go @@ -29,6 +29,7 @@ const ( ) type AccountConfig struct { + CopyTo string Default string From string Name string @@ -118,6 +119,8 @@ func loadAccountConfig(path string) ([]AccountConfig, error) { account.Outgoing = val } else if key == "from" { account.From = val + } else if key == "copy-to" { + account.CopyTo = val } else if key != "name" { account.Params[key] = val } diff --git a/go.mod b/go.mod @@ -17,6 +17,7 @@ require ( github.com/lucasb-eyer/go-colorful v0.0.0-20180531031333-d9cec903b20c // indirect github.com/mattn/go-isatty v0.0.3 github.com/mattn/go-runewidth v0.0.2 + github.com/miolini/datacounter v0.0.0-20171104152933-fd4e42a1d5e0 github.com/mitchellh/go-homedir v1.1.0 github.com/riywo/loginshell v0.0.0-20181227004642-c2f4167b2303 github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a // indirect diff --git a/go.sum b/go.sum @@ -46,6 +46,8 @@ github.com/mattn/go-pointer v0.0.0-20180825124634-49522c3f3791 h1:PfHMsLQJwoc0cc github.com/mattn/go-pointer v0.0.0-20180825124634-49522c3f3791/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/miolini/datacounter v0.0.0-20171104152933-fd4e42a1d5e0 h1:clkDYGefEWUCwyCrwYn900sOaVGDpinPJgD0W6ebEjs= +github.com/miolini/datacounter v0.0.0-20171104152933-fd4e42a1d5e0/go.mod h1:P6fDJzlxN+cWYR09KbE9/ta+Y6JofX9tAUhJpWkWPaM= 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/widgets/compose.go b/widgets/compose.go @@ -15,6 +15,7 @@ import ( "git.sr.ht/~sircmpwn/aerc2/config" "git.sr.ht/~sircmpwn/aerc2/lib/ui" + "git.sr.ht/~sircmpwn/aerc2/worker/types" ) type Composer struct { @@ -30,6 +31,7 @@ type Composer struct { email *os.File grid *ui.Grid review *reviewMessage + worker *types.Worker focusable []ui.DrawableInteractive focused int @@ -37,7 +39,8 @@ type Composer struct { // TODO: Let caller configure headers, initial body (for replies), etc func NewComposer(conf *config.AercConfig, - acct *config.AccountConfig) *Composer { + acct *config.AccountConfig, worker *types.Worker) *Composer { + grid := ui.NewGrid().Rows([]ui.GridSpec{ {ui.SIZE_EXACT, 3}, {ui.SIZE_WEIGHT, 1}, @@ -87,6 +90,7 @@ func NewComposer(conf *config.AercConfig, editor: term, email: email, grid: grid, + worker: worker, // You have to backtab to get to "From", since you usually don't edit it focused: 1, focusable: []ui.DrawableInteractive{from, to, subject, term}, @@ -155,6 +159,10 @@ func (c *Composer) Config() *config.AccountConfig { return c.config } +func (c *Composer) Worker() *types.Worker { + return c.worker +} + func (c *Composer) Header() (*mail.Header, []string, error) { // Extract headers from the email, if present c.email.Seek(0, os.SEEK_SET) diff --git a/worker/imap/movecopy.go b/worker/imap/movecopy.go @@ -1,6 +1,8 @@ package imap import ( + "io" + "git.sr.ht/~sircmpwn/aerc2/worker/types" ) @@ -14,3 +16,28 @@ func (imapw *IMAPWorker) handleCopyMessages(msg *types.CopyMessages) { imapw.worker.PostMessage(&types.Done{types.RespondTo(msg)}, nil) } } + +type appendLiteral struct { + io.Reader + Length int +} + +func (m appendLiteral) Len() int { + return m.Length +} + +func (imapw *IMAPWorker) handleAppendMessage(msg *types.AppendMessage) { + if err := imapw.client.Append(msg.Destination, msg.Flags, msg.Date, + &appendLiteral{ + Reader: msg.Reader, + Length: msg.Length, + }); err != nil { + + imapw.worker.PostMessage(&types.Error{ + Message: types.RespondTo(msg), + Error: err, + }, nil) + } else { + imapw.worker.PostMessage(&types.Done{types.RespondTo(msg)}, nil) + } +} diff --git a/worker/imap/worker.go b/worker/imap/worker.go @@ -153,6 +153,8 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error { } } + c.SetDebug(w.worker.Logger.Writer()) + if _, err := c.Select(imap.InboxName, false); err != nil { return err } @@ -176,6 +178,8 @@ func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error { w.handleDeleteMessages(msg) case *types.CopyMessages: w.handleCopyMessages(msg) + case *types.AppendMessage: + w.handleAppendMessage(msg) default: return errUnsupported } diff --git a/worker/types/messages.go b/worker/types/messages.go @@ -12,10 +12,13 @@ import ( type WorkerMessage interface { InResponseTo() WorkerMessage + getId() int + setId(id int) } type Message struct { inResponseTo WorkerMessage + id int } func RespondTo(msg WorkerMessage) Message { @@ -28,6 +31,14 @@ func (m Message) InResponseTo() WorkerMessage { return m.inResponseTo } +func (m Message) getId() int { + return m.id +} + +func (m Message) setId(id int) { + m.id = id +} + // Meta-messages type Done struct { @@ -103,6 +114,15 @@ type CopyMessages struct { Uids imap.SeqSet } +type AppendMessage struct { + Message + Destination string + Flags []string + Date time.Time + Reader io.Reader + Length int +} + // Messages type CertificateApprovalRequest struct { diff --git a/worker/types/worker.go b/worker/types/worker.go @@ -5,6 +5,8 @@ import ( "sync" ) +var nextId int = 1 + type Backend interface { Run() } @@ -15,7 +17,7 @@ type Worker struct { Messages chan WorkerMessage Logger *log.Logger - callbacks map[WorkerMessage]func(msg WorkerMessage) // protected by mutex + callbacks map[int]func(msg WorkerMessage) // protected by mutex mutex sync.Mutex } @@ -24,16 +26,19 @@ func NewWorker(logger *log.Logger) *Worker { Actions: make(chan WorkerMessage, 50), Messages: make(chan WorkerMessage, 50), Logger: logger, - callbacks: make(map[WorkerMessage]func(msg WorkerMessage)), + callbacks: make(map[int]func(msg WorkerMessage)), } } func (worker *Worker) setCallback(msg WorkerMessage, cb func(msg WorkerMessage)) { + msg.setId(nextId) + nextId++ + if cb != nil { worker.mutex.Lock() - worker.callbacks[msg] = cb + worker.callbacks[msg.getId()] = cb worker.mutex.Unlock() } } @@ -41,8 +46,11 @@ func (worker *Worker) setCallback(msg WorkerMessage, func (worker *Worker) getCallback(msg WorkerMessage) (func(msg WorkerMessage), bool) { + if msg == nil { + return nil, false + } worker.mutex.Lock() - cb, ok := worker.callbacks[msg] + cb, ok := worker.callbacks[msg.getId()] worker.mutex.Unlock() return cb, ok