Email System
Grit integrates with Resend for transactional email delivery using raw net/http calls (no SDK dependency). It includes a Mailer service, Go template rendering, and four built-in email templates styled with the Grit dark theme.
Configuration
Email sending requires a Resend API key and a verified sender address. For local development, Mailhog is included in Docker Compose for testing without sending real emails.
# Email Configuration RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx MAIL_FROM=noreply@yourdomain.com
Mailer Service
The Mailer service at internal/mail/mailer.go provides two methods for sending emails: Send() for template-based emails and SendRaw() for raw HTML emails.
// Mailer sends emails via the Resend API.
type Mailer struct {
apiKey string
from string
client *http.Client
}
// New creates a new Mailer instance.
func New(apiKey, from string) *Mailer
// SendOptions configures an email to send.
type SendOptions struct {
To string
Subject string
Template string // Template name (e.g., "welcome")
Data map[string]interface{} // Template variables
}
// Send renders a template and sends the email via Resend.
func (m *Mailer) Send(ctx context.Context, opts SendOptions) error
// SendRaw sends an email with raw HTML content (no template).
func (m *Mailer) SendRaw(ctx context.Context, to, subject, htmlBody string) errorUsage Examples
// Send a welcome email using a template
err := mailer.Send(ctx, mail.SendOptions{
To: "user@example.com",
Subject: "Welcome to MyApp!",
Template: "welcome",
Data: map[string]interface{}{
"AppName": "MyApp",
"Name": "John",
"DashboardURL": "https://myapp.com/dashboard",
"Year": time.Now().Year(),
},
})
// Send a password reset email
err = mailer.Send(ctx, mail.SendOptions{
To: "user@example.com",
Subject: "Reset Your Password",
Template: "password-reset",
Data: map[string]interface{}{
"AppName": "MyApp",
"ResetURL": "https://myapp.com/reset?token=abc123",
"Year": time.Now().Year(),
},
})
// Send a raw HTML email
err = mailer.SendRaw(ctx, "user@example.com", "Custom Email", "<h1>Hello!</h1>")Template Rendering
Email templates use Go's html/template package. Templates are stored as Go constants in internal/mail/templates.go and compiled at render time. Variables use the standard Go template syntax: {{.VariableName}}.
// EmailTemplates contains all available email templates.
var EmailTemplates = map[string]string{
"welcome": welcomeTemplate,
"password-reset": passwordResetTemplate,
"email-verification": emailVerificationTemplate,
"notification": notificationTemplate,
}Built-in Templates
Grit ships four pre-built email templates, all styled with the dark theme (background: #0a0a0f, accent: #6c5ce7). Each template is a complete HTML document with inline CSS for maximum email client compatibility.
| Template | Variables | Use Case |
|---|---|---|
| welcome | AppName, Name, DashboardURL, Year | After user registration |
| password-reset | AppName, ResetURL, Year | Password reset request |
| email-verification | AppName, VerifyURL, Year | Email address verification |
| notification | AppName, Title, Message, ActionURL, ActionText, Year | Generic notifications |
Template Structure
Each template follows a consistent structure: dark background, card container with logo, heading, body text, optional CTA button, and footer. Here is the welcome template:
<body style="background-color: #0a0a0f; color: #e8e8f0;">
<div class="container">
<div class="card">
<div class="logo">{{.AppName}}</div>
<h1>Welcome, {{.Name}}!</h1>
<p>Thanks for signing up. Your account is ready to use.</p>
<p>Get started by exploring the dashboard:</p>
<p style="text-align: center;">
<a href="{{.DashboardURL}}" class="btn">Go to Dashboard</a>
</p>
</div>
<div class="footer">
<p>© {{.Year}} {{.AppName}}. All rights reserved.</p>
</div>
</div>
</body>Adding Custom Templates
To add a new email template, define it as a Go constant in templates.go and register it in the EmailTemplates map.
// Add to the EmailTemplates map:
var EmailTemplates = map[string]string{
"welcome": welcomeTemplate,
"password-reset": passwordResetTemplate,
"email-verification": emailVerificationTemplate,
"notification": notificationTemplate,
"invoice": invoiceTemplate, // Your custom template
}
// Define the template:
const invoiceTemplate = `<!DOCTYPE html>
<html>
<head>...</head>
<body>
<div class="container">
<div class="card">
<div class="logo">{{.AppName}}</div>
<h1>Invoice #{{.InvoiceNumber}}</h1>
<p>Amount: {{.Amount}}</p>
<p>Due: {{.DueDate}}</p>
<a href="{{.PaymentURL}}" class="btn">Pay Now</a>
</div>
</div>
</body>
</html>`
// Send it:
mailer.Send(ctx, mail.SendOptions{
To: "customer@example.com",
Subject: "Invoice #INV-001",
Template: "invoice",
Data: map[string]interface{}{
"AppName": "MyApp",
"InvoiceNumber": "INV-001",
"Amount": "99.00",
"DueDate": "Feb 28, 2026",
"PaymentURL": "https://myapp.com/pay/inv-001",
},
})Background Email Sending
For better API response times, emails should be sent via background jobs rather than inline. Grit's job queue client provides a EnqueueSendEmail() method that sends the email asynchronously.
// Instead of sending synchronously:
// mailer.Send(ctx, opts)
// Enqueue a background job (returns immediately):
err := jobsClient.EnqueueSendEmail(
"user@example.com",
"Welcome to MyApp!",
"welcome",
map[string]interface{}{
"AppName": "MyApp",
"Name": "John",
"DashboardURL": "https://myapp.com/dashboard",
"Year": 2026,
},
)
// The asynq worker picks it up and calls mailer.Send()Admin email preview: The admin panel includes an email template preview page where you can view how each template renders with sample data before sending. See the Background Jobs page for the full job queue documentation.