code.gitea.io/gitea@v1.22.3/modules/templates/mailer.go (about)

     1  // Copyright 2022 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package templates
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"html/template"
    10  	"regexp"
    11  	"strings"
    12  	texttmpl "text/template"
    13  
    14  	"code.gitea.io/gitea/modules/base"
    15  	"code.gitea.io/gitea/modules/log"
    16  	"code.gitea.io/gitea/modules/setting"
    17  )
    18  
    19  var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`)
    20  
    21  // mailSubjectTextFuncMap returns functions for injecting to text templates, it's only used for mail subject
    22  func mailSubjectTextFuncMap() texttmpl.FuncMap {
    23  	return texttmpl.FuncMap{
    24  		"dict": dict,
    25  		"Eval": Eval,
    26  
    27  		"EllipsisString": base.EllipsisString,
    28  		"AppName": func() string {
    29  			return setting.AppName
    30  		},
    31  		"AppDomain": func() string { // documented in mail-templates.md
    32  			return setting.Domain
    33  		},
    34  	}
    35  }
    36  
    37  func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) error {
    38  	// Split template into subject and body
    39  	var subjectContent []byte
    40  	bodyContent := content
    41  	loc := mailSubjectSplit.FindIndex(content)
    42  	if loc != nil {
    43  		subjectContent = content[0:loc[0]]
    44  		bodyContent = content[loc[1]:]
    45  	}
    46  	if _, err := stpl.New(name).Parse(string(subjectContent)); err != nil {
    47  		return fmt.Errorf("failed to parse template [%s/subject]: %w", name, err)
    48  	}
    49  	if _, err := btpl.New(name).Parse(string(bodyContent)); err != nil {
    50  		return fmt.Errorf("failed to parse template [%s/body]: %w", name, err)
    51  	}
    52  	return nil
    53  }
    54  
    55  // Mailer provides the templates required for sending notification mails.
    56  func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
    57  	subjectTemplates := texttmpl.New("")
    58  	bodyTemplates := template.New("")
    59  
    60  	subjectTemplates.Funcs(mailSubjectTextFuncMap())
    61  	bodyTemplates.Funcs(NewFuncMap())
    62  
    63  	assetFS := AssetFS()
    64  	refreshTemplates := func(firstRun bool) {
    65  		if !firstRun {
    66  			log.Trace("Reloading mail templates")
    67  		}
    68  		assetPaths, err := ListMailTemplateAssetNames(assetFS)
    69  		if err != nil {
    70  			log.Error("Failed to list mail templates: %v", err)
    71  			return
    72  		}
    73  
    74  		for _, assetPath := range assetPaths {
    75  			content, layerName, err := assetFS.ReadLayeredFile(assetPath)
    76  			if err != nil {
    77  				log.Warn("Failed to read mail template %s by %s: %v", assetPath, layerName, err)
    78  				continue
    79  			}
    80  			tmplName := strings.TrimPrefix(strings.TrimSuffix(assetPath, ".tmpl"), "mail/")
    81  			if firstRun {
    82  				log.Trace("Adding mail template %s: %s by %s", tmplName, assetPath, layerName)
    83  			}
    84  			if err = buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content); err != nil {
    85  				if firstRun {
    86  					log.Fatal("Failed to parse mail template, err: %v", err)
    87  				}
    88  				log.Error("Failed to parse mail template, err: %v", err)
    89  			}
    90  		}
    91  	}
    92  
    93  	refreshTemplates(true)
    94  
    95  	if !setting.IsProd {
    96  		// Now subjectTemplates and bodyTemplates are both synchronized
    97  		// thus it is safe to call refresh from a different goroutine
    98  		go assetFS.WatchLocalChanges(ctx, func() {
    99  			refreshTemplates(false)
   100  		})
   101  	}
   102  
   103  	return subjectTemplates, bodyTemplates
   104  }