feat(notification): password reset notification custom templates (#2828)

Implemented a system to allow overriding email templates, including the remote IP, and sending email notifications when the password was reset successfully.

Closes #2755, Closes #2756

Co-authored-by: Manuel Nuñez <@mind-ar>
Co-authored-by: James Elliott <james-d-elliott@users.noreply.github.com>
pull/3097/head^2
Manuel Nuñez 2022-04-03 09:24:51 -03:00 committed by GitHub
parent 9e05066097
commit bfd5d66ed8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 692 additions and 44 deletions

View File

@ -16,6 +16,7 @@ verify their identity.
```yaml ```yaml
notifier: notifier:
disable_startup_check: false disable_startup_check: false
template_path: /path/to/templates/folder
filesystem: {} filesystem: {}
smtp: {} smtp: {}
``` ```
@ -36,6 +37,52 @@ The notifier has a startup check which validates the specified provider
configuration is correct and will be able to send emails. This can be configuration is correct and will be able to send emails. This can be
disabled with the `disable_startup_check` option: disabled with the `disable_startup_check` option:
### template_path
<div markdown="1">
type: string
{: .label .label-config .label-purple }
default: ""
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
This option allows the administrator to set custom templates for notifications
the templates folder should contain the following files
|File |Description |
|------------------------|---------------------------------------------------|
|PasswordResetStep1.html |HTML Template for Step 1 of password reset process |
|PasswordResetStep1.txt |Text Template for Step 1 of password reset process |
|PasswordResetStep2.html |HTML Template for Step 2 of password reset process |
|PasswordResetStep2.txt |Text Template for Step 2 of password reset process |
Note:
* if you don't define some of these files, a default template is used for that notification
In template files, you can use the following variables:
|File |Description |
|------------------------|---------------------------------------------------|
|`{{.title}}`| A predefined title for the email. <br> It will be `"Reset your password"` or `"Password changed successfully"`, depending on the current step |
|`{{.url}}` | The url that allows to reset the user password |
|`{{.displayName}}` |The name of the user, i.e. `John Doe` |
|`{{.button}}` |The content for the password reset button, it's hardcoded to `Reset` |
|`{{.remoteIP}}` |The remote IP address that initiated the request or event |
#### Example
```html
<body>
<h1>{{.title}}</h1>
Hi {{.displayName}} <br/>
This email has been sent to you in order to validate your identity
Click <a href="{{.url}}" >here</a> to change your password
</body>
```
### filesystem ### filesystem
The [filesystem](filesystem.md) provider. The [filesystem](filesystem.md) provider.

View File

@ -31,6 +31,7 @@ type NotifierConfiguration struct {
DisableStartupCheck bool `koanf:"disable_startup_check"` DisableStartupCheck bool `koanf:"disable_startup_check"`
FileSystem *FileSystemNotifierConfiguration `koanf:"filesystem"` FileSystem *FileSystemNotifierConfiguration `koanf:"filesystem"`
SMTP *SMTPNotifierConfiguration `koanf:"smtp"` SMTP *SMTPNotifierConfiguration `koanf:"smtp"`
TemplatePath string `koanf:"template_path"`
} }
// DefaultSMTPNotifierConfiguration represents default configuration parameters for the SMTP notifier. // DefaultSMTPNotifierConfiguration represents default configuration parameters for the SMTP notifier.

View File

