Files
gochat/internal/service/profile_confirmation_mailer.go
T

210 lines
7.0 KiB
Go

package service
import (
"context"
"fmt"
"net/mail"
"net/smtp"
"os"
"strings"
"github.com/gochat/gochat/internal/model"
)
const confirmationInstructionsSubject = "Confirmation Instructions"
// ProfileConfirmationMailer delivers Devise-compatible confirmation instructions.
// Reference: Chatwoot Devise::Mailer#confirmation_instructions.
type ProfileConfirmationMailer interface {
SendConfirmationInstructions(ctx context.Context, req ProfileConfirmationMailRequest) error
}
type ProfileConfirmationMailRequest struct {
Kind string
UserID uint
AccountID uint
InviterID uint
ToEmail string
RecipientName string
BrandName string
AccountName string
InviterName string
Subject string
Eyebrow string
Heading string
IntroText string
SupportingText string
ActionText string
ActionURL string
ConfirmationToken string
ResetPasswordToken string
DetailRows [][2]string
}
type SMTPProfileConfirmationMailer struct {
Address string
Port int
Username string
Password string
From string
FrontendURL string
}
func NewEnvProfileConfirmationMailer() *SMTPProfileConfirmationMailer {
return &SMTPProfileConfirmationMailer{
Address: strings.TrimSpace(os.Getenv("SMTP_ADDRESS")),
Port: envInt("SMTP_PORT", 587),
Username: firstEnv("SMTP_USERNAME", "SMTP_LOGIN"),
Password: os.Getenv("SMTP_PASSWORD"),
From: firstEnv("MAILER_SENDER_EMAIL", "SMTP_FROM"),
FrontendURL: envConfirmationFrontendURL(),
}
}
func (m *SMTPProfileConfirmationMailer) SendConfirmationInstructions(ctx context.Context, req ProfileConfirmationMailRequest) error {
_ = ctx
if m == nil || strings.TrimSpace(m.Address) == "" || strings.TrimSpace(req.ToEmail) == "" {
return nil
}
fromHeader := strings.TrimSpace(m.From)
if fromHeader == "" {
fromHeader = "Chatwoot <accounts@chatwoot.com>"
}
fromAddress := fromHeader
if parsed, err := mail.ParseAddress(fromHeader); err == nil {
fromAddress = parsed.Address
}
if strings.TrimSpace(req.Subject) == "" {
req.Subject = confirmationInstructionsSubject
}
message := smtpMessage(fromHeader, req.ToEmail, req.Subject, profileConfirmationEmailBody(req))
addr := fmt.Sprintf("%s:%d", strings.TrimSpace(m.Address), m.Port)
var auth smtp.Auth
if strings.TrimSpace(m.Username) != "" {
auth = smtp.PlainAuth("", strings.TrimSpace(m.Username), m.Password, strings.TrimSpace(m.Address))
}
return smtp.SendMail(addr, auth, fromAddress, []string{req.ToEmail}, []byte(message))
}
func buildProfileConfirmationMailRequest(user *model.User, account *model.Account, inviter *model.User, brandName, frontendURL, confirmationToken, resetPasswordToken string) ProfileConfirmationMailRequest {
recipientName := strings.TrimSpace(user.Name)
if recipientName == "" {
recipientName = strings.TrimSpace(user.Email)
}
req := ProfileConfirmationMailRequest{
Kind: "confirmation",
UserID: user.ID,
ToEmail: strings.TrimSpace(user.Email),
RecipientName: recipientName,
BrandName: brandName,
Subject: confirmationInstructionsSubject,
Eyebrow: "Welcome",
Heading: "Confirm your email to get started",
IntroText: fmt.Sprintf("Welcome to %s. We just need to verify your email address before you can start using your account.", brandName),
SupportingText: "This only takes a moment.",
ActionText: "Confirm my account",
ActionURL: frontendActionURL(frontendURL, "auth/confirmation", "confirmation_token", confirmationToken),
ConfirmationToken: confirmationToken,
}
if strings.TrimSpace(user.UnconfirmedEmail) != "" {
req.Kind = "email_update"
req.Eyebrow = "Email update"
req.Heading = "Confirm your new email address"
req.IntroText = fmt.Sprintf("We received a request to update the email address on your %s account.", brandName)
req.SupportingText = "Confirm the new address below to finish the change."
req.ActionText = "Confirm email address"
req.DetailRows = append(req.DetailRows, [2]string{"New email", user.UnconfirmedEmail})
return req
}
if user.ConfirmedAt != nil {
req.Kind = "already_confirmed"
req.Eyebrow = "Account ready"
req.Heading = "Your account is ready"
req.IntroText = fmt.Sprintf("Your %s account is already active.", brandName)
req.SupportingText = "Use the button below to sign in and continue where you left off."
req.ActionText = "Open my account"
req.ActionURL = frontendActionURL(frontendURL, "auth/sign_in", "", "")
req.ConfirmationToken = ""
return req
}
if inviter != nil {
req.Kind = "invitation"
req.InviterID = inviter.ID
req.InviterName = strings.TrimSpace(inviter.Name)
if req.InviterName == "" {
req.InviterName = strings.TrimSpace(inviter.Email)
}
req.Eyebrow = "Workspace invitation"
req.ActionText = "Accept invitation"
req.ActionURL = frontendActionURL(frontendURL, "auth/password/edit", "reset_password_token", resetPasswordToken)
req.ConfirmationToken = ""
req.ResetPasswordToken = resetPasswordToken
req.SupportingText = "Create your account to start collaborating with your team."
req.DetailRows = append(req.DetailRows, [2]string{"Invited by", req.InviterName})
if account != nil {
req.AccountID = account.ID
req.AccountName = strings.TrimSpace(account.Name)
}
if req.AccountName != "" {
req.Heading = fmt.Sprintf("You're invited to join %s", req.AccountName)
req.IntroText = fmt.Sprintf("%s invited you to join the %s workspace on %s.", req.InviterName, req.AccountName, brandName)
req.DetailRows = append(req.DetailRows, [2]string{"Workspace", req.AccountName})
} else {
req.Heading = fmt.Sprintf("You're invited to try %s", brandName)
req.IntroText = fmt.Sprintf("%s invited you to try %s.", req.InviterName, brandName)
}
}
return req
}
func profileConfirmationEmailBody(req ProfileConfirmationMailRequest) string {
var b strings.Builder
b.WriteString(fmt.Sprintf("Hi %s,\n\n", req.RecipientName))
if req.Eyebrow != "" {
b.WriteString(req.Eyebrow + "\n")
}
if req.Heading != "" {
b.WriteString(req.Heading + "\n\n")
}
if req.IntroText != "" {
b.WriteString(req.IntroText + "\n")
}
if req.SupportingText != "" {
b.WriteString(req.SupportingText + "\n")
}
for _, row := range req.DetailRows {
if strings.TrimSpace(row[1]) != "" {
b.WriteString(fmt.Sprintf("%s: %s\n", row[0], row[1]))
}
}
if req.ActionText != "" && req.ActionURL != "" {
b.WriteString(fmt.Sprintf("\n%s: %s\n", req.ActionText, req.ActionURL))
}
return b.String()
}
func frontendActionURL(frontendURL, path, key, value string) string {
base := strings.TrimRight(strings.TrimSpace(frontendURL), "/")
if base == "" {
base = "/app"
} else {
base += "/app"
}
url := base + "/" + strings.TrimLeft(path, "/")
if key != "" && value != "" {
url += "?" + key + "=" + value
}
return url
}
func envConfirmationFrontendURL() string {
return strings.TrimRight(os.Getenv("FRONTEND_URL"), "/")
}