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  }