const std = @import("std"); const config = @import("config.zig"); const log = @import("log.zig"); const zat = @import("zat"); pub fn send( io: std.Io, allocator: std.mem.Allocator, to: []const u8, handle: []const u8, subject: []const u8, text: []const u8, ) !void { return switch (config.mailProvider()) { .comail => sendComail(io, allocator, to, handle, subject, text), .resend => sendResend(io, allocator, to, handle, subject, text), }; } fn sendComail( io: std.Io, allocator: std.mem.Allocator, to: []const u8, handle: []const u8, subject: []const u8, text: []const u8, ) !void { const maybe_api_key = config.comailApiKey(); const maybe_did = config.comailDid(); const maybe_from = config.emailFrom(); if (maybe_api_key == null or maybe_did == null or maybe_from == null) { return missingConfig( "comail", subject, to, handle, text, "missing_comail_api_key={} missing_comail_did={} missing_email_from={}", .{ maybe_api_key == null, maybe_did == null, maybe_from == null }, ); } const payload = try std.fmt.allocPrint( allocator, "{{\"from\":{f},\"to\":{f},\"subject\":{f},\"text\":{f},\"category\":\"verification\"}}", .{ std.json.fmt(maybe_from.?, .{}), std.json.fmt(to, .{}), std.json.fmt(subject, .{}), std.json.fmt(text, .{}) }, ); const authorization = try std.fmt.allocPrint(allocator, "Bearer {s}", .{maybe_api_key.?}); const headers = [_]std.http.Header{ .{ .name = "X-Atmos-DID", .value = maybe_did.? }, }; var transport = zat.HttpTransport.init(io, allocator); defer transport.deinit(); const result = try transport.fetch(.{ .url = "https://smtp.atmos.email/v1/send", .method = .POST, .payload = payload, .authorization = authorization, .content_type = "application/json", .extra_headers = &headers, .max_response_size = 64 * 1024, }); defer allocator.free(result.body); return requireOk("comail", result.status, result.body); } fn sendResend( io: std.Io, allocator: std.mem.Allocator, to: []const u8, handle: []const u8, subject: []const u8, text: []const u8, ) !void { const maybe_api_key = config.resendApiKey(); const maybe_from = config.emailFrom(); if (maybe_api_key == null or maybe_from == null) { return missingConfig( "resend", subject, to, handle, text, "missing_resend_api_key={} missing_email_from={}", .{ maybe_api_key == null, maybe_from == null }, ); } const payload = try std.fmt.allocPrint( allocator, "{{\"from\":{f},\"to\":[{f}],\"subject\":{f},\"text\":{f}}}", .{ std.json.fmt(maybe_from.?, .{}), std.json.fmt(to, .{}), std.json.fmt(subject, .{}), std.json.fmt(text, .{}) }, ); const authorization = try std.fmt.allocPrint(allocator, "Bearer {s}", .{maybe_api_key.?}); var transport = zat.HttpTransport.init(io, allocator); defer transport.deinit(); const result = try transport.fetch(.{ .url = "https://api.resend.com/emails", .method = .POST, .payload = payload, .authorization = authorization, .content_type = "application/json", .max_response_size = 64 * 1024, }); defer allocator.free(result.body); return requireOk("resend", result.status, result.body); } pub fn sendCode( io: std.Io, allocator: std.mem.Allocator, to: []const u8, handle: []const u8, subject: []const u8, label: []const u8, code: []const u8, ) !void { const text = try std.fmt.allocPrint( allocator, "Hello {s}. Your {s} code is {s}. This code will expire in ten minutes.", .{ handle, label, code }, ); return send(io, allocator, to, handle, subject, text); } fn logMail(subject: []const u8, email: []const u8, handle: []const u8, text: []const u8) void { log.info("[zds mail] {s} for {s} <{s}>: {s}\n", .{ subject, handle, email, text }); } fn missingConfig( comptime provider: []const u8, subject: []const u8, to: []const u8, handle: []const u8, text: []const u8, comptime fields_fmt: []const u8, fields: anytype, ) !void { if (isLocalDelivery()) { logMail(subject, to, handle, text); return; } log.err("mail delivery disabled provider=" ++ provider ++ " " ++ fields_fmt ++ " subject={s} to={s}\n", fields ++ .{ subject, to }); return error.EmailDeliveryFailed; } fn requireOk(comptime provider: []const u8, status: std.http.Status, body: []const u8) !void { const code = @intFromEnum(status); if (code >= 200 and code < 300) return; log.err("mail delivery failed provider=" ++ provider ++ " status={d} body={s}\n", .{ code, bodyExcerpt(body) }); return error.EmailDeliveryFailed; } fn bodyExcerpt(body: []const u8) []const u8 { return body[0..@min(body.len, 512)]; } fn isLocalDelivery() bool { const public_url = config.publicUrl(); return std.mem.startsWith(u8, public_url, "http://localhost") or std.mem.startsWith(u8, public_url, "http://127.0.0.1") or std.mem.startsWith(u8, public_url, "http://[::1]"); }