@@ -65,6 +65,93 @@ password will not change. |
| 65 | 65 | `))) |
| 66 | 66 | ) |
| 67 | 67 | |
| 68 | +// noticeBodies maps a notice kind to its (subject, plaintext, html) bodies. |
| 69 | +// Each body is run through text/template — only the canonical {{.SiteName}} |
| 70 | +// and {{.Username}} variables are exposed. |
| 71 | +var noticeBodies = map[string]struct { |
| 72 | + Subject, Text, HTML string |
| 73 | +}{ |
| 74 | + "2fa_enabled": { |
| 75 | + Subject: "Two-factor authentication enabled on your {{.SiteName}} account", |
| 76 | + Text: `Hi {{.Username}}, |
| 77 | + |
| 78 | +Two-factor authentication has just been enabled on your {{.SiteName}} account. |
| 79 | +If this wasn't you, sign in immediately and disable 2FA, then change your password. |
| 80 | +Recovery codes are stored on the security settings page — keep them somewhere safe.`, |
| 81 | + HTML: `<p>Hi <strong>{{.Username}}</strong>,</p> |
| 82 | +<p>Two-factor authentication has just been enabled on your {{.SiteName}} account.</p> |
| 83 | +<p>If this wasn't you, sign in immediately and disable 2FA, then change your password.</p> |
| 84 | +<p>Recovery codes are stored on the security settings page — keep them somewhere safe.</p>`, |
| 85 | + }, |
| 86 | + "2fa_disabled": { |
| 87 | + Subject: "Two-factor authentication disabled on your {{.SiteName}} account", |
| 88 | + Text: `Hi {{.Username}}, |
| 89 | + |
| 90 | +Two-factor authentication has been disabled on your {{.SiteName}} account. |
| 91 | +If this wasn't you, sign in immediately and re-enable 2FA, then change your password.`, |
| 92 | + HTML: `<p>Hi <strong>{{.Username}}</strong>,</p> |
| 93 | +<p>Two-factor authentication has been disabled on your {{.SiteName}} account.</p> |
| 94 | +<p>If this wasn't you, sign in immediately and re-enable 2FA, then change your password.</p>`, |
| 95 | + }, |
| 96 | + "recovery_regenerated": { |
| 97 | + Subject: "New recovery codes generated for your {{.SiteName}} account", |
| 98 | + Text: `Hi {{.Username}}, |
| 99 | + |
| 100 | +Your {{.SiteName}} recovery codes were regenerated. Any previous codes |
| 101 | +no longer work. Store the new codes somewhere safe. |
| 102 | + |
| 103 | +If this wasn't you, sign in immediately and review your security settings.`, |
| 104 | + HTML: `<p>Hi <strong>{{.Username}}</strong>,</p> |
| 105 | +<p>Your {{.SiteName}} recovery codes were regenerated. Any previous codes no longer work. Store the new codes somewhere safe.</p> |
| 106 | +<p>If this wasn't you, sign in immediately and review your security settings.</p>`, |
| 107 | + }, |
| 108 | + "admin_cleared_2fa": { |
| 109 | + Subject: "Two-factor authentication cleared by support — {{.SiteName}}", |
| 110 | + Text: `Hi {{.Username}}, |
| 111 | + |
| 112 | +A {{.SiteName}} administrator cleared two-factor authentication from your |
| 113 | +account, typically as part of a support request you initiated. |
| 114 | + |
| 115 | +Sign in and re-enable 2FA at /settings/security/2fa/enable as soon as you can. |
| 116 | + |
| 117 | +If you did NOT request this, sign in immediately and reset your password, |
| 118 | +then contact support.`, |
| 119 | + HTML: `<p>Hi <strong>{{.Username}}</strong>,</p> |
| 120 | +<p>A {{.SiteName}} administrator cleared two-factor authentication from your account, typically as part of a support request you initiated.</p> |
| 121 | +<p>Sign in and re-enable 2FA as soon as you can.</p> |
| 122 | +<p>If you did NOT request this, sign in immediately and reset your password, then contact support.</p>`, |
| 123 | + }, |
| 124 | +} |
| 125 | + |
| 126 | +// NoticeMessage builds a 2FA / security state-change notice for kind. The |
| 127 | +// kind names match audit-log Action values where applicable. |
| 128 | +func NoticeMessage(b Branding, to, username, kind string) (Message, error) { |
| 129 | + body, ok := noticeBodies[kind] |
| 130 | + if !ok { |
| 131 | + return Message{}, fmt.Errorf("email: unknown notice kind %q", kind) |
| 132 | + } |
| 133 | + data := struct{ SiteName, Username string }{b.SiteName, username} |
| 134 | + subj, err := renderText(textTemplate.Must(textTemplate.New("subj").Parse(body.Subject)), data) |
| 135 | + if err != nil { |
| 136 | + return Message{}, err |
| 137 | + } |
| 138 | + txt, err := renderText(textTemplate.Must(textTemplate.New("txt").Parse(body.Text)), data) |
| 139 | + if err != nil { |
| 140 | + return Message{}, err |
| 141 | + } |
| 142 | + html, err := renderHTML(template.Must(template.New("html").Parse(body.HTML)), data) |
| 143 | + if err != nil { |
| 144 | + return Message{}, err |
| 145 | + } |
| 146 | + return Message{ |
| 147 | + From: b.From, |
| 148 | + To: to, |
| 149 | + Subject: strings.TrimSpace(subj), |
| 150 | + Text: txt, |
| 151 | + HTML: html, |
| 152 | + }, nil |
| 153 | +} |
| 154 | + |
| 68 | 155 | // VerifyMessage builds the email-verification message. |
| 69 | 156 | func VerifyMessage(b Branding, to, username, token string) (Message, error) { |
| 70 | 157 | data := struct{ SiteName, BaseURL, Username, Token string }{b.SiteName, b.BaseURL, username, token} |