~sircmpwn/aerc

Add postpone command v1 PROPOSED

I think we should consider using Drafts as the default folder name.
Also, you add it to the list of prompts on the message review screen,
but don't add it to binds.conf.
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/%3C20190917072119.1697707-1-dev%40jeffas.io%3E/mbox | git am -3
Learn more about email & git

[PATCH 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.

Reading back in for editing is not a part of this patch but this sets up
the possibility of that.
---
 commands/compose/postpone.go | 100 +++++++++++++++++++++++++++++++++++
 config/config.go             |   1 +
 widgets/compose.go           |   2 +-
 3 files changed, 102 insertions(+), 1 deletion(-)
 create mode 100644 commands/compose/postpone.go

diff --git a/commands/compose/postpone.go b/commands/compose/postpone.go
new file mode 100644
index 0000000..e466545
--- /dev/null
+++ b/commands/compose/postpone.go
@@ -0,0 +1,100 @@
+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()
+	// to synchronise the creating of the directory
+	errChan := make(chan string)
+	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()
+		}
+	})
+
+	// 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()
+			}
+		})
+		header, _, _ = composer.PrepareHeader()
+		composer.WriteMessage(header, w)
+		w.Close()
+	}()
+	return nil
+}
diff --git a/config/config.go b/config/config.go
index eeaf937..76b9be7 100644
--- a/config/config.go
+++ b/config/config.go
@@ -47,6 +47,7 @@ type AccountConfig struct {
 	Archive         string
 	CopyTo          string
 	Default         string
+	Postpone        string
 	From            string
 	Name            string
 	Source          string
diff --git a/widgets/compose.go b/widgets/compose.go
index 22c58da..3915309 100644
--- a/widgets/compose.go
+++ b/widgets/compose.go
@@ -742,7 +742,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
I think we should consider using Drafts as the default folder name.
Also, you add it to the list of prompts on the message review screen,
but don't add it to binds.conf.

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

Adds docs for the postpone command and the Postpone accounts.conf config
option
---
 doc/aerc-config.5.scd | 5 +++++
 doc/aerc.1.scd        | 4 ++++
 2 files changed, 9 insertions(+)

diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index 91b444a..10f8c9c 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -281,6 +281,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: none
+
 *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 2ec17a4..c5603ea 100644
--- a/doc/aerc.1.scd
+++ b/doc/aerc.1.scd
@@ -243,6 +243,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 3/4] Add edit command Export this patch

This command allows editing any email in the composer.

This is aimed to be used with postponed emails.
---
 commands/msg/edit.go | 112 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 112 insertions(+)
 create mode 100644 commands/msg/edit.go

diff --git a/commands/msg/edit.go b/commands/msg/edit.go
new file mode 100644
index 0000000..b6ef583
--- /dev/null
+++ b/commands/msg/edit.go
@@ -0,0 +1,112 @@
+package msg
+
+import (
+	"errors"
+	"io"
+
+	"github.com/emersion/go-message"
+	_ "github.com/emersion/go-message/charset"
+	"github.com/emersion/go-message/mail"
+
+	"git.sr.ht/~sircmpwn/aerc/models"
+	"git.sr.ht/~sircmpwn/aerc/widgets"
+)
+
+type edit struct{}
+
+func init() {
+	register(edit{})
+}
+
+func (edit) Aliases() []string {
+	return []string{"edit"}
+}
+
+func (edit) Complete(aerc *widgets.Aerc, args []string) []string {
+	return nil
+}
+
+func (edit) Execute(aerc *widgets.Aerc, args []string) error {
+	if len(args) != 1 {
+		return errors.New("Usage: edit")
+	}
+
+	widget := aerc.SelectedTab().(widgets.ProvidesMessage)
+	acct := widget.SelectedAccount()
+	if acct == nil {
+		return errors.New("No account selected")
+	}
+	store := widget.Store()
+	if store == nil {
+		return errors.New("Cannot perform action. Messages still loading")
+	}
+	msg, err := widget.SelectedMessage()
+	if err != nil {
+		return err
+	}
+	acct.Logger().Println("Editing message " + msg.Envelope.MessageId)
+
+	// copy the headers to the defaults map for addition to the composition
+	defaults := make(map[string]string)
+	headerFields := msg.RFC822Headers.Fields()
+	for headerFields.Next() {
+		defaults[headerFields.Key()] = headerFields.Value()
+	}
+
+	composer := widgets.NewComposer(aerc, aerc.Config(),
+		acct.AccountConfig(), acct.Worker(), defaults)
+
+	// focus the terminal since the header fields are likely already done
+	composer.FocusTerminal()
+
+	addTab := func() {
+		tab := aerc.NewTab(composer, msg.Envelope.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(msg.BodyStructure.Parts) != 0 {
+		part, path = findPlaintext(msg.BodyStructure, path)
+	}
+	if part == nil {
+		part = msg.BodyStructure
+		path = []int{1}
+	}
+
+	store.FetchBodyPart(msg.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
+}
-- 
2.23.0

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

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

diff --git a/doc/aerc.1.scd b/doc/aerc.1.scd
index c5603ea..38ad893 100644
--- a/doc/aerc.1.scd
+++ b/doc/aerc.1.scd
@@ -96,6 +96,9 @@ message list, the message in the message viewer, etc).
 *delete*
 	Deletes the selected message.
 
+*edit*
+	Opens the selected message for editing.
+
 *forward* [-A] [address...]
 	Opens the composer to forward the selected message to another recipient.
 
-- 
2.23.0
View this thread in the archives