~sircmpwn/aerc

Add postpone command v3 PROPOSED

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

 10 files changed, 244 insertions(+), 5 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/8485/mbox | git am -3
Learn more about email & git

[PATCH v3 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.
---
 commands/compose/postpone.go | 98 ++++++++++++++++++++++++++++++++++++
 config/config.go             |  1 +
 widgets/compose.go           |  2 +-
 3 files changed, 100 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..4c0f761
--- /dev/null
+++ b/commands/compose/postpone.go
@@ -0,0 +1,98 @@
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()
			}
		})
		composer.WriteMessage(header, w)
		w.Close()
	}()
	return nil
}
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/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

[PATCH v3 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 v3 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..da565aa
--- /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, 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 v3 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