feat: add template engine to mail

This commit also introduced the capability to define multiple mail recipients using a comma separated list of addresses
This commit is contained in:
Giuseppe Pagano 2024-09-19 10:02:56 +02:00
parent 1fb01da341
commit 0b20763a53
4 changed files with 130 additions and 13 deletions

View File

@ -12,6 +12,11 @@ type MailSendConfig struct {
To string `json:"to"`
}
type MailTemplate struct {
Subject string `json:"subject"`
Body string `json:"body"`
}
type SMTPConfig struct {
Host string `json:"host"`
Port string `json:"port"`
@ -19,6 +24,7 @@ type SMTPConfig struct {
Password string `json:"password"`
Insecure bool `json:"insecure"`
Mails []MailSendConfig `json:"mails"`
Template *MailTemplate `json:"template"`
}
type Config struct {
@ -73,8 +79,10 @@ func loadConfig() *Config {
mailInsecureFlag := flag.Bool("mail-insecure", false, "mail notification system: allow insecure communications(optional)")
mailFromFlag := flag.String("mail-from", "", "mail notification system: sender mail(optional)")
mailToFlag := flag.String("mail-to", "", "mail notification system: receiver mail(optional)")
mailSubjectTemplateFlag := flag.String("mail-subject-template", "", "mail notification system: mail subject template(optional)")
mailBodyTemplateFlag := flag.String("mail-body-template", "", "mail notification system: mail body template(optional)")
configFile := flag.String("config", "", "Path to JSON config file. If this flag is provided all the others are ignored")
configFile := flag.String("config", "", "Path to JSON config file. If this flag is provided all the others will override the loaded config file")
// Parse command line flags
flag.Parse()
@ -145,6 +153,14 @@ func loadConfig() *Config {
initMailConfsIfNeeded()
config.SMTP.Mails[0].To = *mailToFlag
}
if *mailSubjectTemplateFlag != "" {
initSmtpConfigIfNeeded()
config.SMTP.Template.Subject = *mailSubjectTemplateFlag
}
if *mailBodyTemplateFlag != "" {
initSmtpConfigIfNeeded()
config.SMTP.Template.Body = *mailBodyTemplateFlag
}
return config
}

View File

@ -13,6 +13,11 @@
"port": "465",
"username": "my-user@example.com",
"password": "my-password",
"insecure": false,
"template": {
"subject": "{{if not .Success}}[FAILED]{{else}}[SUCCESS]{{end}} Backup report for {{.Datastore}}",
"body": "Backup {{if .Success}}completed{{else}}ended with errors{{end}} on host {{.Hostname}} (took {{.FromattedDuration}})\n{{if .Success}}Chunks New {{.NewChunks}}, Reused {{.ReusedChunks}}.{{else}}Error occurred while working, backup may be not completed.\nLast error is: {{.ErrorStr}}{{end}}"
},
"mails": [{
"from": "sender1@example.com",
"to": "receiver1@example.com

58
mail.go
View File

@ -4,7 +4,10 @@ import (
"crypto/tls"
"errors"
"fmt"
"html/template"
"net/smtp"
"strings"
"time"
)
type unencryptedAuth struct {
@ -76,7 +79,12 @@ func sendMail(from, to, subject, body string, c *smtp.Client) error {
// Setup headers
headers := make(map[string]string)
headers["From"] = from
headers["To"] = to
recipients := strings.Split(to, ",")
recipientsStr := make([]string, 0)
for i := range recipients {
recipientsStr = append(recipientsStr, fmt.Sprintf("<%s>", recipients[i]))
}
headers["To"] = strings.Join(recipientsStr, ",")
headers["Subject"] = subject
// Setup message
@ -91,7 +99,7 @@ func sendMail(from, to, subject, body string, c *smtp.Client) error {
return err
}
if err := c.Rcpt(to); err != nil {
if err := c.Rcpt(strings.Join(recipientsStr, ",")); err != nil {
return err
}
@ -113,3 +121,49 @@ func sendMail(from, to, subject, body string, c *smtp.Client) error {
return nil
}
type mailCtx struct {
NewChunks uint64
ReusedChunks uint64
Datastore string
Error error
Hostname string
StartTime time.Time
EndTime time.Time
}
func (m *mailCtx) Duration() time.Duration {
return m.EndTime.Sub(m.StartTime)
}
func (m *mailCtx) FromattedDuration() string {
return m.Duration().String()
}
func (m *mailCtx) ErrorStr() string {
if m.Error != nil {
return m.Error.Error()
}
return ""
}
func (m *mailCtx) Success() bool {
return m.Error == nil
}
func (m *mailCtx) Status() string {
if m.Success() {
return "Success"
}
return "Failed"
}
func (m *mailCtx) buildStr(txt string) (string, error) {
tmpl, err := template.New("mail").Parse(txt)
if err != nil {
return "", err
}
strBuff := &strings.Builder{}
err = tmpl.Execute(strBuff, m)
return strBuff.String(), err
}

62
main.go
View File

@ -11,6 +11,7 @@ import (
"os"
"runtime"
"sync/atomic"
"time"
"github.com/cornelk/hashmap"
"github.com/gen2brain/beeep"
@ -18,6 +19,11 @@ import (
"github.com/tawesoft/golib/v2/dialog"
)
var defaultMailSubjectTemplate = "Backup {{.Status}}"
var defaultMailBodyTemplate = `{{if .Success}}Backup complete ({{.FromattedDuration}})
Chunks New {{.NewChunks}}, Reused {{.ReusedChunks}}.{{else}}Error occurred while working, backup may be not completed.
Last error is: {{.ErrorStr}}{{end}}`
var didxMagic = []byte{28, 145, 78, 165, 25, 186, 179, 205}
type ChunkState struct {
@ -94,15 +100,41 @@ func main() {
BackupID: cfg.BackupID,
},
}
hostname, err := os.Hostname()
if err != nil {
fmt.Println("Failed to retrieve hostname:", err)
hostname = "unknown"
}
err := backup(client, newchunk, reusechunk, cfg.PxarOut, cfg.BackupSourceDir)
begin := time.Now()
err = backup(client, newchunk, reusechunk, cfg.PxarOut, cfg.BackupSourceDir)
end := time.Now()
fmt.Printf("New %d , Reused %d\n", newchunk.Load(), reusechunk.Load())
mailCtx := mailCtx{
NewChunks: newchunk.Load(),
ReusedChunks: reusechunk.Load(),
Error: err,
Hostname: hostname,
Datastore: cfg.Datastore,
StartTime: begin,
EndTime: end,
}
mailBodyTemplate := defaultMailBodyTemplate
if cfg.SMTP != nil && cfg.SMTP.Template != nil && cfg.SMTP.Template.Body != "" {
mailBodyTemplate = cfg.SMTP.Template.Body
}
fmt.Printf("New %d, Reused %d, backup took %s.\n", newchunk.Load(), reusechunk.Load(), end.Sub(begin))
var msg string
if err == nil {
msg = fmt.Sprintf("Backup complete\nChunks New %d , Reused %d\n", newchunk.Load(), reusechunk.Load())
} else {
msg = fmt.Sprintf("Error occurred while working, backup may be not completed.\nLast error is: %s\n", err.Error())
msg, err = mailCtx.buildStr(mailBodyTemplate)
if err != nil {
fmt.Println("Cannot use custom mail body: " + err.Error())
msg, err = mailCtx.buildStr(defaultMailBodyTemplate)
if err != nil {
// this should never happen
panic(err)
}
}
if runtime.GOOS == "windows" {
systray.Quit()
@ -110,10 +142,20 @@ func main() {
}
if cfg.SMTP != nil {
var subject string
if err == nil {
subject = "Backup complete"
} else {
subject = "Backup error"
mailSubjectTemplate := defaultMailSubjectTemplate
if cfg.SMTP.Template != nil && cfg.SMTP.Template.Subject != "" {
mailSubjectTemplate = cfg.SMTP.Template.Subject
}
subject, err = mailCtx.buildStr(mailSubjectTemplate)
if err != nil {
fmt.Println("Cannot use custom mail subject: " + err.Error())
msg, err = mailCtx.buildStr(defaultMailSubjectTemplate)
if err != nil {
// this should never happen
panic(err)
}
}
client, err := setupClient(cfg.SMTP.Host, cfg.SMTP.Port, cfg.SMTP.Username, cfg.SMTP.Password, cfg.SMTP.Insecure)
if err != nil {