diff options
author | Srivathsan Murali <sri@vathsan.com> | 2019-11-03 13:51:14 +0100 |
---|---|---|
committer | Drew DeVault <sir@cmpwn.com> | 2019-11-10 10:15:49 -0500 |
commit | 3ba69edab5f0c787424dac9649e43a7743da13ca (patch) | |
tree | bccbdd4e1844cc89f011839f0d6557012a14d1d0 | |
parent | ad68a9e4e471eb708893ad16601ab14a4672a2da (diff) | |
download | aerc-3ba69edab5f0c787424dac9649e43a7743da13ca.tar.gz |
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.
-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 039eb923..24e460b3 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 494072d0..75701772 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 9ef7a3b8..b13e254a 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 15a9411c..5ffec465 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 ec89ff7e..16e3da11 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 133a7f4e..f46af09c 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 f4f02f2c..0cde160e 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 00000000..8504a60a --- /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 00000000..c09bf4d2 --- /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 00000000..a487224f --- /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 00000000..ee4e1f7e --- /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 af51a0f4..d324908b 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 22c58da5..a55d147f 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 != "" { |