~sircmpwn/aerc

Add postpone command v4 PROPOSED

Jeffas: 4
 Add postpone command
 Add documentation for postpone
 Add recall command
 Add documentation for edit command

 15 files changed, 289 insertions(+), 26 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~sircmpwn/aerc/patches/8583/mbox | git am -3
Learn more about email & git

[PATCH v4 1/4] Add postpone command Export this patch

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 revision checks the list of directories for an existing one and
only creates a new one if not present.

 commands/account/compose.go  |   2 +-
 commands/compose/postpone.go | 113 +++++++++++++++++++++++++++++++++++
 commands/msg/forward.go      |   2 +-
 commands/msg/reply.go        |   2 +-
 commands/msg/unsubscribe.go  |   1 +
 config/config.go             |   1 +
 widgets/aerc.go              |   4 +-
 widgets/compose.go           |  42 +++++++------
 8 files changed, 145 insertions(+), 22 deletions(-)
 create mode 100644 commands/compose/postpone.go

diff --git a/commands/account/compose.go b/commands/account/compose.go
index 039eb92..b1c3ddd 100644
--- a/commands/account/compose.go
+++ b/commands/account/compose.go
@@ -29,7 +29,7 @@ func (Compose) Execute(aerc *widgets.Aerc, args []string) error {
		return err
	}
	acct := aerc.SelectedAccount()
	composer := widgets.NewComposer(aerc,
	composer := widgets.NewComposer(aerc, acct,
		aerc.Config(), acct.AccountConfig(), acct.Worker(), nil)
	tab := aerc.NewTab(composer, "New email")
	composer.OnHeaderChange("Subject", func(subject string) {
diff --git a/commands/compose/postpone.go b/commands/compose/postpone.go
new file mode 100644
index 0000000..45ed764
--- /dev/null
+++ b/commands/compose/postpone.go
@@ -0,0 +1,113 @@
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")
	}
	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)
		composer.WriteMessage(header, ctr)
		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:      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 494072d..5008952 100644
--- a/commands/msg/forward.go
+++ b/commands/msg/forward.go
@@ -69,7 +69,7 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {
		"To":      to,
		"Subject": subject,
	}
	composer := widgets.NewComposer(aerc, aerc.Config(), acct.AccountConfig(),
	composer := widgets.NewComposer(aerc, acct, aerc.Config(), acct.AccountConfig(),
		acct.Worker(), defaults)

	addTab := func() {
diff --git a/commands/msg/reply.go b/commands/msg/reply.go
index 9ef7a3b..35de2ab 100644
--- a/commands/msg/reply.go
+++ b/commands/msg/reply.go
@@ -116,7 +116,7 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error {
		"In-Reply-To": msg.Envelope.MessageId,
	}

	composer := widgets.NewComposer(aerc, aerc.Config(),
	composer := widgets.NewComposer(aerc, acct, aerc.Config(),
		acct.AccountConfig(), acct.Worker(), defaults)

	if args[0] == "reply" {
diff --git a/commands/msg/unsubscribe.go b/commands/msg/unsubscribe.go
index 15a9411..8d13dc1 100644
--- a/commands/msg/unsubscribe.go
+++ b/commands/msg/unsubscribe.go
@@ -89,6 +89,7 @@ func unsubscribeMailto(aerc *widgets.Aerc, u *url.URL) error {
	}
	composer := widgets.NewComposer(
		aerc,
		acct,
		aerc.Config(),
		acct.AccountConfig(),
		acct.Worker(),
diff --git a/config/config.go b/config/config.go
index 133a7f4..d639ee4 100644
--- a/config/config.go
+++ b/config/config.go
@@ -49,6 +49,7 @@ type AccountConfig struct {
	Archive         string
	CopyTo          string
	Default         string
	Postpone        string
	From            string
	Name            string
	Source          string
diff --git a/widgets/aerc.go b/widgets/aerc.go
index af51a0f..660a7c6 100644
--- a/widgets/aerc.go
+++ b/widgets/aerc.go
@@ -274,6 +274,8 @@ func (aerc *Aerc) SelectedAccount() *AccountView {
		return tab
	case *MessageViewer:
		return tab.SelectedAccount()
	case *Composer:
		return tab.Account()
	}
	return nil
}
@@ -431,7 +433,7 @@ func (aerc *Aerc) Mailto(addr *url.URL) error {
			defaults[header] = strings.Join(vals, ",")
		}
	}
	composer := NewComposer(aerc, aerc.Config(),
	composer := NewComposer(aerc, acct, aerc.Config(),
		acct.AccountConfig(), acct.Worker(), defaults)
	composer.FocusSubject()
	title := "New email"
diff --git a/widgets/compose.go b/widgets/compose.go
index 22c58da..8ae6da6 100644
--- a/widgets/compose.go
+++ b/widgets/compose.go
@@ -30,9 +30,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

	defaults    map[string]string
	editor      *Terminal
@@ -52,14 +53,14 @@ type Composer struct {
	width int
}

func NewComposer(aerc *Aerc, conf *config.AercConfig,
	acct *config.AccountConfig, worker *types.Worker, defaults map[string]string) *Composer {
func NewComposer(aerc *Aerc, acct *AccountView, conf *config.AercConfig,
	acctConfig *config.AccountConfig, worker *types.Worker, defaults map[string]string) *Composer {

	if defaults == nil {
		defaults = make(map[string]string)
	}
	if from := defaults["From"]; from == "" {
		defaults["From"] = acct.From
		defaults["From"] = acctConfig.From
	}

	layout, editors, focusable := buildComposeHeader(
@@ -72,14 +73,15 @@ func NewComposer(aerc *Aerc, conf *config.AercConfig,
	}

	c := &Composer{
		aerc:     aerc,
		editors:  editors,
		acct:     acct,
		config:   conf,
		defaults: defaults,
		email:    email,
		worker:   worker,
		layout:   layout,
		aerc:       aerc,
		acct:       acct,
		editors:    editors,
		acctConfig: acctConfig,
		config:     conf,
		defaults:   defaults,
		email:      email,
		worker:     worker,
		layout:     layout,
		// You have to backtab to get to "From", since you usually don't edit it
		focused:   1,
		focusable: focusable,
@@ -165,7 +167,7 @@ func (c *Composer) AppendContents(reader io.Reader) {

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 {
@@ -178,7 +180,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 {
@@ -188,7 +190,7 @@ func (c *Composer) readSignatureFromCmd() ([]byte, error) {
}

func (c *Composer) readSignatureFromFile() []byte {
	sigFile := c.acct.SignatureFile
	sigFile := c.acctConfig.SignatureFile
	if sigFile == "" {
		return nil
	}
@@ -294,6 +296,10 @@ func (c *Composer) Focus(focus bool) {
}

func (c *Composer) Config() *config.AccountConfig {
	return c.acctConfig
}

func (c *Composer) Account() *AccountView {
	return c.acct
}

@@ -742,7 +748,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 {
--
2.23.0
Thanks! I still have two issues with this.

1. The docs should be added together with the feature in the same
   commit - please rebase.
2. Using this command causes my IMAP connection to immediately EOF.

[PATCH v4 2/4] Add documentation for postpone Export this patch

Adds docs for the postpone command and the Postpone accounts.conf config
option. A default for the postpone config option is provided of: Drafts.
---
 config/binds.conf     | 1 +
 config/config.go      | 9 +++++----
 doc/aerc-config.5.scd | 5 +++++
 doc/aerc.1.scd        | 4 ++++
 4 files changed, 15 insertions(+), 4 deletions(-)

diff --git a/config/binds.conf b/config/binds.conf
index ac49bd0..ea6b99e 100644
--- a/config/binds.conf
+++ b/config/binds.conf
@@ -87,6 +87,7 @@ $ex = <C-x>
# Keybindings used when reviewing a message to be sent
y = :send<Enter>
n = :abort<Enter>
p = :postpone<Enter>
q = :abort<Enter>
e = :edit<Enter>
a = :attach<space>
diff --git a/config/config.go b/config/config.go
index d639ee4..be021c7 100644
--- a/config/config.go
+++ b/config/config.go
@@ -141,10 +141,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 f4f02f2..e753b99 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -295,6 +295,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 e76b519..f534f05 100644
--- a/doc/aerc.1.scd
+++ b/doc/aerc.1.scd
@@ -268,6 +268,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] <path>
	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
-- 
2.23.0

[PATCH v4 3/4] Add recall command Export this patch

This command allows recalling the selected postponed email to edit in
the composer. The command only allows recalling from the postpone
directory.
---
 commands/msg/recall.go | 121 +++++++++++++++++++++++++++++++++++++++++
 widgets/account.go     |   4 ++
 2 files changed, 125 insertions(+)
 create mode 100644 commands/msg/recall.go

diff --git a/commands/msg/recall.go b/commands/msg/recall.go
new file mode 100644
index 0000000..116d489
--- /dev/null
+++ b/commands/msg/recall.go
@@ -0,0 +1,121 @@
package msg

import (
	"io"

	"github.com/emersion/go-message"
	_ "github.com/emersion/go-message/charset"
	"github.com/emersion/go-message/mail"
	"github.com/pkg/errors"

	"git.sr.ht/~sircmpwn/aerc/models"
	"git.sr.ht/~sircmpwn/aerc/widgets"
)

type Recall struct{}

func init() {
	register(Recall{})
}

func (Recall) Aliases() []string {
	return []string{"recall"}
}

func (Recall) Complete(aerc *widgets.Aerc, args []string) []string {
	return nil
}

func (Recall) Execute(aerc *widgets.Aerc, args []string) error {
	if len(args) != 1 {
		return errors.New("Usage: recall")
	}

	widget := aerc.SelectedTab().(widgets.ProvidesMessage)
	acct := widget.SelectedAccount()
	if acct == nil {
		return errors.New("No account selected")
	}
	if acct.SelectedDirectory() != acct.AccountConfig().Postpone {
		return errors.New("Can only recall from the postpone directory: " +
			acct.AccountConfig().Postpone)
	}
	store := widget.Store()
	if store == nil {
		return errors.New("Cannot perform action. Messages still loading")
	}

	msgInfo, err := widget.SelectedMessage()
	if err != nil {
		return errors.Wrap(err, "Recall failed")
	}
	acct.Logger().Println("Recalling message " + msgInfo.Envelope.MessageId)

	// copy the headers to the defaults map for addition to the composition
	defaults := make(map[string]string)
	headerFields := msgInfo.RFC822Headers.Fields()
	for headerFields.Next() {
		defaults[headerFields.Key()] = headerFields.Value()
	}

	composer := widgets.NewComposer(aerc, acct, aerc.Config(),
		acct.AccountConfig(), acct.Worker(), defaults)

	// focus the terminal since the header fields are likely already done
	composer.FocusTerminal()

	addTab := func() {
		subject := msgInfo.Envelope.Subject
		if subject == "" {
			subject = "Recalled email"
		}
		tab := aerc.NewTab(composer, subject)
		composer.OnHeaderChange("Subject", func(subject string) {
			if subject == "" {
				tab.Name = "New email"
			} else {
				tab.Name = subject
			}
			tab.Content.Invalidate()
		})
	}

	// find the main body part and add it to the editor
	// TODO: copy all parts of the message over?
	var (
		path []int
		part *models.BodyStructure
	)
	if len(msgInfo.BodyStructure.Parts) != 0 {
		part, path = findPlaintext(msgInfo.BodyStructure, path)
	}
	if part == nil {
		part = msgInfo.BodyStructure
		path = []int{1}
	}

	store.FetchBodyPart(msgInfo.Uid, path, func(reader io.Reader) {
		header := message.Header{}
		header.SetText(
			"Content-Transfer-Encoding", part.Encoding)
		header.SetContentType(part.MIMEType, part.Params)
		header.SetText("Content-Description", part.Description)
		entity, err := message.New(header, reader)
		if err != nil {
			// TODO: Do something with the error
			addTab()
			return
		}
		mreader := mail.NewReader(entity)
		part, err := mreader.NextPart()
		if err != nil {
			// TODO: Do something with the error
			addTab()
			return
		}
		composer.SetContents(part.Body)
		addTab()
	})

	return nil
}
diff --git a/widgets/account.go b/widgets/account.go
index 4e8dd17..213f1a6 100644
--- a/widgets/account.go
+++ b/widgets/account.go
@@ -182,6 +182,10 @@ func (acct *AccountView) SelectedAccount() *AccountView {
	return acct
}

func (acct *AccountView) SelectedDirectory() string {
	return acct.dirlist.Selected()
}

func (acct *AccountView) SelectedMessage() (*models.MessageInfo, error) {
	if len(acct.msglist.Store().Uids()) == 0 {
		return nil, errors.New("no message selected")
-- 
2.23.0

[PATCH v4 4/4] Add documentation for edit command Export this patch

---
 doc/aerc.1.scd | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd
index f534f05..cbe8e1f 100644
--- a/doc/aerc.1.scd
+++ b/doc/aerc.1.scd
@@ -96,6 +96,10 @@ message list, the message in the message viewer, etc).
*delete*
	Deletes the selected message.

*recall*
	Opens the selected message for re-editing. Messages can only be
	recalled from the postpone directory.

*forward* [-A] [address...]
	Opens the composer to forward the selected message to another recipient.

--
2.23.0
View this thread in the archives