diff options
| -rw-r--r-- | Makefile | 7 | ||||
| -rw-r--r-- | commands/account/compose.go | 24 | ||||
| -rw-r--r-- | commands/msg/forward.go | 155 | ||||
| -rw-r--r-- | commands/msg/reply.go | 73 | ||||
| -rw-r--r-- | commands/msg/unsubscribe.go | 6 | ||||
| -rw-r--r-- | config/aerc.conf.in | 22 | ||||
| -rw-r--r-- | config/config.go | 44 | ||||
| -rw-r--r-- | doc/aerc-config.5.scd | 25 | ||||
| -rw-r--r-- | doc/aerc-templates.7.scd | 89 | ||||
| -rw-r--r-- | lib/templates/template.go | 160 | ||||
| -rw-r--r-- | templates/forward_as_body | 2 | ||||
| -rw-r--r-- | templates/quoted_reply | 2 | ||||
| -rw-r--r-- | widgets/aerc.go | 7 | ||||
| -rw-r--r-- | widgets/compose.go | 37 | 
14 files changed, 510 insertions, 143 deletions
| @@ -35,7 +35,8 @@ DOCS := \  	aerc-sendmail.5 \  	aerc-notmuch.5 \  	aerc-smtp.5 \ -	aerc-tutorial.7 +	aerc-tutorial.7 \ +	aerc-templates.7  .1.scd.1:  	scdoc < $< > $@ @@ -58,7 +59,7 @@ clean:  install: all  	mkdir -p $(BINDIR) $(MANDIR)/man1 $(MANDIR)/man5 $(MANDIR)/man7 \ -		$(SHAREDIR) $(SHAREDIR)/filters +		$(SHAREDIR) $(SHAREDIR)/filters $(SHAREDIR)/templates  	install -m755 aerc $(BINDIR)/aerc  	install -m644 aerc.1 $(MANDIR)/man1/aerc.1  	install -m644 aerc-search.1 $(MANDIR)/man1/aerc-search.1 @@ -75,6 +76,8 @@ install: all  	install -m755 filters/hldiff $(SHAREDIR)/filters/hldiff  	install -m755 filters/html $(SHAREDIR)/filters/html  	install -m755 filters/plaintext $(SHAREDIR)/filters/plaintext +	install -m644 templates/quoted_reply $(SHAREDIR)/templates/quoted_reply +	install -m644 templates/forward_as_body $(SHAREDIR)/templates/forward_as_body  RMDIR_IF_EMPTY:=sh -c '\  if test -d $$0 && ! ls -1qA $$0 | grep -q . ; then \ diff --git a/commands/account/compose.go b/commands/account/compose.go index 039eb92..24e460b 100644 --- a/commands/account/compose.go +++ b/commands/account/compose.go @@ -24,13 +24,17 @@ func (Compose) Complete(aerc *widgets.Aerc, args []string) []string {  }  func (Compose) Execute(aerc *widgets.Aerc, args []string) error { -	body, err := buildBody(args) +	body, template, err := buildBody(args)  	if err != nil {  		return err  	}  	acct := aerc.SelectedAccount() -	composer := widgets.NewComposer(aerc, -		aerc.Config(), acct.AccountConfig(), acct.Worker(), nil) + +	composer, err := widgets.NewComposer(aerc, +		aerc.Config(), acct.AccountConfig(), acct.Worker(), template, nil) +	if err != nil { +		return err +	}  	tab := aerc.NewTab(composer, "New email")  	composer.OnHeaderChange("Subject", func(subject string) {  		if subject == "" { @@ -44,11 +48,11 @@ func (Compose) Execute(aerc *widgets.Aerc, args []string) error {  	return nil  } -func buildBody(args []string) (string, error) { -	var body, headers string -	opts, optind, err := getopt.Getopts(args, "H:") +func buildBody(args []string) (string, string, error) { +	var body, template, headers string +	opts, optind, err := getopt.Getopts(args, "H:T:")  	if err != nil { -		return "", err +		return "", "", err  	}  	for _, opt := range opts {  		switch opt.Option { @@ -60,11 +64,13 @@ func buildBody(args []string) (string, error) {  			} else {  				headers += opt.Value + ":\n"  			} +		case 'T': +			template = opt.Value  		}  	}  	posargs := args[optind:]  	if len(posargs) > 1 { -		return "", errors.New("Usage: compose [-H] [body]") +		return "", template, errors.New("Usage: compose [-H] [body]")  	}  	if len(posargs) == 1 {  		body = posargs[0] @@ -76,5 +82,5 @@ func buildBody(args []string) (string, error) {  			body = headers + "\n\n"  		}  	} -	return body, nil +	return body, template, nil  } diff --git a/commands/msg/forward.go b/commands/msg/forward.go index 494072d..7570177 100644 --- a/commands/msg/forward.go +++ b/commands/msg/forward.go @@ -1,20 +1,21 @@  package msg  import ( -	"bufio" +	"bytes"  	"errors"  	"fmt" -	"git.sr.ht/~sircmpwn/aerc/lib" -	"git.sr.ht/~sircmpwn/aerc/models" -	"git.sr.ht/~sircmpwn/aerc/widgets" -	"git.sr.ht/~sircmpwn/getopt" -	"github.com/emersion/go-message" -	"github.com/emersion/go-message/mail"  	"io"  	"io/ioutil"  	"os"  	"path"  	"strings" + +	"github.com/emersion/go-message" +	"github.com/emersion/go-message/mail" + +	"git.sr.ht/~sircmpwn/aerc/models" +	"git.sr.ht/~sircmpwn/aerc/widgets" +	"git.sr.ht/~sircmpwn/getopt"  )  type forward struct{} @@ -32,15 +33,18 @@ func (forward) Complete(aerc *widgets.Aerc, args []string) []string {  }  func (forward) Execute(aerc *widgets.Aerc, args []string) error { -	opts, optind, err := getopt.Getopts(args, "A") +	opts, optind, err := getopt.Getopts(args, "AT:")  	if err != nil {  		return err  	}  	attach := false +	template := ""  	for _, opt := range opts {  		switch opt.Option {  		case 'A':  			attach = true +		case 'T': +			template = opt.Value  		}  	} @@ -69,10 +73,20 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {  		"To":      to,  		"Subject": subject,  	} -	composer := widgets.NewComposer(aerc, aerc.Config(), acct.AccountConfig(), -		acct.Worker(), defaults) -	addTab := func() { +	addTab := func() (*widgets.Composer, error) { +		if template != "" { +			defaults["OriginalFrom"] = models.FormatAddresses(msg.Envelope.From) +			defaults["OriginalDate"] = msg.Envelope.Date.Format("Mon Jan 2, 2006 at 3:04 PM") +		} + +		composer, err := widgets.NewComposer(aerc, aerc.Config(), acct.AccountConfig(), +			acct.Worker(), template, defaults) +		if err != nil { +			aerc.PushError("Error: " + err.Error()) +			return nil, err +		} +  		tab := aerc.NewTab(composer, subject)  		if to == "" {  			composer.FocusRecipient() @@ -87,83 +101,68 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {  			}  			tab.Content.Invalidate()  		}) +		return composer, nil  	}  	if attach { -		forwardAttach(store, composer, msg, addTab) -	} else { -		forwardBodyPart(store, composer, msg, addTab) -	} -	return nil -} - -func forwardAttach(store *lib.MessageStore, composer *widgets.Composer, -	msg *models.MessageInfo, addTab func()) { - -	store.FetchFull([]uint32{msg.Uid}, func(reader io.Reader) {  		tmpDir, err := ioutil.TempDir("", "aerc-tmp-attachment")  		if err != nil { -			// TODO: Do something with the error -			addTab() -			return +			return err  		}  		tmpFileName := path.Join(tmpDir,  			strings.ReplaceAll(fmt.Sprintf("%s.eml", msg.Envelope.Subject), "/", "-")) -		tmpFile, err := os.Create(tmpFileName) -		if err != nil { -			println(err) -			// TODO: Do something with the error -			addTab() -			return -		} +		store.FetchFull([]uint32{msg.Uid}, func(reader io.Reader) { +			tmpFile, err := os.Create(tmpFileName) +			if err != nil { +				println(err) +				// TODO: Do something with the error +				addTab() +				return +			} -		defer tmpFile.Close() -		io.Copy(tmpFile, reader) -		composer.AddAttachment(tmpFileName) -		composer.OnClose(func(composer *widgets.Composer) { -			os.RemoveAll(tmpDir) +			defer tmpFile.Close() +			io.Copy(tmpFile, reader) +			composer, err := addTab() +			if err != nil { +				return +			} +			composer.AddAttachment(tmpFileName) +			composer.OnClose(func(composer *widgets.Composer) { +				os.RemoveAll(tmpDir) +			})  		}) -		addTab() -	}) -} - -func forwardBodyPart(store *lib.MessageStore, composer *widgets.Composer, -	msg *models.MessageInfo, addTab func()) { -	// TODO: something more intelligent than fetching the 1st part -	// TODO: add attachments! -	store.FetchBodyPart(msg.Uid, []int{1}, func(reader io.Reader) { -		header := message.Header{} -		header.SetText( -			"Content-Transfer-Encoding", msg.BodyStructure.Encoding) -		header.SetContentType( -			msg.BodyStructure.MIMEType, msg.BodyStructure.Params) -		header.SetText("Content-Description", msg.BodyStructure.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 +	} else { +		if template == "" { +			template = aerc.Config().Templates.Forwards  		} -		pipeout, pipein := io.Pipe() -		scanner := bufio.NewScanner(part.Body) -		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, -			msg.Envelope.Date.Format("Mon Jan 2, 2006 at 3:04 PM"))) -		for scanner.Scan() { -			io.WriteString(pipein, fmt.Sprintf("%s\n", scanner.Text())) -		} -		pipein.Close() -		pipeout.Close() -		addTab() -	}) +		// TODO: something more intelligent than fetching the 1st part +		// TODO: add attachments! +		store.FetchBodyPart(msg.Uid, []int{1}, func(reader io.Reader) { +			header := message.Header{} +			header.SetText( +				"Content-Transfer-Encoding", msg.BodyStructure.Encoding) +			header.SetContentType( +				msg.BodyStructure.MIMEType, msg.BodyStructure.Params) +			header.SetText("Content-Description", msg.BodyStructure.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 +			} +			buf := new(bytes.Buffer) +			buf.ReadFrom(part.Body) +			defaults["Original"] = buf.String() +			addTab() +		}) +	} +	return nil  } diff --git a/commands/msg/reply.go b/commands/msg/reply.go index 9ef7a3b..b13e254 100644 --- a/commands/msg/reply.go +++ b/commands/msg/reply.go @@ -1,7 +1,7 @@  package msg  import ( -	"bufio" +	"bytes"  	"errors"  	"fmt"  	"io" @@ -32,16 +32,17 @@ func (reply) Complete(aerc *widgets.Aerc, args []string) []string {  }  func (reply) Execute(aerc *widgets.Aerc, args []string) error { -	opts, optind, err := getopt.Getopts(args, "aq") +	opts, optind, err := getopt.Getopts(args, "aqT:")  	if err != nil {  		return err  	}  	if optind != len(args) { -		return errors.New("Usage: reply [-aq]") +		return errors.New("Usage: reply [-aq -T <template>]")  	}  	var (  		quote    bool  		replyAll bool +		template string  	)  	for _, opt := range opts {  		switch opt.Option { @@ -49,11 +50,14 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error {  			replyAll = true  		case 'q':  			quote = true +		case 'T': +			template = opt.Value  		}  	}  	widget := aerc.SelectedTab().(widgets.ProvidesMessage)  	acct := widget.SelectedAccount() +  	if acct == nil {  		return errors.New("No account selected")  	} @@ -116,14 +120,23 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error {  		"In-Reply-To": msg.Envelope.MessageId,  	} -	composer := widgets.NewComposer(aerc, aerc.Config(), -		acct.AccountConfig(), acct.Worker(), defaults) +	addTab := func() error { +		if template != "" { +			defaults["OriginalFrom"] = models.FormatAddresses(msg.Envelope.From) +			defaults["OriginalDate"] = msg.Envelope.Date.Format("Mon Jan 2, 2006 at 3:04 PM") +		} -	if args[0] == "reply" { -		composer.FocusTerminal() -	} +		composer, err := widgets.NewComposer(aerc, aerc.Config(), +			acct.AccountConfig(), acct.Worker(), template, defaults) +		if err != nil { +			aerc.PushError("Error: " + err.Error()) +			return err +		} + +		if args[0] == "reply" { +			composer.FocusTerminal() +		} -	addTab := func() {  		tab := aerc.NewTab(composer, subject)  		composer.OnHeaderChange("Subject", func(subject string) {  			if subject == "" { @@ -133,27 +146,21 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error {  			}  			tab.Content.Invalidate()  		}) + +		return nil  	}  	if quote { -		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} +		if template == "" { +			template = aerc.Config().Templates.QuotedReply  		} -		store.FetchBodyPart(msg.Uid, path, func(reader io.Reader) { +		store.FetchBodyPart(msg.Uid, []int{1}, 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) +				"Content-Transfer-Encoding", msg.BodyStructure.Encoding) +			header.SetContentType(msg.BodyStructure.MIMEType, msg.BodyStructure.Params) +			header.SetText("Content-Description", msg.BodyStructure.Description)  			entity, err := message.New(header, reader)  			if err != nil {  				// TODO: Do something with the error @@ -168,25 +175,15 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error {  				return  			} -			pipeout, pipein := io.Pipe() -			scanner := bufio.NewScanner(part.Body) -			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"), -				msg.Envelope.From[0].Name)) -			for scanner.Scan() { -				io.WriteString(pipein, fmt.Sprintf("> %s\n", scanner.Text())) -			} -			pipein.Close() -			pipeout.Close() +			buf := new(bytes.Buffer) +			buf.ReadFrom(part.Body) +			defaults["Original"] = buf.String()  			addTab()  		}) +		return nil  	} else { -		addTab() +		return addTab()  	} - -	return nil  }  func findPlaintext(bs *models.BodyStructure, diff --git a/commands/msg/unsubscribe.go b/commands/msg/unsubscribe.go index 15a9411..5ffec46 100644 --- a/commands/msg/unsubscribe.go +++ b/commands/msg/unsubscribe.go @@ -87,13 +87,17 @@ func unsubscribeMailto(aerc *widgets.Aerc, u *url.URL) error {  		"To":      u.Opaque,  		"Subject": u.Query().Get("subject"),  	} -	composer := widgets.NewComposer( +	composer, err := widgets.NewComposer(  		aerc,  		aerc.Config(),  		acct.AccountConfig(),  		acct.Worker(), +		"",  		defaults,  	) +	if err != nil { +		return err +	}  	composer.SetContents(strings.NewReader(u.Query().Get("body")))  	tab := aerc.NewTab(composer, "unsubscribe")  	composer.OnHeaderChange("Subject", func(subject string) { diff --git a/config/aerc.conf.in b/config/aerc.conf.in index ec89ff7..16e3da1 100644 --- a/config/aerc.conf.in +++ b/config/aerc.conf.in @@ -108,7 +108,7 @@ editor=  #  # Default header fields to display when composing a message. To display -# multiple headers in the same row, separate them with a pipe, e.g. "To|From".  +# multiple headers in the same row, separate them with a pipe, e.g. "To|From".  #  # Default: To|From,Subject  header-layout=To|From,Subject @@ -139,3 +139,23 @@ text/*=awk -f @SHAREDIR@/filters/plaintext  #  # Executed when a new email arrives in the selected folder  new-email= + +[templates] +# Templates are used to populate email bodies automatically. +# + +# The directories where the templates are stored. It takes a colon-separated +# list of directories. +# +# default: @SHAREDIR@/templates/ +template-dirs=@SHAREDIR@/templates/ + +# The template to be used for quoted replies. +# +# default: quoted_reply +quoted-reply=quoted_reply + +# The template to be used for forward as body. +# +# default: forward_as_body +forwards=forward_as_body diff --git a/config/config.go b/config/config.go index 133a7f4..f46af09 100644 --- a/config/config.go +++ b/config/config.go @@ -16,6 +16,8 @@ import (  	"github.com/gdamore/tcell"  	"github.com/go-ini/ini"  	"github.com/kyoh86/xdg" + +	"git.sr.ht/~sircmpwn/aerc/lib/templates"  )  type GeneralConfig struct { @@ -98,16 +100,23 @@ type TriggersConfig struct {  	ExecuteCommand func(command []string) error  } +type TemplateConfig struct { +	TemplateDirs []string +	QuotedReply  string `ini:"quoted-reply"` +	Forwards     string `ini:"forwards"` +} +  type AercConfig struct { -	Bindings BindingConfig -	Compose  ComposeConfig -	Ini      *ini.File       `ini:"-"` -	Accounts []AccountConfig `ini:"-"` -	Filters  []FilterConfig  `ini:"-"` -	Viewer   ViewerConfig    `ini:"-"` -	Triggers TriggersConfig  `ini:"-"` -	Ui       UIConfig -	General  GeneralConfig +	Bindings  BindingConfig +	Compose   ComposeConfig +	Ini       *ini.File       `ini:"-"` +	Accounts  []AccountConfig `ini:"-"` +	Filters   []FilterConfig  `ini:"-"` +	Viewer    ViewerConfig    `ini:"-"` +	Triggers  TriggersConfig  `ini:"-"` +	Ui        UIConfig +	General   GeneralConfig +	Templates TemplateConfig  }  // Input: TimestampFormat @@ -305,6 +314,23 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {  			return err  		}  	} +	if templatesSec, err := file.GetSection("templates"); err == nil { +		if err := templatesSec.MapTo(&config.Templates); err != nil { +			return err +		} +		templateDirs := templatesSec.Key("template-dirs").String() +		config.Templates.TemplateDirs = strings.Split(templateDirs, ":") +		for key, val := range templatesSec.KeysHash() { +			if key == "template-dirs" { +				continue +			} +			_, err := templates.ParseTemplateFromFile( +				val, config.Templates.TemplateDirs, templates.TestTemplateData()) +			if err != nil { +				return err +			} +		} +	}  	return nil  } diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd index f4f02f2..0cde160 100644 --- a/doc/aerc-config.5.scd +++ b/doc/aerc-config.5.scd @@ -240,6 +240,31 @@ They are configured in the *[triggers]* section of aerc.conf.  	Format specifiers from *index-format* are expanded with respect to the new  	message. +## Templates + +Templates are used to populate the body of an email. The compose, reply +and forward commands can be called with the -T flag with the name of the +template name. + +aerc ships with some default templates installed in the share directory (usually +_/usr/share/aerc/templates_). + +*template-dirs* +	The directory where the templates are stored. The config takes a +	colon-separated list of dirs. + +	Default: "/usr/share/aerc/templates" + +*quoted-reply* +	The template to be used for quoted replies. + +	Default: "quoted_reply" + +*forwards* +	The template to be used for forward as body. + +	Default: "forward_as_body" +  # ACCOUNTS.CONF  This file is used for configuring each mail account used for aerc. Each section diff --git a/doc/aerc-templates.7.scd b/doc/aerc-templates.7.scd new file mode 100644 index 0000000..8504a60 --- /dev/null +++ b/doc/aerc-templates.7.scd @@ -0,0 +1,89 @@ +aerc-templates(7) + +# NAME + +aerc-templates - template file specification for *aerc*(1) + +# SYNOPSIS + +aerc uses the go "text/template" package for the template parsing +which supports basic go lang operations. + +# MESSAGE DATA + +The following data can be used in templates. Though they are not all +available always. + +*Addresses* +	An array of mail.Address. That can be used to add sender or recipient +	names to the template. + +	- From: List of senders. +	- To: List of To recipients. Not always Available. +	- Cc: List of Cc recipients. Not always Available. +	- Bcc: List of Cc recipients. Not always Available. +	- OriginalFrom: List of senders of the original message. +	  Available for quoted reply and forward. + +	Example: + +	Get the name of the first sender. +	``` +	{{(index .From 0).Name}} +	``` + +	Get the email address of the first sender +	``` +	{{(index .From 0).Address}} +	``` + +*Date and Time* +	The date and time information is always available and can be easily +	formated. + +	- Date: Date and Time information when the compose window is opened. +	- OriginalDate: Date and Time when the original message of received. +	  Available for quoted reply and forward. + +	The _dateFormat_ function can be used to format the date and time. + +	Example: + +	Format the date to go's time package format options. +	``` +	{{dateFormat .Date "Mon Jan 2 15:04:05 -0700 MST 2006"}} +	``` + +*Subject* +	The subject of the email is available for quoted reply and forward. + +	Example: +	{{.Subject}} + +*Original Message* +	When using quoted reply or forward, the original message is available. +	It can be used using two functions that are available to templates. + +	Example: + +	_wrapText_ function can be used to wrap the original text to a number +	of characters per line. +	``` +	{{wrapText .OriginalText 72}} +	``` + +	_quote_ function prepends each line with "> " and wraps the text to +	72 characters pre line. +	``` +	{{quote .OriginalText}} +	``` + +# SEE ALSO + +*aerc*(1) *aerc-config*(5) + +# AUTHORS + +Maintained by Drew DeVault <sir@cmpwn.com>, who is assisted by other open +source contributors. For more information about aerc development, see +https://git.sr.ht/~sircmpwn/aerc. diff --git a/lib/templates/template.go b/lib/templates/template.go new file mode 100644 index 0000000..c09bf4d --- /dev/null +++ b/lib/templates/template.go @@ -0,0 +1,160 @@ +package templates + +import ( +	"bytes" +	"errors" +	"net/mail" +	"os" +	"path" +	"strings" +	"text/template" +	"time" + +	"github.com/mitchellh/go-homedir" +) + +type TemplateData struct { +	To      []*mail.Address +	Cc      []*mail.Address +	Bcc     []*mail.Address +	From    []*mail.Address +	Date    time.Time +	Subject string +	// Only available when replying with a quote +	OriginalText string +	OriginalFrom []*mail.Address +	OriginalDate time.Time +} + +func TestTemplateData() TemplateData { +	defaults := map[string]string{ +		"To":           "John Doe <john@example.com>", +		"Cc":           "Josh Doe <josh@example.com>", +		"From":         "Jane Smith <jane@example.com>", +		"Subject":      "This is only a test", +		"OriginalText": "This is only a test text", +		"OriginalFrom": "John Doe <john@example.com>", +		"OriginalDate": time.Now().Format("Mon Jan 2, 2006 at 3:04 PM"), +	} + +	return ParseTemplateData(defaults) +} + +func ParseTemplateData(defaults map[string]string) TemplateData { +	originalDate, _ := time.Parse("Mon Jan 2, 2006 at 3:04 PM", defaults["OriginalDate"]) +	td := TemplateData{ +		To:           parseAddressList(defaults["To"]), +		Cc:           parseAddressList(defaults["Cc"]), +		Bcc:          parseAddressList(defaults["Bcc"]), +		From:         parseAddressList(defaults["From"]), +		Date:         time.Now(), +		Subject:      defaults["Subject"], +		OriginalText: defaults["Original"], +		OriginalFrom: parseAddressList(defaults["OriginalFrom"]), +		OriginalDate: originalDate, +	} +	return td +} + +func parseAddressList(list string) []*mail.Address { +	addrs, err := mail.ParseAddressList(list) +	if err != nil { +		return nil +	} + +	return addrs +} + +func wrapLine(text string, lineWidth int) string { +	words := strings.Fields(text) +	if len(words) == 0 { +		return text +	} +	wrapped := words[0] +	spaceLeft := lineWidth - len(wrapped) +	for _, word := range words[1:] { +		if len(word)+1 > spaceLeft { +			wrapped += "\n" + word +			spaceLeft = lineWidth - len(word) +		} else { +			wrapped += " " + word +			spaceLeft -= 1 + len(word) +		} +	} + +	return wrapped +} + +func wrapText(text string, lineWidth int) string { +	text = strings.ReplaceAll(text, "\r\n", "\n") +	lines := strings.Split(text, "\n") +	var wrapped string + +	for _, line := range lines { +		wrapped += wrapLine(line, lineWidth) + "\n" +	} +	return wrapped +} + +// Wraping lines at 70 so that with the "> " of the quote it is under 72 +func quote(text string) string { +	text = strings.ReplaceAll(text, "\r\n", "\n") + +	quoted := "> " + wrapText(text, 70) +	quoted = strings.ReplaceAll(quoted, "\n", "\n> ") +	return quoted +} + +var templateFuncs = template.FuncMap{ +	"quote":      quote, +	"wrapText":   wrapText, +	"dateFormat": time.Time.Format, +} + +func findTemplate(templateName string, templateDirs []string) (string, error) { +	for _, dir := range templateDirs { +		templateFile, err := homedir.Expand(path.Join(dir, templateName)) +		if err != nil { +			return "", err +		} + +		if _, err := os.Stat(templateFile); os.IsNotExist(err) { +			continue +		} +		return templateFile, nil +	} + +	return "", errors.New("Can't find template - " + templateName) +} + +func ParseTemplateFromFile(templateName string, templateDirs []string, data interface{}) ([]byte, error) { +	templateFile, err := findTemplate(templateName, templateDirs) +	if err != nil { +		return nil, err +	} +	emailTemplate, err := +		template.New(templateName).Funcs(templateFuncs).ParseFiles(templateFile) +	if err != nil { +		return nil, err +	} + +	var outString bytes.Buffer +	if err := emailTemplate.Execute(&outString, data); err != nil { +		return nil, err +	} +	return outString.Bytes(), nil +} + +func ParseTemplate(templateText string, data interface{}) ([]byte, error) { +	emailTemplate, err := +		template.New("email_template").Funcs(templateFuncs).Parse(templateText) +	if err != nil { +		return nil, err +	} + +	var outString bytes.Buffer +	if err := emailTemplate.Execute(&outString, data); err != nil { +		return nil, err +	} +	return outString.Bytes(), nil +} diff --git a/templates/forward_as_body b/templates/forward_as_body new file mode 100644 index 0000000..a487224 --- /dev/null +++ b/templates/forward_as_body @@ -0,0 +1,2 @@ +Forwarded message from {{(index .OriginalFrom 0).Name}} on {{dateFormat .OriginalDate "Mon Jan 2, 2006 at 3:04 PM"}}: +{{wrapText .OriginalText 72}} diff --git a/templates/quoted_reply b/templates/quoted_reply new file mode 100644 index 0000000..ee4e1f7 --- /dev/null +++ b/templates/quoted_reply @@ -0,0 +1,2 @@ +on {{dateFormat .OriginalDate "Mon Jan 2, 2006 at 3:04 PM"}}, {{(index .OriginalFrom 0).Name}} wrote: +{{quote .OriginalText}} diff --git a/widgets/aerc.go b/widgets/aerc.go index af51a0f..d324908 100644 --- a/widgets/aerc.go +++ b/widgets/aerc.go @@ -431,8 +431,11 @@ func (aerc *Aerc) Mailto(addr *url.URL) error {  			defaults[header] = strings.Join(vals, ",")  		}  	} -	composer := NewComposer(aerc, aerc.Config(), -		acct.AccountConfig(), acct.Worker(), defaults) +	composer, err := NewComposer(aerc, aerc.Config(), +		acct.AccountConfig(), acct.Worker(), "", defaults) +	if err != nil { +		return nil +	}  	composer.FocusSubject()  	title := "New email"  	if subj, ok := defaults["Subject"]; ok { diff --git a/widgets/compose.go b/widgets/compose.go index 22c58da..a55d147 100644 --- a/widgets/compose.go +++ b/widgets/compose.go @@ -23,6 +23,7 @@ import (  	"github.com/pkg/errors"  	"git.sr.ht/~sircmpwn/aerc/config" +	"git.sr.ht/~sircmpwn/aerc/lib/templates"  	"git.sr.ht/~sircmpwn/aerc/lib/ui"  	"git.sr.ht/~sircmpwn/aerc/worker/types"  ) @@ -53,7 +54,7 @@ type Composer struct {  }  func NewComposer(aerc *Aerc, conf *config.AercConfig, -	acct *config.AccountConfig, worker *types.Worker, defaults map[string]string) *Composer { +	acct *config.AccountConfig, worker *types.Worker, template string, defaults map[string]string) (*Composer, error) {  	if defaults == nil {  		defaults = make(map[string]string) @@ -62,13 +63,14 @@ func NewComposer(aerc *Aerc, conf *config.AercConfig,  		defaults["From"] = acct.From  	} +	templateData := templates.ParseTemplateData(defaults)  	layout, editors, focusable := buildComposeHeader(  		conf.Compose.HeaderLayout, defaults)  	email, err := ioutil.TempFile("", "aerc-compose-*.eml")  	if err != nil {  		// TODO: handle this better -		return nil +		return nil, err  	}  	c := &Composer{ @@ -86,11 +88,14 @@ func NewComposer(aerc *Aerc, conf *config.AercConfig,  	}  	c.AddSignature() +	if err := c.AddTemplate(template, templateData); err != nil { +		return nil, err +	}  	c.updateGrid()  	c.ShowTerminal() -	return c +	return c, nil  }  func buildComposeHeader(layout HeaderLayout, defaults map[string]string) ( @@ -163,6 +168,32 @@ func (c *Composer) AppendContents(reader io.Reader) {  	c.email.Sync()  } +func (c *Composer) AddTemplate(template string, data interface{}) error { +	if template == "" { +		return nil +	} + +	templateText, err := templates.ParseTemplateFromFile(template, c.config.Templates.TemplateDirs, data) +	if err != nil { +		return err +	} +	c.PrependContents(bytes.NewReader(templateText)) +	return nil +} + +func (c *Composer) AddTemplateFromString(template string, data interface{}) error { +	if template == "" { +		return nil +	} + +	templateText, err := templates.ParseTemplate(template, data) +	if err != nil { +		return err +	} +	c.PrependContents(bytes.NewReader(templateText)) +	return nil +} +  func (c *Composer) AddSignature() {  	var signature []byte  	if c.acct.SignatureCmd != "" { | 
