github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/worker/mails/mail_templates.go (about) 1 package mails 2 3 import ( 4 "bytes" 5 "fmt" 6 "html/template" 7 "io" 8 text "text/template" 9 10 "github.com/cozy/cozy-stack/model/job" 11 "github.com/cozy/cozy-stack/pkg/assets" 12 "github.com/cozy/cozy-stack/pkg/i18n" 13 "github.com/cozy/cozy-stack/pkg/mail" 14 ) 15 16 const templateTitleVar = "template_title" 17 18 func initMailTemplates() { 19 mailTemplater = MailTemplater{ 20 "passphrase_hint": subjectEntry{"Mail Hint Subject", nil}, 21 "passphrase_reset": subjectEntry{"Mail Reset Passphrase Subject", nil}, 22 "archiver": subjectEntry{"Mail Archive Subject", nil}, 23 "import_success": subjectEntry{"Mail Import Success Subject", nil}, 24 "import_error": subjectEntry{"Mail Import Error Subject", nil}, 25 "export_error": subjectEntry{"Mail Export Error Subject", nil}, 26 "move_confirm": subjectEntry{"Mail Move Confirm Subject", nil}, 27 "move_success": subjectEntry{"Mail Move Success Subject", nil}, 28 "move_error": subjectEntry{"Mail Move Error Subject", nil}, 29 "magic_link": subjectEntry{"Mail Magic Link Subject", nil}, 30 "two_factor": subjectEntry{"Mail Two Factor Subject", nil}, 31 "two_factor_mail_confirmation": subjectEntry{"Mail Two Factor Mail Confirmation Subject", []string{templateTitleVar}}, 32 "new_connection": subjectEntry{"Mail New Connection Subject", []string{templateTitleVar}}, 33 "new_registration": subjectEntry{"Mail New Registration Subject", []string{templateTitleVar}}, 34 "confirm_flagship": subjectEntry{"Mail Confirm Flagship Subject", nil}, 35 "alert_account": subjectEntry{"Mail Alert Account Subject", nil}, 36 "support_request": subjectEntry{"Mail Support Confirmation Subject", nil}, 37 "sharing_request": subjectEntry{"Mail Sharing Request Subject", []string{"SharerPublicName"}}, 38 "sharing_to_confirm": subjectEntry{"Mail Sharing Member To Confirm Subject", nil}, 39 "notifications_sharing": subjectEntry{"Notification Sharing Subject", nil}, 40 "notifications_diskquota": subjectEntry{"Notifications Disk Quota Subject", nil}, 41 "notifications_oauthclients": subjectEntry{"Notifications OAuth Clients Subject", nil}, 42 "update_email": subjectEntry{"Mail Update Email Subject", nil}, 43 } 44 } 45 46 // RenderMail returns a rendered mail for the given template name with the 47 // specified locale, recipient name and template data values. 48 func RenderMail(ctx *job.TaskContext, name, layout, locale, recipientName string, templateValues map[string]interface{}) (string, []*mail.Part, error) { 49 return mailTemplater.Execute(ctx, name, layout, locale, recipientName, templateValues) 50 } 51 52 // MailTemplater is the list of templates for emails. 53 type MailTemplater map[string]subjectEntry 54 55 // subjectEntry is a i18n key for the subject message, and some optional 56 // variable names. 57 type subjectEntry struct { 58 Key string 59 Variables []string 60 } 61 62 // Execute will execute the HTML and text templates for the template with the 63 // specified name. It returns the mail parts that should be added to the sent 64 // mail. 65 func (m MailTemplater) Execute(ctx *job.TaskContext, name, layout, locale string, recipientName string, data map[string]interface{}) (string, []*mail.Part, error) { 66 entry, ok := m[name] 67 if !ok { 68 err := fmt.Errorf("Could not find email named %q", name) 69 return "", nil, err 70 } 71 72 var vars []interface{} 73 for _, name := range entry.Variables { 74 if name == templateTitleVar { 75 vars = append(vars, ctx.Instance.TemplateTitle()) 76 } else { 77 vars = append(vars, data[name]) 78 } 79 } 80 81 context := ctx.Instance.ContextName 82 assets.LoadContextualizedLocale(context, locale) 83 subject := i18n.Translate(entry.Key, locale, context, vars...) 84 if data == nil { 85 data = map[string]interface{}{"Locale": locale} 86 } else { 87 data["Locale"] = locale 88 } 89 if ctx.Instance != nil { 90 data["InstanceURL"] = ctx.Instance.PageURL("/", nil) 91 } 92 93 txt, err := buildText(name, context, locale, data) 94 if err != nil { 95 return "", nil, err 96 } 97 parts := []*mail.Part{ 98 {Body: txt, Type: "text/plain"}, 99 } 100 101 // If we can generate the HTML, we should still send the mail with the text 102 // part. 103 if html, err := buildHTML(name, layout, ctx, context, locale, data); err == nil { 104 parts = append(parts, &mail.Part{Body: html, Type: "text/html"}) 105 } else { 106 ctx.Logger().Errorf("Cannot generate HTML mail: %s", err) 107 } 108 return subject, parts, nil 109 } 110 111 func buildText(name, context, locale string, data map[string]interface{}) (string, error) { 112 buf := new(bytes.Buffer) 113 b, err := loadTemplate("/mails/"+name+".text", context) 114 if err != nil { 115 return "", err 116 } 117 funcMap := text.FuncMap{"t": i18n.Translator(locale, context)} 118 t, err := text.New("text").Funcs(funcMap).Parse(string(b)) 119 if err != nil { 120 return "", err 121 } 122 if err := t.Execute(buf, data); err != nil { 123 return "", err 124 } 125 return buf.String(), nil 126 } 127 128 func buildHTML(name string, layout string, ctx *job.TaskContext, context, locale string, data map[string]interface{}) (string, error) { 129 buf := new(bytes.Buffer) 130 b, err := loadTemplate("/mails/"+name+".mjml", context) 131 if err != nil { 132 return "", err 133 } 134 funcMap := template.FuncMap{ 135 "t": i18n.Translator(locale, context), 136 "tHTML": i18n.TranslatorHTML(locale, context), 137 } 138 t, err := template.New("content").Funcs(funcMap).Parse(string(b)) 139 if err != nil { 140 return "", err 141 } 142 b, err = loadTemplate("/mails/"+layout+".mjml", context) 143 if err != nil { 144 return "", err 145 } 146 t, err = t.New("layout").Funcs(funcMap).Parse(string(b)) 147 if err != nil { 148 return "", err 149 } 150 if err := t.Execute(buf, data); err != nil { 151 return "", err 152 } 153 html, err := execMjml(ctx, buf.Bytes()) 154 if err != nil { 155 return "", err 156 } 157 return string(html), nil 158 } 159 160 func loadTemplate(name, context string) ([]byte, error) { 161 f, err := assets.Open(name, context) 162 if err != nil { 163 return nil, err 164 } 165 return io.ReadAll(f) 166 }