// App-owned mail service: the guarded egress layer over the dumb mail // transport, analogous to a store over the database. Callers use intent // methods (SendVerification, ...) and never touch the transport's Content or // Mailer directly, the way they never touch SQL. Message composition lives // here, and every message crosses a single capability-checked chokepoint. package mail import ( "context" "fmt" "html" "atlas9.dev/c/core/iam" "atlas9.dev/c/demo/lib/access" mailtransport "atlas9.dev/c/mail" ) // CapMailSend is the app's egress boundary for the mail channel. Mail is not // tenant-scoped, so it is checked system-wide. Granularity can split later (per // type or tenant) without touching callers, because policy lives here rather // than in the transport. var CapMailSend = iam.NewCap("Mail_Send") // Mailer composes application emails and enforces the egress cap before handing // bytes to the transport. type Mailer struct { Transport mailtransport.Mailer Guard access.Guard BaseURL string } func (s *Mailer) SendVerification(ctx context.Context, email, token string) error { url := s.BaseURL + "/verify?token=" + token return s.send(ctx, email, mailtransport.Content{ Subject: "Verify your email", TextBody: "Click here to verify your email: " + url, HtmlBody: fmt.Sprintf(`

Click here to verify your email.

`, html.EscapeString(url)), }) } func (s *Mailer) SendPasswordReset(ctx context.Context, email, token string) error { url := s.BaseURL + "/reset-password?token=" + token return s.send(ctx, email, mailtransport.Content{ Subject: "Reset your password", TextBody: "Click here to reset your password: " + url, HtmlBody: fmt.Sprintf(`

Click here to reset your password.

`, html.EscapeString(url)), }) } func (s *Mailer) SendInvitation(ctx context.Context, email, token string) error { url := s.BaseURL + "/accept-invitation?token=" + token return s.send(ctx, email, mailtransport.Content{ Subject: "You've been invited", TextBody: "You've been invited to join a team. Click here to accept: " + url, HtmlBody: fmt.Sprintf(`

You've been invited to join a team. Click here to accept.

`, html.EscapeString(url)), }) } // send is the chokepoint: every message crosses the egress boundary here, so the // cap is checked exactly once, in one place. func (s *Mailer) send(ctx context.Context, email string, c mailtransport.Content) error { if err := s.Guard.System(ctx, CapMailSend); err != nil { return err } return s.Transport.Send(ctx, email, c) }