aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/aerc-config.5.scd7
-rw-r--r--doc/aerc-templates.7.scd149
-rw-r--r--lib/templates/data.go321
-rw-r--r--lib/templates/functions.go100
-rw-r--r--lib/templates/template.go4
-rw-r--r--widgets/compose.go15
6 files changed, 556 insertions, 40 deletions
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index d48e38ad..dcaa3dd4 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -805,9 +805,10 @@ They are configured in the *[triggers]* section of _aerc.conf_.
# 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.
+Template files 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. The available symbols and functions are described in
+*aerc-templates*(7).
aerc ships with some default templates installed in the share directory (usually
_/usr/share/aerc/templates_).
diff --git a/doc/aerc-templates.7.scd b/doc/aerc-templates.7.scd
index 6c9e3190..67d5c112 100644
--- a/doc/aerc-templates.7.scd
+++ b/doc/aerc-templates.7.scd
@@ -8,6 +8,8 @@ aerc-templates - template file specification for *aerc*(1)
aerc uses the go text/template package for the template parsing.
Refer to the go text/template documentation for the general syntax.
+The template syntax described below can be used for message template files and
+for dynamic formatting of some UI widgets.
Template files are composed of headers, followed by a newline, followed by the
body text.
@@ -39,11 +41,14 @@ available always.
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.
+ - _{{.From}}_: List of senders.
+ - _{{.Peer}}_: List of senders or To recipients if the message is from
+ you.
+ - _{{.To}}_: List of To recipients. Not always Available.
+ - _{{.ReplyTo}}_: List of ReplyTo 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:
@@ -51,6 +56,7 @@ available always.
Get the name of the first sender.
```
{{(index .From 0).Name}}
+ {{index (.From | names) 0}}
```
Get the email address of the first sender.
@@ -62,22 +68,63 @@ available always.
The date and time information is always available and can be easily
formatted.
- - _Date_: Date and time information when the compose window is opened.
- - _OriginalDate_: Date and time when the original message of received.
+ - _{{.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.
- To format the date fields, _dateFormat_ and _toLocal_ are provided.
+ To format the date fields, _dateFormat_ and _.Local_ are provided.
Refer to the *TEMPLATE FUNCTIONS* section for details.
*Subject*
- The subject of the email is available for quoted reply and forward.
+ The subject of the email.
+ ```
{{.Subject}}
+ ```
+
+*Flags*
+ List of message flags, not available when composing, replying nor
+ forwarding. This is a list of strings that may be converted to a single
+ string with *join*.
+
+ ```
+ {{.Flags | join ""}}
+ ```
+
+*Labels*
+ Message labels (for example notmuch tags). Not available when composing,
+ replying nor forwarding. This is a list of strings that may be converted
+ to a single string with *join*.
+
+ ```
+ {{.Labels | join " "}}
+ ```
+
+*Size*
+ The size of the message in bytes. Not available when composing, replying
+ nor forwarding. It can be formatted with *humanReadable*.
+
+ ```
+ {{.Size | humanReadable}}
+ ```
+
+*Any header value*
+ Any header value of the email.
+
+ ```
+ {{.Header "x-foo-bar"}}
+ ```
+
+ Any header values of the original forwared or replied message:
+
+ ```
+ {{.OriginalHeader "x-foo-bar"}}
+ ```
*MIME Type*
MIME Type is available for quoted reply and forward.
- - _OriginalMIMEType_: MIME type info of quoted mail part. Usually
+ - _{{.OriginalMIMEType}}_: MIME type info of quoted mail part. Usually
_text/plain_ or _text/html_.
*Original Message*
@@ -88,6 +135,19 @@ available always.
{{.OriginalText}}
```
+*Account info*
+ The current account name:
+
+ ```
+ {{.Account}}
+ ```
+
+ Currently selected mailbox folder:
+
+ ```
+ {{.Folder}}
+ ```
+
# TEMPLATE FUNCTIONS
Besides the standard functions described in go's text/template documentation,
@@ -107,6 +167,49 @@ aerc provides the following additional functions:
{{quote .OriginalText}}
```
+*join*
+ Join the provided list of strings with a separator:
+
+ ```
+ {{.To | names | join ", "}}
+ ```
+
+*names*
+ Extracts the names part from a mail.Address list. If there is no name
+ available, the email address is returned instead.
+
+ ```
+ {{.To | names | join ", "}}
+ {{index (.To | names) 0}}
+ ```
+
+*emails*
+ Extracts the addresses part from a mail.Address list.
+
+ ```
+ {{.To | emails | join ", "}}
+ {{index (.To | emails) 0}}
+ ```
+
+*mboxes*
+ Extracts the mbox part from a mail.Address list (i.e. _smith_ from
+ _smith@example.com_).
+
+ ```
+ {{.To | mboxes | join ", "}}
+ {{index (.To | mboxes) 0}}
+ ```
+
+*persons*
+ Formats a list of mail.Address into a list of strings containing the
+ human readable form of RFC5322 (e.g. _Firstname Lastname
+ <email@address.tld>_).
+
+ ```
+ {{.To | persons | join ", "}}
+ {{index (.To | persons) 0}}
+ ```
+
*exec*
Execute external command, provide the second argument to its stdin.
@@ -114,11 +217,11 @@ aerc provides the following additional functions:
{{exec `/usr/local/share/aerc/filters/html` .OriginalText}}
```
-*toLocal*
+*.Local*
Convert the date to the local timezone as specified by the locale.
```
- {{toLocal .Date}}
+ {{.Date.Local}}
```
*dateFormat*
@@ -129,6 +232,28 @@ aerc provides the following additional functions:
{{dateFormat .Date "Mon Jan 2 15:04:05 -0700 MST 2006"}}
```
+ You can also use the _.DateAutoFormat_ method to format the date
+ according to *\*-time\*format* settings:
+
+ ```
+ {{.DateAutoFormat .OriginalDate.Local}}
+ ```
+
+*humanReadable*
+ Return the human readable form of an integer value.
+
+ ```
+ {{humanReadable 3217653721}}
+ ```
+
+*cwd*
+ Return the current working directory with the user home dir replaced by
+ _~_.
+
+ ```
+ {{cwd}}
+ ```
+
*version*
Returns the version of aerc, which can be useful for things like X-Mailer.
diff --git a/lib/templates/data.go b/lib/templates/data.go
index 8637aa7f..dab698d5 100644
--- a/lib/templates/data.go
+++ b/lib/templates/data.go
@@ -1,59 +1,289 @@
package templates
import (
+ "fmt"
+ "strings"
"time"
"git.sr.ht/~rjarry/aerc/models"
+ sortthread "github.com/emersion/go-imap-sortthread"
"github.com/emersion/go-message/mail"
)
type TemplateData struct {
- msg *mail.Header
- // Only available when replying with a quote
+ // only available when composing/replying/forwarding
+ headers *mail.Header
+ // only available when replying with a quote
parent *models.OriginalMail
+ // only available for the message list
+ info *models.MessageInfo
+ marked bool
+ msgNum int
+
+ // account config
+ myAddresses map[string]bool
+ account string
+ folder string // selected folder name
+
+ // ui config
+ timeFmt string
+ thisDayTimeFmt string
+ thisWeekTimeFmt string
+ thisYearTimeFmt string
+ iconAttachment string
}
func NewTemplateData(
- msg *mail.Header, parent *models.OriginalMail,
+ from *mail.Address,
+ aliases []*mail.Address,
+ account string,
+ folder string,
+ timeFmt string,
+ thisDayTimeFmt string,
+ thisWeekTimeFmt string,
+ thisYearTimeFmt string,
+ iconAttachment string,
) *TemplateData {
+ myAddresses := map[string]bool{from.Address: true}
+ for _, addr := range aliases {
+ myAddresses[addr.Address] = true
+ }
return &TemplateData{
- msg: msg,
- parent: parent,
+ myAddresses: myAddresses,
+ account: account,
+ folder: folder,
+ timeFmt: timeFmt,
+ thisDayTimeFmt: thisDayTimeFmt,
+ thisWeekTimeFmt: thisWeekTimeFmt,
+ thisYearTimeFmt: thisYearTimeFmt,
+ iconAttachment: iconAttachment,
}
}
+// only used for compose/reply/forward
+func (d *TemplateData) SetHeaders(h *mail.Header, o *models.OriginalMail) {
+ d.headers = h
+ d.parent = o
+}
+
+// only used for message list templates
+func (d *TemplateData) SetInfo(info *models.MessageInfo, num int, marked bool) {
+ d.info = info
+ d.msgNum = num
+ d.marked = marked
+}
+
+func (d *TemplateData) Account() string {
+ return d.account
+}
+
+func (d *TemplateData) Folder() string {
+ return d.folder
+}
+
func (d *TemplateData) To() []*mail.Address {
- to, _ := d.msg.AddressList("to")
+ var to []*mail.Address
+ switch {
+ case d.info != nil && d.info.Envelope != nil:
+ to = d.info.Envelope.To
+ case d.headers != nil:
+ to, _ = d.headers.AddressList("to")
+ }
return to
}
func (d *TemplateData) Cc() []*mail.Address {
- to, _ := d.msg.AddressList("cc")
- return to
+ var cc []*mail.Address
+ switch {
+ case d.info != nil && d.info.Envelope != nil:
+ cc = d.info.Envelope.Cc
+ case d.headers != nil:
+ cc, _ = d.headers.AddressList("cc")
+ }
+ return cc
}
func (d *TemplateData) Bcc() []*mail.Address {
- to, _ := d.msg.AddressList("bcc")
- return to
+ var bcc []*mail.Address
+ switch {
+ case d.info != nil && d.info.Envelope != nil:
+ bcc = d.info.Envelope.Bcc
+ case d.headers != nil:
+ bcc, _ = d.headers.AddressList("bcc")
+ }
+ return bcc
}
func (d *TemplateData) From() []*mail.Address {
- to, _ := d.msg.AddressList("from")
- return to
+ var from []*mail.Address
+ switch {
+ case d.info != nil && d.info.Envelope != nil:
+ from = d.info.Envelope.From
+ case d.headers != nil:
+ from, _ = d.headers.AddressList("from")
+ }
+ return from
+}
+
+func (d *TemplateData) Peer() []*mail.Address {
+ var from, to []*mail.Address
+ switch {
+ case d.info != nil && d.info.Envelope != nil:
+ from = d.info.Envelope.From
+ to = d.info.Envelope.To
+ case d.headers != nil:
+ from, _ = d.headers.AddressList("from")
+ to, _ = d.headers.AddressList("to")
+ }
+ for _, addr := range from {
+ if d.myAddresses[addr.Address] {
+ return to
+ }
+ }
+ return from
+}
+
+func (d *TemplateData) ReplyTo() []*mail.Address {
+ var replyTo []*mail.Address
+ switch {
+ case d.info != nil && d.info.Envelope != nil:
+ replyTo = d.info.Envelope.ReplyTo
+ case d.headers != nil:
+ replyTo, _ = d.headers.AddressList("reply-to")
+ }
+ return replyTo
}
func (d *TemplateData) Date() time.Time {
- return time.Now()
+ var date time.Time
+ switch {
+ case d.info != nil && d.info.Envelope != nil:
+ date = d.info.Envelope.Date
+ case d.info != nil:
+ date = d.info.InternalDate
+ default:
+ date = time.Now()
+ }
+ return date
}
-func (d *TemplateData) Subject() string {
- subject, err := d.msg.Text("subject")
+func (d *TemplateData) DateAutoFormat(date time.Time) string {
+ if date.IsZero() {
+ return ""
+ }
+ year := date.Year()
+ day := date.YearDay()
+ now := time.Now()
+ thisYear := now.Year()
+ thisDay := now.YearDay()
+ fmt := d.timeFmt
+ if year == thisYear {
+ switch {
+ case day == thisDay && d.thisDayTimeFmt != "":
+ fmt = d.thisDayTimeFmt
+ case day > thisDay-7 && d.thisWeekTimeFmt != "":
+ fmt = d.thisDayTimeFmt
+ case d.thisYearTimeFmt != "":
+ fmt = d.thisYearTimeFmt
+ }
+ }
+ return date.Format(fmt)
+}
+
+func (d *TemplateData) Header(name string) string {
+ var h *mail.Header
+ switch {
+ case d.headers != nil:
+ h = d.headers
+ case d.info != nil && d.info.RFC822Headers != nil:
+ h = d.info.RFC822Headers
+ default:
+ return ""
+ }
+ text, err := h.Text(name)
if err != nil {
- subject = d.msg.Get("subject")
+ text = h.Get(name)
+ }
+ return text
+}
+
+func (d *TemplateData) Subject() string {
+ var subject string
+ switch {
+ case d.info != nil && d.info.Envelope != nil:
+ subject = d.info.Envelope.Subject
+ case d.headers != nil:
+ subject = d.Header("subject")
}
return subject
}
+func (d *TemplateData) SubjectBase() string {
+ base, _ := sortthread.GetBaseSubject(d.Subject())
+ return base
+}
+
+func (d *TemplateData) Number() string {
+ return fmt.Sprintf("%d", d.msgNum)
+}
+
+func (d *TemplateData) Labels() []string {
+ if d.info == nil {
+ return nil
+ }
+ return d.info.Labels
+}
+
+func (d *TemplateData) Flags() []string {
+ var flags []string
+ if d.info == nil {
+ return flags
+ }
+
+ switch {
+ case d.info.Flags.Has(models.SeenFlag | models.AnsweredFlag):
+ flags = append(flags, "r") // message has been replied to
+ case d.info.Flags.Has(models.SeenFlag):
+ break
+ case d.info.Flags.Has(models.RecentFlag):
+ flags = append(flags, "N") // message is new
+ default:
+ flags = append(flags, "O") // message is old
+ }
+ if d.info.Flags.Has(models.DeletedFlag) {
+ flags = append(flags, "D")
+ }
+ if d.info.BodyStructure != nil {
+ for _, bS := range d.info.BodyStructure.Parts {
+ if strings.ToLower(bS.Disposition) == "attachment" {
+ flags = append(flags, d.iconAttachment)
+ break
+ }
+ }
+ }
+ if d.info.Flags.Has(models.FlaggedFlag) {
+ flags = append(flags, "!")
+ }
+ if d.marked {
+ flags = append(flags, "*")
+ }
+ return flags
+}
+
+func (d *TemplateData) MessageId() string {
+ if d.info == nil || d.info.Envelope == nil {
+ return ""
+ }
+ return d.info.Envelope.MessageId
+}
+
+func (d *TemplateData) Size() uint32 {
+ if d.info == nil || d.info.Envelope == nil {
+ return 0
+ }
+ return d.info.Size
+}
+
func (d *TemplateData) OriginalText() string {
if d.parent == nil {
return ""
@@ -83,6 +313,17 @@ func (d *TemplateData) OriginalMIMEType() string {
return d.parent.MIMEType
}
+func (d *TemplateData) OriginalHeader(name string) string {
+ if d.parent == nil || d.parent.RFC822Headers == nil {
+ return ""
+ }
+ text, err := d.parent.RFC822Headers.Text(name)
+ if err != nil {
+ text = d.parent.RFC822Headers.Get(name)
+ }
+ return text
+}
+
// DummyData provides dummy data to test template validity
func DummyData() *TemplateData {
from := &mail.Address{
@@ -108,5 +349,51 @@ func DummyData() *TemplateData {
MIMEType: "text/plain",
RFC822Headers: oh,
}
- return NewTemplateData(h, &original)
+ data := NewTemplateData(
+ to,
+ nil,
+ "account",
+ "folder",
+ "2006 Jan 02, 15:04 GMT-0700",
+ "15:04",
+ "Monday 15:04",
+ "Jan 02",
+ "a",
+ )
+ data.SetHeaders(h, &original)
+
+ info := &models.MessageInfo{
+ BodyStructure: &models.BodyStructure{
+ MIMEType: "text",
+ MIMESubType: "plain",
+ Params: make(map[string]string),
+ Description: "",
+ Encoding: "",
+ Parts: []*models.BodyStructure{},
+ Disposition: "",
+ DispositionParams: make(map[string]string),
+ },
+ Envelope: &models.Envelope{
+ Date: time.Date(1981, 6, 23, 16, 52, 0, 0, time.UTC),
+ Subject: "[PATCH aerc 2/3] foo: baz bar buz",
+ From: []*mail.Address{from},
+ ReplyTo: []*mail.Address{},
+ To: []*mail.Address{to},
+ Cc: []*mail.Address{},
+ Bcc: []*mail.Address{},
+ MessageId: "",
+ InReplyTo: "",
+ },
+ Flags: models.FlaggedFlag,
+ Labels: []string{"inbox", "patch"},
+ InternalDate: time.Now(),
+ RFC822Headers: nil,
+ Refs: []string{},
+ Size: 65512,
+ Uid: 12345,
+ Error: nil,
+ }
+ data.SetInfo(info, 42, true)
+
+ return data
}
diff --git a/lib/templates/functions.go b/lib/templates/functions.go
index 41c899c8..2e551594 100644
--- a/lib/templates/functions.go
+++ b/lib/templates/functions.go
@@ -2,10 +2,15 @@ package templates
import (
"bytes"
+ "fmt"
+ "os"
"os/exec"
"strings"
"text/template"
"time"
+
+ "git.sr.ht/~rjarry/aerc/lib/format"
+ "github.com/emersion/go-message/mail"
)
var version string
@@ -102,12 +107,93 @@ func toLocal(t time.Time) time.Time {
return time.Time.In(t, time.Local)
}
+func names(addresses []*mail.Address) []string {
+ n := make([]string, len(addresses))
+ for i, addr := range addresses {
+ name := addr.Name
+ if name == "" {
+ name = addr.Address
+ }
+ n[i] = name
+ }
+ return n
+}
+
+func emails(addresses []*mail.Address) []string {
+ e := make([]string, len(addresses))
+ for i, addr := range addresses {
+ e[i] = addr.Address
+ }
+ return e
+}
+
+func mboxes(addresses []*mail.Address) []string {
+ e := make([]string, len(addresses))
+ for i, addr := range addresses {
+ parts := strings.SplitN(addr.Address, "@", 1)
+ e[i] = parts[0]
+ }
+ return e
+}
+
+func persons(addresses []*mail.Address) []string {
+ e := make([]string, len(addresses))
+ for i, addr := range addresses {
+ e[i] = format.AddressForHumans(addr)
+ }
+ return e
+}
+
+var units = []string{"K", "M", "G", "T"}
+
+func humanReadable(value uint32) string {
+ if value < 1000 {
+ return fmt.Sprintf("%d", value)
+ }
+ val := float64(value)
+ unit := ""
+ for i := 0; val >= 1000 && i < len(units); i++ {
+ unit = units[i]
+ val /= 1000.0
+ }
+ if val < 100.0 {
+ return fmt.Sprintf("%.1f%s", val, unit)
+ }
+ return fmt.Sprintf("%.0f%s", val, unit)
+}
+
+func cwd() string {
+ path, err := os.Getwd()
+ if err != nil {
+ return err.Error()
+ }
+ home, err := os.UserHomeDir()
+ if err != nil {
+ return err.Error()
+ }
+ if strings.HasPrefix(path, home) {
+ path = strings.Replace(path, home, "~", 1)
+ }
+ return path
+}
+
+func join(sep string, elems []string) string {
+ return strings.Join(elems, sep)
+}
+
var templateFuncs = template.FuncMap{
- "quote": quote,
- "wrapText": wrapText,
- "wrap": wrap,
- "dateFormat": time.Time.Format,
- "toLocal": toLocal,
- "exec": cmd,
- "version": func() string { return version },
+ "quote": quote,
+ "wrapText": wrapText,
+ "wrap": wrap,
+ "dateFormat": time.Time.Format,
+ "toLocal": toLocal,
+ "exec": cmd,
+ "version": func() string { return version },
+ "names": names,
+ "emails": emails,
+ "mboxes": mboxes,
+ "persons": persons,
+ "humanReadable": humanReadable,
+ "cwd": cwd,
+ "join": join,
}
diff --git a/lib/templates/template.go b/lib/templates/template.go
index baf7c2cf..28785356 100644
--- a/lib/templates/template.go
+++ b/lib/templates/template.go
@@ -46,6 +46,10 @@ func ParseTemplateFromFile(templateName string, templateDirs []string, data inte
return &body, nil
}
+func ParseTemplate(name, content string) (*template.Template, error) {
+ return template.New(name).Funcs(templateFuncs).Parse(content)
+}
+
func CheckTemplate(templateName string, templateDirs []string) error {
if templateName != "" {
_, err := ParseTemplateFromFile(templateName, templateDirs, DummyData())
diff --git a/widgets/compose.go b/widgets/compose.go
index c59fa8ef..578c761c 100644
--- a/widgets/compose.go
+++ b/widgets/compose.go
@@ -93,7 +93,20 @@ func NewComposer(
completer: nil,
}
- templateData := templates.NewTemplateData(h, orig)
+ uiConfig := acct.UiConfig()
+
+ templateData := templates.NewTemplateData(
+ acct.acct.From,
+ acct.acct.Aliases,
+ acct.Name(),
+ acct.Directories().Selected(),
+ uiConfig.MessageViewTimestampFormat,
+ uiConfig.MessageViewThisDayTimeFormat,
+ uiConfig.MessageViewThisWeekTimeFormat,
+ uiConfig.MessageViewThisYearTimeFormat,
+ uiConfig.IconAttachment,
+ )
+ templateData.SetHeaders(h, orig)
if err := c.AddTemplate(template, templateData); err != nil {
return nil, err
}