diff options
-rw-r--r-- | doc/aerc-config.5.scd | 7 | ||||
-rw-r--r-- | doc/aerc-templates.7.scd | 149 | ||||
-rw-r--r-- | lib/templates/data.go | 321 | ||||
-rw-r--r-- | lib/templates/functions.go | 100 | ||||
-rw-r--r-- | lib/templates/template.go | 4 | ||||
-rw-r--r-- | widgets/compose.go | 15 |
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 } |