Add Templates with Parsing

+ Changes NewComposer to return error.
+ Add lib to handle templates using "text/template".
+ Add -T option to following commands
    - compose.
    - reply
    - forward
+ Quoted replies using templates.
+ Forwards as body using templates
+ Default templates are installed similar to filters.
+ Templates Config in aerc.conf.
    - Required templates are parsed while loading config.
+ Add aerc-templates.7 manual for using template data.
This commit is contained in:
Srivathsan Murali 2019-11-03 13:51:14 +01:00 committed by Drew DeVault
parent ad68a9e4e4
commit 3ba69edab5
14 changed files with 510 additions and 143 deletions

View File

@ -35,7 +35,8 @@ DOCS := \
aerc-sendmail.5 \ aerc-sendmail.5 \
aerc-notmuch.5 \ aerc-notmuch.5 \
aerc-smtp.5 \ aerc-smtp.5 \
aerc-tutorial.7 aerc-tutorial.7 \
aerc-templates.7
.1.scd.1: .1.scd.1:
scdoc < $< > $@ scdoc < $< > $@
@ -58,7 +59,7 @@ clean:
install: all install: all
mkdir -p $(BINDIR) $(MANDIR)/man1 $(MANDIR)/man5 $(MANDIR)/man7 \ mkdir -p $(BINDIR) $(MANDIR)/man1 $(MANDIR)/man5 $(MANDIR)/man7 \
$(SHAREDIR) $(SHAREDIR)/filters $(SHAREDIR) $(SHAREDIR)/filters $(SHAREDIR)/templates
install -m755 aerc $(BINDIR)/aerc install -m755 aerc $(BINDIR)/aerc
install -m644 aerc.1 $(MANDIR)/man1/aerc.1 install -m644 aerc.1 $(MANDIR)/man1/aerc.1
install -m644 aerc-search.1 $(MANDIR)/man1/aerc-search.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/hldiff $(SHAREDIR)/filters/hldiff
install -m755 filters/html $(SHAREDIR)/filters/html install -m755 filters/html $(SHAREDIR)/filters/html
install -m755 filters/plaintext $(SHAREDIR)/filters/plaintext 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 '\ RMDIR_IF_EMPTY:=sh -c '\
if test -d $$0 && ! ls -1qA $$0 | grep -q . ; then \ if test -d $$0 && ! ls -1qA $$0 | grep -q . ; then \

View File

