- Add avatar upload functionality with public URL access. - Implement user login with email and password authentication. - Create endpoints for fetching and updating the authenticated user's profile. - Set up database migrations for user profiles, including email and role management. - Introduce personal data management linked to user profiles. - Add email verification process with welcome email templates.
99 lines
2.8 KiB
Go
99 lines
2.8 KiB
Go
// Service mails handles transactional email delivery.
|
|
package mails
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"encore.dev/beta/errs"
|
|
"encore.dev/types/uuid"
|
|
)
|
|
|
|
const outboxDir = "mails/outbox"
|
|
|
|
type TransactionalMailParams struct {
|
|
To string `json:"to"`
|
|
Subject string `json:"subject"`
|
|
TextBody string `json:"text_body"`
|
|
HTMLBody string `json:"html_body"`
|
|
Metadata map[string]string `json:"metadata"`
|
|
}
|
|
|
|
type TransactionalMail struct {
|
|
ID uuid.UUID `json:"id"`
|
|
To string `json:"to"`
|
|
Subject string `json:"subject"`
|
|
TextBody string `json:"text_body"`
|
|
HTMLBody string `json:"html_body"`
|
|
Metadata map[string]string `json:"metadata"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// SendTransactional records a transactional email in the local outbox.
|
|
// TODO: replace the file-backed outbox with an SMTP sender.
|
|
//
|
|
//encore:api private method=POST path=/mails/transactional
|
|
func SendTransactional(ctx context.Context, p *TransactionalMailParams) (*TransactionalMail, error) {
|
|
if strings.TrimSpace(p.To) == "" {
|
|
return nil, &errs.Error{Code: errs.InvalidArgument, Message: "recipient email is required"}
|
|
}
|
|
if strings.TrimSpace(p.Subject) == "" {
|
|
return nil, &errs.Error{Code: errs.InvalidArgument, Message: "email subject is required"}
|
|
}
|
|
|
|
id, err := uuid.NewV4()
|
|
if err != nil {
|
|
return nil, errs.WrapCode(err, errs.Internal, "failed to generate mail id")
|
|
}
|
|
|
|
mail := TransactionalMail{
|
|
ID: id,
|
|
To: p.To,
|
|
Subject: p.Subject,
|
|
TextBody: p.TextBody,
|
|
HTMLBody: p.HTMLBody,
|
|
Metadata: p.Metadata,
|
|
CreatedAt: time.Now().UTC(),
|
|
}
|
|
if mail.Metadata == nil {
|
|
mail.Metadata = map[string]string{}
|
|
}
|
|
|
|
if err := saveToOutbox(ctx, &mail); err != nil {
|
|
return nil, err
|
|
}
|
|
return &mail, nil
|
|
}
|
|
|
|
func saveToOutbox(ctx context.Context, mail *TransactionalMail) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(outboxDir, 0o755); err != nil {
|
|
return errs.WrapCode(err, errs.Internal, "failed to create mail outbox")
|
|
}
|
|
|
|
payload, err := json.MarshalIndent(mail, "", " ")
|
|
if err != nil {
|
|
return errs.WrapCode(err, errs.Internal, "failed to encode mail")
|
|
}
|
|
|
|
baseFilename := mail.CreatedAt.Format("20060102T150405Z") + "_" + mail.ID.String()
|
|
jsonPath := filepath.Join(outboxDir, baseFilename+".json")
|
|
if err := os.WriteFile(jsonPath, payload, 0o644); err != nil {
|
|
return errs.WrapCode(err, errs.Internal, "failed to write mail to outbox")
|
|
}
|
|
|
|
if strings.TrimSpace(mail.HTMLBody) != "" {
|
|
htmlPath := filepath.Join(outboxDir, baseFilename+".html")
|
|
if err := os.WriteFile(htmlPath, []byte(mail.HTMLBody), 0o644); err != nil {
|
|
return errs.WrapCode(err, errs.Internal, "failed to write mail html to outbox")
|
|
}
|
|
}
|
|
return nil
|
|
}
|