~sircmpwn/aerc

e2d5c456dc27f958d79fdf740c7b0852b2af4160 — Jeffas 10 months ago a93b4de
Add signatures

This adds the ability for per-account signatures in the accounts.conf
config file. The signature is added to emails in the editor at the
bottom of the email. This includes when forwarding, replying to, and
composing emails.

There are two config options: signature-file and signature-cmd. The
former allows a signature to be read from a file and the latter allows
an arbitrary command to be executed to return the signature.

The config options have been documented in aerc-config
M commands/account/compose.go => commands/account/compose.go +2 -2
@@ 29,7 29,7 @@ func (Compose) Execute(aerc *widgets.Aerc, args []string) error {
		return err
	}
	acct := aerc.SelectedAccount()
	composer := widgets.NewComposer(
	composer := widgets.NewComposer(aerc,
		aerc.Config(), acct.AccountConfig(), acct.Worker(), nil)
	tab := aerc.NewTab(composer, "New email")
	composer.OnHeaderChange("Subject", func(subject string) {


@@ 40,7 40,7 @@ func (Compose) Execute(aerc *widgets.Aerc, args []string) error {
		}
		tab.Content.Invalidate()
	})
	go composer.SetContents(strings.NewReader(body))
	go composer.PrependContents(strings.NewReader(body))
	return nil
}


M commands/msg/forward.go => commands/msg/forward.go +2 -2
@@ 69,7 69,7 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {
		"To":      to,
		"Subject": subject,
	}
	composer := widgets.NewComposer(aerc.Config(), acct.AccountConfig(),
	composer := widgets.NewComposer(aerc, aerc.Config(), acct.AccountConfig(),
		acct.Worker(), defaults)

	addTab := func() {


@@ 154,7 154,7 @@ func forwardBodyPart(store *lib.MessageStore, composer *widgets.Composer,

		pipeout, pipein := io.Pipe()
		scanner := bufio.NewScanner(part.Body)
		go composer.SetContents(pipeout)
		go composer.PrependContents(pipeout)
		// TODO: Let user customize the date format used here
		io.WriteString(pipein, fmt.Sprintf("Forwarded message from %s on %s:\n\n",
			msg.Envelope.From[0].Name,

M commands/msg/reply.go => commands/msg/reply.go +3 -3
@@ 116,8 116,8 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error {
		"In-Reply-To": msg.Envelope.MessageId,
	}

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

	if args[0] == "reply" {
		composer.FocusTerminal()


@@ 170,7 170,7 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error {

			pipeout, pipein := io.Pipe()
			scanner := bufio.NewScanner(part.Body)
			go composer.SetContents(pipeout)
			go composer.PrependContents(pipeout)
			// TODO: Let user customize the date format used here
			io.WriteString(pipein, fmt.Sprintf("On %s %s wrote:\n",
				msg.Envelope.Date.Format("Mon Jan 2, 2006 at 3:04 PM"),

M commands/msg/unsubscribe.go => commands/msg/unsubscribe.go +1 -0
@@ 88,6 88,7 @@ func unsubscribeMailto(aerc *widgets.Aerc, u *url.URL) error {
		"Subject": u.Query().Get("subject"),
	}
	composer := widgets.NewComposer(
		aerc,
		aerc.Config(),
		acct.AccountConfig(),
		acct.Worker(),

M config/config.go => config/config.go +2 -0
@@ 55,6 55,8 @@ type AccountConfig struct {
	Params          map[string]string
	Outgoing        string
	OutgoingCredCmd string
	SignatureFile   string
	SignatureCmd    string
}

type BindingConfig struct {

M doc/aerc-config.5.scd => doc/aerc-config.5.scd +8 -0
@@ 297,6 297,14 @@ Note that many of these configuration options are written for you, such as
	Specifies an optional command that is run to get the source account's
	password. See each protocol's man page for more details.

*signature-file*
	Specifies the file to read in order to obtain the signature to be added
	to emails sent from this account.

*signature-cmd*
	Specifies the command to execute in *sh* in order to obtain the
	signature to be added to emails sent from this account. If the command
	fails then *signature-file* is used instead.

# BINDS.CONF


M widgets/aerc.go => widgets/aerc.go +1 -1
@@ 430,7 430,7 @@ func (aerc *Aerc) Mailto(addr *url.URL) error {
			defaults[header] = strings.Join(vals, ",")
		}
	}
	composer := NewComposer(aerc.Config(),
	composer := NewComposer(aerc, aerc.Config(),
		acct.AccountConfig(), acct.Worker(), defaults)
	composer.FocusSubject()
	title := "New email"

M widgets/compose.go => widgets/compose.go +65 -1
@@ 2,6 2,8 @@ package widgets

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"mime"


@@ 17,6 19,7 @@ import (
	"github.com/emersion/go-message/mail"
	"github.com/gdamore/tcell"
	"github.com/mattn/go-runewidth"
	"github.com/mitchellh/go-homedir"
	"github.com/pkg/errors"

	"git.sr.ht/~sircmpwn/aerc/config"


@@ 29,6 32,7 @@ type Composer struct {

	acct   *config.AccountConfig
	config *config.AercConfig
	aerc   *Aerc

	defaults    map[string]string
	editor      *Terminal


@@ 48,7 52,7 @@ type Composer struct {
	width int
}

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

	if defaults == nil {


@@ 68,6 72,7 @@ func NewComposer(conf *config.AercConfig,
	}

	c := &Composer{
		aerc:     aerc,
		editors:  editors,
		acct:     acct,
		config:   conf,


@@ 80,6 85,8 @@ func NewComposer(conf *config.AercConfig,
		focusable: focusable,
	}

	c.AddSignature()

	c.updateGrid()
	c.ShowTerminal()



@@ 140,6 147,63 @@ func (c *Composer) SetContents(reader io.Reader) *Composer {
	return c
}

func (c *Composer) PrependContents(reader io.Reader) {
	buf := bytes.NewBuffer(nil)
	c.email.Seek(0, io.SeekStart)
	io.Copy(buf, c.email)
	c.email.Seek(0, io.SeekStart)
	io.Copy(c.email, reader)
	io.Copy(c.email, buf)
	c.email.Sync()
}

func (c *Composer) AppendContents(reader io.Reader) {
	c.email.Seek(0, io.SeekEnd)
	io.Copy(c.email, reader)
	c.email.Sync()
}

func (c *Composer) AddSignature() {
	var signature []byte
	if c.acct.SignatureCmd != "" {
		var err error
		signature, err = c.readSignatureFromCmd()
		if err != nil {
			signature = c.readSignatureFromFile()
		}
	} else {
		signature = c.readSignatureFromFile()
	}
	c.AppendContents(bytes.NewReader(signature))
}

func (c *Composer) readSignatureFromCmd() ([]byte, error) {
	sigCmd := c.acct.SignatureCmd
	cmd := exec.Command("sh", "-c", sigCmd)
	signature, err := cmd.Output()
	if err != nil {
		return nil, err
	}
	return signature, nil
}

func (c *Composer) readSignatureFromFile() []byte {
	sigFile := c.acct.SignatureFile
	if sigFile == "" {
		return nil
	}
	sigFile, err := homedir.Expand(sigFile)
	if err != nil {
		return nil
	}
	signature, err := ioutil.ReadFile(sigFile)
	if err != nil {
		c.aerc.PushError(fmt.Sprintf(" Error loading signature from file: %v", sigFile))
		return nil
	}
	return signature
}

func (c *Composer) FocusTerminal() *Composer {
	if c.editor == nil {
		return c