init push
This commit is contained in:
559
internal/service/auth_email_template.go
Normal file
559
internal/service/auth_email_template.go
Normal file
@@ -0,0 +1,559 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
filemanager "wucher/internal/domain/file_manager"
|
||||
mastersettings "wucher/internal/domain/master_settings"
|
||||
"wucher/internal/queue"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultEmailBrandingCompanyName = "Wucher"
|
||||
authEmailLogoCID = "branding-email-logo"
|
||||
authEmailLogoFilename = "email-logo.png"
|
||||
authEmailLogoFetchTimeout = 10 * time.Second
|
||||
authEmailLogoMaxBytes = 5 << 20
|
||||
authEmailLogoPresignTTL = time.Minute
|
||||
)
|
||||
|
||||
type AuthEmailBranding struct {
|
||||
CompanyName string
|
||||
LogoContentID string
|
||||
LogoAttachment *queue.EmailAttachment
|
||||
}
|
||||
|
||||
type BuiltAuthEmail struct {
|
||||
Subject string
|
||||
TextBody string
|
||||
HTMLBody string
|
||||
Attachments []queue.EmailAttachment
|
||||
}
|
||||
|
||||
type AuthEmailBrandingResolver interface {
|
||||
ResolveEmailBranding(ctx context.Context) AuthEmailBranding
|
||||
}
|
||||
|
||||
type authEmailBrandingResolver struct {
|
||||
masterSvc mastersettings.Service
|
||||
fileSvc filemanager.Service
|
||||
storage filemanager.ObjectStorage
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewAuthEmailBrandingResolver(masterSvc mastersettings.Service, fileSvc filemanager.Service, storage filemanager.ObjectStorage) AuthEmailBrandingResolver {
|
||||
return &authEmailBrandingResolver{
|
||||
masterSvc: masterSvc,
|
||||
fileSvc: fileSvc,
|
||||
storage: storage,
|
||||
httpClient: &http.Client{
|
||||
Timeout: authEmailLogoFetchTimeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *authEmailBrandingResolver) ResolveEmailBranding(ctx context.Context) AuthEmailBranding {
|
||||
branding := AuthEmailBranding{
|
||||
CompanyName: defaultEmailBrandingCompanyName,
|
||||
}
|
||||
if r == nil || r.masterSvc == nil {
|
||||
return branding
|
||||
}
|
||||
|
||||
rows, _, err := r.masterSvc.List(ctx, "", "-updated_at", 1, 0)
|
||||
if err != nil || len(rows) == 0 {
|
||||
return branding
|
||||
}
|
||||
row := rows[0]
|
||||
if companyName := strings.TrimSpace(row.CompanyName); companyName != "" {
|
||||
branding.CompanyName = companyName
|
||||
}
|
||||
if len(row.LogoFileID) == 0 {
|
||||
return branding
|
||||
}
|
||||
|
||||
attachment, err := r.resolveInlineLogoAttachment(ctx, row.LogoFileID)
|
||||
if err != nil || attachment == nil {
|
||||
return branding
|
||||
}
|
||||
branding.LogoContentID = attachment.ContentID
|
||||
branding.LogoAttachment = attachment
|
||||
return branding
|
||||
}
|
||||
|
||||
func (r *authEmailBrandingResolver) resolveInlineLogoAttachment(ctx context.Context, fileID []byte) (*queue.EmailAttachment, error) {
|
||||
if r == nil || r.fileSvc == nil {
|
||||
return nil, errors.New("file manager service is not configured")
|
||||
}
|
||||
if r.storage == nil {
|
||||
return nil, errors.New("object storage is not configured")
|
||||
}
|
||||
|
||||
fileRow, err := r.fileSvc.GetFileByID(ctx, fileID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get managed branding file: %w", err)
|
||||
}
|
||||
if fileRow == nil {
|
||||
return nil, errors.New("managed branding file not found")
|
||||
}
|
||||
if !strings.EqualFold(strings.TrimSpace(fileRow.Status), filemanager.FileStatusReady) {
|
||||
return nil, errors.New("managed branding file is not ready")
|
||||
}
|
||||
|
||||
objectKey, declaredContentType, err := selectAuthEmailLogoVariant(fileRow)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
presigned, err := r.storage.PresignGetObject(ctx, objectKey, authEmailLogoPresignTTL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("presign managed branding object: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, presigned, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build object request: %w", err)
|
||||
}
|
||||
resp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("download logo object: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("object storage returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
limited := io.LimitReader(resp.Body, authEmailLogoMaxBytes+1)
|
||||
body, err := io.ReadAll(limited)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read logo object body: %w", err)
|
||||
}
|
||||
if len(body) == 0 {
|
||||
return nil, errors.New("logo object body is empty")
|
||||
}
|
||||
if len(body) > authEmailLogoMaxBytes {
|
||||
return nil, errors.New("logo object exceeds max allowed size")
|
||||
}
|
||||
|
||||
contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
|
||||
if contentType == "" {
|
||||
contentType = declaredContentType
|
||||
}
|
||||
if !isAuthEmailSafeImageContentType(contentType) {
|
||||
return nil, errors.New("managed branding asset is not email-safe")
|
||||
}
|
||||
|
||||
return &queue.EmailAttachment{
|
||||
Filename: authEmailLogoFilename,
|
||||
ContentType: normalizeAuthEmailImageContentType(contentType),
|
||||
ContentID: authEmailLogoCID,
|
||||
Content: body,
|
||||
Inline: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func selectAuthEmailLogoVariant(fileRow *filemanager.File) (string, string, error) {
|
||||
if fileRow == nil {
|
||||
return "", "", errors.New("managed branding file is missing")
|
||||
}
|
||||
if fileRow.ThumbnailObjectKey != nil {
|
||||
thumbKey := strings.TrimSpace(*fileRow.ThumbnailObjectKey)
|
||||
thumbType := strings.TrimSpace(authEmailPtrStringValue(fileRow.ThumbnailMimeType))
|
||||
if thumbKey != "" && isAuthEmailSafeImageContentType(thumbType) {
|
||||
return thumbKey, thumbType, nil
|
||||
}
|
||||
}
|
||||
originalKey := strings.TrimSpace(fileRow.ObjectKey)
|
||||
originalType := strings.TrimSpace(fileRow.MimeType)
|
||||
if originalKey != "" && isAuthEmailSafeImageContentType(originalType) {
|
||||
return originalKey, originalType, nil
|
||||
}
|
||||
return "", "", errors.New("managed branding file has no email-safe variant")
|
||||
}
|
||||
|
||||
func isAuthEmailSafeImageContentType(contentType string) bool {
|
||||
contentType = normalizeAuthEmailImageContentType(contentType)
|
||||
switch contentType {
|
||||
case "image/png", "image/jpeg", "image/gif":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeAuthEmailImageContentType(contentType string) string {
|
||||
contentType = strings.ToLower(strings.TrimSpace(contentType))
|
||||
if idx := strings.Index(contentType, ";"); idx >= 0 {
|
||||
contentType = strings.TrimSpace(contentType[:idx])
|
||||
}
|
||||
if contentType == "image/jpg" {
|
||||
return "image/jpeg"
|
||||
}
|
||||
return contentType
|
||||
}
|
||||
|
||||
func authEmailPtrStringValue(v *string) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
type AuthEmailTemplateContent struct {
|
||||
Subject string
|
||||
Greeting string
|
||||
Title string
|
||||
Message []string
|
||||
Code string
|
||||
CTALabel string
|
||||
CTAURL string
|
||||
HelpText string
|
||||
FooterText string
|
||||
}
|
||||
|
||||
func buildAuthEmailTemplate(ctx context.Context, resolver AuthEmailBrandingResolver, content AuthEmailTemplateContent) BuiltAuthEmail {
|
||||
subject := strings.TrimSpace(content.Subject)
|
||||
greeting := strings.TrimSpace(content.Greeting)
|
||||
title := strings.TrimSpace(content.Title)
|
||||
ctaLabel := strings.TrimSpace(content.CTALabel)
|
||||
ctaURL := strings.TrimSpace(content.CTAURL)
|
||||
helpText := strings.TrimSpace(content.HelpText)
|
||||
footerText := strings.TrimSpace(content.FooterText)
|
||||
code := strings.TrimSpace(content.Code)
|
||||
|
||||
messageParts := make([]string, 0, len(content.Message))
|
||||
for i := range content.Message {
|
||||
part := strings.TrimSpace(content.Message[i])
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
messageParts = append(messageParts, part)
|
||||
}
|
||||
|
||||
textLines := make([]string, 0, len(messageParts)+10)
|
||||
if greeting != "" {
|
||||
textLines = append(textLines, greeting, "")
|
||||
}
|
||||
textLines = append(textLines, title, "")
|
||||
textLines = append(textLines, messageParts...)
|
||||
if code != "" {
|
||||
textLines = append(textLines, code)
|
||||
}
|
||||
if ctaURL != "" {
|
||||
textLines = append(textLines, "", ctaLabel+":", ctaURL)
|
||||
}
|
||||
if helpText != "" {
|
||||
textLines = append(textLines, "", helpText)
|
||||
}
|
||||
if footerText != "" {
|
||||
textLines = append(textLines, "", footerText)
|
||||
}
|
||||
|
||||
branding := AuthEmailBranding{CompanyName: defaultEmailBrandingCompanyName}
|
||||
if resolver != nil {
|
||||
branding = resolver.ResolveEmailBranding(ctx)
|
||||
if strings.TrimSpace(branding.CompanyName) == "" {
|
||||
branding.CompanyName = defaultEmailBrandingCompanyName
|
||||
}
|
||||
}
|
||||
|
||||
var attachments []queue.EmailAttachment
|
||||
if branding.LogoAttachment != nil {
|
||||
attachments = append(attachments, cloneAuthEmailAttachment(*branding.LogoAttachment))
|
||||
}
|
||||
|
||||
return BuiltAuthEmail{
|
||||
Subject: subject,
|
||||
TextBody: strings.Join(textLines, "\n"),
|
||||
HTMLBody: renderAuthEmailHTML(branding, AuthEmailTemplateContent{Subject: subject, Greeting: greeting, Title: title, Message: messageParts, Code: code, CTALabel: ctaLabel, CTAURL: ctaURL, HelpText: helpText, FooterText: footerText}),
|
||||
Attachments: attachments,
|
||||
}
|
||||
}
|
||||
|
||||
func cloneAuthEmailAttachment(attachment queue.EmailAttachment) queue.EmailAttachment {
|
||||
attachment.Content = append([]byte(nil), attachment.Content...)
|
||||
return attachment
|
||||
}
|
||||
|
||||
// emailFontStack is the cross-client system font stack used in all email cells.
|
||||
const emailFontStack = `-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif`
|
||||
|
||||
// emailCell writes an opening <tr><td> with the given inline style.
|
||||
func emailCell(b *strings.Builder, style string) {
|
||||
b.WriteString(`<tr><td style="`)
|
||||
b.WriteString(style)
|
||||
b.WriteString(`">`)
|
||||
}
|
||||
|
||||
func emailCellClose(b *strings.Builder) {
|
||||
b.WriteString(`</td></tr>`)
|
||||
}
|
||||
|
||||
// emailSpacer writes a zero-content spacer row.
|
||||
func emailSpacer(b *strings.Builder, height string) {
|
||||
b.WriteString(`<tr><td style="height:`)
|
||||
b.WriteString(height)
|
||||
b.WriteString(`;font-size:0;line-height:0;"> </td></tr>`)
|
||||
}
|
||||
|
||||
// emailDivider writes a 1px horizontal rule as a nested table (Outlook-safe).
|
||||
func emailDivider(b *strings.Builder) {
|
||||
b.WriteString(`<tr><td style="padding:0 32px;">`)
|
||||
b.WriteString(`<table role="presentation" width="100%" cellspacing="0" cellpadding="0">`)
|
||||
b.WriteString(`<tr><td style="height:1px;background-color:#e4e4e7;font-size:0;line-height:0;"> </td></tr>`)
|
||||
b.WriteString(`</table></td></tr>`)
|
||||
}
|
||||
|
||||
func renderAuthEmailHTML(branding AuthEmailBranding, content AuthEmailTemplateContent) string {
|
||||
subject := html.EscapeString(strings.TrimSpace(content.Subject))
|
||||
greeting := html.EscapeString(strings.TrimSpace(content.Greeting))
|
||||
title := html.EscapeString(strings.TrimSpace(content.Title))
|
||||
ctaLabel := html.EscapeString(strings.TrimSpace(content.CTALabel))
|
||||
ctaURL := html.EscapeString(strings.TrimSpace(content.CTAURL))
|
||||
helpText := html.EscapeString(strings.TrimSpace(content.HelpText))
|
||||
footerText := html.EscapeString(strings.TrimSpace(content.FooterText))
|
||||
code := html.EscapeString(strings.TrimSpace(content.Code))
|
||||
companyName := html.EscapeString(strings.TrimSpace(branding.CompanyName))
|
||||
logoContentID := html.EscapeString(strings.TrimSpace(branding.LogoContentID))
|
||||
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(`<!DOCTYPE html><html lang="en">`)
|
||||
b.WriteString(`<head>`)
|
||||
b.WriteString(`<meta charset="UTF-8">`)
|
||||
b.WriteString(`<meta name="viewport" content="width=device-width,initial-scale=1.0">`)
|
||||
b.WriteString(`<meta http-equiv="X-UA-Compatible" content="IE=edge">`)
|
||||
b.WriteString(`<title>`)
|
||||
b.WriteString(subject)
|
||||
b.WriteString(`</title>`)
|
||||
b.WriteString(`<style>`)
|
||||
b.WriteString(`@media only screen and (max-width:620px){`)
|
||||
b.WriteString(`.em-cell{padding-left:20px!important;padding-right:20px!important;}`)
|
||||
b.WriteString(`.em-cta a{display:block!important;text-align:center!important;}`)
|
||||
b.WriteString(`}`)
|
||||
b.WriteString(`</style>`)
|
||||
b.WriteString(`</head>`)
|
||||
|
||||
b.WriteString(`<body style="margin:0;padding:0;background-color:#ffffff;`)
|
||||
b.WriteString(`-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;">`)
|
||||
|
||||
b.WriteString(`<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;font-size:1px;color:#ffffff;">`)
|
||||
b.WriteString(subject)
|
||||
b.WriteString(strings.Repeat(" ‌ ", 50))
|
||||
b.WriteString(`</div>`)
|
||||
|
||||
b.WriteString(`<table role="presentation" width="100%" cellspacing="0" cellpadding="0" bgcolor="#ffffff">`)
|
||||
b.WriteString(`<tr><td align="center" style="padding:0;">`)
|
||||
|
||||
b.WriteString(`<table role="presentation" cellspacing="0" cellpadding="0" style="width:100%;max-width:600px;">`)
|
||||
b.WriteString(`<tr><td style="height:4px;background-color:#18181b;font-size:0;line-height:0;"> </td></tr>`)
|
||||
b.WriteString(`</table>`)
|
||||
|
||||
b.WriteString(`<table role="presentation" cellspacing="0" cellpadding="0" style="width:100%;max-width:600px;">`)
|
||||
|
||||
emailSpacer(&b, "44px")
|
||||
|
||||
b.WriteString(`<tr><td class="em-cell" align="left" style="padding:0 32px 36px;">`)
|
||||
if logoContentID != "" {
|
||||
b.WriteString(`<img src="cid:`)
|
||||
b.WriteString(logoContentID)
|
||||
b.WriteString(`" alt="`)
|
||||
b.WriteString(companyName)
|
||||
b.WriteString(`" height="36" style="display:block;height:36px;width:auto;max-width:200px;border:0;outline:none;text-decoration:none;">`)
|
||||
} else {
|
||||
b.WriteString(`<span style="font-size:16px;font-weight:700;color:#18181b;font-family:`)
|
||||
b.WriteString(emailFontStack)
|
||||
b.WriteString(`;letter-spacing:-0.3px;">`)
|
||||
b.WriteString(companyName)
|
||||
b.WriteString(`</span>`)
|
||||
}
|
||||
b.WriteString(`</td></tr>`)
|
||||
|
||||
emailDivider(&b)
|
||||
emailSpacer(&b, "40px")
|
||||
|
||||
if greeting != "" {
|
||||
b.WriteString(`<tr><td class="em-cell" style="padding:0 32px 6px;font-size:14px;line-height:1.6;color:#71717a;font-family:`)
|
||||
b.WriteString(emailFontStack)
|
||||
b.WriteString(`;">`)
|
||||
b.WriteString(greeting)
|
||||
b.WriteString(`</td></tr>`)
|
||||
}
|
||||
|
||||
b.WriteString(`<tr><td class="em-cell" style="padding:0 32px 20px;font-size:26px;line-height:1.25;font-weight:700;`)
|
||||
b.WriteString(`letter-spacing:-0.5px;color:#18181b;font-family:`)
|
||||
b.WriteString(emailFontStack)
|
||||
b.WriteString(`;">`)
|
||||
b.WriteString(title)
|
||||
b.WriteString(`</td></tr>`)
|
||||
|
||||
for i := range content.Message {
|
||||
msg := html.EscapeString(strings.TrimSpace(content.Message[i]))
|
||||
if msg == "" {
|
||||
continue
|
||||
}
|
||||
b.WriteString(`<tr><td class="em-cell" style="padding:0 32px 12px;font-size:15px;line-height:1.75;color:#3f3f46;font-family:`)
|
||||
b.WriteString(emailFontStack)
|
||||
b.WriteString(`;">`)
|
||||
b.WriteString(msg)
|
||||
b.WriteString(`</td></tr>`)
|
||||
}
|
||||
if code != "" {
|
||||
b.WriteString(`<tr><td class="em-cell" style="padding:4px 32px 16px;">`)
|
||||
b.WriteString(`<span style="display:inline-block;background-color:#f4f4f5;border:1px solid #e4e4e7;`)
|
||||
b.WriteString(`border-radius:10px;padding:12px 16px;font-size:28px;line-height:1;letter-spacing:6px;`)
|
||||
b.WriteString(`color:#111827;font-weight:700;font-family:`)
|
||||
b.WriteString(emailFontStack)
|
||||
b.WriteString(`;">`)
|
||||
b.WriteString(code)
|
||||
b.WriteString(`</span></td></tr>`)
|
||||
}
|
||||
|
||||
if ctaURL != "" && ctaLabel != "" {
|
||||
emailSpacer(&b, "20px")
|
||||
|
||||
b.WriteString(`<tr><td class="em-cell em-cta" align="left" style="padding:0 32px 16px;">`)
|
||||
b.WriteString(`<!--[if mso]>`)
|
||||
b.WriteString(`<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word"`)
|
||||
b.WriteString(` href="`)
|
||||
b.WriteString(ctaURL)
|
||||
b.WriteString(`" style="height:44px;v-text-anchor:middle;width:180px;" arcsize="18%" stroke="f" fillcolor="#18181b">`)
|
||||
b.WriteString(`<w:anchorlock/>`)
|
||||
b.WriteString(`<center style="color:#ffffff;font-family:sans-serif;font-size:14px;font-weight:700;">`)
|
||||
b.WriteString(ctaLabel)
|
||||
b.WriteString(`</center></v:roundrect><![endif]-->`)
|
||||
b.WriteString(`<!--[if !mso]><!-->`)
|
||||
b.WriteString(`<a href="`)
|
||||
b.WriteString(ctaURL)
|
||||
b.WriteString(`" target="_blank" style="display:inline-block;background-color:#18181b;color:#ffffff;`)
|
||||
b.WriteString(`font-family:`)
|
||||
b.WriteString(emailFontStack)
|
||||
b.WriteString(`;font-size:14px;font-weight:600;letter-spacing:0.1px;line-height:1;`)
|
||||
b.WriteString(`text-decoration:none;padding:13px 26px;border-radius:8px;`)
|
||||
b.WriteString(`-webkit-text-size-adjust:none;">`)
|
||||
b.WriteString(ctaLabel)
|
||||
b.WriteString(`</a>`)
|
||||
b.WriteString(`<!--<![endif]-->`)
|
||||
b.WriteString(`</td></tr>`)
|
||||
|
||||
b.WriteString(`<tr><td class="em-cell" style="padding:0 32px 4px;font-size:12px;line-height:1.6;color:#a1a1aa;font-family:`)
|
||||
b.WriteString(emailFontStack)
|
||||
b.WriteString(`;">Or copy and paste this link:</td></tr>`)
|
||||
b.WriteString(`<tr><td class="em-cell" style="padding:0 32px 36px;font-size:12px;line-height:1.6;color:#71717a;`)
|
||||
b.WriteString(`word-break:break-all;font-family:`)
|
||||
b.WriteString(emailFontStack)
|
||||
b.WriteString(`;">`)
|
||||
b.WriteString(ctaURL)
|
||||
b.WriteString(`</td></tr>`)
|
||||
}
|
||||
|
||||
emailDivider(&b)
|
||||
emailSpacer(&b, "24px")
|
||||
|
||||
if helpText != "" {
|
||||
b.WriteString(`<tr><td class="em-cell" style="padding:0 32px 32px;font-size:13px;line-height:1.7;color:#71717a;font-family:`)
|
||||
b.WriteString(emailFontStack)
|
||||
b.WriteString(`;">`)
|
||||
b.WriteString(helpText)
|
||||
b.WriteString(`</td></tr>`)
|
||||
}
|
||||
|
||||
b.WriteString(`<tr><td class="em-cell" style="padding:20px 32px 0;border-top:1px solid #e4e4e7;">`)
|
||||
b.WriteString(`<span style="font-size:12px;line-height:1.7;color:#a1a1aa;font-family:`)
|
||||
b.WriteString(emailFontStack)
|
||||
b.WriteString(`;">`)
|
||||
b.WriteString(companyName)
|
||||
b.WriteString(`</span>`)
|
||||
if footerText != "" {
|
||||
b.WriteString(`<br><span style="font-size:12px;line-height:1.7;color:#a1a1aa;font-family:`)
|
||||
b.WriteString(emailFontStack)
|
||||
b.WriteString(`;">`)
|
||||
b.WriteString(footerText)
|
||||
b.WriteString(`</span>`)
|
||||
}
|
||||
b.WriteString(`</td></tr>`)
|
||||
|
||||
emailSpacer(&b, "48px")
|
||||
b.WriteString(`</table>`)
|
||||
b.WriteString(`</td></tr>`)
|
||||
b.WriteString(`</table>`)
|
||||
b.WriteString(`</body></html>`)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatEmailTTLMinutes(ttl time.Duration, fallback int) int {
|
||||
minutes := int(ttl.Minutes())
|
||||
if minutes < 1 {
|
||||
return fallback
|
||||
}
|
||||
return minutes
|
||||
}
|
||||
|
||||
func buildPasswordResetEmail(ctx context.Context, resolver AuthEmailBrandingResolver, userName string, link string, ttl time.Duration) BuiltAuthEmail {
|
||||
minutes := formatEmailTTLMinutes(ttl, 20)
|
||||
greeting := ""
|
||||
if name := strings.TrimSpace(userName); name != "" {
|
||||
greeting = "Hi " + name + ","
|
||||
}
|
||||
return buildAuthEmailTemplate(ctx, resolver, AuthEmailTemplateContent{
|
||||
Subject: "Reset your password",
|
||||
Greeting: greeting,
|
||||
Title: "Reset your password",
|
||||
Message: []string{
|
||||
"We received a request to reset your password.",
|
||||
"Click the button below to choose a new password. This link expires in " + strconv.Itoa(minutes) + " minutes and can only be used once.",
|
||||
},
|
||||
CTALabel: "Reset Password",
|
||||
CTAURL: link,
|
||||
HelpText: "If you did not request this, you can safely ignore this email. Your password will remain unchanged.",
|
||||
FooterText: "For your security, never share this link with anyone.",
|
||||
})
|
||||
}
|
||||
|
||||
func buildSecurityPINResetEmail(ctx context.Context, resolver AuthEmailBrandingResolver, userName string, link string, ttl time.Duration) BuiltAuthEmail {
|
||||
minutes := formatEmailTTLMinutes(ttl, 20)
|
||||
greeting := ""
|
||||
if name := strings.TrimSpace(userName); name != "" {
|
||||
greeting = "Hi " + name + ","
|
||||
}
|
||||
return buildAuthEmailTemplate(ctx, resolver, AuthEmailTemplateContent{
|
||||
Subject: "Reset your security PIN",
|
||||
Greeting: greeting,
|
||||
Title: "Reset your security PIN",
|
||||
Message: []string{
|
||||
"We received a request to reset your security PIN.",
|
||||
"Click the button below to set a new 6-digit PIN. This link expires in " + strconv.Itoa(minutes) + " minutes and can only be used once.",
|
||||
},
|
||||
CTALabel: "Reset Security PIN",
|
||||
CTAURL: link,
|
||||
HelpText: "If you did not request this, you can safely ignore this email. Your PIN will remain unchanged.",
|
||||
FooterText: "For your security, never share this link or your PIN with anyone.",
|
||||
})
|
||||
}
|
||||
|
||||
func buildLoginOTPEmail(ctx context.Context, resolver AuthEmailBrandingResolver, userName, otpCode string, ttl time.Duration) BuiltAuthEmail {
|
||||
minutes := formatEmailTTLMinutes(ttl, 10)
|
||||
greeting := ""
|
||||
if name := strings.TrimSpace(userName); name != "" {
|
||||
greeting = "Hi " + name + ","
|
||||
}
|
||||
return buildAuthEmailTemplate(ctx, resolver, AuthEmailTemplateContent{
|
||||
Subject: "Your login verification code",
|
||||
Greeting: greeting,
|
||||
Title: "Verify your login",
|
||||
Message: []string{
|
||||
"Use this one-time code to complete your login:",
|
||||
"This code expires in " + strconv.Itoa(minutes) + " minutes.",
|
||||
},
|
||||
Code: strings.TrimSpace(otpCode),
|
||||
HelpText: "If this wasn't you, change your password immediately and contact support.",
|
||||
FooterText: "For your security, never share this code with anyone.",
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user