@ -24,13 +24,17 @@ func (Compose) Complete(aerc *widgets.Aerc, args []string) []string {
} }
func (Compose) Execute(aerc *widgets.Aerc, args []string) error { func (Compose) Execute(aerc *widgets.Aerc, args []string) error {
body, err := buildBody(args) body, template, err := buildBody(args)
if err != nil { if err != nil {
return err return err
} }
acct := aerc.SelectedAccount() 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") tab := aerc.NewTab(composer, "New email")
composer.OnHeaderChange("Subject", func(subject string) { composer.OnHeaderChange("Subject", func(subject string) {
if subject == "" { if subject == "" {
@ -44,11 +48,11 @@ func (Compose) Execute(aerc *widgets.Aerc, args []string) error {
return nil return nil
} }
func buildBody(args []string) (string, error) { func buildBody(args []string) (string, string, error) {
var body, headers string var body, template, headers string
opts, optind, err := getopt.Getopts(args, "H:") opts, optind, err := getopt.Getopts(args, "H:T:")
if err != nil { if err != nil {
return "", err return "", "", err
} }
for _, opt := range opts { for _, opt := range opts {
switch opt.Option { switch opt.Option {
@ -60,11 +64,13 @@ func buildBody(args []string) (string, error) {
} else { } else {
headers += opt.Value + ":\n" headers += opt.Value + ":\n"
} }
case 'T':
template = opt.Value
} }
} }
posargs := args[optind:] posargs := args[optind:]
if len(posargs) > 1 { if len(posargs) > 1 {
return "", errors.New("Usage: compose [-H] [body]") return "", template, errors.New("Usage: compose [-H] [body]")
} }
if len(posargs) == 1 { if len(posargs) == 1 {
body = posargs[0] body = posargs[0]
@ -76,5 +82,5 @@ func buildBody(args []string) (string, error) {
body = headers + "\n\n" body = headers + "\n\n"
} }
} }
return body, nil return body, template, nil
} }

View File

@ -1,20 +1,21 @@
package msg package msg
import ( import (
"bufio" "bytes"
"errors" "errors"
"fmt" "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"
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
"strings" "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{} 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 { 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 { if err != nil {
return err return err
} }
attach := false attach := false
template := ""
for _, opt := range opts { for _, opt := range opts {
switch opt.Option { switch opt.Option {
case 'A': case 'A':
attach = true attach = true
case 'T':
template = opt.Value
} }
} }
@ -69,10 +73,20 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {
"To": to, "To": to,
"Subject": subject, "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) tab := aerc.NewTab(composer, subject)
if to == "" { if to == "" {
composer.FocusRecipient() composer.FocusRecipient()
@ -87,83 +101,68 @@ func (forward) Execute(aerc *widgets.Aerc, args []string) error {
} }
tab.Content.Invalidate() tab.Content.Invalidate()
}) })
return composer, nil
} }
if attach { 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") tmpDir, err := ioutil.TempDir("", "aerc-tmp-attachment")
if err != nil { if err != nil {
// TODO: Do something with the error return err
addTab()
return
} }
tmpFileName := path.Join(tmpDir, tmpFileName := path.Join(tmpDir,
strings.ReplaceAll(fmt.Sprintf("%s.eml", msg.Envelope.Subject), "/", "-")) strings.ReplaceAll(fmt.Sprintf("%s.eml", msg.Envelope.Subject), "/", "-"))
tmpFile, err := os.Create(tmpFileName) store.FetchFull([]uint32{msg.Uid}, func(reader io.Reader) {
if err != nil { tmpFile, err := os.Create(tmpFileName)
println(err) if err != nil {
// TODO: Do something with the error println(err)
addTab() // TODO: Do something with the error
return addTab()
} return
}
defer tmpFile.Close() defer tmpFile.Close()
io.Copy(tmpFile, reader) io.Copy(tmpFile, reader)
composer.AddAttachment(tmpFileName) composer, err := addTab()
composer.OnClose(func(composer *widgets.Composer) { if err != nil {
os.RemoveAll(tmpDir) return
}
composer.AddAttachment(tmpFileName)
composer.OnClose(func(composer *widgets.Composer) {
os.RemoveAll(tmpDir)
})
}) })
addTab() } else {
}) if template == "" {
} template = aerc.Config().Templates.Forwards
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
} }
pipeout, pipein := io.Pipe() // TODO: something more intelligent than fetching the 1st part
scanner := bufio.NewScanner(part.Body) // TODO: add attachments!
go composer.PrependContents(pipeout) store.FetchBodyPart(msg.Uid, []int{1}, func(reader io.Reader) {
// TODO: Let user customize the date format used here header := message.Header{}
io.WriteString(pipein, fmt.Sprintf("Forwarded message from %s on %s:\n\n", header.SetText(
msg.Envelope.From[0].Name, "Content-Transfer-Encoding", msg.BodyStructure.Encoding)
msg.Envelope.Date.Format("Mon Jan 2, 2006 at 3:04 PM"))) header.SetContentType(
for scanner.Scan() { msg.BodyStructure.MIMEType, msg.BodyStructure.Params)
io.WriteString(pipein, fmt.Sprintf("%s\n", scanner.Text())) header.SetText("Content-Description", msg.BodyStructure.Description)
} entity, err := message.New(header, reader)
pipein.Close() if err != nil {
pipeout.Close() // TODO: Do something with the error
addTab() 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
} }

View File

@ -1,7 +1,7 @@
package msg package msg
import ( import (
"bufio" "bytes"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -32,16 +32,17 @@ func (reply) Complete(aerc *widgets.Aerc, args []string) []string {
} }
func (reply) Execute(aerc *widgets.Aerc, args []string) error { 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 { if err != nil {
return err return err
} }
if optind != len(args) { if optind != len(args) {
return errors.New("Usage: reply [-aq]") return errors.New("Usage: reply [-aq -T <template>]")
} }
var ( var (
quote bool quote bool
replyAll bool replyAll bool
template string
) )
for _, opt := range opts { for _, opt := range opts {
switch opt.Option { switch opt.Option {
@ -49,11 +50,14 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error {
replyAll = true replyAll = true
case 'q': case 'q':
quote = true quote = true
case 'T':
template = opt.Value
} }
} }
widget := aerc.SelectedTab().(widgets.ProvidesMessage) widget := aerc.SelectedTab().(widgets.ProvidesMessage)
acct := widget.SelectedAccount() acct := widget.SelectedAccount()
if acct == nil { if acct == nil {
return errors.New("No account selected") 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, "In-Reply-To": msg.Envelope.MessageId,
} }
composer := widgets.NewComposer(aerc, aerc.Config(), addTab := func() error {
acct.AccountConfig(), acct.Worker(), defaults) 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, err := widgets.NewComposer(aerc, aerc.Config(),
composer.FocusTerminal() 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) tab := aerc.NewTab(composer, subject)
composer.OnHeaderChange("Subject", func(subject string) { composer.OnHeaderChange("Subject", func(subject string) {
if subject == "" { if subject == "" {
@ -133,27 +146,21 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error {
} }
tab.Content.Invalidate() tab.Content.Invalidate()
}) })
return nil
} }
if quote { if quote {
var ( if template == "" {
path []int template = aerc.Config().Templates.QuotedReply
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) { store.FetchBodyPart(msg.Uid, []int{1}, func(reader io.Reader) {
header := message.Header{} header := message.Header{}
header.SetText( header.SetText(
"Content-Transfer-Encoding", part.Encoding) "Content-Transfer-Encoding", msg.BodyStructure.Encoding)
header.SetContentType(part.MIMEType, part.Params) header.SetContentType(msg.BodyStructure.MIMEType, msg.BodyStructure.Params)
header.SetText("Content-Description", part.Description) header.SetText("Content-Description", msg.BodyStructure.Description)
entity, err := message.New(header, reader) entity, err := message.New(header, reader)
if err != nil { if err != nil {
// TODO: Do something with the error // TODO: Do something with the error
@ -168,25 +175,15 @@ func (reply) Execute(aerc *widgets.Aerc, args []string) error {
return return
} }
pipeout, pipein := io.Pipe() buf := new(bytes.Buffer)
scanner := bufio.NewScanner(part.Body) buf.ReadFrom(part.Body)
go composer.PrependContents(pipeout) defaults["Original"] = buf.String()
// 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()
addTab() addTab()
}) })
return nil
} else { } else {
addTab() return addTab()
} }
return nil
} }
func findPlaintext(bs *models.BodyStructure, func findPlaintext(bs *models.BodyStructure,

View File

@ -87,13 +87,17 @@ func unsubscribeMailto(aerc *widgets.Aerc, u *url.URL) error {
"To": u.Opaque, "To": u.Opaque,
"Subject": u.Query().Get("subject"), "Subject": u.Query().Get("subject"),
} }
composer := widgets.NewComposer( composer, err := widgets.NewComposer(
aerc, aerc,
aerc.Config(), aerc.Config(),
acct.AccountConfig(), acct.AccountConfig(),
acct.Worker(), acct.Worker(),
"",
defaults, defaults,
) )
if err != nil {
return err
}
composer.SetContents(strings.NewReader(u.Query().Get("body"))) composer.SetContents(strings.NewReader(u.Query().Get("body")))
tab := aerc.NewTab(composer, "unsubscribe") tab := aerc.NewTab(composer, "unsubscribe")
composer.OnHeaderChange("Subject", func(subject string) { composer.OnHeaderChange("Subject", func(subject string) {

View File

@ -108,7 +108,7 @@ editor=
# #
# Default header fields to display when composing a message. To display # 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 # Default: To|From,Subject
header-layout=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 # Executed when a new email arrives in the selected folder
new-email= 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

View File

@ -16,6 +16,8 @@ import (
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
"github.com/go-ini/ini" "github.com/go-ini/ini"
"github.com/kyoh86/xdg" "github.com/kyoh86/xdg"
"git.sr.ht/~sircmpwn/aerc/lib/templates"
) )
type GeneralConfig struct { type GeneralConfig struct {
@ -98,16 +100,23 @@ type TriggersConfig struct {
ExecuteCommand func(command []string) error ExecuteCommand func(command []string) error
} }
type TemplateConfig struct {
TemplateDirs []string
QuotedReply string `ini:"quoted-reply"`
Forwards string `ini:"forwards"`
}
type AercConfig struct { type AercConfig struct {
Bindings BindingConfig Bindings BindingConfig
Compose ComposeConfig Compose ComposeConfig
Ini *ini.File `ini:"-"` Ini *ini.File `ini:"-"`
Accounts []AccountConfig `ini:"-"` Accounts []AccountConfig `ini:"-"`
Filters []FilterConfig `ini:"-"` Filters []FilterConfig `ini:"-"`
Viewer ViewerConfig `ini:"-"` Viewer ViewerConfig `ini:"-"`
Triggers TriggersConfig `ini:"-"` Triggers TriggersConfig `ini:"-"`
Ui UIConfig Ui UIConfig
General GeneralConfig General GeneralConfig
Templates TemplateConfig
} }
// Input: TimestampFormat // Input: TimestampFormat
@ -305,6 +314,23 @@ func (config *AercConfig) LoadConfig(file *ini.File) error {
return err 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 return nil
} }

View File

@ -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 Format specifiers from *index-format* are expanded with respect to the new
message. 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 # ACCOUNTS.CONF
This file is used for configuring each mail account used for aerc. Each section This file is used for configuring each mail account used for aerc. Each section

89
doc/aerc-templates.7.scd Normal file
View File

@ -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.

160
lib/templates/template.go Normal file
View File

@ -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
}

View File

@ -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}}

2
templates/quoted_reply Normal file
View File

@ -0,0 +1,2 @@
on {{dateFormat .OriginalDate "Mon Jan 2, 2006 at 3:04 PM"}}, {{(index .OriginalFrom 0).Name}} wrote:
{{quote .OriginalText}}

View File

@ -431,8 +431,11 @@ func (aerc *Aerc) Mailto(addr *url.URL) error {
defaults[header] = strings.Join(vals, ",") defaults[header] = strings.Join(vals, ",")
} }
} }
composer := NewComposer(aerc, aerc.Config(), composer, err := NewComposer(aerc, aerc.Config(),
acct.AccountConfig(), acct.Worker(), defaults) acct.AccountConfig(), acct.Worker(), "", defaults)
if err != nil {
return nil
}
composer.FocusSubject() composer.FocusSubject()
title := "New email" title := "New email"
if subj, ok := defaults["Subject"]; ok { if subj, ok := defaults["Subject"]; ok {

View File

@ -23,6 +23,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"git.sr.ht/~sircmpwn/aerc/config" "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/lib/ui"
"git.sr.ht/~sircmpwn/aerc/worker/types" "git.sr.ht/~sircmpwn/aerc/worker/types"
) )
@ -53,7 +54,7 @@ type Composer struct {
} }
func NewComposer(aerc *Aerc, conf *config.AercConfig, 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 { if defaults == nil {
defaults = make(map[string]string) defaults = make(map[string]string)
@ -62,13 +63,14 @@ func NewComposer(aerc *Aerc, conf *config.AercConfig,
defaults["From"] = acct.From defaults["From"] = acct.From
} }
templateData := templates.ParseTemplateData(defaults)
layout, editors, focusable := buildComposeHeader( layout, editors, focusable := buildComposeHeader(
conf.Compose.HeaderLayout, defaults) conf.Compose.HeaderLayout, defaults)
email, err := ioutil.TempFile("", "aerc-compose-*.eml") email, err := ioutil.TempFile("", "aerc-compose-*.eml")
if err != nil { if err != nil {
// TODO: handle this better // TODO: handle this better
return nil return nil, err
} }
c := &Composer{ c := &Composer{
@ -86,11 +88,14 @@ func NewComposer(aerc *Aerc, conf *config.AercConfig,
} }
c.AddSignature() c.AddSignature()
if err := c.AddTemplate(template, templateData); err != nil {
return nil, err
}
c.updateGrid() c.updateGrid()
c.ShowTerminal() c.ShowTerminal()
return c return c, nil
} }
func buildComposeHeader(layout HeaderLayout, defaults map[string]string) ( func buildComposeHeader(layout HeaderLayout, defaults map[string]string) (
@ -163,6 +168,32 @@ func (c *Composer) AppendContents(reader io.Reader) {
c.email.Sync() 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() { func (c *Composer) AddSignature() {
var signature []byte var signature []byte
if c.acct.SignatureCmd != "" { if c.acct.SignatureCmd != "" {