@ -54,6 +54,9 @@ const (
errFmtNotifierMultipleConfigured = "notifier: please ensure only one of the 'smtp' or 'filesystem' notifier is configured" errFmtNotifierMultipleConfigured = "notifier: please ensure only one of the 'smtp' or 'filesystem' notifier is configured"
errFmtNotifierNotConfigured = "notifier: you must ensure either the 'smtp' or 'filesystem' notifier " + errFmtNotifierNotConfigured = "notifier: you must ensure either the 'smtp' or 'filesystem' notifier " +
"is configured" "is configured"
errFmtNotifierTemplatePathNotExist = "notifier: option 'template_path' refers to location '%s' which does not exist"
errFmtNotifierTemplatePathUnknownError = "notifier: option 'template_path' refers to location '%s' which couldn't be opened: %w"
errFmtNotifierTemplateLoad = "notifier: error loading template '%s': %w"
errFmtNotifierFileSystemFileNameNotConfigured = "notifier: filesystem: option 'filename' is required " errFmtNotifierFileSystemFileNameNotConfigured = "notifier: filesystem: option 'filename' is required "
errFmtNotifierSMTPNotConfigured = "notifier: smtp: option '%s' is required" errFmtNotifierSMTPNotConfigured = "notifier: smtp: option '%s' is required"
) )
@ -412,6 +415,7 @@ var ValidKeys = []string{
"notifier.smtp.tls.minimum_version", "notifier.smtp.tls.minimum_version",
"notifier.smtp.tls.skip_verify", "notifier.smtp.tls.skip_verify",
"notifier.smtp.tls.server_name", "notifier.smtp.tls.server_name",
"notifier.template_path",
// Regulation Keys. // Regulation Keys.
"regulation.max_retries", "regulation.max_retries",

View File

@ -2,8 +2,12 @@ package validator
import ( import (
"fmt" "fmt"
"os"
"path/filepath"
"text/template"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/templates"
) )
// ValidateNotifier validates and update notifier configuration. // ValidateNotifier validates and update notifier configuration.
@ -27,6 +31,54 @@ func ValidateNotifier(config *schema.NotifierConfiguration, validator *schema.St
} }
validateSMTPNotifier(config.SMTP, validator) validateSMTPNotifier(config.SMTP, validator)
validateNotifierTemplates(config, validator)
}
func validateNotifierTemplates(config *schema.NotifierConfiguration, validator *schema.StructValidator) {
if config.TemplatePath == "" {
return
}
var (
err error
t *template.Template
)
_, err = os.Stat(config.TemplatePath)
switch {
case os.IsNotExist(err):
validator.Push(fmt.Errorf(errFmtNotifierTemplatePathNotExist, config.TemplatePath))
return
case err != nil:
validator.Push(fmt.Errorf(errFmtNotifierTemplatePathUnknownError, config.TemplatePath, err))
return
}
if t, err = template.ParseFiles(filepath.Join(config.TemplatePath, templates.TemplateNameStep1+".html")); err == nil {
templates.HTMLEmailTemplateStep1 = t
} else {
validator.PushWarning(fmt.Errorf(errFmtNotifierTemplateLoad, templates.TemplateNameStep1+".html", err))
}
if t, err = template.ParseFiles(filepath.Join(config.TemplatePath, templates.TemplateNameStep1+".txt")); err == nil {
templates.PlainTextEmailTemplateStep1 = t
} else {
validator.PushWarning(fmt.Errorf(errFmtNotifierTemplateLoad, templates.TemplateNameStep1+".txt", err))
}
if t, err = template.ParseFiles(filepath.Join(config.TemplatePath, templates.TemplateNameStep2+".html")); err == nil {
templates.HTMLEmailTemplateStep2 = t
} else {
validator.PushWarning(fmt.Errorf(errFmtNotifierTemplateLoad, templates.TemplateNameStep2+".html", err))
}
if t, err = template.ParseFiles(filepath.Join(config.TemplatePath, templates.TemplateNameStep2+".txt")); err == nil {
templates.PlainTextEmailTemplateStep2 = t
} else {
validator.PushWarning(fmt.Errorf(errFmtNotifierTemplateLoad, templates.TemplateNameStep2+".txt", err))
}
} }
func validateSMTPNotifier(config *schema.SMTPNotifierConfiguration, validator *schema.StructValidator) { func validateSMTPNotifier(config *schema.SMTPNotifierConfiguration, validator *schema.StructValidator) {

View File

@ -28,8 +28,9 @@ func identityRetrieverFromStorage(ctx *middlewares.AutheliaCtx) (*session.Identi
} }
return &session.Identity{ return &session.Identity{
Username: requestBody.Username, Username: requestBody.Username,
Email: details.Emails[0], Email: details.Emails[0],
DisplayName: details.DisplayName,
}, nil }, nil
} }

View File

