From 7f033278eb3afc3b9ae2dca28efe8d4a3514d14a Mon Sep 17 00:00:00 2001 From: Jeffas Date: Fri, 24 Apr 2020 11:42:21 +0200 Subject: [PATCH] Add postpone command This command uses the Postpone folder from the account config to save messages to. Messages are saved as though they were sent so have a valid 'to' recipient address and should be able to be read back in for later editing. --- commands/account/compose.go | 2 +- commands/compose/postpone.go | 119 +++++++++++++++++++++++++++++++++++ commands/msg/forward.go | 1 - commands/msg/reply.go | 2 +- commands/msg/unsubscribe.go | 1 + config/binds.conf | 1 + config/config.go | 10 +-- doc/aerc-config.5.scd | 5 ++ doc/aerc.1.scd | 4 ++ widgets/aerc.go | 4 +- widgets/compose.go | 44 +++++++------ 11 files changed, 166 insertions(+), 27 deletions(-) create mode 100644 commands/compose/postpone.go diff --git a/commands/account/compose.go b/commands/account/compose.go index 8115faa..42d51d5 100644 --- a/commands/account/compose.go +++ b/commands/account/compose.go @@ -31,7 +31,7 @@ func (Compose) Execute(aerc *widgets.Aerc, args []string) error { } acct := aerc.SelectedAccount() - composer, err := widgets.NewComposer(aerc, + composer, err := widgets.NewComposer(aerc, acct aerc.Config(), acct.AccountConfig(), acct.Worker(), template, nil, models.OriginalMail{}) if err != nil { diff --git a/commands/compose/postpone.go b/commands/compose/postpone.go new file mode 100644 index 0000000..44aa411 --- /dev/null +++ b/commands/compose/postpone.go @@ -0,0 +1,119 @@ +package compose + +import ( + "io" + "io/ioutil" + "time" + + "github.com/emersion/go-imap" + "github.com/miolini/datacounter" + "github.com/pkg/errors" + + "git.sr.ht/~sircmpwn/aerc/widgets" + "git.sr.ht/~sircmpwn/aerc/worker/types" +) + +type Postpone struct{} + +func init() { + register(Postpone{}) +} + +func (Postpone) Aliases() []string { + return []string{"postpone"} +} + +func (Postpone) Complete(aerc *widgets.Aerc, args []string) []string { + return nil +} + +func (Postpone) Execute(aerc *widgets.Aerc, args []string) error { + if len(args) != 1 { + return errors.New("Usage: postpone") + } + composer, _ := aerc.SelectedTab().(*widgets.Composer) + config := composer.Config() + + if config.Postpone == "" { + return errors.New("No Postpone location configured") + } + + aerc.Logger().Println("Postponing mail") + + header, _, err := composer.PrepareHeader() + if err != nil { + return errors.Wrap(err, "PrepareHeader") + } + header.SetContentType("text/plain", map[string]string{"charset": "UTF-8"}) + header.Set("Content-Transfer-Encoding", "quoted-printable") + worker := composer.Worker() + dirs := aerc.SelectedAccount().Directories().List() + alreadyCreated := false + for _, dir := range dirs { + if dir == config.Postpone { + alreadyCreated = true + break + } + } + + errChan := make(chan string) + + // run this as a goroutine so we can make other progress. The message + // will be saved once the directory is created. + go func() { + errStr := <-errChan + if errStr != "" { + aerc.PushError(" " + errStr) + return + } + + aerc.RemoveTab(composer) + ctr := datacounter.NewWriterCounter(ioutil.Discard) + err = composer.WriteMessage(header, ctr) + if err != nil { + aerc.PushError(errors.Wrap(err, "WriteMessage").Error()) + composer.Close() + return + } + nbytes := int(ctr.Count()) + r, w := io.Pipe() + worker.PostAction(&types.AppendMessage{ + Destination: config.Postpone, + Flags: []string{imap.SeenFlag}, + Date: time.Now(), + Reader: r, + Length: int(nbytes), + }, func(msg types.WorkerMessage) { + switch msg := msg.(type) { + case *types.Done: + aerc.PushStatus("Message postponed.", 10*time.Second) + r.Close() + composer.Close() + case *types.Error: + aerc.PushError(" " + msg.Error.Error()) + r.Close() + composer.Close() + } + }) + composer.WriteMessage(header, w) + w.Close() + }() + + if !alreadyCreated { + // to synchronise the creating of the directory + worker.PostAction(&types.CreateDirectory{ + Directory: config.Postpone, + }, func(msg types.WorkerMessage) { + switch msg := msg.(type) { + case *types.Done: + errChan <- "" + case *types.Error: + errChan <- msg.Error.Error() + } + }) + } else { + errChan <- "" + } + + return nil +} diff --git a/commands/msg/forward.go b/commands/msg/forward.go index c51949e..833eb9f 100644 --- a/commands/msg/forward.go +++ b/commands/msg/forward.go @@ -70,7 +70,6 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error { "To": to, "Subject": subject, } - original := models.OriginalMail{} addTab := func() (*widgets.Composer, error) { diff --git a/commands/msg/reply.go b/commands/msg/reply.go index c5ae1b6..291fc4b 100644 --- a/commands/msg/reply.go +++ b/commands/msg/reply.go @@ -124,7 +124,7 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error { original.Date = msg.Envelope.Date.Format("Mon Jan 2, 2006 at 3:04 PM") } - composer, err := widgets.NewComposer(aerc, aerc.Config(), + composer, err := widgets.NewComposer(aerc, acct, aerc.Config(), acct.AccountConfig(), acct.Worker(), template, defaults, original) if err != nil { aerc.PushError("Error: " + err.Error()) diff --git a/commands/msg/unsubscribe.go b/commands/msg/unsubscribe.go index 1bf3a83..1a2dd37 100644 --- a/commands/msg/unsubscribe.go +++ b/commands/msg/unsubscribe.go @@ -90,6 +90,7 @@ func unsubscribeMailto(aerc *widgets.Aerc, u *url.URL) error { } composer, err := widgets.NewComposer( aerc, + acct, aerc.Config(), acct.AccountConfig(), acct.Worker(), diff --git a/config/binds.conf b/config/binds.conf index 5887203..abfd77e 100644 --- a/config/binds.conf +++ b/config/binds.conf @@ -90,6 +90,7 @@ $ex = # Keybindings used when reviewing a message to be sent y = :send n = :abort +p = :postpone q = :abort e = :edit a = :attach diff --git a/config/config.go b/config/config.go index 5794388..e00518c 100644 --- a/config/config.go +++ b/config/config.go @@ -70,6 +70,7 @@ type AccountConfig struct { Archive string CopyTo string Default string + Postpone string From string Name string Source string @@ -171,10 +172,11 @@ func loadAccountConfig(path string) ([]AccountConfig, error) { } sec := file.Section(_sec) account := AccountConfig{ - Archive: "Archive", - Default: "INBOX", - Name: _sec, - Params: make(map[string]string), + Archive: "Archive", + Default: "INBOX", + Postpone: "Drafts", + Name: _sec, + Params: make(map[string]string), } if err = sec.MapTo(&account); err != nil { return nil, err diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd index 36ac9c6..c309f2f 100644 --- a/doc/aerc-config.5.scd +++ b/doc/aerc-config.5.scd @@ -402,6 +402,11 @@ Note that many of these configuration options are written for you, such as Default: none +*postpone* + Specifies the folder to save *postpone*d messages to. + + Default: Drafts + *source* Specifies the source for reading incoming emails on this account. This key is required for all accounts. It should be a connection string, and the diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd index d2e52a9..230361d 100644 --- a/doc/aerc.1.scd +++ b/doc/aerc.1.scd @@ -299,6 +299,10 @@ message list, the message in the message viewer, etc). *next-field*, *prev-field* Cycles between input fields in the compose window. +*postpone* + Saves the current state of the message to the *postpone* folder for the + current account. + *save* [-p] Saves the selected message part to the specified path. If -p is selected, aerc will create any missing directories in the specified path. If the path diff --git a/widgets/aerc.go b/widgets/aerc.go index 8307bd0..779e386 100644 --- a/widgets/aerc.go +++ b/widgets/aerc.go @@ -293,6 +293,8 @@ func (aerc *Aerc) SelectedAccount() *AccountView { return tab case *MessageViewer: return tab.SelectedAccount() + case *Composer: + return tab.Account() } return nil } @@ -494,7 +496,7 @@ func (aerc *Aerc) Mailto(addr *url.URL) error { defaults[header] = strings.Join(vals, ",") } } - composer, err := NewComposer(aerc, aerc.Config(), + composer, err := NewComposer(aerc, acct, aerc.Config(), acct.AccountConfig(), acct.Worker(), "", defaults, models.OriginalMail{}) if err != nil { return nil diff --git a/widgets/compose.go b/widgets/compose.go index 9ceabf6..66877cc 100644 --- a/widgets/compose.go +++ b/widgets/compose.go @@ -32,9 +32,10 @@ import ( type Composer struct { editors map[string]*headerEditor - acct *config.AccountConfig - config *config.AercConfig - aerc *Aerc + acctConfig *config.AccountConfig + config *config.AercConfig + acct *AccountView + aerc *Aerc attachments []string date time.Time @@ -57,7 +58,7 @@ type Composer struct { width int } -func NewComposer(aerc *Aerc, conf *config.AercConfig, +func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig, acct *config.AccountConfig, worker *types.Worker, template string, defaults map[string]string, original models.OriginalMail) (*Composer, error) { @@ -65,7 +66,7 @@ func NewComposer(aerc *Aerc, conf *config.AercConfig, defaults = make(map[string]string) } if from := defaults["From"]; from == "" { - defaults["From"] = acct.From + defaults["From"] = acctConfig.From } templateData := templates.ParseTemplateData(defaults, original) @@ -82,16 +83,17 @@ func NewComposer(aerc *Aerc, conf *config.AercConfig, } c := &Composer{ - acct: acct, - aerc: aerc, - config: conf, - date: time.Now(), - defaults: defaults, - editors: editors, - email: email, - layout: layout, - msgId: mail.GenerateMessageID(), - worker: worker, + acct: acct, + acctConfig: acctConfig, + aerc: aerc, + config: conf, + date: time.Now(), + defaults: defaults, + editors: editors, + email: email, + layout: layout, + msgId: mail.GenerateMessageID(), + worker: worker, // You have to backtab to get to "From", since you usually don't edit it focused: 1, focusable: focusable, @@ -215,7 +217,7 @@ func (c *Composer) AddTemplate(template string, data interface{}) error { func (c *Composer) AddSignature() { var signature []byte - if c.acct.SignatureCmd != "" { + if c.acctConfig.SignatureCmd != "" { var err error signature, err = c.readSignatureFromCmd() if err != nil { @@ -228,7 +230,7 @@ func (c *Composer) AddSignature() { } func (c *Composer) readSignatureFromCmd() ([]byte, error) { - sigCmd := c.acct.SignatureCmd + sigCmd := c.acctConfig.SignatureCmd cmd := exec.Command("sh", "-c", sigCmd) signature, err := cmd.Output() if err != nil { @@ -238,7 +240,7 @@ func (c *Composer) readSignatureFromCmd() ([]byte, error) { } func (c *Composer) readSignatureFromFile() []byte { - sigFile := c.acct.SignatureFile + sigFile := c.acctConfig.SignatureFile if sigFile == "" { return nil } @@ -354,6 +356,10 @@ func (c *Composer) Focus(focus bool) { } func (c *Composer) Config() *config.AccountConfig { + return c.acctConfig +} + +func (c *Composer) Account() *AccountView { return c.acct } @@ -771,7 +777,7 @@ func newReviewMessage(composer *Composer, err error) *reviewMessage { } else { // TODO: source this from actual keybindings? grid.AddChild(ui.NewText( - "Send this email? [y]es/[n]o/[e]dit/[a]ttach")).At(0, 0) + "Send this email? [y]es/[n]o/[p]ostpone/[e]dit/[a]ttach")).At(0, 0) grid.AddChild(ui.NewText("Attachments:"). Reverse(true)).At(1, 0) if len(composer.attachments) == 0 {