mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 22:16:08 +00:00
Email framework
Create mailers with `jetzig generate mailer <name>`. Mailers define default values for email fields (e.g. subject, from address, etc.). Mailers use Zmpl for rendering text/HTML parts. Send an email from a request with `request.mail()`. Call `deliver(.background, .{})` on the return value to use the built-in mail job to send the email asynchronously. Improve query/HTTP request body param parsing - unescape `+` and `%XX` characters.
This commit is contained in:
parent
3f6c1a4919
commit
47c35632b5
72
build.zig
72
build.zig
@ -15,15 +15,13 @@ const zmpl_build = @import("zmpl");
|
||||
pub fn build(b: *std.Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
const template_path_option = b.option([]const u8, "zmpl_templates_path", "Path to templates") orelse
|
||||
"src/app/views/";
|
||||
const template_path: []const u8 = if (std.fs.path.isAbsolute(template_path_option))
|
||||
try b.allocator.dupe(u8, template_path_option)
|
||||
else
|
||||
std.fs.cwd().realpathAlloc(b.allocator, template_path_option) catch |err| switch (err) {
|
||||
error.FileNotFound => "",
|
||||
else => return err,
|
||||
};
|
||||
const templates_paths = try zmpl_build.templatesPaths(
|
||||
b.allocator,
|
||||
&.{
|
||||
.{ .prefix = "views", .path = &.{ "src", "app", "views" } },
|
||||
.{ .prefix = "mailers", .path = &.{ "src", "app", "mailers" } },
|
||||
},
|
||||
);
|
||||
|
||||
const lib = b.addStaticLibrary(.{
|
||||
.name = "jetzig",
|
||||
@ -40,32 +38,13 @@ pub fn build(b: *std.Build) !void {
|
||||
jetzig_module.addImport("mime_types", mime_module);
|
||||
lib.root_module.addImport("jetzig", jetzig_module);
|
||||
|
||||
const zmpl_version = b.option(
|
||||
enum { v1, v2 },
|
||||
"zmpl_version",
|
||||
"Zmpl syntax version (default: v1)",
|
||||
) orelse .v2;
|
||||
|
||||
if (zmpl_version == .v1) {
|
||||
std.debug.print(
|
||||
\\[WARN] Zmpl v1 is deprecated and will soon be removed.
|
||||
\\ Update to v2 by modifying `jetzigInit` in your `build.zig`:
|
||||
\\
|
||||
\\ try jetzig.jetzigInit(b, exe, .{{ .zmpl_version = .v2 }});
|
||||
\\
|
||||
\\ See https://jetzig.dev/documentation.html for information on migrating to Zmpl v2.
|
||||
\\
|
||||
, .{});
|
||||
}
|
||||
|
||||
const zmpl_dep = b.dependency(
|
||||
"zmpl",
|
||||
.{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.zmpl_templates_path = template_path,
|
||||
.zmpl_templates_paths = templates_paths,
|
||||
.zmpl_auto_build = false,
|
||||
.zmpl_version = zmpl_version,
|
||||
.zmpl_constants = try zmpl_build.addTemplateConstants(b, struct {
|
||||
jetzig_view: []const u8,
|
||||
jetzig_action: []const u8,
|
||||
@ -84,11 +63,17 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
const zmd_dep = b.dependency("zmd", .{ .target = target, .optimize = optimize });
|
||||
|
||||
const smtp_client_dep = b.dependency("smtp_client", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
lib.root_module.addImport("zmpl", zmpl_module);
|
||||
jetzig_module.addImport("zmpl", zmpl_module);
|
||||
jetzig_module.addImport("args", zig_args_dep.module("args"));
|
||||
jetzig_module.addImport("zmd", zmd_dep.module("zmd"));
|
||||
jetzig_module.addImport("jetkv", jetkv_dep.module("jetkv"));
|
||||
jetzig_module.addImport("smtp", smtp_client_dep.module("smtp_client"));
|
||||
|
||||
const main_tests = b.addTest(.{
|
||||
.root_source_file = .{ .path = "src/tests.zig" },
|
||||
@ -115,15 +100,20 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
/// Build-time options for Jetzig.
|
||||
pub const JetzigInitOptions = struct {
|
||||
zmpl_version: enum { v1, v2 } = .v1,
|
||||
zmpl_version: enum { v1, v2 } = .v2,
|
||||
};
|
||||
|
||||
pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigInitOptions) !void {
|
||||
if (options.zmpl_version == .v1) {
|
||||
std.debug.print("Zmpl v1 has now been removed. Please upgrade to v2.\n", .{});
|
||||
return error.ZmplVersionNotSupported;
|
||||
}
|
||||
|
||||
const target = b.host;
|
||||
const optimize = exe.root_module.optimize orelse .Debug;
|
||||
const jetzig_dep = b.dependency(
|
||||
"jetzig",
|
||||
.{ .optimize = optimize, .target = target, .zmpl_version = options.zmpl_version },
|
||||
.{ .optimize = optimize, .target = target },
|
||||
);
|
||||
const jetzig_module = jetzig_dep.module("jetzig");
|
||||
const zmpl_module = jetzig_dep.module("zmpl");
|
||||
@ -140,17 +130,31 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn
|
||||
}
|
||||
|
||||
const root_path = b.build_root.path orelse try std.fs.cwd().realpathAlloc(b.allocator, ".");
|
||||
const templates_path = try std.fs.path.join(
|
||||
const templates_path: []const u8 = try std.fs.path.join(
|
||||
b.allocator,
|
||||
&[_][]const u8{ root_path, "src", "app" },
|
||||
);
|
||||
const views_path: []const u8 = try std.fs.path.join(
|
||||
b.allocator,
|
||||
&[_][]const u8{ root_path, "src", "app", "views" },
|
||||
);
|
||||
|
||||
const jobs_path = try std.fs.path.join(
|
||||
b.allocator,
|
||||
&[_][]const u8{ root_path, "src", "app", "jobs" },
|
||||
);
|
||||
const mailers_path = try std.fs.path.join(
|
||||
b.allocator,
|
||||
&[_][]const u8{ root_path, "src", "app", "mailers" },
|
||||
);
|
||||
|
||||
var generate_routes = try Routes.init(b.allocator, root_path, templates_path, jobs_path);
|
||||
var generate_routes = try Routes.init(
|
||||
b.allocator,
|
||||
root_path,
|
||||
templates_path,
|
||||
views_path,
|
||||
jobs_path,
|
||||
mailers_path,
|
||||
);
|
||||
try generate_routes.generateRoutes();
|
||||
const write_files = b.addWriteFiles();
|
||||
const routes_file = write_files.add("routes.zig", generate_routes.buffer.items);
|
||||
|
@ -7,8 +7,8 @@
|
||||
.hash = "12207d49df326e0c180a90fa65d9993898e0a0ffd8e79616b4b81f81769261858856",
|
||||
},
|
||||
.zmpl = .{
|
||||
.url = "https://github.com/jetzig-framework/zmpl/archive/1fcdfc42444224d43eb6e5389d9b4aa239fcd787.tar.gz",
|
||||
.hash = "1220f7bc1e2b2317790db37b7dec685c7d9a2ece9708108a4b636477d996be002c71",
|
||||
.url = "https://github.com/jetzig-framework/zmpl/archive/4511ae706e8679385d38cc1366497082f8f53afb.tar.gz",
|
||||
.hash = "1220d493e6fdfaccbafff41df2b7b407728ed11619bebb198c90dae9420f03a6d29d",
|
||||
},
|
||||
.args = .{
|
||||
.url = "https://github.com/MasterQ32/zig-args/archive/01d72b9a0128c474aeeb9019edd48605fa6d95f7.tar.gz",
|
||||
@ -18,6 +18,10 @@
|
||||
.url = "https://github.com/jetzig-framework/jetkv/archive/a6fcc2df220c1a40094e167eeb567bb5888404e9.tar.gz",
|
||||
.hash = "12207bd2d7465b33e745a5b0567172377f94a221d1fc9aab238bb1b372c64f4ec1a0",
|
||||
},
|
||||
.smtp_client = .{
|
||||
.url = "https://github.com/karlseguin/smtp_client.zig/archive/e79e411862d4f4d41657bf41efb884efca3d67dd.tar.gz",
|
||||
.hash = "12209907c69891a38e6923308930ac43bfb40135bc609ea370b5759fc2e1c4f57284",
|
||||
},
|
||||
},
|
||||
|
||||
.paths = .{
|
||||
|
@ -1,17 +1,19 @@
|
||||
const std = @import("std");
|
||||
const args = @import("args");
|
||||
const secret = @import("generate/secret.zig");
|
||||
const util = @import("../util.zig");
|
||||
|
||||
const view = @import("generate/view.zig");
|
||||
const partial = @import("generate/partial.zig");
|
||||
const layout = @import("generate/layout.zig");
|
||||
const middleware = @import("generate/middleware.zig");
|
||||
const job = @import("generate/job.zig");
|
||||
const secret = @import("generate/secret.zig");
|
||||
const util = @import("../util.zig");
|
||||
const mailer = @import("generate/mailer.zig");
|
||||
|
||||
/// Command line options for the `generate` command.
|
||||
pub const Options = struct {
|
||||
pub const meta = .{
|
||||
.usage_summary = "[view|partial|layout|middleware|job|secret] [options]",
|
||||
.usage_summary = "[view|partial|layout|mailer|middleware|job|secret] [options]",
|
||||
.full_text =
|
||||
\\Generate scaffolding for views, middleware, and other objects.
|
||||
\\
|
||||
@ -36,34 +38,38 @@ pub fn run(
|
||||
|
||||
_ = options;
|
||||
|
||||
var generate_type: ?enum { view, partial, layout, middleware, job, secret } = null;
|
||||
const Generator = enum { view, partial, layout, mailer, middleware, job, secret };
|
||||
var sub_args = std.ArrayList([]const u8).init(allocator);
|
||||
defer sub_args.deinit();
|
||||
|
||||
for (positionals) |arg| {
|
||||
if (generate_type == null and std.mem.eql(u8, arg, "view")) {
|
||||
generate_type = .view;
|
||||
} else if (generate_type == null and std.mem.eql(u8, arg, "partial")) {
|
||||
generate_type = .partial;
|
||||
} else if (generate_type == null and std.mem.eql(u8, arg, "layout")) {
|
||||
generate_type = .layout;
|
||||
} else if (generate_type == null and std.mem.eql(u8, arg, "job")) {
|
||||
generate_type = .job;
|
||||
} else if (generate_type == null and std.mem.eql(u8, arg, "middleware")) {
|
||||
generate_type = .middleware;
|
||||
} else if (generate_type == null and std.mem.eql(u8, arg, "secret")) {
|
||||
generate_type = .secret;
|
||||
} else if (generate_type == null) {
|
||||
std.debug.print("Unknown generator command: {s}\n", .{arg});
|
||||
return error.JetzigCommandError;
|
||||
} else {
|
||||
try sub_args.append(arg);
|
||||
}
|
||||
const map = std.ComptimeStringMap(Generator, .{
|
||||
.{ "view", .view },
|
||||
.{ "partial", .partial },
|
||||
.{ "layout", .layout },
|
||||
.{ "job", .job },
|
||||
.{ "mailer", .mailer },
|
||||
.{ "middleware", .middleware },
|
||||
.{ "secret", .secret },
|
||||
});
|
||||
|
||||
var available_buf = std.ArrayList([]const u8).init(allocator);
|
||||
defer available_buf.deinit();
|
||||
for (map.kvs) |kv| try available_buf.append(kv.key);
|
||||
const available_help = try std.mem.join(allocator, "|", available_buf.items);
|
||||
defer allocator.free(available_help);
|
||||
|
||||
const generate_type: ?Generator = if (positionals.len > 0) map.get(positionals[0]) else null;
|
||||
|
||||
if (positionals.len > 1) {
|
||||
for (positionals[1..]) |arg| try sub_args.append(arg);
|
||||
}
|
||||
|
||||
if (other_options.help and generate_type == null) {
|
||||
try args.printHelp(Options, "jetzig generate", writer);
|
||||
return;
|
||||
} else if (generate_type == null) {
|
||||
std.debug.print("Missing sub-command. Expected: [{s}]\n", .{available_help});
|
||||
return error.JetzigCommandError;
|
||||
}
|
||||
|
||||
if (generate_type) |capture| {
|
||||
@ -71,12 +77,10 @@ pub fn run(
|
||||
.view => view.run(allocator, cwd, sub_args.items, other_options.help),
|
||||
.partial => partial.run(allocator, cwd, sub_args.items, other_options.help),
|
||||
.layout => layout.run(allocator, cwd, sub_args.items, other_options.help),
|
||||
.mailer => mailer.run(allocator, cwd, sub_args.items, other_options.help),
|
||||
.job => job.run(allocator, cwd, sub_args.items, other_options.help),
|
||||
.middleware => middleware.run(allocator, cwd, sub_args.items, other_options.help),
|
||||
.secret => secret.run(allocator, cwd, sub_args.items, other_options.help),
|
||||
};
|
||||
} else {
|
||||
std.debug.print("Missing sub-command. Expected: [view|partial|layout|job|middleware|secret]\n", .{});
|
||||
return error.JetzigCommandError;
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, he
|
||||
const file = dir.createFile(filename, .{ .exclusive = true }) catch |err| {
|
||||
switch (err) {
|
||||
error.PathAlreadyExists => {
|
||||
std.debug.print("Partial already exists: {s}\n", .{filename});
|
||||
std.debug.print("Job already exists: {s}\n", .{filename});
|
||||
return error.JetzigCommandError;
|
||||
},
|
||||
else => return err,
|
||||
@ -42,10 +42,20 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, he
|
||||
\\const std = @import("std");
|
||||
\\const jetzig = @import("jetzig");
|
||||
\\
|
||||
\\/// The `run` function for all jobs receives an arena allocator, a logger, and the params
|
||||
\\/// passed to the job when it was created.
|
||||
\\// The `run` function for a job is invoked every time the job is processed by a queue worker
|
||||
\\// (or by the Jetzig server if the job is processed in-line).
|
||||
\\//
|
||||
\\// Arguments:
|
||||
\\// * allocator: Arena allocator for use during the job execution process.
|
||||
\\// * params: Params assigned to a job (from a request, any values added to `data`).
|
||||
\\// * env: Provides the following fields:
|
||||
\\// - logger: Logger attached to the same stream as the Jetzig server.
|
||||
\\// - environment: Enum of `{ production, development }`.
|
||||
\\pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, logger: jetzig.Logger) !void {
|
||||
\\ // Job execution code
|
||||
\\ _ = allocator;
|
||||
\\ _ = params;
|
||||
\\ // Job execution code goes here. Add any code that you would like to run in the background.
|
||||
\\ try env.logger.INFO("Running a job.", .{});
|
||||
\\}
|
||||
\\
|
||||
);
|
||||
|
146
cli/commands/generate/mailer.zig
Normal file
146
cli/commands/generate/mailer.zig
Normal file
@ -0,0 +1,146 @@
|
||||
const std = @import("std");
|
||||
|
||||
/// Run the mailer generator. Create a mailer in `src/app/mailers/`
|
||||
pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, help: bool) !void {
|
||||
if (help or args.len != 1) {
|
||||
std.debug.print(
|
||||
\\Generate a new Mailer. Mailers provide an interface for sending emails from a Jetzig application.
|
||||
\\
|
||||
\\Example:
|
||||
\\
|
||||
\\ jetzig generate mailer iguana
|
||||
\\
|
||||
, .{});
|
||||
|
||||
if (help) return;
|
||||
|
||||
return error.JetzigCommandError;
|
||||
}
|
||||
|
||||
const name = args[0];
|
||||
|
||||
const dir_path = try std.fs.path.join(allocator, &[_][]const u8{ "src", "app", "mailers" });
|
||||
defer allocator.free(dir_path);
|
||||
|
||||
var dir = try cwd.makeOpenPath(dir_path, .{});
|
||||
defer dir.close();
|
||||
|
||||
const filename = try std.mem.concat(allocator, u8, &[_][]const u8{ name, ".zig" });
|
||||
defer allocator.free(filename);
|
||||
|
||||
const mailer_file = dir.createFile(filename, .{ .exclusive = true }) catch |err| {
|
||||
switch (err) {
|
||||
error.PathAlreadyExists => {
|
||||
std.debug.print("Mailer already exists: {s}\n", .{filename});
|
||||
return error.JetzigCommandError;
|
||||
},
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
|
||||
try mailer_file.writeAll(
|
||||
\\const std = @import("std");
|
||||
\\const jetzig = @import("jetzig");
|
||||
\\
|
||||
\\// Default values for this mailer.
|
||||
\\pub const defaults: jetzig.mail.DefaultMailParams = .{
|
||||
\\ .from = "no-reply@example.com",
|
||||
\\ .subject = "Default subject",
|
||||
\\};
|
||||
\\
|
||||
\\// The `deliver` function is invoked every time this mailer is used to send an email.
|
||||
\\// Use this function to modify mail parameters before the mail is delivered, or simply
|
||||
\\// to log all uses of this mailer.
|
||||
\\//
|
||||
\\// To use this mailer from a request:
|
||||
\\// ```
|
||||
\\// const mail = request.mail("<mailer-name>", .{ .to = &.{"user@example.com"} });
|
||||
\\// try mail.deliver(.background, .{});
|
||||
\\// ```
|
||||
\\// A mailer can provide two Zmpl templates for rendering email content:
|
||||
\\// * `src/app/mailers/<mailer-name>/html.zmpl
|
||||
\\// * `src/app/mailers/<mailer-name>/text.zmpl
|
||||
\\//
|
||||
\\// Arguments:
|
||||
\\// * allocator: Arena allocator for use during the mail delivery process.
|
||||
\\// * mail: Mail parameters. Inspect or override any values assigned when the mail was created.
|
||||
\\// * params: Params assigned to a mail (from a request, any values added to `data`). Params
|
||||
\\// can be modified before email delivery.
|
||||
\\// * env: Provides the following fields:
|
||||
\\// - logger: Logger attached to the same stream as the Jetzig server.
|
||||
\\// - environment: Enum of `{ production, development }`.
|
||||
\\pub fn deliver(
|
||||
\\ allocator: std.mem.Allocator,
|
||||
\\ mail: *jetzig.mail.MailParams,
|
||||
\\ params: *jetzig.data.Value,
|
||||
\\ env: jetzig.jobs.JobEnv,
|
||||
\\) !void {
|
||||
\\ _ = allocator;
|
||||
\\ _ = params;
|
||||
\\
|
||||
\\ try env.logger.INFO("Delivering email with subject: '{?s}'", .{mail.get(.subject)});
|
||||
\\}
|
||||
\\
|
||||
);
|
||||
|
||||
mailer_file.close();
|
||||
|
||||
const realpath = try dir.realpathAlloc(allocator, filename);
|
||||
defer allocator.free(realpath);
|
||||
|
||||
std.debug.print("Generated mailer: {s}\n", .{realpath});
|
||||
|
||||
const template_dir_path = try std.fs.path.join(allocator, &[_][]const u8{ "src", "app", "mailers", name });
|
||||
defer allocator.free(template_dir_path);
|
||||
|
||||
var template_dir = try cwd.makeOpenPath(template_dir_path, .{});
|
||||
defer template_dir.close();
|
||||
|
||||
const html_template_file: ?std.fs.File = template_dir.createFile(
|
||||
"html.zmpl",
|
||||
.{ .exclusive = true },
|
||||
) catch |err| blk: {
|
||||
switch (err) {
|
||||
error.PathAlreadyExists => {
|
||||
std.debug.print("Template already exists: `{s}/html.zmpl` - skipping.\n", .{template_dir_path});
|
||||
break :blk null;
|
||||
},
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
|
||||
const text_template_file: ?std.fs.File = template_dir.createFile(
|
||||
"text.zmpl",
|
||||
.{ .exclusive = true },
|
||||
) catch |err| blk: {
|
||||
switch (err) {
|
||||
error.PathAlreadyExists => {
|
||||
std.debug.print("Template already exists: `{s}/text.zmpl` - skipping.\n", .{template_dir_path});
|
||||
break :blk null;
|
||||
},
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
|
||||
if (html_template_file) |file| {
|
||||
try file.writeAll(
|
||||
\\<div>HTML content goes here</div>
|
||||
\\
|
||||
);
|
||||
file.close();
|
||||
const html_template_realpath = try template_dir.realpathAlloc(allocator, "html.zmpl");
|
||||
defer allocator.free(html_template_realpath);
|
||||
std.debug.print("Generated mailer template: {s}\n", .{html_template_realpath});
|
||||
}
|
||||
|
||||
if (text_template_file) |file| {
|
||||
try file.writeAll(
|
||||
\\Text content goes here
|
||||
\\
|
||||
);
|
||||
file.close();
|
||||
const text_template_realpath = try template_dir.realpathAlloc(allocator, "text.zmpl");
|
||||
defer allocator.free(text_template_realpath);
|
||||
std.debug.print("Generated mailer template: {s}\n", .{text_template_realpath});
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
// All dependencies **must** be added to imports above this line.
|
||||
|
||||
try jetzig.jetzigInit(b, exe, .{ .zmpl_version = .v2 });
|
||||
try jetzig.jetzigInit(b, exe, .{});
|
||||
|
||||
b.installArtifact(exe);
|
||||
|
||||
|
@ -1,9 +1,22 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
/// The `run` function for all jobs receives an arena allocator, a logger, and the params
|
||||
/// passed to the job when it was created.
|
||||
pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, logger: jetzig.Logger) !void {
|
||||
_ = allocator;
|
||||
try logger.INFO("Job received params: {s}", .{try params.toJson()});
|
||||
/// The `run` function for all jobs receives an arena allocator, the params passed to the job
|
||||
/// when it was created, and an environment which provides a logger, the current server
|
||||
/// environment `{ development, production }`.
|
||||
pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig.jobs.JobEnv) !void {
|
||||
try env.logger.INFO("Job received params: {s}", .{try params.toJson()});
|
||||
|
||||
const mail = jetzig.mail.Mail.init(
|
||||
allocator,
|
||||
.{
|
||||
.subject = "Hello!!!",
|
||||
.from = "bob@jetzig.dev",
|
||||
.to = &.{"bob@jetzig.dev"},
|
||||
.html = "<div>Hello!</div>",
|
||||
.text = "Hello!",
|
||||
},
|
||||
);
|
||||
|
||||
try mail.deliver();
|
||||
}
|
||||
|
36
demo/src/app/mailers/welcome.zig
Normal file
36
demo/src/app/mailers/welcome.zig
Normal file
@ -0,0 +1,36 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
// Default values for this mailer.
|
||||
pub const defaults: jetzig.mail.DefaultMailParams = .{
|
||||
.from = "no-reply@example.com",
|
||||
.subject = "Default subject",
|
||||
};
|
||||
|
||||
// The `deliver` function is invoked every time this mailer is used to send an email.
|
||||
// Use this function to set default mail params (e.g. a default `from` address or
|
||||
// `subject`) before the mail is delivered.
|
||||
//
|
||||
// A mailer can provide two Zmpl templates for rendering email content:
|
||||
// * `src/app/mailers/<mailer-name>/html.zmpl
|
||||
// * `src/app/mailers/<mailer-name>/text.zmpl
|
||||
//
|
||||
// Arguments:
|
||||
// * allocator: Arena allocator for use during the mail delivery process.
|
||||
// * mail: Mail parameters. Inspect or override any values assigned when the mail was created.
|
||||
// * params: Params assigned to a mail (from a request, any values added to `data`). Params
|
||||
// can be modified before email delivery.
|
||||
// * env: Provides the following fields:
|
||||
// - logger: Logger attached to the same stream as the Jetzig server.
|
||||
// - environment: Enum of `{ production, development }`.
|
||||
pub fn deliver(
|
||||
allocator: std.mem.Allocator,
|
||||
mail: *jetzig.mail.MailParams,
|
||||
params: *jetzig.data.Value,
|
||||
env: jetzig.jobs.JobEnv,
|
||||
) !void {
|
||||
_ = allocator;
|
||||
_ = params;
|
||||
|
||||
try env.logger.INFO("Delivering email with subject: '{?s}'", .{mail.get(.subject)});
|
||||
}
|
1
demo/src/app/mailers/welcome/html.zmpl
Normal file
1
demo/src/app/mailers/welcome/html.zmpl
Normal file
@ -0,0 +1 @@
|
||||
<div>{{.message}}</div>
|
1
demo/src/app/mailers/welcome/text.zmpl
Normal file
1
demo/src/app/mailers/welcome/text.zmpl
Normal file
@ -0,0 +1 @@
|
||||
{{.message}}
|
@ -8,8 +8,8 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
var job = try request.job("example");
|
||||
|
||||
// Add a param `foo` to the job.
|
||||
try job.put("foo", data.string("bar"));
|
||||
try job.put("id", data.integer(std.crypto.random.int(u32)));
|
||||
try job.params.put("foo", data.string("bar"));
|
||||
try job.params.put("id", data.integer(std.crypto.random.int(u32)));
|
||||
|
||||
// Schedule the job for background processing. The job is added to the queue. When the job is
|
||||
// processed a new instance of `example_job` is created and its `run` function is invoked.
|
||||
|
20
demo/src/app/views/mail.zig
Normal file
20
demo/src/app/views/mail.zig
Normal file
@ -0,0 +1,20 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
var root = try data.object();
|
||||
try root.put("message", data.string("Welcome to Jetzig!"));
|
||||
|
||||
// Create a new mail using `src/app/mailers/welcome.zig`.
|
||||
// HTML and text parts are rendered using Zmpl templates:
|
||||
// * `src/app/mailers/welcome/html.zmpl`
|
||||
// * `src/app/mailers/welcome/text.zmpl`
|
||||
// All mailer templates have access to the same template data as a view template.
|
||||
const mail = request.mail("welcome", .{ .to = &.{"hello.dev"} });
|
||||
|
||||
// Deliver the email asynchronously via a built-in mail Job. Use `.now` to send the email
|
||||
// synchronously (i.e. before the request has returned).
|
||||
try mail.deliver(.background, .{});
|
||||
|
||||
return request.render(.ok);
|
||||
}
|
3
demo/src/app/views/mail/index.zmpl
Normal file
3
demo/src/app/views/mail/index.zmpl
Normal file
@ -0,0 +1,3 @@
|
||||
<div>
|
||||
<span>Your email has been sent!</span>
|
||||
</div>
|
1
demo/src/app/views/mailers/welcome.html.zmpl
Normal file
1
demo/src/app/views/mailers/welcome.html.zmpl
Normal file
@ -0,0 +1 @@
|
||||
<div>{{.data.test}}</div>
|
1
demo/src/app/views/mailers/welcome.text.zmpl
Normal file
1
demo/src/app/views/mailers/welcome.text.zmpl
Normal file
@ -0,0 +1 @@
|
||||
{{.data.test}}
|
1
demo/src/app/views/welcome.html.zmpl
Normal file
1
demo/src/app/views/welcome.html.zmpl
Normal file
@ -0,0 +1 @@
|
||||
<div>Hello</div>
|
@ -40,6 +40,19 @@ pub const jetzig_options = struct {
|
||||
// milliseconds.
|
||||
// pub const job_worker_sleep_interval_ms: usize = 10;
|
||||
|
||||
/// SMTP configuration for Jetzig Mail. It is recommended to use a local SMTP relay,
|
||||
/// e.g.: https://github.com/juanluisbaptiste/docker-postfix
|
||||
// pub const smtp: jetzig.mail.SMTPConfig = .{
|
||||
// .port = 25,
|
||||
// .encryption = .none, // .insecure, .none, .tls, .start_tls
|
||||
// .host = "localhost",
|
||||
// .username = null,
|
||||
// .password = null,
|
||||
// };
|
||||
|
||||
/// Force email delivery in development mode (instead of printing email body to logger).
|
||||
// pub const force_development_email_delivery = false;
|
||||
|
||||
// Set custom fragments for rendering markdown templates. Any values will fall back to
|
||||
// defaults provided by Zmd (https://github.com/bobf/zmd/blob/main/src/zmd/html.zig).
|
||||
pub const markdown_fragments = struct {
|
||||
|
@ -4,8 +4,10 @@ const jetzig = @import("jetzig.zig");
|
||||
ast: std.zig.Ast = undefined,
|
||||
allocator: std.mem.Allocator,
|
||||
root_path: []const u8,
|
||||
templates_path: []const u8,
|
||||
views_path: []const u8,
|
||||
jobs_path: []const u8,
|
||||
mailers_path: []const u8,
|
||||
buffer: std.ArrayList(u8),
|
||||
dynamic_routes: std.ArrayList(Function),
|
||||
static_routes: std.ArrayList(Function),
|
||||
@ -87,8 +89,10 @@ const Arg = struct {
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
root_path: []const u8,
|
||||
templates_path: []const u8,
|
||||
views_path: []const u8,
|
||||
jobs_path: []const u8,
|
||||
mailers_path: []const u8,
|
||||
) !Routes {
|
||||
const data = try allocator.create(jetzig.data.Data);
|
||||
data.* = jetzig.data.Data.init(allocator);
|
||||
@ -96,8 +100,10 @@ pub fn init(
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.root_path = root_path,
|
||||
.templates_path = templates_path,
|
||||
.views_path = views_path,
|
||||
.jobs_path = jobs_path,
|
||||
.mailers_path = mailers_path,
|
||||
.buffer = std.ArrayList(u8).init(allocator),
|
||||
.static_routes = std.ArrayList(Function).init(allocator),
|
||||
.dynamic_routes = std.ArrayList(Function).init(allocator),
|
||||
@ -122,9 +128,18 @@ pub fn generateRoutes(self: *Routes) !void {
|
||||
\\pub const routes = [_]jetzig.Route{
|
||||
\\
|
||||
);
|
||||
|
||||
try self.writeRoutes(writer);
|
||||
try writer.writeAll(
|
||||
\\};
|
||||
\\
|
||||
);
|
||||
|
||||
try writer.writeAll(
|
||||
\\
|
||||
\\pub const mailers = [_]jetzig.MailerDefinition{
|
||||
\\
|
||||
);
|
||||
try self.writeMailers(writer);
|
||||
try writer.writeAll(
|
||||
\\};
|
||||
\\
|
||||
@ -133,11 +148,10 @@ pub fn generateRoutes(self: *Routes) !void {
|
||||
try writer.writeAll(
|
||||
\\
|
||||
\\pub const jobs = [_]jetzig.JobDefinition{
|
||||
\\ .{ .name = "__jetzig_mail", .runFn = jetzig.mail.Job.run },
|
||||
\\
|
||||
);
|
||||
|
||||
try self.writeJobs(writer);
|
||||
|
||||
try writer.writeAll(
|
||||
\\};
|
||||
\\
|
||||
@ -148,13 +162,14 @@ pub fn generateRoutes(self: *Routes) !void {
|
||||
|
||||
pub fn relativePathFrom(
|
||||
self: Routes,
|
||||
root: enum { root, views, jobs },
|
||||
root: enum { root, views, mailers, jobs },
|
||||
sub_path: []const u8,
|
||||
format: enum { os, posix },
|
||||
) ![]u8 {
|
||||
const root_path = switch (root) {
|
||||
.root => self.root_path,
|
||||
.views => self.views_path,
|
||||
.mailers => self.mailers_path,
|
||||
.jobs => self.jobs_path,
|
||||
};
|
||||
|
||||
@ -250,7 +265,11 @@ fn writeRoute(self: *Routes, writer: std.ArrayList(u8).Writer, route: Function)
|
||||
const view_name = try route.viewName();
|
||||
defer self.allocator.free(view_name);
|
||||
|
||||
const template = try std.mem.concat(self.allocator, u8, &[_][]const u8{ view_name, "/", route.name });
|
||||
const template = try std.mem.concat(
|
||||
self.allocator,
|
||||
u8,
|
||||
&[_][]const u8{ view_name, "/", route.name },
|
||||
);
|
||||
|
||||
std.mem.replaceScalar(u8, module_path, '\\', '/');
|
||||
|
||||
@ -579,9 +598,61 @@ fn normalizePosix(self: Routes, path: []const u8) ![]u8 {
|
||||
return try std.mem.join(self.allocator, std.fs.path.sep_str_posix, buf.items);
|
||||
}
|
||||
|
||||
//
|
||||
// Generate Jobs
|
||||
//
|
||||
fn writeMailers(self: Routes, writer: anytype) !void {
|
||||
var dir = std.fs.openDirAbsolute(self.mailers_path, .{ .iterate = true }) catch |err| {
|
||||
switch (err) {
|
||||
error.FileNotFound => {
|
||||
std.debug.print(
|
||||
"[jetzig] Mailers directory not found, no mailers generated: `{s}`\n",
|
||||
.{self.mailers_path},
|
||||
);
|
||||
return;
|
||||
},
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
defer dir.close();
|
||||
|
||||
var count: usize = 0;
|
||||
var walker = try dir.walk(self.allocator);
|
||||
while (try walker.next()) |entry| {
|
||||
if (!std.mem.eql(u8, std.fs.path.extension(entry.path), ".zig")) continue;
|
||||
|
||||
const realpath = try dir.realpathAlloc(self.allocator, entry.path);
|
||||
defer self.allocator.free(realpath);
|
||||
|
||||
const root_relative_path = try self.relativePathFrom(.root, realpath, .posix);
|
||||
defer self.allocator.free(root_relative_path);
|
||||
|
||||
const mailers_relative_path = try self.relativePathFrom(.mailers, realpath, .posix);
|
||||
defer self.allocator.free(mailers_relative_path);
|
||||
|
||||
const module_path = try self.zigEscape(root_relative_path);
|
||||
defer self.allocator.free(module_path);
|
||||
|
||||
const name_path = try self.zigEscape(mailers_relative_path);
|
||||
defer self.allocator.free(name_path);
|
||||
|
||||
const name = chompExtension(name_path);
|
||||
|
||||
try writer.writeAll(try std.fmt.allocPrint(
|
||||
self.allocator,
|
||||
\\ .{{
|
||||
\\ .name = "{0s}",
|
||||
\\ .deliverFn = @import("{1s}").deliver,
|
||||
\\ .defaults = if (@hasDecl(@import("{1s}"), "defaults")) @import("{1s}").defaults else null,
|
||||
\\ .html_template = "{0s}/html",
|
||||
\\ .text_template = "{0s}/text",
|
||||
\\ }},
|
||||
\\
|
||||
,
|
||||
.{ name, module_path },
|
||||
));
|
||||
count += 1;
|
||||
}
|
||||
|
||||
std.debug.print("[jetzig] Imported {} mailer(s)\n", .{count});
|
||||
}
|
||||
|
||||
fn writeJobs(self: Routes, writer: anytype) !void {
|
||||
var dir = std.fs.openDirAbsolute(self.jobs_path, .{ .iterate = true }) catch |err| {
|
||||
|
@ -118,7 +118,7 @@ fn renderMarkdown(
|
||||
defer allocator.free(prefixed_name);
|
||||
defer allocator.free(prefixed_name);
|
||||
|
||||
if (zmpl.find(prefixed_name)) |layout| {
|
||||
if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
|
||||
view.data.content = .{ .data = content };
|
||||
return try layout.render(view.data);
|
||||
} else {
|
||||
@ -133,7 +133,7 @@ fn renderZmplTemplate(
|
||||
route: jetzig.views.Route,
|
||||
view: jetzig.views.View,
|
||||
) !?[]const u8 {
|
||||
if (zmpl.find(route.template)) |template| {
|
||||
if (zmpl.findPrefixed("views", route.template)) |template| {
|
||||
try view.data.addConst("jetzig_view", view.data.string(route.name));
|
||||
try view.data.addConst("jetzig_action", view.data.string(@tagName(route.action)));
|
||||
|
||||
@ -142,7 +142,7 @@ fn renderZmplTemplate(
|
||||
const prefixed_name = try std.mem.concat(allocator, u8, &[_][]const u8{ "layouts_", layout_name });
|
||||
defer allocator.free(prefixed_name);
|
||||
|
||||
if (zmpl.find(prefixed_name)) |layout| {
|
||||
if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
|
||||
return try template.renderWithLayout(layout, view.data);
|
||||
} else {
|
||||
std.debug.print("Unknown layout: {s}\n", .{layout_name});
|
||||
|
@ -14,6 +14,7 @@ pub const util = @import("jetzig/util.zig");
|
||||
pub const types = @import("jetzig/types.zig");
|
||||
pub const markdown = @import("jetzig/markdown.zig");
|
||||
pub const jobs = @import("jetzig/jobs.zig");
|
||||
pub const mail = @import("jetzig/mail.zig");
|
||||
|
||||
/// The primary interface for a Jetzig application. Create an `App` in your application's
|
||||
/// `src/main.zig` and call `start` to launch the application.
|
||||
@ -52,6 +53,9 @@ pub const Job = jobs.Job;
|
||||
/// A container for a job definition, includes the job name and run function.
|
||||
pub const JobDefinition = jobs.Job.JobDefinition;
|
||||
|
||||
/// A container for a mailer definition, includes mailer name and mail function.
|
||||
pub const MailerDefinition = mail.MailerDefinition;
|
||||
|
||||
/// A generic logger type. Provides all standard log levels as functions (`INFO`, `WARN`,
|
||||
/// `ERROR`, etc.). Note that all log functions are CAPITALIZED.
|
||||
pub const Logger = loggers.Logger;
|
||||
@ -95,6 +99,18 @@ pub const config = struct {
|
||||
/// milliseconds.
|
||||
pub const job_worker_sleep_interval_ms: usize = 10;
|
||||
|
||||
/// SMTP configuration for Jetzig Mail.
|
||||
pub const smtp: mail.SMTPConfig = .{
|
||||
.port = 25,
|
||||
.encryption = .none, // .insecure, .none, .tls, .start_tls
|
||||
.host = "localhost",
|
||||
.username = null,
|
||||
.password = null,
|
||||
};
|
||||
|
||||
/// Force email delivery in development mode (instead of printing email body to logger).
|
||||
pub const force_development_email_delivery = false;
|
||||
|
||||
/// Reconciles a configuration value from user-defined values and defaults provided by Jetzig.
|
||||
pub fn get(T: type, comptime key: []const u8) T {
|
||||
const self = @This();
|
||||
|
@ -79,6 +79,7 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void {
|
||||
self.server_options,
|
||||
routes.items,
|
||||
&routes_module.jobs,
|
||||
&routes_module.mailers,
|
||||
&mime_map,
|
||||
&jet_kv,
|
||||
);
|
||||
@ -87,8 +88,13 @@ pub fn start(self: App, routes_module: type, options: AppOptions) !void {
|
||||
var worker_pool = jetzig.jobs.Pool.init(
|
||||
self.allocator,
|
||||
&jet_kv,
|
||||
&routes_module.jobs,
|
||||
server.logger, // TODO: Optional separate log streams for workers
|
||||
.{
|
||||
.logger = server.logger,
|
||||
.environment = server.options.environment,
|
||||
.routes = routes.items,
|
||||
.jobs = &routes_module.jobs,
|
||||
.mailers = &routes_module.mailers,
|
||||
},
|
||||
);
|
||||
defer worker_pool.deinit();
|
||||
|
||||
|
@ -17,7 +17,7 @@ const Options = struct {
|
||||
environment: EnvironmentName = .development,
|
||||
log: []const u8 = "-",
|
||||
@"log-error": []const u8 = "-",
|
||||
@"log-level": jetzig.loggers.LogLevel = .INFO,
|
||||
@"log-level": ?jetzig.loggers.LogLevel = null,
|
||||
@"log-format": jetzig.loggers.LogFormat = .development,
|
||||
detach: bool = false,
|
||||
|
||||
@ -41,7 +41,7 @@ const Options = struct {
|
||||
\\Optional path to separate error log file. Use '-' for stderr. If omitted, errors are logged to the location specified by the `log` option (or stderr if `log` is '-').
|
||||
,
|
||||
.@"log-level" =
|
||||
\\Minimum log level. Log events below the given level are ignored. Must be one of: { TRACE, DEBUG, INFO, WARN, ERROR, FATAL } (default: DEBUG)
|
||||
\\Minimum log level. Log events below the given level are ignored. Must be one of: { TRACE, DEBUG, INFO, WARN, ERROR, FATAL } (default: DEBUG in development, INFO in production)
|
||||
,
|
||||
.@"log-format" =
|
||||
\\Output logs in the given format. Must be one of: { development, json } (default: development)
|
||||
@ -69,11 +69,13 @@ pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions {
|
||||
std.process.exit(0);
|
||||
}
|
||||
|
||||
const environment = options.options.environment;
|
||||
|
||||
var logger = switch (options.options.@"log-format") {
|
||||
.development => jetzig.loggers.Logger{
|
||||
.development_logger = jetzig.loggers.DevelopmentLogger.init(
|
||||
self.allocator,
|
||||
options.options.@"log-level",
|
||||
resolveLogLevel(options.options.@"log-level", environment),
|
||||
try getLogFile(.stdout, options.options),
|
||||
try getLogFile(.stderr, options.options),
|
||||
),
|
||||
@ -81,7 +83,7 @@ pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions {
|
||||
.json => jetzig.loggers.Logger{
|
||||
.json_logger = jetzig.loggers.JsonLogger.init(
|
||||
self.allocator,
|
||||
options.options.@"log-level",
|
||||
resolveLogLevel(options.options.@"log-level", environment),
|
||||
try getLogFile(.stdout, options.options),
|
||||
try getLogFile(.stderr, options.options),
|
||||
),
|
||||
@ -93,8 +95,6 @@ pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions {
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
const environment = options.options.environment;
|
||||
|
||||
// TODO: Generate nonce per session - do research to confirm correct best practice.
|
||||
const secret_len = jetzig.http.Session.Cipher.key_length + jetzig.http.Session.Cipher.nonce_length;
|
||||
const secret = (try self.getSecret(&logger, secret_len, environment))[0..secret_len];
|
||||
@ -162,3 +162,10 @@ fn getSecret(self: Environment, logger: *jetzig.loggers.Logger, comptime len: u1
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn resolveLogLevel(level: ?jetzig.loggers.LogLevel, environment: EnvironmentName) jetzig.loggers.LogLevel {
|
||||
return level orelse switch (environment) {
|
||||
.development => .DEBUG,
|
||||
.production => .INFO,
|
||||
};
|
||||
}
|
||||
|
@ -109,12 +109,18 @@ fn mappingParam(input: []const u8) ?struct { key: []const u8, field: []const u8
|
||||
|
||||
fn dataValue(self: Query, value: ?[]const u8) *jetzig.data.Data.Value {
|
||||
if (value) |item_value| {
|
||||
return self.data.string(item_value);
|
||||
const duped = self.data.getAllocator().dupe(u8, item_value) catch @panic("OOM");
|
||||
return self.data.string(uriDecode(duped));
|
||||
} else {
|
||||
return jetzig.zmpl.Data._null(self.data.getAllocator());
|
||||
}
|
||||
}
|
||||
|
||||
fn uriDecode(input: []u8) []const u8 {
|
||||
std.mem.replaceScalar(u8, input, '+', ' ');
|
||||
return std.Uri.percentDecodeInPlace(input);
|
||||
}
|
||||
|
||||
test "simple query string" {
|
||||
const allocator = std.testing.allocator;
|
||||
const query_string = "foo=bar&baz=qux";
|
||||
@ -190,3 +196,22 @@ test "query string with param without value" {
|
||||
else => std.testing.expect(false),
|
||||
};
|
||||
}
|
||||
|
||||
test "query string with encoded characters" {
|
||||
const allocator = std.testing.allocator;
|
||||
const query_string = "foo=bar+baz+qux&bar=hello%20%21%20how%20are%20you%20doing%20%3F%3F%3F";
|
||||
var data = jetzig.data.Data.init(allocator);
|
||||
|
||||
var query = init(allocator, query_string, &data);
|
||||
defer query.deinit();
|
||||
|
||||
try query.parse();
|
||||
try std.testing.expectEqualStrings(
|
||||
"bar baz qux",
|
||||
(data.getT(.string, "foo")).?,
|
||||
);
|
||||
try std.testing.expectEqualStrings(
|
||||
"hello ! how are you doing ???",
|
||||
(data.getT(.string, "bar")).?,
|
||||
);
|
||||
}
|
||||
|
@ -330,6 +330,65 @@ pub fn job(self: *Request, job_name: []const u8) !*jetzig.Job {
|
||||
return background_job;
|
||||
}
|
||||
|
||||
const RequestMail = struct {
|
||||
request: *Request,
|
||||
mail_params: jetzig.mail.MailParams,
|
||||
name: []const u8,
|
||||
|
||||
// Will allow scheduling when strategy is `.later` (e.g.).
|
||||
const DeliveryOptions = struct {};
|
||||
|
||||
pub fn deliver(self: RequestMail, strategy: enum { background, now }, options: DeliveryOptions) !void {
|
||||
_ = options;
|
||||
var mail_job = try self.request.job("__jetzig_mail");
|
||||
|
||||
try mail_job.params.put("mailer_name", mail_job.data.string(self.name));
|
||||
|
||||
const from = if (self.mail_params.from) |from| mail_job.data.string(from) else null;
|
||||
try mail_job.params.put("from", from);
|
||||
|
||||
var to_array = try mail_job.data.array();
|
||||
if (self.mail_params.to) |capture| {
|
||||
for (capture) |to| try to_array.append(mail_job.data.string(to));
|
||||
}
|
||||
try mail_job.params.put("to", to_array);
|
||||
|
||||
const subject = if (self.mail_params.subject) |subject| mail_job.data.string(subject) else null;
|
||||
try mail_job.params.put("subject", subject);
|
||||
|
||||
const html = if (self.mail_params.html) |html| mail_job.data.string(html) else null;
|
||||
try mail_job.params.put("html", html);
|
||||
|
||||
const text = if (self.mail_params.text) |text| mail_job.data.string(text) else null;
|
||||
try mail_job.params.put("text", text);
|
||||
|
||||
if (self.request.response_data.value) |value| try mail_job.params.put("params", value);
|
||||
|
||||
switch (strategy) {
|
||||
.background => try mail_job.schedule(),
|
||||
.now => try mail_job.definition.?.runFn(
|
||||
self.request.allocator,
|
||||
mail_job.params,
|
||||
jetzig.jobs.JobEnv{
|
||||
.environment = self.request.server.options.environment,
|
||||
.logger = self.request.server.logger,
|
||||
.routes = self.request.server.routes,
|
||||
.mailers = self.request.server.mailer_definitions,
|
||||
.jobs = self.request.server.job_definitions,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn mail(self: *Request, name: []const u8, mail_params: jetzig.mail.MailParams) RequestMail {
|
||||
return .{
|
||||
.request = self,
|
||||
.name = name,
|
||||
.mail_params = mail_params,
|
||||
};
|
||||
}
|
||||
|
||||
fn extensionFormat(self: *Request) ?jetzig.http.Request.Format {
|
||||
const extension = self.path.extension orelse return null;
|
||||
if (std.mem.eql(u8, extension, ".html")) {
|
||||
|
@ -17,6 +17,7 @@ logger: jetzig.loggers.Logger,
|
||||
options: ServerOptions,
|
||||
routes: []*jetzig.views.Route,
|
||||
job_definitions: []const jetzig.JobDefinition,
|
||||
mailer_definitions: []const jetzig.MailerDefinition,
|
||||
mime_map: *jetzig.http.mime.MimeMap,
|
||||
std_net_server: std.net.Server = undefined,
|
||||
initialized: bool = false,
|
||||
@ -29,6 +30,7 @@ pub fn init(
|
||||
options: ServerOptions,
|
||||
routes: []*jetzig.views.Route,
|
||||
job_definitions: []const jetzig.JobDefinition,
|
||||
mailer_definitions: []const jetzig.MailerDefinition,
|
||||
mime_map: *jetzig.http.mime.MimeMap,
|
||||
jet_kv: *jetzig.jetkv.JetKV,
|
||||
) Server {
|
||||
@ -38,6 +40,7 @@ pub fn init(
|
||||
.options = options,
|
||||
.routes = routes,
|
||||
.job_definitions = job_definitions,
|
||||
.mailer_definitions = mailer_definitions,
|
||||
.mime_map = mime_map,
|
||||
.jet_kv = jet_kv,
|
||||
};
|
||||
@ -150,7 +153,7 @@ fn renderHTML(
|
||||
route: ?*jetzig.views.Route,
|
||||
) !void {
|
||||
if (route) |matched_route| {
|
||||
const template = zmpl.find(matched_route.template);
|
||||
const template = zmpl.findPrefixed("views", matched_route.template);
|
||||
if (template == null) {
|
||||
request.response_data.noop(bool, false); // FIXME: Weird Zig bug ? Any call here fixes it.
|
||||
if (try self.renderMarkdown(request, route)) |rendered_markdown| {
|
||||
@ -238,7 +241,7 @@ fn renderMarkdown(
|
||||
);
|
||||
defer self.allocator.free(prefixed_name);
|
||||
|
||||
if (zmpl.find(prefixed_name)) |layout| {
|
||||
if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
|
||||
rendered.view.data.content = .{ .data = markdown_content };
|
||||
rendered.content = try layout.render(rendered.view.data);
|
||||
} else {
|
||||
@ -307,7 +310,7 @@ fn renderTemplateWithLayout(
|
||||
const prefixed_name = try std.mem.concat(self.allocator, u8, &[_][]const u8{ "layouts", "/", layout_name });
|
||||
defer self.allocator.free(prefixed_name);
|
||||
|
||||
if (zmpl.find(prefixed_name)) |layout| {
|
||||
if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
|
||||
return try template.renderWithLayout(layout, view.data);
|
||||
} else {
|
||||
try self.logger.WARN("Unknown layout: {s}", .{layout_name});
|
||||
|
@ -1,4 +1,6 @@
|
||||
pub const Job = @import("jobs/Job.zig");
|
||||
pub const JobEnv = Job.JobEnv;
|
||||
pub const JobConfig = Job.JobConfig;
|
||||
pub const JobDefinition = Job.JobDefinition;
|
||||
pub const Pool = @import("jobs/Pool.zig");
|
||||
pub const Worker = @import("jobs/Worker.zig");
|
||||
|
@ -4,7 +4,15 @@ const jetzig = @import("../../jetzig.zig");
|
||||
/// Job name and run function, used when generating an array of job definitions at build time.
|
||||
pub const JobDefinition = struct {
|
||||
name: []const u8,
|
||||
runFn: *const fn (std.mem.Allocator, *jetzig.data.Value, jetzig.loggers.Logger) anyerror!void,
|
||||
runFn: *const fn (std.mem.Allocator, *jetzig.data.Value, JobEnv) anyerror!void,
|
||||
};
|
||||
|
||||
pub const JobEnv = struct {
|
||||
logger: jetzig.loggers.Logger,
|
||||
environment: jetzig.Environment.EnvironmentName,
|
||||
routes: []*const jetzig.Route,
|
||||
mailers: []const jetzig.MailerDefinition,
|
||||
jobs: []const jetzig.JobDefinition,
|
||||
};
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
@ -12,8 +20,8 @@ jet_kv: *jetzig.jetkv.JetKV,
|
||||
logger: jetzig.loggers.Logger,
|
||||
name: []const u8,
|
||||
definition: ?JobDefinition,
|
||||
data: ?*jetzig.data.Data = null,
|
||||
_params: ?*jetzig.data.Value = null,
|
||||
data: *jetzig.data.Data,
|
||||
params: *jetzig.data.Value,
|
||||
|
||||
const Job = @This();
|
||||
|
||||
@ -34,45 +42,30 @@ pub fn init(
|
||||
}
|
||||
}
|
||||
|
||||
const data = allocator.create(jetzig.data.Data) catch @panic("OOM");
|
||||
data.* = jetzig.data.Data.init(allocator);
|
||||
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.jet_kv = jet_kv,
|
||||
.logger = logger,
|
||||
.name = name,
|
||||
.definition = definition,
|
||||
.data = data,
|
||||
.params = data.object() catch @panic("OOM"),
|
||||
};
|
||||
}
|
||||
|
||||
/// Deinitialize the Job and frees memory
|
||||
pub fn deinit(self: *Job) void {
|
||||
if (self.data) |data| {
|
||||
data.deinit();
|
||||
self.allocator.destroy(data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a parameter to the Job
|
||||
pub fn put(self: *Job, key: []const u8, value: *jetzig.data.Value) !void {
|
||||
var job_params = try self.params();
|
||||
try job_params.put(key, value);
|
||||
self.data.deinit();
|
||||
self.allocator.destroy(self.data);
|
||||
}
|
||||
|
||||
/// Add a Job to the queue
|
||||
pub fn schedule(self: *Job) !void {
|
||||
_ = try self.params();
|
||||
const json = try self.data.?.toJson();
|
||||
try self.params.put("__jetzig_job_name", self.data.string(self.name));
|
||||
const json = try self.data.toJson();
|
||||
try self.jet_kv.prepend("__jetzig_jobs", json);
|
||||
try self.logger.INFO("Scheduled job: {s}", .{self.name});
|
||||
}
|
||||
|
||||
fn params(self: *Job) !*jetzig.data.Value {
|
||||
if (self.data == null) {
|
||||
self.data = try self.allocator.create(jetzig.data.Data);
|
||||
self.data.?.* = jetzig.data.Data.init(self.allocator);
|
||||
self._params = try self.data.?.object();
|
||||
try self._params.?.put("__jetzig_job_name", self.data.?.string(self.name));
|
||||
}
|
||||
return self._params.?;
|
||||
}
|
||||
|
||||
// TODO: Tests :)
|
||||
|
@ -6,8 +6,7 @@ const Pool = @This();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
jet_kv: *jetzig.jetkv.JetKV,
|
||||
job_definitions: []const jetzig.jobs.JobDefinition,
|
||||
logger: jetzig.loggers.Logger,
|
||||
job_env: jetzig.jobs.JobEnv,
|
||||
pool: std.Thread.Pool = undefined,
|
||||
workers: std.ArrayList(*jetzig.jobs.Worker),
|
||||
|
||||
@ -15,14 +14,12 @@ workers: std.ArrayList(*jetzig.jobs.Worker),
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
jet_kv: *jetzig.jetkv.JetKV,
|
||||
job_definitions: []const jetzig.jobs.JobDefinition,
|
||||
logger: jetzig.loggers.Logger,
|
||||
job_env: jetzig.jobs.JobEnv,
|
||||
) Pool {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.jet_kv = jet_kv,
|
||||
.job_definitions = job_definitions,
|
||||
.logger = logger,
|
||||
.job_env = job_env,
|
||||
.workers = std.ArrayList(*jetzig.jobs.Worker).init(allocator),
|
||||
};
|
||||
}
|
||||
@ -43,10 +40,9 @@ pub fn work(self: *Pool, threads: usize, interval: usize) !void {
|
||||
const worker = try self.allocator.create(jetzig.jobs.Worker);
|
||||
worker.* = jetzig.jobs.Worker.init(
|
||||
self.allocator,
|
||||
self.logger,
|
||||
self.job_env,
|
||||
index,
|
||||
self.jet_kv,
|
||||
self.job_definitions,
|
||||
interval,
|
||||
);
|
||||
try self.workers.append(worker);
|
||||
|
@ -4,26 +4,23 @@ const jetzig = @import("../../jetzig.zig");
|
||||
const Worker = @This();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
logger: jetzig.loggers.Logger,
|
||||
job_env: jetzig.jobs.JobEnv,
|
||||
id: usize,
|
||||
jet_kv: *jetzig.jetkv.JetKV,
|
||||
job_definitions: []const jetzig.jobs.JobDefinition,
|
||||
interval: usize,
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
logger: jetzig.loggers.Logger,
|
||||
job_env: jetzig.jobs.JobEnv,
|
||||
id: usize,
|
||||
jet_kv: *jetzig.jetkv.JetKV,
|
||||
job_definitions: []const jetzig.jobs.JobDefinition,
|
||||
interval: usize,
|
||||
) Worker {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.logger = logger,
|
||||
.job_env = job_env,
|
||||
.id = id,
|
||||
.jet_kv = jet_kv,
|
||||
.job_definitions = job_definitions,
|
||||
.interval = interval * 1000 * 1000, // millisecond => nanosecond
|
||||
};
|
||||
}
|
||||
@ -65,7 +62,7 @@ fn matchJob(self: Worker, json: []const u8) ?jetzig.jobs.JobDefinition {
|
||||
const job_name = parsed_json.value.__jetzig_job_name;
|
||||
|
||||
// TODO: Hashmap
|
||||
for (self.job_definitions) |job_definition| {
|
||||
for (self.job_env.jobs) |job_definition| {
|
||||
if (std.mem.eql(u8, job_definition.name, job_name)) {
|
||||
parsed_json.deinit();
|
||||
return job_definition;
|
||||
@ -94,7 +91,7 @@ fn processJob(self: Worker, job_definition: jetzig.JobDefinition, json: []const
|
||||
defer arena.deinit();
|
||||
|
||||
if (data.value) |params| {
|
||||
job_definition.runFn(arena.allocator(), params, self.logger) catch |err| {
|
||||
job_definition.runFn(arena.allocator(), params, self.job_env) catch |err| {
|
||||
self.log(
|
||||
.ERROR,
|
||||
"[worker-{}] Encountered error processing job `{s}`: {s}",
|
||||
@ -115,7 +112,7 @@ fn log(
|
||||
comptime message: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
self.logger.log(level, message, args) catch |err| {
|
||||
self.job_env.logger.log(level, message, args) catch |err| {
|
||||
// XXX: In (daemonized) deployment stderr will not be available, find a better solution.
|
||||
// Note that this only occurs if logging itself fails.
|
||||
std.debug.print("[worker-{}] Logger encountered error: {s}\n", .{ self.id, @errorName(err) });
|
||||
|
9
src/jetzig/mail.zig
Normal file
9
src/jetzig/mail.zig
Normal file
@ -0,0 +1,9 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const Mail = @import("mail/Mail.zig");
|
||||
pub const SMTPConfig = @import("mail/SMTPConfig.zig");
|
||||
pub const MailParams = @import("mail/MailParams.zig");
|
||||
pub const DefaultMailParams = MailParams.DefaultMailParams;
|
||||
pub const components = @import("mail/components.zig");
|
||||
pub const Job = @import("mail/Job.zig");
|
||||
pub const MailerDefinition = @import("mail/MailerDefinition.zig");
|
159
src/jetzig/mail/Job.zig
Normal file
159
src/jetzig/mail/Job.zig
Normal file
@ -0,0 +1,159 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
/// Default Mail Job. Send an email with the given params in the background.
|
||||
pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig.jobs.JobEnv) !void {
|
||||
const mailer_name = if (params.get("mailer_name")) |param| switch (param.*) {
|
||||
.Null => null,
|
||||
.string => |string| string.value,
|
||||
else => null,
|
||||
} else null;
|
||||
|
||||
if (mailer_name == null) {
|
||||
try env.logger.ERROR("Missing mailer name parameter", .{});
|
||||
return error.JetzigMissingMailerName;
|
||||
}
|
||||
|
||||
const mailer = findMailer(mailer_name.?, env) orelse {
|
||||
try env.logger.ERROR("Unknown mailer: `{s}`", .{mailer_name.?});
|
||||
return error.JetzigUnknownMailerName;
|
||||
};
|
||||
|
||||
const subject = params.get("subject");
|
||||
const from = params.get("from");
|
||||
|
||||
const html = params.get("html");
|
||||
const text = params.get("text");
|
||||
|
||||
const to = try resolveTo(allocator, params);
|
||||
|
||||
var mail_params = jetzig.mail.MailParams{
|
||||
.subject = resolveSubject(subject),
|
||||
.from = resolveFrom(from),
|
||||
.to = to,
|
||||
.defaults = mailer.defaults,
|
||||
};
|
||||
|
||||
try mailer.deliverFn(allocator, &mail_params, params, env);
|
||||
|
||||
const mail = jetzig.mail.Mail.init(allocator, .{
|
||||
.subject = mail_params.get(.subject) orelse "(No subject)",
|
||||
.from = mail_params.get(.from) orelse return error.JetzigMailerMissingFromAddress,
|
||||
.to = mail_params.get(.to) orelse return error.JetzigMailerMissingToAddress,
|
||||
.html = mail_params.get(.html) orelse try resolveHtml(allocator, mailer, html, params),
|
||||
.text = mail_params.get(.text) orelse try resolveText(allocator, mailer, text, params),
|
||||
});
|
||||
|
||||
if (env.environment == .development and !jetzig.config.get(bool, "force_development_email_delivery")) {
|
||||
try env.logger.INFO(
|
||||
"Skipping mail delivery in development environment:\n{s}",
|
||||
.{try mail.generateData()},
|
||||
);
|
||||
} else {
|
||||
try mail.deliver();
|
||||
try env.logger.INFO("Delivered mail to: {s}", .{
|
||||
try std.mem.join(allocator, ", ", mail.params.to.?),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn resolveSubject(subject: ?*const jetzig.data.Value) ?[]const u8 {
|
||||
if (subject) |capture| {
|
||||
return switch (capture.*) {
|
||||
.Null => null,
|
||||
.string => |string| string.value,
|
||||
else => unreachable,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
fn resolveFrom(from: ?*const jetzig.data.Value) ?[]const u8 {
|
||||
return if (from) |capture| switch (capture.*) {
|
||||
.Null => null,
|
||||
.string => |string| string.value,
|
||||
else => unreachable,
|
||||
} else null;
|
||||
}
|
||||
|
||||
fn resolveTo(allocator: std.mem.Allocator, params: *const jetzig.data.Value) !?[]const []const u8 {
|
||||
var to = std.ArrayList([]const u8).init(allocator);
|
||||
if (params.get("to")) |capture| {
|
||||
for (capture.items(.array)) |recipient| {
|
||||
try to.append(recipient.string.value);
|
||||
}
|
||||
}
|
||||
return if (to.items.len > 0) try to.toOwnedSlice() else null;
|
||||
}
|
||||
|
||||
fn resolveText(
|
||||
allocator: std.mem.Allocator,
|
||||
mailer: jetzig.mail.MailerDefinition,
|
||||
text: ?*const jetzig.data.Value,
|
||||
params: *jetzig.data.Value,
|
||||
) !?[]const u8 {
|
||||
if (text) |capture| {
|
||||
return switch (capture.*) {
|
||||
.Null => try defaultText(allocator, mailer, params),
|
||||
.string => |string| string.value,
|
||||
else => unreachable,
|
||||
};
|
||||
} else {
|
||||
return try defaultText(allocator, mailer, params);
|
||||
}
|
||||
}
|
||||
|
||||
fn resolveHtml(
|
||||
allocator: std.mem.Allocator,
|
||||
mailer: jetzig.mail.MailerDefinition,
|
||||
text: ?*const jetzig.data.Value,
|
||||
params: *jetzig.data.Value,
|
||||
) !?[]const u8 {
|
||||
if (text) |capture| {
|
||||
return switch (capture.*) {
|
||||
.Null => try defaultHtml(allocator, mailer, params),
|
||||
.string => |string| string.value,
|
||||
else => unreachable,
|
||||
};
|
||||
} else {
|
||||
return try defaultHtml(allocator, mailer, params);
|
||||
}
|
||||
}
|
||||
|
||||
fn defaultHtml(
|
||||
allocator: std.mem.Allocator,
|
||||
mailer: jetzig.mail.MailerDefinition,
|
||||
params: *jetzig.data.Value,
|
||||
) !?[]const u8 {
|
||||
var data = jetzig.data.Data.init(allocator);
|
||||
data.value = if (params.get("params")) |capture| capture else try data.createObject();
|
||||
try data.addConst("jetzig_view", data.string(""));
|
||||
try data.addConst("jetzig_action", data.string(""));
|
||||
return if (jetzig.zmpl.findPrefixed("mailers", mailer.html_template)) |template|
|
||||
try template.render(&data)
|
||||
else
|
||||
null;
|
||||
}
|
||||
|
||||
fn defaultText(
|
||||
allocator: std.mem.Allocator,
|
||||
mailer: jetzig.mail.MailerDefinition,
|
||||
params: *jetzig.data.Value,
|
||||
) !?[]const u8 {
|
||||
var data = jetzig.data.Data.init(allocator);
|
||||
data.value = if (params.get("params")) |capture| capture else try data.createObject();
|
||||
try data.addConst("jetzig_view", data.string(""));
|
||||
try data.addConst("jetzig_action", data.string(""));
|
||||
return if (jetzig.zmpl.findPrefixed("mailers", mailer.text_template)) |template|
|
||||
try template.render(&data)
|
||||
else
|
||||
null;
|
||||
}
|
||||
|
||||
fn findMailer(name: []const u8, env: jetzig.jobs.JobEnv) ?jetzig.mail.MailerDefinition {
|
||||
for (env.mailers) |mailer| {
|
||||
if (std.mem.eql(u8, mailer.name, name)) return mailer;
|
||||
}
|
||||
return null;
|
||||
}
|
343
src/jetzig/mail/Mail.zig
Normal file
343
src/jetzig/mail/Mail.zig
Normal file
@ -0,0 +1,343 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
const smtp = @import("smtp");
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
config: jetzig.mail.SMTPConfig,
|
||||
params: jetzig.mail.MailParams,
|
||||
boundary: u32,
|
||||
|
||||
const Mail = @This();
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
params: jetzig.mail.MailParams,
|
||||
) Mail {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.config = jetzig.config.get(jetzig.mail.SMTPConfig, "smtp"),
|
||||
.params = params,
|
||||
.boundary = std.crypto.random.int(u32),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deliver(self: Mail) !void {
|
||||
const data = try self.generateData();
|
||||
defer self.allocator.free(data);
|
||||
|
||||
try smtp.send(.{
|
||||
.from = self.params.from.?,
|
||||
.to = self.params.to.?,
|
||||
.data = data,
|
||||
}, self.config.toSMTP(self.allocator));
|
||||
}
|
||||
|
||||
pub fn generateData(self: Mail) ![]const u8 {
|
||||
try self.validate();
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(self.allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const allocator = arena.allocator();
|
||||
|
||||
var sections = std.ArrayList([]const u8).init(allocator);
|
||||
try sections.append(try std.fmt.allocPrint(allocator, "From: {s}", .{self.params.get(.from).?}));
|
||||
try sections.append(try std.fmt.allocPrint(allocator, "Subject: {s}", .{self.params.get(.subject).?}));
|
||||
|
||||
if (self.params.get(.cc)) |cc| {
|
||||
for (cc) |recipient| {
|
||||
try sections.append(try std.fmt.allocPrint(allocator, "Cc: {s}", .{recipient}));
|
||||
}
|
||||
}
|
||||
|
||||
const body = try std.mem.concat(allocator, u8, &.{
|
||||
try self.header(allocator),
|
||||
try self.textPart(allocator),
|
||||
if (self.params.get(.html) != null and self.params.get(.text) != null) "\r\n" else "",
|
||||
try self.htmlPart(allocator),
|
||||
jetzig.mail.components.footer,
|
||||
});
|
||||
|
||||
try sections.append(body);
|
||||
|
||||
return std.mem.join(self.allocator, "\r\n", sections.items);
|
||||
}
|
||||
|
||||
fn validate(self: Mail) !void {
|
||||
if (self.params.get(.from) == null) return error.JetzigMailMissingFromAddress;
|
||||
if (self.params.get(.to) == null) return error.JetzigMailMissingFromAddress;
|
||||
if (self.params.get(.subject) == null) return error.JetzigMailMissingSubject;
|
||||
}
|
||||
|
||||
fn header(self: Mail, allocator: std.mem.Allocator) ![]const u8 {
|
||||
return try std.fmt.allocPrint(
|
||||
allocator,
|
||||
jetzig.mail.components.header,
|
||||
.{self.boundary},
|
||||
);
|
||||
}
|
||||
|
||||
fn footer(self: Mail, allocator: std.mem.Allocator) ![]const u8 {
|
||||
return try std.fmt.allocPrint(
|
||||
allocator,
|
||||
jetzig.mail.components.footer,
|
||||
.{self.boundary},
|
||||
);
|
||||
}
|
||||
|
||||
fn textPart(self: Mail, allocator: std.mem.Allocator) ![]const u8 {
|
||||
if (self.params.get(.text)) |content| {
|
||||
return try std.fmt.allocPrint(
|
||||
allocator,
|
||||
jetzig.mail.components.text,
|
||||
.{ self.boundary, try encode(allocator, content) },
|
||||
);
|
||||
} else return "";
|
||||
}
|
||||
|
||||
fn htmlPart(self: Mail, allocator: std.mem.Allocator) ![]const u8 {
|
||||
if (self.params.get(.html)) |content| {
|
||||
return try std.fmt.allocPrint(
|
||||
allocator,
|
||||
jetzig.mail.components.html,
|
||||
.{ self.boundary, try encode(allocator, content) },
|
||||
);
|
||||
} else return "";
|
||||
}
|
||||
|
||||
fn encode(allocator: std.mem.Allocator, content: []const u8) ![]const u8 {
|
||||
var buf = std.ArrayList(u8).init(allocator);
|
||||
const writer = buf.writer();
|
||||
var line_len: u8 = 0;
|
||||
|
||||
for (content) |char| {
|
||||
const encoded = isEncoded(char);
|
||||
const encoded_len: u2 = if (encoded) 3 else 1;
|
||||
|
||||
if (encoded_len + line_len >= 76) {
|
||||
try writer.writeAll("=\r\n");
|
||||
line_len = encoded_len;
|
||||
} else {
|
||||
line_len += encoded_len;
|
||||
}
|
||||
|
||||
if (encoded) {
|
||||
try writer.print("={X:0>2}", .{char});
|
||||
} else {
|
||||
try writer.writeByte(char);
|
||||
}
|
||||
}
|
||||
|
||||
return buf.toOwnedSlice();
|
||||
}
|
||||
|
||||
inline fn isEncoded(char: u8) bool {
|
||||
return char == '=' or !std.ascii.isPrint(char);
|
||||
}
|
||||
|
||||
test "HTML part only" {
|
||||
const mail = Mail{
|
||||
.allocator = std.testing.allocator,
|
||||
.config = .{},
|
||||
.boundary = 123456789,
|
||||
.params = .{
|
||||
.from = "user@example.com",
|
||||
.to = &.{"user@example.com"},
|
||||
.subject = "Test subject",
|
||||
.html = "<div>Hello</div>",
|
||||
},
|
||||
};
|
||||
|
||||
const actual = try generateData(mail);
|
||||
defer std.testing.allocator.free(actual);
|
||||
|
||||
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
|
||||
\\From: user@example.com
|
||||
\\Subject: Test subject
|
||||
\\MIME-Version: 1.0
|
||||
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
|
||||
\\--=_alternative_123456789
|
||||
\\Content-Type: text/html; charset="UTF-8"
|
||||
\\Content-Transfer-Encoding: quoted-printable
|
||||
\\
|
||||
\\<div>Hello</div>
|
||||
\\
|
||||
\\.
|
||||
\\
|
||||
, "\n", "\r\n");
|
||||
defer std.testing.allocator.free(expected);
|
||||
|
||||
try std.testing.expectEqualStrings(expected, actual);
|
||||
}
|
||||
|
||||
test "text part only" {
|
||||
const mail = Mail{
|
||||
.allocator = std.testing.allocator,
|
||||
.config = .{},
|
||||
.boundary = 123456789,
|
||||
.params = .{
|
||||
.from = "user@example.com",
|
||||
.to = &.{"user@example.com"},
|
||||
.subject = "Test subject",
|
||||
.text = "Hello",
|
||||
},
|
||||
};
|
||||
|
||||
const actual = try generateData(mail);
|
||||
defer std.testing.allocator.free(actual);
|
||||
|
||||
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
|
||||
\\From: user@example.com
|
||||
\\Subject: Test subject
|
||||
\\MIME-Version: 1.0
|
||||
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
|
||||
\\--=_alternative_123456789
|
||||
\\Content-Type: text/plain; charset="UTF-8"
|
||||
\\Content-Transfer-Encoding: quoted-printable
|
||||
\\
|
||||
\\Hello
|
||||
\\
|
||||
\\.
|
||||
\\
|
||||
, "\n", "\r\n");
|
||||
defer std.testing.allocator.free(expected);
|
||||
|
||||
try std.testing.expectEqualStrings(expected, actual);
|
||||
}
|
||||
|
||||
test "HTML and text parts" {
|
||||
const mail = Mail{
|
||||
.allocator = std.testing.allocator,
|
||||
.config = .{},
|
||||
.boundary = 123456789,
|
||||
.params = .{
|
||||
.from = "user@example.com",
|
||||
.to = &.{"user@example.com"},
|
||||
.subject = "Test subject",
|
||||
.html = "<div>Hello</div>",
|
||||
.text = "Hello",
|
||||
},
|
||||
};
|
||||
|
||||
const actual = try generateData(mail);
|
||||
defer std.testing.allocator.free(actual);
|
||||
|
||||
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
|
||||
\\From: user@example.com
|
||||
\\Subject: Test subject
|
||||
\\MIME-Version: 1.0
|
||||
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
|
||||
\\--=_alternative_123456789
|
||||
\\Content-Type: text/plain; charset="UTF-8"
|
||||
\\Content-Transfer-Encoding: quoted-printable
|
||||
\\
|
||||
\\Hello
|
||||
\\
|
||||
\\--=_alternative_123456789
|
||||
\\Content-Type: text/html; charset="UTF-8"
|
||||
\\Content-Transfer-Encoding: quoted-printable
|
||||
\\
|
||||
\\<div>Hello</div>
|
||||
\\
|
||||
\\.
|
||||
\\
|
||||
, "\n", "\r\n");
|
||||
defer std.testing.allocator.free(expected);
|
||||
|
||||
try std.testing.expectEqualStrings(expected, actual);
|
||||
}
|
||||
|
||||
test "long content encoding" {
|
||||
const mail = Mail{
|
||||
.allocator = std.testing.allocator,
|
||||
.config = .{},
|
||||
.boundary = 123456789,
|
||||
.params = .{
|
||||
.from = "user@example.com",
|
||||
.to = &.{"user@example.com"},
|
||||
.subject = "Test subject",
|
||||
.html = "<html><body><div style=\"background-color: black; color: #ff00ff;\">Hellooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo!!!</div></body></html>",
|
||||
.text = "Hello",
|
||||
},
|
||||
};
|
||||
|
||||
const actual = try generateData(mail);
|
||||
defer std.testing.allocator.free(actual);
|
||||
|
||||
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
|
||||
\\From: user@example.com
|
||||
\\Subject: Test subject
|
||||
\\MIME-Version: 1.0
|
||||
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
|
||||
\\--=_alternative_123456789
|
||||
\\Content-Type: text/plain; charset="UTF-8"
|
||||
\\Content-Transfer-Encoding: quoted-printable
|
||||
\\
|
||||
\\Hello
|
||||
\\
|
||||
\\--=_alternative_123456789
|
||||
\\Content-Type: text/html; charset="UTF-8"
|
||||
\\Content-Transfer-Encoding: quoted-printable
|
||||
\\
|
||||
\\<html><body><div style=3D"background-color: black; color: #ff00ff;">Hellooo=
|
||||
\\ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo=
|
||||
\\ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo=
|
||||
\\ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo=
|
||||
\\ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo=
|
||||
\\ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo=
|
||||
\\ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo!!!</div></bod=
|
||||
\\y></html>
|
||||
\\
|
||||
\\.
|
||||
\\
|
||||
, "\n", "\r\n");
|
||||
defer std.testing.allocator.free(expected);
|
||||
|
||||
try std.testing.expectEqualStrings(expected, actual);
|
||||
}
|
||||
|
||||
test "non-latin alphabet encoding" {
|
||||
const mail = Mail{
|
||||
.allocator = std.testing.allocator,
|
||||
.config = .{},
|
||||
.boundary = 123456789,
|
||||
.params = .{
|
||||
.from = "user@example.com",
|
||||
.to = &.{"user@example.com"},
|
||||
.subject = "Test subject",
|
||||
.html = "<html><body><div>你爱学习 Zig 吗?</div></body></html>",
|
||||
|
||||
.text = "Hello",
|
||||
},
|
||||
};
|
||||
|
||||
const actual = try generateData(mail);
|
||||
defer std.testing.allocator.free(actual);
|
||||
|
||||
const expected = try std.mem.replaceOwned(u8, std.testing.allocator,
|
||||
\\From: user@example.com
|
||||
\\Subject: Test subject
|
||||
\\MIME-Version: 1.0
|
||||
\\Content-Type: multipart/alternative; boundary="=_alternative_123456789"
|
||||
\\--=_alternative_123456789
|
||||
\\Content-Type: text/plain; charset="UTF-8"
|
||||
\\Content-Transfer-Encoding: quoted-printable
|
||||
\\
|
||||
\\Hello
|
||||
\\
|
||||
\\--=_alternative_123456789
|
||||
\\Content-Type: text/html; charset="UTF-8"
|
||||
\\Content-Transfer-Encoding: quoted-printable
|
||||
\\
|
||||
\\<html><body><div>=E4=BD=A0=E7=88=B1=E5=AD=A6=E4=B9=A0 Zig =E5=90=97=EF=BC=
|
||||
\\=9F</div></body></html>
|
||||
\\
|
||||
\\.
|
||||
\\
|
||||
, "\n", "\r\n");
|
||||
defer std.testing.allocator.free(expected);
|
||||
|
||||
try std.testing.expectEqualStrings(expected, actual);
|
||||
}
|
38
src/jetzig/mail/MailParams.zig
Normal file
38
src/jetzig/mail/MailParams.zig
Normal file
@ -0,0 +1,38 @@
|
||||
subject: ?[]const u8 = null,
|
||||
from: ?[]const u8 = null,
|
||||
to: ?[]const []const u8 = null,
|
||||
cc: ?[]const []const u8 = null,
|
||||
bcc: ?[]const []const u8 = null, // TODO
|
||||
html: ?[]const u8 = null,
|
||||
text: ?[]const u8 = null,
|
||||
defaults: ?DefaultMailParams = null,
|
||||
|
||||
pub const DefaultMailParams = struct {
|
||||
subject: ?[]const u8 = null,
|
||||
from: ?[]const u8 = null,
|
||||
to: ?[]const []const u8 = null,
|
||||
cc: ?[]const []const u8 = null,
|
||||
bcc: ?[]const []const u8 = null, // TODO
|
||||
html: ?[]const u8 = null,
|
||||
text: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
const MailParams = @This();
|
||||
|
||||
pub fn get(
|
||||
self: MailParams,
|
||||
comptime field: enum { subject, from, to, cc, bcc, html, text },
|
||||
) ?switch (field) {
|
||||
.subject => []const u8,
|
||||
.from => []const u8,
|
||||
.to => []const []const u8,
|
||||
.cc => []const []const u8,
|
||||
.bcc => []const []const u8,
|
||||
.html => []const u8,
|
||||
.text => []const u8,
|
||||
} {
|
||||
return @field(self, @tagName(field)) orelse if (self.defaults) |defaults|
|
||||
@field(defaults, @tagName(field))
|
||||
else
|
||||
null;
|
||||
}
|
15
src/jetzig/mail/MailerDefinition.zig
Normal file
15
src/jetzig/mail/MailerDefinition.zig
Normal file
@ -0,0 +1,15 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
pub const DeliverFn = *const fn (
|
||||
std.mem.Allocator,
|
||||
*jetzig.mail.MailParams,
|
||||
*jetzig.data.Value,
|
||||
jetzig.jobs.JobEnv,
|
||||
) anyerror!void;
|
||||
|
||||
name: []const u8,
|
||||
deliverFn: DeliverFn,
|
||||
defaults: ?jetzig.mail.DefaultMailParams,
|
||||
text_template: []const u8,
|
||||
html_template: []const u8,
|
31
src/jetzig/mail/SMTPConfig.zig
Normal file
31
src/jetzig/mail/SMTPConfig.zig
Normal file
@ -0,0 +1,31 @@
|
||||
const std = @import("std");
|
||||
|
||||
const smtp = @import("smtp");
|
||||
|
||||
port: u16 = 25,
|
||||
encryption: enum { none, insecure, tls, start_tls } = .none,
|
||||
host: []const u8 = "localhost",
|
||||
username: ?[]const u8 = null,
|
||||
password: ?[]const u8 = null,
|
||||
|
||||
const SMTPConfig = @This();
|
||||
|
||||
pub fn toSMTP(self: SMTPConfig, allocator: std.mem.Allocator) smtp.Config {
|
||||
return smtp.Config{
|
||||
.allocator = allocator,
|
||||
.port = self.port,
|
||||
.encryption = self.getEncryption(),
|
||||
.host = self.host,
|
||||
.username = self.username,
|
||||
.password = self.password,
|
||||
};
|
||||
}
|
||||
|
||||
fn getEncryption(self: SMTPConfig) smtp.Encryption {
|
||||
return switch (self.encryption) {
|
||||
.none => smtp.Encryption.none,
|
||||
.insecure => smtp.Encryption.insecure,
|
||||
.tls => smtp.Encryption.tls,
|
||||
.start_tls => smtp.Encryption.start_tls,
|
||||
};
|
||||
}
|
18
src/jetzig/mail/components.zig
Normal file
18
src/jetzig/mail/components.zig
Normal file
@ -0,0 +1,18 @@
|
||||
pub const header =
|
||||
"MIME-Version: 1.0\r\n" ++
|
||||
"Content-Type: multipart/alternative; boundary=\"=_alternative_{0}\"\r\n";
|
||||
|
||||
pub const footer =
|
||||
"\r\n.\r\n";
|
||||
|
||||
pub const text =
|
||||
"--=_alternative_{0}\r\n" ++
|
||||
"Content-Type: text/plain; charset=\"UTF-8\"\r\n" ++
|
||||
"Content-Transfer-Encoding: quoted-printable\r\n\r\n" ++
|
||||
"{1s}\r\n";
|
||||
|
||||
pub const html =
|
||||
"--=_alternative_{0}\r\n" ++
|
||||
"Content-Type: text/html; charset=\"UTF-8\"\r\n" ++
|
||||
"Content-Transfer-Encoding: quoted-printable\r\n\r\n" ++
|
||||
"{1s}\r\n";
|
@ -2,11 +2,11 @@ const std = @import("std");
|
||||
|
||||
const jetzig = @import("../../jetzig.zig");
|
||||
|
||||
const Self = @This();
|
||||
const Route = @This();
|
||||
|
||||
pub const Action = enum { index, get, post, put, patch, delete };
|
||||
pub const RenderFn = *const fn (Self, *jetzig.http.Request) anyerror!jetzig.views.View;
|
||||
pub const RenderStaticFn = *const fn (Self, *jetzig.http.StaticRequest) anyerror!jetzig.views.View;
|
||||
pub const RenderFn = *const fn (Route, *jetzig.http.Request) anyerror!jetzig.views.View;
|
||||
pub const RenderStaticFn = *const fn (Route, *jetzig.http.StaticRequest) anyerror!jetzig.views.View;
|
||||
|
||||
const ViewWithoutId = *const fn (*jetzig.http.Request, *jetzig.data.Data) anyerror!jetzig.views.View;
|
||||
const ViewWithId = *const fn (id: []const u8, *jetzig.http.Request, *jetzig.data.Data) anyerror!jetzig.views.View;
|
||||
@ -52,7 +52,7 @@ params: std.ArrayList(*jetzig.data.Data) = undefined,
|
||||
|
||||
/// Initializes a route's static params on server launch. Converts static params (JSON strings)
|
||||
/// to `jetzig.data.Data` values. Memory is owned by caller (`App.start()`).
|
||||
pub fn initParams(self: *Self, allocator: std.mem.Allocator) !void {
|
||||
pub fn initParams(self: *Route, allocator: std.mem.Allocator) !void {
|
||||
self.params = std.ArrayList(*jetzig.data.Data).init(allocator);
|
||||
for (self.json_params) |params| {
|
||||
var data = try allocator.create(jetzig.data.Data);
|
||||
@ -62,7 +62,7 @@ pub fn initParams(self: *Self, allocator: std.mem.Allocator) !void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinitParams(self: *const Self) void {
|
||||
pub fn deinitParams(self: *const Route) void {
|
||||
for (self.params.items) |data| {
|
||||
data.deinit();
|
||||
data._allocator.destroy(data);
|
||||
@ -70,7 +70,7 @@ pub fn deinitParams(self: *const Self) void {
|
||||
self.params.deinit();
|
||||
}
|
||||
|
||||
fn renderFn(self: Self, request: *jetzig.http.Request) anyerror!jetzig.views.View {
|
||||
fn renderFn(self: Route, request: *jetzig.http.Request) anyerror!jetzig.views.View {
|
||||
switch (self.view.?) {
|
||||
.dynamic => {},
|
||||
// We only end up here if a static route is defined but its output is not found in the
|
||||
@ -89,7 +89,7 @@ fn renderFn(self: Self, request: *jetzig.http.Request) anyerror!jetzig.views.Vie
|
||||
}
|
||||
}
|
||||
|
||||
fn renderStaticFn(self: Self, request: *jetzig.http.StaticRequest) anyerror!jetzig.views.View {
|
||||
fn renderStaticFn(self: Route, request: *jetzig.http.StaticRequest) anyerror!jetzig.views.View {
|
||||
request.response_data.* = jetzig.data.Data.init(request.allocator);
|
||||
|
||||
switch (self.view.?.static) {
|
||||
|
@ -4,4 +4,5 @@ test {
|
||||
_ = @import("jetzig/http/Cookies.zig");
|
||||
_ = @import("jetzig/http/Path.zig");
|
||||
_ = @import("jetzig/jobs/Job.zig");
|
||||
_ = @import("jetzig/mail/Mail.zig");
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user