Skip to content

Mailer Overview

zzz_mailer provides a generic, adapter-based email sending system for zzz applications. It supports multiple delivery backends (SMTP, SendGrid, Mailgun), a development mailbox UI, rate limiting, telemetry hooks, and MIME message construction — all with zero heap allocations in the core path.

The mailer is built around a generic Mailer(Adapter) type. You choose an adapter at comptime, and the mailer handles sending, rate limiting, and telemetry transparently:

Application --> Mailer(Adapter) --> SmtpAdapter / SendGridAdapter / DevAdapter / ...
| |
RateLimiter Telemetry
  1. Add zzz_mailer to your build.zig.zon

    .dependencies = .{
    .zzz_mailer = .{
    .path = "../zzz_mailer",
    },
    },
  2. Wire it into build.zig

    const zzz_mailer = b.dependency("zzz_mailer", .{
    .target = target,
    .optimize = optimize,
    });
    exe.root_module.addImport("zzz_mailer", zzz_mailer.module("zzz_mailer"));

The main types you work with live in zzz_mailer:

TypeDescription
EmailRepresents a complete email message (from, to, cc, bcc, subject, bodies, attachments)
AddressAn email address with an optional display name
AttachmentA file attachment with content type and disposition
HeaderA custom email header (name/value pair)
SendResultThe result of a send operation (success, message_id, error_message)
Mailer(Adapter)Generic mailer parameterized by a delivery adapter

Build an Email struct with the fields you need. All fields except from have sensible defaults:

const zzz_mailer = @import("zzz_mailer");
const Email = zzz_mailer.Email;
const email = Email{
.from = .{ .email = "noreply@example.com", .name = "My App" },
.to = &.{
.{ .email = "user@example.com", .name = "Alice" },
.{ .email = "other@example.com" },
},
.cc = &.{.{ .email = "cc@example.com" }},
.subject = "Welcome to My App",
.text_body = "Hello from My App!",
.html_body = "<h1>Hello from My App!</h1>",
};

Create a Mailer with your chosen adapter, then call send:

const zzz_mailer = @import("zzz_mailer");
const SmtpAdapter = zzz_mailer.SmtpAdapter;
const Mailer = zzz_mailer.Mailer;
var mailer = Mailer(SmtpAdapter).init(.{
.adapter = .{
.host = "smtp.example.com",
.port = 587,
.username = "apikey",
.password = "your-smtp-password",
},
});
defer mailer.deinit();
const result = mailer.send(email, allocator);
if (result.success) {
// Email sent, result.message_id contains the provider message ID
} else {
// Failed, result.error_message describes the issue
std.log.err("Email failed: {s}", .{result.error_message orelse "unknown"});
}

zzz_mailer provides pre-configured type aliases so you do not need to specify the generic parameter every time:

const zzz_mailer = @import("zzz_mailer");
// These are equivalent:
var mailer1 = zzz_mailer.SmtpMailer.init(.{ .adapter = .{ ... } });
var mailer2 = zzz_mailer.Mailer(zzz_mailer.SmtpAdapter).init(.{ .adapter = .{ ... } });

Available aliases: SmtpMailer, SendGridMailer, MailgunMailer, DevMailer, TestMailer, LogMailer.

Enable token-bucket rate limiting by passing a rate_limit configuration:

var mailer = Mailer(SmtpAdapter).init(.{
.adapter = .{ .host = "smtp.example.com" },
.rate_limit = .{ .max_per_second = 10.0 },
});

When the rate limit is exceeded, the mailer blocks until a token becomes available. This prevents overloading your email provider.

Attach telemetry handlers to observe email lifecycle events:

const zzz_mailer = @import("zzz_mailer");
const Telemetry = zzz_mailer.Telemetry;
fn onEmailEvent(event: zzz_mailer.Event) void {
switch (event) {
.email_sending => |e| std.log.info("Sending to {s}", .{e.subject}),
.email_sent => |r| std.log.info("Sent in {d}ms", .{r.duration_ms}),
.email_failed => |r| std.log.err("Failed: {s}", .{r.error_msg orelse "unknown"}),
.rate_limited => std.log.warn("Rate limited", .{}),
else => {},
}
}
var telemetry = Telemetry{};
telemetry.attach(&onEmailEvent);
var mailer = Mailer(SmtpAdapter).init(.{ .adapter = .{ ... } });
mailer.telemetry = &telemetry;

The telemetry system emits these events:

EventWhen
email_sendingBefore the adapter send is called
email_sentAfter a successful send (includes duration_ms and message_id)
email_failedAfter a failed send (includes duration_ms and error_msg)
rate_limitedWhen an email is held by the rate limiter

Here is a complete example of sending email from a zzz controller:

const std = @import("std");
const zzz = @import("zzz");
const zzz_mailer = @import("zzz_mailer");
const Email = zzz_mailer.Email;
const DevAdapter = zzz_mailer.DevAdapter;
const DevMailer = zzz_mailer.DevMailer;
var mailer: DevMailer = DevMailer.init(.{});
fn sendWelcome(ctx: *zzz.Context) !void {
const email = Email{
.from = .{ .email = "noreply@example.com", .name = "Example App" },
.to = &.{.{ .email = "user@example.com", .name = "Test User" }},
.subject = "Welcome to our app!",
.text_body = "Hello from zzz_mailer!",
.html_body = "<h1>Welcome!</h1><p>Hello from zzz_mailer!</p>",
};
const result = mailer.send(email, ctx.allocator);
if (result.success) {
ctx.text(.ok, "Email sent!");
} else {
ctx.text(.internal_server_error, "Failed to send email");
}
}
  • Adapters — configure SMTP, SendGrid, Mailgun, and development adapters
  • Email Templates — build dynamic HTML and plain text email bodies
  • Environment Config — manage adapter credentials across environments