@ -1,9 +1,11 @@
package handlers package handlers
import ( import (
"bytes"
"fmt" "fmt"
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/templates"
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
) )
@ -19,6 +21,8 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
return return
} }
username := *userSession.PasswordResetUsername
var requestBody resetPasswordStep2RequestBody var requestBody resetPasswordStep2RequestBody
err := ctx.ParseBody(&requestBody) err := ctx.ParseBody(&requestBody)
@ -32,7 +36,7 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
return return
} }
err = ctx.Providers.UserProvider.UpdatePassword(*userSession.PasswordResetUsername, requestBody.Password) err = ctx.Providers.UserProvider.UpdatePassword(username, requestBody.Password)
if err != nil { if err != nil {
switch { switch {
@ -46,7 +50,7 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
return return
} }
ctx.Logger.Debugf("Password of user %s has been reset", *userSession.PasswordResetUsername) ctx.Logger.Debugf("Password of user %s has been reset", username)
// Reset the request. // Reset the request.
userSession.PasswordResetUsername = nil userSession.PasswordResetUsername = nil
@ -57,5 +61,69 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
return return
} }
ctx.ReplyOK() // Send Notification.
userInfo, err := ctx.Providers.UserProvider.GetDetails(username)
if err != nil {
ctx.Logger.Error(err)
ctx.ReplyOK()
return
}
if len(userInfo.Emails) == 0 {
ctx.Logger.Error(fmt.Errorf("user %s has no email address configured", username))
ctx.ReplyOK()
return
}
bufHTML := new(bytes.Buffer)
disableHTML := false
if ctx.Configuration.Notifier != nil && ctx.Configuration.Notifier.SMTP != nil {
disableHTML = ctx.Configuration.Notifier.SMTP.DisableHTMLEmails
}
if !disableHTML {
htmlParams := map[string]interface{}{
"title": "Password changed successfully",
"displayName": userInfo.DisplayName,
"remoteIP": ctx.RemoteIP().String(),
}
err = templates.HTMLEmailTemplateStep2.Execute(bufHTML, htmlParams)
if err != nil {
ctx.Logger.Error(err)
ctx.ReplyOK()
return
}
}
bufText := new(bytes.Buffer)
textParams := map[string]interface{}{
"displayName": userInfo.DisplayName,
}
err = templates.PlainTextEmailTemplateStep2.Execute(bufText, textParams)
if err != nil {
ctx.Logger.Error(err)
ctx.ReplyOK()
return
}
ctx.Logger.Debugf("Sending an email to user %s (%s) to inform that the password has changed.",
username, userInfo.Emails[0])
err = ctx.Providers.Notifier.Send(userInfo.Emails[0], "Password changed successfully", bufText.String(), bufHTML.String())
if err != nil {
ctx.Logger.Error(err)
ctx.ReplyOK()
return
}
} }

View File

