210 lines
7.0 KiB
Go
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"), "/")
|
|
}
|