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:
parent
447e662057
commit
7f033278eb
11 changed files with 166 additions and 27 deletions
|
@ -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 {
|
||||||
|
|
119
commands/compose/postpone.go
Normal file
119
commands/compose/postpone.go
Normal 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
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue