Skip to content
Tikab's Toolkit

Email

The send_mail counterpart with one twist that makes development and review calm: every email is always recorded to the mailbox table (and mirrored to the server console). Real delivery is additive.

The contract

Loading diagram...
  • No SMTP_HOST → outbox only. Perfect for dev: the reset link, the invite, the notification are all readable in the outbox UI instead of a mail spool.
  • SMTP_HOST set → additionally delivered for real (a hosted SMTP endpoint, or an internal relay).
  • Errors never bubble. A failed email must not fail the action that triggered it (assigning a task, resetting a password).

The types

Live from the source — the kind drives icons and filtering in the outbox surfaces:

export type EmailKind =
  | "task_assigned"
  | "project_invitation"
  | "mention"
  // Triggered by the Hatchet `sendNotification` job once a document's AI
  // analysis is ready (the "fast lane").
  | "document_processed"
  // Better Auth's password-reset flow (wired in @repo/auth).
  | "password_reset";
 
export interface OutgoingEmail {
  kind: EmailKind;
  toEmail: string;
  toUserId?: string | null;
  subject: string;
  body: string;
  meta?: Record<string, unknown>;
}

Adding a kind is a code-only change (the column is a plain-text enum — no migration).

Reading the outbox

listMailbox(limit) is the read side — wire-friendly rows (ISO dates) for whatever surface the consumer builds. The example renders it twice: a per-project outbox panel and the global /admin/outbox, both through the same presentational OutboxLog component. Access control belongs to the caller's server function, not the block.

Configuration

VariableMeaning
SMTP_HOSTenables real delivery
SMTP_PORTdefault 587
SMTP_SECURE"true" = implicit TLS (465); default STARTTLS
SMTP_USER / SMTP_PASSoptional — internal relays often need none
MAIL_FROMsender, e.g. Toolkit <no-reply@example.com>