2019-11-03 13:51:14 +01:00
|
|
|
package templates
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2020-11-11 13:57:42 +01:00
|
|
|
"fmt"
|
2020-04-23 20:55:18 +02:00
|
|
|
"io"
|
2019-11-03 13:51:14 +01:00
|
|
|
"os"
|
2020-01-08 21:44:18 +01:00
|
|
|
"os/exec"
|
2019-11-03 13:51:14 +01:00
|
|
|
"path"
|
|
|
|
"strings"
|
|
|
|
"text/template"
|
|
|
|
"time"
|
|
|
|
|
2020-11-10 20:27:30 +01:00
|
|
|
"github.com/emersion/go-message/mail"
|
|
|
|
|
2021-11-05 10:19:46 +01:00
|
|
|
"git.sr.ht/~rjarry/aerc/models"
|
2019-11-03 13:51:14 +01:00
|
|
|
"github.com/mitchellh/go-homedir"
|
|
|
|
)
|
|
|
|
|
2020-05-02 14:06:02 +02:00
|
|
|
var version string
|
|
|
|
|
2022-07-31 22:16:40 +02:00
|
|
|
// SetVersion initializes the aerc version displayed in template functions
|
2020-05-02 14:06:02 +02:00
|
|
|
func SetVersion(v string) {
|
|
|
|
version = v
|
|
|
|
}
|
|
|
|
|
2019-11-03 13:51:14 +01:00
|
|
|
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
|
2020-01-08 21:44:16 +01:00
|
|
|
OriginalText string
|
|
|
|
OriginalFrom []*mail.Address
|
|
|
|
OriginalDate time.Time
|
|
|
|
OriginalMIMEType string
|
2019-11-03 13:51:14 +01:00
|
|
|
}
|
|
|
|
|
2020-11-10 20:27:30 +01:00
|
|
|
func ParseTemplateData(h *mail.Header, original models.OriginalMail) TemplateData {
|
|
|
|
// we ignore errors as this shouldn't fail the sending / replying even if
|
|
|
|
// something is wrong with the message we reply to
|
|
|
|
to, _ := h.AddressList("to")
|
|
|
|
cc, _ := h.AddressList("cc")
|
|
|
|
bcc, _ := h.AddressList("bcc")
|
|
|
|
from, _ := h.AddressList("from")
|
|
|
|
subject, err := h.Text("subject")
|
|
|
|
if err != nil {
|
|
|
|
subject = h.Get("subject")
|
2020-01-08 21:44:14 +01:00
|
|
|
}
|
|
|
|
|
2019-11-03 13:51:14 +01:00
|
|
|
td := TemplateData{
|
2020-11-10 20:27:30 +01:00
|
|
|
To: to,
|
|
|
|
Cc: cc,
|
|
|
|
Bcc: bcc,
|
|
|
|
From: from,
|
2020-01-08 21:44:16 +01:00
|
|
|
Date: time.Now(),
|
2020-11-10 20:27:30 +01:00
|
|
|
Subject: subject,
|
2020-01-08 21:44:16 +01:00
|
|
|
OriginalText: original.Text,
|
2020-06-26 09:25:53 +02:00
|
|
|
OriginalDate: original.Date,
|
2020-01-08 21:44:16 +01:00
|
|
|
OriginalMIMEType: original.MIMEType,
|
2019-11-03 13:51:14 +01:00
|
|
|
}
|
2020-11-10 20:27:30 +01:00
|
|
|
if original.RFC822Headers != nil {
|
|
|
|
origFrom, _ := original.RFC822Headers.AddressList("from")
|
|
|
|
td.OriginalFrom = origFrom
|
2019-11-03 13:51:14 +01:00
|
|
|
}
|
2020-11-10 20:27:30 +01:00
|
|
|
return td
|
2019-11-03 13:51:14 +01:00
|
|
|
}
|
|
|
|
|
2020-01-08 21:44:18 +01:00
|
|
|
// wrap allows to chain wrapText
|
|
|
|
func wrap(lineWidth int, text string) string {
|
|
|
|
return wrapText(text, lineWidth)
|
|
|
|
}
|
|
|
|
|
2019-11-03 13:51:14 +01:00
|
|
|
func wrapLine(text string, lineWidth int) string {
|
|
|
|
words := strings.Fields(text)
|
|
|
|
if len(words) == 0 {
|
|
|
|
return text
|
|
|
|
}
|
2019-11-10 19:35:09 +01:00
|
|
|
var wrapped strings.Builder
|
|
|
|
wrapped.WriteString(words[0])
|
|
|
|
spaceLeft := lineWidth - wrapped.Len()
|
2019-11-03 13:51:14 +01:00
|
|
|
for _, word := range words[1:] {
|
|
|
|
if len(word)+1 > spaceLeft {
|
2019-11-10 19:35:09 +01:00
|
|
|
wrapped.WriteRune('\n')
|
|
|
|
wrapped.WriteString(word)
|
2019-11-03 13:51:14 +01:00
|
|
|
spaceLeft = lineWidth - len(word)
|
|
|
|
} else {
|
2019-11-10 19:35:09 +01:00
|
|
|
wrapped.WriteRune(' ')
|
|
|
|
wrapped.WriteString(word)
|
2019-11-03 13:51:14 +01:00
|
|
|
spaceLeft -= 1 + len(word)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-10 19:35:09 +01:00
|
|
|
return wrapped.String()
|
2019-11-03 13:51:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func wrapText(text string, lineWidth int) string {
|
|
|
|
text = strings.ReplaceAll(text, "\r\n", "\n")
|
2020-01-08 21:44:17 +01:00
|
|
|
text = strings.TrimRight(text, "\n")
|
2019-11-03 13:51:14 +01:00
|
|
|
lines := strings.Split(text, "\n")
|
2019-11-10 19:35:09 +01:00
|
|
|
var wrapped strings.Builder
|
2019-11-03 13:51:14 +01:00
|
|
|
|
|
|
|
for _, line := range lines {
|
2019-11-10 19:35:09 +01:00
|
|
|
switch {
|
|
|
|
case line == "":
|
|
|
|
// deliberately left blank
|
|
|
|
case line[0] == '>':
|
|
|
|
// leave quoted text alone
|
|
|
|
wrapped.WriteString(line)
|
|
|
|
default:
|
|
|
|
wrapped.WriteString(wrapLine(line, lineWidth))
|
|
|
|
}
|
|
|
|
wrapped.WriteRune('\n')
|
2019-11-03 13:51:14 +01:00
|
|
|
}
|
2019-11-10 19:35:09 +01:00
|
|
|
return wrapped.String()
|
2019-11-03 13:51:14 +01:00
|
|
|
}
|
|
|
|
|
2019-11-10 19:35:09 +01:00
|
|
|
// quote prepends "> " in front of every line in text
|
2019-11-03 13:51:14 +01:00
|
|
|
func quote(text string) string {
|
|
|
|
text = strings.ReplaceAll(text, "\r\n", "\n")
|
2020-01-08 21:44:17 +01:00
|
|
|
text = strings.TrimRight(text, "\n")
|
2019-11-10 19:35:09 +01:00
|
|
|
lines := strings.Split(text, "\n")
|
|
|
|
var quoted strings.Builder
|
|
|
|
for _, line := range lines {
|
|
|
|
if line == "" {
|
|
|
|
quoted.WriteString(">\n")
|
2019-12-10 11:54:53 +01:00
|
|
|
continue
|
2019-11-10 19:35:09 +01:00
|
|
|
}
|
|
|
|
quoted.WriteString("> ")
|
|
|
|
quoted.WriteString(line)
|
|
|
|
quoted.WriteRune('\n')
|
|
|
|
}
|
2019-11-03 13:51:14 +01:00
|
|
|
|
2019-11-10 19:35:09 +01:00
|
|
|
return quoted.String()
|
2019-11-03 13:51:14 +01:00
|
|
|
}
|
|
|
|
|
2020-01-08 21:44:18 +01:00
|
|
|
// cmd allow to parse reply by shell command
|
|
|
|
// text have to be passed by cmd param
|
|
|
|
// if there is error, original string is returned
|
|
|
|
func cmd(cmd, text string) string {
|
|
|
|
var out bytes.Buffer
|
|
|
|
c := exec.Command("sh", "-c", cmd)
|
|
|
|
c.Stdin = strings.NewReader(text)
|
|
|
|
c.Stdout = &out
|
|
|
|
err := c.Run()
|
|
|
|
if err != nil {
|
|
|
|
return text
|
|
|
|
}
|
|
|
|
return out.String()
|
|
|
|
}
|
|
|
|
|
2020-02-25 00:38:20 +01:00
|
|
|
func toLocal(t time.Time) time.Time {
|
|
|
|
return time.Time.In(t, time.Local)
|
|
|
|
}
|
|
|
|
|
2019-11-03 13:51:14 +01:00
|
|
|
var templateFuncs = template.FuncMap{
|
|
|
|
"quote": quote,
|
|
|
|
"wrapText": wrapText,
|
2020-01-08 21:44:18 +01:00
|
|
|
"wrap": wrap,
|
2019-11-03 13:51:14 +01:00
|
|
|
"dateFormat": time.Time.Format,
|
2020-02-25 00:38:20 +01:00
|
|
|
"toLocal": toLocal,
|
2020-01-08 21:44:18 +01:00
|
|
|
"exec": cmd,
|
2020-05-02 14:06:02 +02:00
|
|
|
"version": func() string { return version },
|
2019-11-03 13:51:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-11-11 13:57:42 +01:00
|
|
|
return "", fmt.Errorf(
|
|
|
|
"Can't find template %q in any of %v ", templateName, templateDirs)
|
2019-11-03 13:51:14 +01:00
|
|
|
}
|
|
|
|
|
2022-07-31 22:16:40 +02:00
|
|
|
// DummyData provides dummy data to test template validity
|
2020-11-10 20:27:30 +01:00
|
|
|
func DummyData() interface{} {
|
|
|
|
from := &mail.Address{
|
|
|
|
Name: "John Doe",
|
|
|
|
Address: "john@example.com",
|
|
|
|
}
|
|
|
|
to := &mail.Address{
|
|
|
|
Name: "Alice Doe",
|
|
|
|
Address: "alice@example.com",
|
|
|
|
}
|
|
|
|
h := &mail.Header{}
|
|
|
|
h.SetAddressList("from", []*mail.Address{from})
|
|
|
|
h.SetAddressList("to", []*mail.Address{to})
|
|
|
|
|
|
|
|
oh := &mail.Header{}
|
|
|
|
oh.SetAddressList("from", []*mail.Address{to})
|
|
|
|
oh.SetAddressList("to", []*mail.Address{from})
|
|
|
|
|
|
|
|
original := models.OriginalMail{
|
|
|
|
Date: time.Now(),
|
|
|
|
From: from.String(),
|
|
|
|
Text: "This is only a test text",
|
|
|
|
MIMEType: "text/plain",
|
|
|
|
RFC822Headers: oh,
|
|
|
|
}
|
|
|
|
return ParseTemplateData(h, original)
|
|
|
|
}
|
|
|
|
|
2020-04-23 20:55:18 +02:00
|
|
|
func ParseTemplateFromFile(templateName string, templateDirs []string, data interface{}) (io.Reader, error) {
|
2019-11-03 13:51:14 +01:00
|
|
|
templateFile, err := findTemplate(templateName, templateDirs)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-11-10 17:00:21 +01:00
|
|
|
emailTemplate, err := template.New(templateName).
|
|
|
|
Funcs(templateFuncs).ParseFiles(templateFile)
|
2019-11-03 13:51:14 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-04-23 20:55:18 +02:00
|
|
|
var body bytes.Buffer
|
|
|
|
if err := emailTemplate.Execute(&body, data); err != nil {
|
2019-11-03 13:51:14 +01:00
|
|
|
return nil, err
|
|
|
|
}
|
2020-04-23 20:55:18 +02:00
|
|
|
return &body, nil
|
2019-11-03 13:51:14 +01:00
|
|
|
}
|
2022-02-19 14:06:57 +01:00
|
|
|
|
|
|
|
func CheckTemplate(templateName string, templateDirs []string) error {
|
|
|
|
if templateName != "" {
|
|
|
|
_, err := ParseTemplateFromFile(templateName, templateDirs, DummyData())
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|