@ -79,12 +79,14 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs, delayFunc Tim
if !disableHTML { if !disableHTML {
htmlParams := map[string]interface{}{ htmlParams := map[string]interface{}{
"title": args.MailTitle, "title": args.MailTitle,
"url": link, "url": link,
"button": args.MailButtonContent, "button": args.MailButtonContent,
"displayName": identity.DisplayName,
"remoteIP": ctx.RemoteIP().String(),
} }
err = templates.HTMLEmailTemplate.Execute(bufHTML, htmlParams) err = templates.HTMLEmailTemplateStep1.Execute(bufHTML, htmlParams)
if err != nil { if err != nil {
ctx.Error(err, messageOperationFailed) ctx.Error(err, messageOperationFailed)
@ -94,10 +96,11 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs, delayFunc Tim
bufText := new(bytes.Buffer) bufText := new(bytes.Buffer)
textParams := map[string]interface{}{ textParams := map[string]interface{}{
"url": link, "url": link,
"displayName": identity.DisplayName,
} }
err = templates.PlainTextEmailTemplate.Execute(bufText, textParams) err = templates.PlainTextEmailTemplateStep1.Execute(bufText, textParams)
if err != nil { if err != nil {
ctx.Error(err, messageOperationFailed) ctx.Error(err, messageOperationFailed)

View File

@ -55,8 +55,9 @@ type UserSession struct {
// Identity identity of the user who is being verified. // Identity identity of the user who is being verified.
type Identity struct { type Identity struct {
Username string Username string
Email string Email string
DisplayName string
} }
func newRedisLogger() *redisLogger { func newRedisLogger() *redisLogger {

View File

@ -6,6 +6,8 @@ services:
volumes: volumes:
- ./example/compose/nginx/portal/nginx.conf:/etc/nginx/nginx.conf - ./example/compose/nginx/portal/nginx.conf:/etc/nginx/nginx.conf
- ./example/compose/nginx/portal/ssl:/etc/ssl - ./example/compose/nginx/portal/ssl:/etc/ssl
ports:
- 8080:8080
networks: networks:
authelianet: authelianet:
aliases: aliases:

View File

@ -0,0 +1,7 @@
package templates
// Template File Names.
const (
TemplateNameStep1 = "PasswordResetStep1"
TemplateNameStep2 = "PasswordResetStep2"
)

View File

@ -4,19 +4,19 @@ import (
"text/template" "text/template"
) )
// HTMLEmailTemplate the template of email that the user will receive for identity verification. // HTMLEmailTemplateStep1 the template of email that the user will receive for identity verification.
var HTMLEmailTemplate *template.Template var HTMLEmailTemplateStep1 *template.Template
func init() { func init() {
t, err := template.New("html_email_template").Parse(emailHTMLContent) t, err := template.New("html_email_template").Parse(emailHTMLContentStep1)
if err != nil { if err != nil {
panic(err) panic(err)
} }
HTMLEmailTemplate = t HTMLEmailTemplateStep1 = t
} }
const emailHTMLContent = ` const emailHTMLContentStep1 = `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"> <html xmlns="http://www.w3.org/1999/xhtml">
@ -93,25 +93,28 @@ const emailHTMLContent = `
} }
a { a {
color: #ffffff;
text-decoration: none; text-decoration: none;
text-decoration: none !important; text-decoration: none !important;
} }
.link {
color: #0645AD;
}
h1 { h1 {
line-height: 30px; line-height: 30px;
} }
.button { .button {
padding: 15px 30px; color: #ffffff;
border-radius: 10px; padding: 15px 30px;
background: rgb(25, 118, 210); border-radius: 10px;
text-decoration: none; background: rgb(25, 118, 210);
text-decoration: none;
} }
.link {
color: rgb(25, 118, 210);
text-decoration: none;
}
/*STYLES*/ /*STYLES*/
table[class=full] { table[class=full] {
width: 100%; width: 100%;
@ -311,6 +314,12 @@ const emailHTMLContent = `
class="devicewidthinner"> class="devicewidthinner">
<tbody> <tbody>
<!-- Title --> <!-- Title -->
<tr>
<td style="font-family: Helvetica, arial, sans-serif; font-size: 16px; color: #333333; text-align:center; line-height: 30px;"
st-title="fulltext-content">
Hi {{.displayName}}
</td>
</tr>
<tr> <tr>
<td style="font-family: Helvetica, arial, sans-serif; font-size: 16px; color: #333333; text-align:center; line-height: 30px;" <td style="font-family: Helvetica, arial, sans-serif; font-size: 16px; color: #333333; text-align:center; line-height: 30px;"
st-title="fulltext-content"> st-title="fulltext-content">
@ -333,17 +342,13 @@ const emailHTMLContent = `
<a href="{{.url}}" class="button">{{.button}}</a> <a href="{{.url}}" class="button">{{.button}}</a>
</td> </td>
</tr> </tr>
<!-- spacing -->
<tr> <tr>
<td width="100%" height="20" <td width="100%" height="20"
style="font-size:1px; line-height:1px; mso-line-height-rule: exactly;"> style="font-size:1px; line-height:1px; mso-line-height-rule: exactly;">
&nbsp;</td> &nbsp;</td>
</tr> </tr>
<tr> <!-- End of spacing -->
<td style="font-family: Helvetica, arial, sans-serif; font-size: 16px; color: #333333; text-align:center; line-height: 30px;"
st-title="fulltext-content">
Or
</td>
</tr>
<tr> <tr>
<td style="word-break: break-word; overflow-wrap: break-word; text-align:center; line-height: 30px;"> <td style="word-break: break-word; overflow-wrap: break-word; text-align:center; line-height: 30px;">
<a href="{{.url}}" class="link">{{.url}}</a> <a href="{{.url}}" class="link">{{.url}}</a>
@ -354,13 +359,6 @@ const emailHTMLContent = `
</table> </table>
</td> </td>
</tr> </tr>
<!-- Spacing -->
<tr>
<td height="20"
style="font-size:1px; line-height:1px; mso-line-height-rule: exactly;">&nbsp;
</td>
</tr>
<!-- Spacing -->
</tbody> </tbody>
</table> </table>
</td> </td>
@ -417,6 +415,19 @@ const emailHTMLContent = `
Please contact an administrator if you did not initiate the process. Please contact an administrator if you did not initiate the process.
</td> </td>
</tr> </tr>
<!-- spacing -->
<tr>
<td width="100%" height="20"
style="font-size:1px; line-height:1px; mso-line-height-rule: exactly;">
&nbsp;</td>
</tr>
<!-- End of spacing -->
<tr>
<td style="font-family: Helvetica, arial, sans-serif; font-style: italic; font-size: 12px; color: #333333; text-align:center; line-height: 30px;"
st-title="fulltext-content">
This email was generated by some with the IP address {{.remoteIP}}.
</td>
</tr>
<!-- Spacing --> <!-- Spacing -->
<tr> <tr>
<td width="100%" height="20"></td> <td width="100%" height="20"></td>

View File

@ -0,0 +1,423 @@
package templates
import (
"text/template"
)
// HTMLEmailTemplateStep2 the template of email that the user will receive for identity verification.
var HTMLEmailTemplateStep2 *template.Template
func init() {
t, err := template.New("html_email_template").Parse(emailHTMLContentStep2)
if err != nil {
panic(err)
}
HTMLEmailTemplateStep2 = t
}
const emailHTMLContentStep2 = `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Authelia</title>
<style type="text/css">
/* client-specific Styles */
#outlook a {
padding: 0;
}
/* Force Outlook to provide a "view in browser" menu link. */
body {
width: 100% !important;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
margin: 0;
padding: 0;
}
/* Prevent Webkit and Windows Mobile platforms from changing default font sizes, while not breaking desktop design. */
.ExternalClass {
width: 100%;
}
/* Force Hotmail to display emails at full width */
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
/* Force Hotmail to display normal line spacing.*/
#backgroundTable {
margin: 0;
padding: 0;
width: 100% !important;
line-height: 100% !important;
}
img {
outline: none;
text-decoration: none;
border: none;
-ms-interpolation-mode: bicubic;
}
a img {
border: none;
}
.image_fix {
display: block;
}
p {
margin: 0px 0px !important;
}
table td {
border-collapse: collapse;
}
table {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
a {
color: #ffffff;
text-decoration: none;
text-decoration: none !important;
}
.link {
color: #0645AD;
}
h1 {
line-height: 30px;
}
.button {
padding: 15px 30px;
border-radius: 10px;
background: rgb(25, 118, 210);
text-decoration: none;
}
/*STYLES*/
table[class=full] {
width: 100%;
clear: both;
}
/*IPAD STYLES*/
@media only screen and (max-width: 640px) {
a[href^="tel"],
a[href^="sms"] {
text-decoration: none;
color: #0a8cce;
/* or whatever your want */
pointer-events: none;
cursor: default;
}
.mobile_link a[href^="tel"],
.mobile_link a[href^="sms"] {
text-decoration: default;
color: #0a8cce !important;
pointer-events: auto;
cursor: default;
}
table[class=devicewidth] {
width: 440px !important;
text-align: center !important;
}
table[class=devicewidthinner] {
width: 420px !important;
text-align: center !important;
}
img[class=banner] {
width: 440px !important;
height: 220px !important;
}
img[class=colimg2] {
width: 440px !important;
height: 220px !important;
}
}
/*IPHONE STYLES*/
@media only screen and (max-width: 480px) {
a[href^="tel"],
a[href^="sms"] {
text-decoration: none;
color: #0a8cce;
/* or whatever your want */
pointer-events: none;
cursor: default;
}
.mobile_link a[href^="tel"],
.mobile_link a[href^="sms"] {
text-decoration: default;
color: #0a8cce !important;
pointer-events: auto;
cursor: default;
}
table[class=devicewidth] {
width: 280px !important;
text-align: center !important;
}
table[class=devicewidthinner] {
width: 260px !important;
text-align: center !important;
}
img[class=banner] {
width: 280px !important;
height: 140px !important;
}
img[class=colimg2] {
width: 280px !important;
height: 140px !important;
}
td[class=mobile-hide] {
display: none !important;
}
td[class="padding-bottom25"] {
padding-bottom: 25px !important;
}
}
</style>
</head>
<body>
<!-- Start of header -->
<table width="100%" bgcolor="#ffffff" cellpadding="0" cellspacing="0" border="0" id="backgroundTable"
st-sortable="header">
<tbody>
<tr>
<td>
<table width="600" cellpadding="0" cellspacing="0" border="0" align="center" class="devicewidth">
<tbody>
<tr>
<td width="100%">
<table width="600" cellpadding="0" cellspacing="0" border="0" align="center"
class="devicewidth">
<tbody>
<!-- Spacing -->
<tr>
<td height="20"
style="font-size:1px; line-height:1px; mso-line-height-rule: exactly;">&nbsp;
</td>
</tr>
<!-- Spacing -->
<tr>
<td>
<!-- logo -->
<table width="140" align="center" border="0" cellpadding="0" cellspacing="0"
class="devicewidth">
<tbody>
<tr>
<td width="300" height="50" align="center">
<h1>{{.title}}</h1>
</td>
</tr>
</tbody>
</table>
<!-- end of logo -->
</td>
</tr>
<!-- Spacing -->
<tr>
<td height="20"
style="font-size:1px; line-height:1px; mso-line-height-rule: exactly;">&nbsp;
</td>
</tr>
<!-- Spacing -->
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!-- End of Header -->
<!-- Start of separator -->
<table width="100%" bgcolor="#ffffff" cellpadding="0" cellspacing="0" border="0" id="backgroundTable"
st-sortable="separator">
<tbody>
<tr>
<td>
<table width="600" align="center" cellspacing="0" cellpadding="0" border="0" class="devicewidth">
<tbody>
<tr>
<td align="center" height="20" style="font-size:1px; line-height:1px;">&nbsp;</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!-- End of separator -->
<!-- Start Full Text -->
<table width="100%" bgcolor="#ffffff" cellpadding="0" cellspacing="0" border="0" id="backgroundTable"
st-sortable="full-text">
<tbody>
<tr>
<td>
<table width="600" cellpadding="0" cellspacing="0" border="0" align="center" class="devicewidth">
<tbody>
<tr>
<td width="100%">
<table width="600" cellpadding="0" cellspacing="0" border="0" align="center"
class="devicewidth">
<tbody>
<!-- Spacing -->
<tr>
<td height="20"
style="font-size:1px; line-height:1px; mso-line-height-rule: exactly;">&nbsp;
</td>
</tr>
<!-- Spacing -->
<tr>
<td>
<table width="560" align="center" cellpadding="0" cellspacing="0" border="0"
class="devicewidthinner">
<tbody>
<!-- Title -->
<tr>
<td style="font-family: Helvetica, arial, sans-serif; font-size: 16px; color: #333333; text-align:center; line-height: 30px;"
st-title="fulltext-content">
Hi {{.displayName}} <br/>
Your password has been successfully reset.
If you did not initiate the process your credentials might have been compromised. You should reset your password and contact an administrator.
</td>
</tr>
<!-- End of Title -->
</tbody>
</table>
</td>
</tr>
<!-- Spacing -->
<tr>
<td height="20"
style="font-size:1px; line-height:1px; mso-line-height-rule: exactly;">&nbsp;
</td>
</tr>
<!-- Spacing -->
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!-- end of full text -->
<!-- Start of separator -->
<table width="100%" bgcolor="#ffffff" cellpadding="0" cellspacing="0" border="0" id="backgroundTable"
st-sortable="separator">
<tbody>
<tr>
<td>
<table width="600" align="center" cellspacing="0" cellpadding="0" border="0" class="devicewidth">
<tbody>
<tr>
<td align="center" height="30" style="font-size:1px; line-height:1px;">&nbsp;</td>
</tr>
<tr>
<td width="550" align="center" height="1" bgcolor="#d1d1d1"
style="font-size:1px; line-height:1px;">&nbsp;</td>
</tr>
<tr>
<td align="center" height="30" style="font-size:1px; line-height:1px;">&nbsp;</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!-- End of separator -->
<!-- Start of Postfooter -->
<table width="100%" bgcolor="#ffffff" cellpadding="0" cellspacing="0" border="0" id="backgroundTable"
st-sortable="postfooter">
<tbody>
<tr>
<td>
<table width="600" cellpadding="0" cellspacing="0" border="0" align="center" class="devicewidth">
<tbody>
<tr>
<td width="100%">
<table width="600" cellpadding="0" cellspacing="0" border="0" align="center"
class="devicewidth">
<tbody>
<tr>
<td align="center" valign="middle"
style="font-family: Helvetica, arial, sans-serif; font-size: 14px;color: #666666"
st-content="postfooter">
Please contact an administrator if you did not initiate the process.
</td>
</tr>
<!-- spacing -->
<tr>
<td width="100%" height="20"
style="font-size:1px; line-height:1px; mso-line-height-rule: exactly;">
&nbsp;</td>
</tr>
<!-- End of spacing -->
<tr>
<td style="font-family: Helvetica, arial, sans-serif; font-style: italic; font-size: 12px; color: #333333; text-align:center; line-height: 30px;"
st-title="fulltext-content">
This email was generated by some with the IP address {{.remoteIP}}.
</td>
</tr>
<!-- Spacing -->
<tr>
<td width="100%" height="20"></td>
</tr>
<!-- Spacing -->
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!-- End of postfooter -->
</body>
</html>
`

View File

@ -4,23 +4,25 @@ import (
"text/template" "text/template"
) )
// PlainTextEmailTemplate the template of email that the user will receive for identity verification. // PlainTextEmailTemplateStep1 the template of email that the user will receive for identity verification.
var PlainTextEmailTemplate *template.Template var PlainTextEmailTemplateStep1 *template.Template
func init() { func init() {
t, err := template.New("text_email_template").Parse(emailPlainTextContent) t, err := template.New("text_email_template").Parse(emailPlainTextContentStep1)
if err != nil { if err != nil {
panic(err) panic(err)
} }
PlainTextEmailTemplate = t PlainTextEmailTemplateStep1 = t
} }
const emailPlainTextContent = ` const emailPlainTextContentStep1 = `
This email has been sent to you in order to validate your identity. This email has been sent to you in order to validate your identity.
If you did not initiate the process your credentials might have been compromised. You should reset your password and contact an administrator. If you did not initiate the process your credentials might have been compromised. You should reset your password and contact an administrator.
To setup your 2FA please visit the following URL: {{.url}} To setup your 2FA please visit the following URL: {{.url}}
This email was generated by a user with the IP {{.remoteIP}}.
Please contact an administrator if you did not initiate the process. Please contact an administrator if you did not initiate the process.
` `

View File

@ -0,0 +1,26 @@
package templates
import (
"text/template"
)
// PlainTextEmailTemplateStep2 the template of email that the user will receive for identity verification.
var PlainTextEmailTemplateStep2 *template.Template
func init() {
t, err := template.New("text_email_template").Parse(emailPlainTextContentStep2)
if err != nil {
panic(err)
}
PlainTextEmailTemplateStep2 = t
}
const emailPlainTextContentStep2 = `
Your password has been successfully reset.
If you did not initiate the process your credentials might have been compromised. You should reset your password and contact an administrator.
This email was generated by a user with the IP {{.remoteIP}}.
Please contact an administrator if you did not initiate the process.
`