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.
This commit is contained in:
Jeffas 2020-04-24 11:42:21 +02:00 committed by Drew DeVault
parent 447e662057
commit 7f033278eb
11 changed files with 166 additions and 27 deletions

View File

@ -31,7 +31,7 @@ func (Compose) Execute(aerc *widgets.Aerc, args []string) error {
} }
acct := aerc.SelectedAccount() acct := aerc.SelectedAccount()
composer, err := widgets.NewComposer(aerc, composer, err := widgets.NewComposer(aerc, acct
aerc.Config(), acct.AccountConfig(), acct.Worker(), aerc.Config(), acct.AccountConfig(), acct.Worker(),
template, nil, models.OriginalMail{}) template, nil, models.OriginalMail{})
if err != nil { if err != nil {

View File

@ -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
}

View File

@ -70,7 +70,6 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {
"To": to, "To": to,
"Subject": subject, "Subject": subject,
} }
original := models.OriginalMail{} original := models.OriginalMail{}
addTab := func() (*widgets.Composer, error) { addTab := func() (*widgets.Composer, error) {

View File

@ -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") 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) acct.AccountConfig(), acct.Worker(), template, defaults, original)
if err != nil { if err != nil {
aerc.PushError("Error: " + err.Error()) aerc.PushError("Error: " + err.Error())

View File

@ -90,6 +90,7 @@ func unsubscribeMailto(aerc *widgets.Aerc, u *url.URL) error {
} }
composer, err := widgets.NewComposer( composer, err := widgets.NewComposer(
aerc, aerc,
acct,
aerc.Config(), aerc.Config(),
acct.AccountConfig(), acct.AccountConfig(),
acct.Worker(), acct.Worker(),

View File

@ -90,6 +90,7 @@ $ex = <C-x>
# Keybindings used when reviewing a message to be sent # Keybindings used when reviewing a message to be sent
y = :send<Enter> y = :send<Enter>
n = :abort<Enter> n = :abort<Enter>
p = :postpone<Enter>
q = :abort<Enter> q = :abort<Enter>
e = :edit<Enter> e = :edit<Enter>
a = :attach<space> a = :attach<space>

View File

@ -70,6 +70,7 @@ type AccountConfig struct {
Archive string Archive string
CopyTo string CopyTo string
Default string Default string
Postpone string
From string From string
Name string Name string
Source string Source string
@ -173,6 +174,7 @@ func loadAccountConfig(path string) ([]AccountConfig, error) {
account := AccountConfig{ account := AccountConfig{
Archive: "Archive", Archive: "Archive",
Default: "INBOX", Default: "INBOX",
Postpone: "Drafts",
Name: _sec, Name: _sec,
Params: make(map[string]string), Params: make(map[string]string),
} }

View File

@ -402,6 +402,11 @@ Note that many of these configuration options are written for you, such as
Default: none Default: none
*postpone*
Specifies the folder to save *postpone*d messages to.
Default: Drafts
*source* *source*
Specifies the source for reading incoming emails on this account. This key 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 is required for all accounts. It should be a connection string, and the

View File

@ -299,6 +299,10 @@ message list, the message in the message viewer, etc).
*next-field*, *prev-field* *next-field*, *prev-field*
Cycles between input fields in the compose window. 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] <path> *save* [-p] <path>
Saves the selected message part to the specified path. If -p is selected, 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 aerc will create any missing directories in the specified path. If the path

View File

@ -293,6 +293,8 @@ func (aerc *Aerc) SelectedAccount() *AccountView {
return tab return tab
case *MessageViewer: case *MessageViewer:
return tab.SelectedAccount() return tab.SelectedAccount()
case *Composer:
return tab.Account()
} }
return nil return nil
} }
@ -494,7 +496,7 @@ func (aerc *Aerc) Mailto(addr *url.URL) error {
defaults[header] = strings.Join(vals, ",") 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{}) acct.AccountConfig(), acct.Worker(), "", defaults, models.OriginalMail{})
if err != nil { if err != nil {
return nil return nil

View File

@ -32,8 +32,9 @@ import (
type Composer struct { type Composer struct {
editors map[string]*headerEditor editors map[string]*headerEditor
acct *config.AccountConfig acctConfig *config.AccountConfig
config *config.AercConfig config *config.AercConfig
acct *AccountView
aerc *Aerc aerc *Aerc
attachments []string attachments []string
@ -57,7 +58,7 @@ type Composer struct {
width int 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, acct *config.AccountConfig, worker *types.Worker, template string,
defaults map[string]string, original models.OriginalMail) (*Composer, error) { 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) defaults = make(map[string]string)
} }
if from := defaults["From"]; from == "" { if from := defaults["From"]; from == "" {
defaults["From"] = acct.From defaults["From"] = acctConfig.From
} }
templateData := templates.ParseTemplateData(defaults, original) templateData := templates.ParseTemplateData(defaults, original)
@ -83,6 +84,7 @@ func NewComposer(aerc *Aerc, conf *config.AercConfig,
c := &Composer{ c := &Composer{
acct: acct, acct: acct,
acctConfig: acctConfig,
aerc: aerc, aerc: aerc,
config: conf, config: conf,
date: time.Now(), date: time.Now(),
@ -215,7 +217,7 @@ func (c *Composer) AddTemplate(template string, data interface{}) error {
func (c *Composer) AddSignature() { func (c *Composer) AddSignature() {
var signature []byte var signature []byte
if c.acct.SignatureCmd != "" { if c.acctConfig.SignatureCmd != "" {
var err error var err error
signature, err = c.readSignatureFromCmd() signature, err = c.readSignatureFromCmd()
if err != nil { if err != nil {
@ -228,7 +230,7 @@ func (c *Composer) AddSignature() {
} }
func (c *Composer) readSignatureFromCmd() ([]byte, error) { func (c *Composer) readSignatureFromCmd() ([]byte, error) {
sigCmd := c.acct.SignatureCmd sigCmd := c.acctConfig.SignatureCmd
cmd := exec.Command("sh", "-c", sigCmd) cmd := exec.Command("sh", "-c", sigCmd)
signature, err := cmd.Output() signature, err := cmd.Output()
if err != nil { if err != nil {
@ -238,7 +240,7 @@ func (c *Composer) readSignatureFromCmd() ([]byte, error) {
} }
func (c *Composer) readSignatureFromFile() []byte { func (c *Composer) readSignatureFromFile() []byte {
sigFile := c.acct.SignatureFile sigFile := c.acctConfig.SignatureFile
if sigFile == "" { if sigFile == "" {
return nil return nil
} }
@ -354,6 +356,10 @@ func (c *Composer) Focus(focus bool) {
} }
func (c *Composer) Config() *config.AccountConfig { func (c *Composer) Config() *config.AccountConfig {
return c.acctConfig
}
func (c *Composer) Account() *AccountView {
return c.acct return c.acct
} }
@ -771,7 +777,7 @@ func newReviewMessage(composer *Composer, err error) *reviewMessage {
} else { } else {
// TODO: source this from actual keybindings? // TODO: source this from actual keybindings?
grid.AddChild(ui.NewText( 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:"). grid.AddChild(ui.NewText("Attachments:").
Reverse(true)).At(1, 0) Reverse(true)).At(1, 0)
if len(composer.attachments) == 0 { if len(composer.attachments) == 0 {