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
parent
9e05066097
commit
bfd5d66ed8
|
@ -16,6 +16,7 @@ verify their identity.
|
|||
```yaml
|
||||
notifier:
|
||||
disable_startup_check: false
|
||||
template_path: /path/to/templates/folder
|
||||
filesystem: {}
|
||||
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
|
||||
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
|
||||
|
||||
The [filesystem](filesystem.md) provider.
|
||||
|
|
|
@ -31,6 +31,7 @@ type NotifierConfiguration struct {
|
|||
DisableStartupCheck bool `koanf:"disable_startup_check"`
|
||||
FileSystem *FileSystemNotifierConfiguration `koanf:"filesystem"`
|
||||
SMTP *SMTPNotifierConfiguration `koanf:"smtp"`
|
||||
TemplatePath string `koanf:"template_path"`
|
||||
}
|
||||
|
||||
// DefaultSMTPNotifierConfiguration represents default configuration parameters for the SMTP notifier.
|
||||
|
|
|
@ -54,6 +54,9 @@ const (
|
|||
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 " +
|
||||
"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 "
|
||||
errFmtNotifierSMTPNotConfigured = "notifier: smtp: option '%s' is required"
|
||||
)
|
||||
|
@ -412,6 +415,7 @@ var ValidKeys = []string{
|
|||
"notifier.smtp.tls.minimum_version",
|
||||
"notifier.smtp.tls.skip_verify",
|
||||
"notifier.smtp.tls.server_name",
|
||||
"notifier.template_path",
|
||||
|
||||
// Regulation Keys.
|
||||
"regulation.max_retries",
|
||||
|
|
|
@ -2,8 +2,12 @@ package validator
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/templates"
|
||||
)
|
||||
|
||||
// ValidateNotifier validates and update notifier configuration.
|
||||
|
@ -27,6 +31,54 @@ func ValidateNotifier(config *schema.NotifierConfiguration, validator *schema.St
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -30,6 +30,7 @@ func identityRetrieverFromStorage(ctx *middlewares.AutheliaCtx) (*session.Identi
|
|||
return &session.Identity{
|
||||
Username: requestBody.Username,
|
||||
Email: details.Emails[0],
|
||||
DisplayName: details.DisplayName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/templates"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
|
@ -19,6 +21,8 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
username := *userSession.PasswordResetUsername
|
||||
|
||||
var requestBody resetPasswordStep2RequestBody
|
||||
err := ctx.ParseBody(&requestBody)
|
||||
|
||||
|
@ -32,7 +36,7 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
err = ctx.Providers.UserProvider.UpdatePassword(*userSession.PasswordResetUsername, requestBody.Password)
|
||||
err = ctx.Providers.UserProvider.UpdatePassword(username, requestBody.Password)
|
||||
|
||||
if err != nil {
|
||||
switch {
|
||||
|
@ -46,7 +50,7 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
|
|||
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.
|
||||
userSession.PasswordResetUsername = nil
|
||||
|
@ -57,5 +61,69 @@ func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,9 +82,11 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs, delayFunc Tim
|
|||
"title": args.MailTitle,
|
||||
"url": link,
|
||||
"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 {
|
||||
ctx.Error(err, messageOperationFailed)
|
||||
|
@ -95,9 +97,10 @@ func IdentityVerificationStart(args IdentityVerificationStartArgs, delayFunc Tim
|
|||
bufText := new(bytes.Buffer)
|
||||
textParams := map[string]interface{}{
|
||||
"url": link,
|
||||
"displayName": identity.DisplayName,
|
||||
}
|
||||
|
||||
err = templates.PlainTextEmailTemplate.Execute(bufText, textParams)
|
||||
err = templates.PlainTextEmailTemplateStep1.Execute(bufText, textParams)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(err, messageOperationFailed)
|
||||
|
|
|
@ -57,6 +57,7 @@ type UserSession struct {
|
|||
type Identity struct {
|
||||
Username string
|
||||
Email string
|
||||
DisplayName string
|
||||
}
|
||||
|
||||
func newRedisLogger() *redisLogger {
|
||||
|
|
|
@ -6,6 +6,8 @@ services:
|
|||
volumes:
|
||||
- ./example/compose/nginx/portal/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./example/compose/nginx/portal/ssl:/etc/ssl
|
||||
ports:
|
||||
- 8080:8080
|
||||
networks:
|
||||
authelianet:
|
||||
aliases:
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package templates
|
||||
|
||||
// Template File Names.
|
||||
const (
|
||||
TemplateNameStep1 = "PasswordResetStep1"
|
||||
TemplateNameStep2 = "PasswordResetStep2"
|
||||
)
|
|
@ -4,19 +4,19 @@ import (
|
|||
"text/template"
|
||||
)
|
||||
|
||||
// HTMLEmailTemplate the template of email that the user will receive for identity verification.
|
||||
var HTMLEmailTemplate *template.Template
|
||||
// HTMLEmailTemplateStep1 the template of email that the user will receive for identity verification.
|
||||
var HTMLEmailTemplateStep1 *template.Template
|
||||
|
||||
func init() {
|
||||
t, err := template.New("html_email_template").Parse(emailHTMLContent)
|
||||
t, err := template.New("html_email_template").Parse(emailHTMLContentStep1)
|
||||
if err != nil {
|
||||
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">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
||||
|
@ -93,25 +93,28 @@ const emailHTMLContent = `
|
|||
}
|
||||
|
||||
a {
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
.link {
|
||||
color: #0645AD;
|
||||
}
|
||||
|
||||
h1 {
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.button {
|
||||
color: #ffffff;
|
||||
padding: 15px 30px;
|
||||
border-radius: 10px;
|
||||
background: rgb(25, 118, 210);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: rgb(25, 118, 210);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
/*STYLES*/
|
||||
table[class=full] {
|
||||
width: 100%;
|
||||
|
@ -311,6 +314,12 @@ const emailHTMLContent = `
|
|||
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}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-family: Helvetica, arial, sans-serif; font-size: 16px; color: #333333; text-align:center; line-height: 30px;"
|
||||
st-title="fulltext-content">
|
||||
|
@ -333,17 +342,13 @@ const emailHTMLContent = `
|
|||
<a href="{{.url}}" class="button">{{.button}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- spacing -->
|
||||
<tr>
|
||||
<td width="100%" height="20"
|
||||
style="font-size:1px; line-height:1px; mso-line-height-rule: exactly;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<!-- End of spacing -->
|
||||
<tr>
|
||||
<td style="word-break: break-word; overflow-wrap: break-word; text-align:center; line-height: 30px;">
|
||||
<a href="{{.url}}" class="link">{{.url}}</a>
|
||||
|
@ -354,13 +359,6 @@ const emailHTMLContent = `
|
|||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Spacing -->
|
||||
<tr>
|
||||
<td height="20"
|
||||
style="font-size:1px; line-height:1px; mso-line-height-rule: exactly;">
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Spacing -->
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
@ -417,6 +415,19 @@ const emailHTMLContent = `
|
|||
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;">
|
||||
</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>
|
|
@ -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;">
|
||||
</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;">
|
||||
</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;"> </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;">
|
||||
</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;">
|
||||
</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;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="550" align="center" height="1" bgcolor="#d1d1d1"
|
||||
style="font-size:1px; line-height:1px;"> </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" height="30" style="font-size:1px; line-height:1px;"> </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;">
|
||||
</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>
|
||||
`
|
|
@ -4,23 +4,25 @@ import (
|
|||
"text/template"
|
||||
)
|
||||
|
||||
// PlainTextEmailTemplate the template of email that the user will receive for identity verification.
|
||||
var PlainTextEmailTemplate *template.Template
|
||||
// PlainTextEmailTemplateStep1 the template of email that the user will receive for identity verification.
|
||||
var PlainTextEmailTemplateStep1 *template.Template
|
||||
|
||||
func init() {
|
||||
t, err := template.New("text_email_template").Parse(emailPlainTextContent)
|
||||
t, err := template.New("text_email_template").Parse(emailPlainTextContentStep1)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
PlainTextEmailTemplate = t
|
||||
PlainTextEmailTemplateStep1 = t
|
||||
}
|
||||
|
||||
const emailPlainTextContent = `
|
||||
const emailPlainTextContentStep1 = `
|
||||
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.
|
||||
|
||||
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.
|
||||
`
|
|
@ -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.
|
||||
`
|
Loading…
Reference in New Issue