diff --git a/build.zig b/build.zig index c0e81e8..ca055a0 100644 --- a/build.zig +++ b/build.zig @@ -315,7 +315,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn .root_source_file = tests_file_path, .target = target, .optimize = optimize, - .test_runner = jetzig_dep.path("src/test_runner.zig"), + .test_runner = .{ .mode = .simple, .path = jetzig_dep.path("src/test_runner.zig") }, }); exe_unit_tests.root_module.addImport("jetzig", jetzig_module); exe_unit_tests.root_module.addImport("static", static_module); diff --git a/build.zig.zon b/build.zig.zon index 5fd50b9..549e518 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -6,37 +6,37 @@ .url = "https://github.com/jetzig-framework/zmd/archive/d6c8aa9a9cde99674ccb096d8f94ed09cba8dab.tar.gz", .hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163", }, - .zmpl = .{ - .url = "https://github.com/jetzig-framework/zmpl/archive/fb58c2d793576407a8a78942cc9e5a177c75d0b1.tar.gz", - .hash = "1220cf76b9c5209fa6e6a6b894dad95cfa5c226445d6b31dd4cf0a72507ca27b4cd6", - }, .jetkv = .{ - .url = "https://github.com/jetzig-framework/jetkv/archive/acaa30db281f1c331d20c48cfe6539186549ad45.tar.gz", - .hash = "1220b260b20cb65d801a00a39dc6506387f5faa1a225f85160e011bd2aabd2ce6e0b", - }, - .jetquery = .{ - .url = "https://github.com/jetzig-framework/jetquery/archive/ec99c0accedbf783c9836f096e2381e4d8b396eb.tar.gz", - .hash = "1220d03534fb9e30dbe46d9450e4a8a9530cd0cc76b88ba37f3e44337c017943b859", - }, - .jetcommon = .{ - .url = "https://github.com/jetzig-framework/jetcommon/archive/86f24cfdf2aaa0e8ada4539a6edef882708ced2b.tar.gz", - .hash = "12200439fc28aa7fa08f0e8fea100f6724c34c9dbfaaae4feec482c80e5ac08ea4f6", + .url = "https://github.com/jetzig-framework/jetkv/archive/9d754e552e7569239a900ed9e0f313a0554ed2d3.tar.gz", + .hash = "122013f8596bc615990fd7771c833cab4d2959ecac8d05c4f6c973aa46624e43afea", }, .args = .{ .url = "https://github.com/ikskuh/zig-args/archive/968258dc1b1230493d8f1677097c832a3d7e0bd8.tar.gz", .hash = "1220bdedf1a993d852d8aebcd63922a8fb163fac37b9c6ff72d187b2847a4a3a4248", }, - .pg = .{ - .url = "https://github.com/karlseguin/pg.zig/archive/4ddae09948cb1563b394cd724b95de14cc88fc12.tar.gz", - .hash = "1220779868e6a2f387addec799f176342f5d9a0277139cdb51336e0c1c1b904fcffa", + .jetcommon = .{ + .url = "https://github.com/jetzig-framework/jetcommon/archive/5be57d534b3d469f5570cd4b373b8d61032b1b8b.tar.gz", + .hash = "122079c6ceb28fa93163c2f95e2f175bb8f93f3075fa34af63045671ab7dd824e756", }, - .smtp_client = .{ - .url = "https://github.com/karlseguin/smtp_client.zig/archive/3cbe8f269e4c3a6bce407e7ae48b2c76307c559f.tar.gz", - .hash = "1220de146446d0cae4396e346cb8283dd5e086491f8577ddbd5e03ad0928111d8bc6", + .pg = .{ + .url = "https://github.com/karlseguin/pg.zig/archive/0110cfdf387403a5a326115b5184861c4604d711.tar.gz", + .hash = "12205019ce2bc2e08c76352ea37a14600d412e5e0ecdd7ddd27b4e83a62f37d8ba94", }, .httpz = .{ - .url = "https://github.com/karlseguin/http.zig/archive/da9e944de0be6e5c67ca711dd238ce82d81558b4.tar.gz", - .hash = "12201df692f62d526fdf94e6000cf8de2142edf27484887e2e8f1ec5db4c9b808e5c", + .url = "https://github.com/karlseguin/http.zig/archive/46753ab508a86d0eb510510fc2ed6940a1ebf20a.tar.gz", + .hash = "12207dbe64a04fb960156cbc990153cb3637a08e3fe23077c7199621b5c6377f5d20", + }, + .smtp_client = .{ + .url = "https://github.com/karlseguin/smtp_client.zig/archive/5163c66cc42cdd93176a6b1cad45f3db3a291a6a.tar.gz", + .hash = "1220a7807b5161550cb0cba772689c4872bfeee8305a26c3cd0e12a8ccde1d546910", + }, + .jetquery = .{ + .url = "https://github.com/jetzig-framework/jetquery/archive/d4010cfd9ced2e7deb0f3a6cc64e2d32b8db95ba.tar.gz", + .hash = "122069eeb0d43931e49f93419bdb5930ac3a6bc35d1e977738fe872ecaac8ff32aec", + }, + .zmpl = .{ + .url = "https://github.com/jetzig-framework/zmpl/archive/b1dfca8eec73520af5b029016c5b5914da659b6d.tar.gz", + .hash = "1220e70c218c89de219d4f9506a4ad69bd1b5257cd8c7cdc2ea823830e1d8b9dc4df", }, }, diff --git a/cli/build.zig.zon b/cli/build.zig.zon index ac73538..f70f9c7 100644 --- a/cli/build.zig.zon +++ b/cli/build.zig.zon @@ -9,8 +9,8 @@ .hash = "1220bdedf1a993d852d8aebcd63922a8fb163fac37b9c6ff72d187b2847a4a3a4248", }, .jetquery = .{ - .url = "https://github.com/jetzig-framework/jetquery/archive/ec99c0accedbf783c9836f096e2381e4d8b396eb.tar.gz", - .hash = "1220d03534fb9e30dbe46d9450e4a8a9530cd0cc76b88ba37f3e44337c017943b859", + .url = "https://github.com/jetzig-framework/jetquery/archive/d4010cfd9ced2e7deb0f3a6cc64e2d32b8db95ba.tar.gz", + .hash = "122069eeb0d43931e49f93419bdb5930ac3a6bc35d1e977738fe872ecaac8ff32aec", }, }, .paths = .{ diff --git a/cli/commands/generate/mailer.zig b/cli/commands/generate/mailer.zig index 8853b06..25c0b3a 100644 --- a/cli/commands/generate/mailer.zig +++ b/cli/commands/generate/mailer.zig @@ -56,21 +56,16 @@ pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8, he \\// * allocator: Arena allocator for use during the mail delivery process. \\// * mail: Mail parameters (from, to, subject, etc.). Inspect or override any values \\// assigned when the mail was created. - \\// * data: Provides `data.string()` etc. for generating Jetzig Values. \\// * params: Template data for `text.zmpl` and `html.zmpl`. Inherits all response data \\// assigned in a view function and can be modified for email-specific content. - \\// * env: Provides the following fields: - \\// - logger: Logger attached to the same stream as the Jetzig server. - \\// - environment: Enum of `{ production, development }`. + \\// * env: Provides various information about the environment. See `jetzig.jobs.JobEnv`. \\pub fn deliver( \\ allocator: std.mem.Allocator, \\ mail: *jetzig.mail.MailParams, - \\ data: *jetzig.data.Data, \\ params: *jetzig.data.Value, \\ env: jetzig.jobs.JobEnv, \\) !void { \\ _ = allocator; - \\ _ = data; \\ _ = params; \\ try env.logger.INFO("Delivering email with subject: '{?s}'", .{mail.get(.subject)}); \\} diff --git a/demo/src/app/jobs/example.zig b/demo/src/app/jobs/example.zig index 1e9f818..5421f56 100644 --- a/demo/src/app/jobs/example.zig +++ b/demo/src/app/jobs/example.zig @@ -12,8 +12,8 @@ pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig env, .{ .subject = "Hello!!!", - .from = "bob@jetzig.dev", - .to = &.{"bob@jetzig.dev"}, + .from = .{ .email = "bob@jetzig.dev" }, + .to = &.{.{ .email = "bob@jetzig.dev" }}, .html = "
Hello!
", .text = "Hello!", }, diff --git a/demo/src/app/mailers/welcome.zig b/demo/src/app/mailers/welcome.zig index b53c1c5..a704d93 100644 --- a/demo/src/app/mailers/welcome.zig +++ b/demo/src/app/mailers/welcome.zig @@ -3,7 +3,7 @@ const jetzig = @import("jetzig"); // Default values for this mailer. pub const defaults: jetzig.mail.DefaultMailParams = .{ - .from = "no-reply@example.com", + .from = .{ .email = "no-reply@example.com" }, .subject = "Default subject", }; diff --git a/demo/src/app/views/mail.zig b/demo/src/app/views/mail.zig index 5e8a2f3..e497d9f 100644 --- a/demo/src/app/views/mail.zig +++ b/demo/src/app/views/mail.zig @@ -10,7 +10,7 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View { // * `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@jetzig.dev"} }); + const mail = request.mail("welcome", .{ .to = &.{.{ .email = "hello@jetzig.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). diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig index 0b47ed6..5eb58ab 100644 --- a/src/jetzig/http/Request.zig +++ b/src/jetzig/http/Request.zig @@ -141,6 +141,7 @@ pub fn init( .HEAD => Method.HEAD, .PUT => Method.PUT, .OPTIONS => Method.OPTIONS, + .CONNECT, .OTHER => return error.JetzigUnsupportedHttpMethod, }; const response_data = try allocator.create(jetzig.data.Data); @@ -580,25 +581,18 @@ const RequestMail = struct { _ = 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); + try mail_job.params.put("mailer_name", self.name); + try mail_job.params.put("from", self.mail_params.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)); + if (self.mail_params.to) |to| { + for (to) |each| try to_array.append(each); } 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); + try mail_job.params.put("subject", self.mail_params.subject); + try mail_job.params.put("html", self.mail_params.html); + try mail_job.params.put("text", self.mail_params.text); if (self.request.response_data.value) |value| try mail_job.params.put( "params", diff --git a/src/jetzig/mail.zig b/src/jetzig/mail.zig index 5fd7c6c..020a17d 100644 --- a/src/jetzig/mail.zig +++ b/src/jetzig/mail.zig @@ -3,6 +3,7 @@ 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 Address = MailParams.Address; pub const DefaultMailParams = MailParams.DefaultMailParams; pub const components = @import("mail/components.zig"); pub const Job = @import("mail/Job.zig"); diff --git a/src/jetzig/mail/Job.zig b/src/jetzig/mail/Job.zig index b29bb99..50c4679 100644 --- a/src/jetzig/mail/Job.zig +++ b/src/jetzig/mail/Job.zig @@ -51,9 +51,7 @@ pub fn run(allocator: std.mem.Allocator, params: *jetzig.data.Value, env: jetzig ); } else { try mail.deliver(); - try env.logger.INFO("Delivered mail to: {s}", .{ - try std.mem.join(allocator, ", ", mail.params.to.?), - }); + try env.logger.INFO("Delivered mail to: {s}", .{mail.params.to.?}); } } @@ -69,19 +67,32 @@ fn resolveSubject(subject: ?*const jetzig.data.Value) ?[]const u8 { } } -fn resolveFrom(from: ?*const jetzig.data.Value) ?[]const u8 { +fn resolveFrom(from: ?*const jetzig.data.Value) ?jetzig.mail.Address { return if (from) |capture| switch (capture.*) { .null => null, - .string => |string| string.value, + .string => |string| .{ .email = string.value }, + .object => |object| .{ + .email = object.getT(.string, "email") orelse return null, + .name = object.getT(.string, "name") orelse return null, + }, 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); +fn resolveTo(allocator: std.mem.Allocator, params: *const jetzig.data.Value) !?[]const jetzig.mail.Address { + var to = std.ArrayList(jetzig.mail.Address).init(allocator); if (params.get("to")) |capture| { for (capture.items(.array)) |recipient| { - try to.append(recipient.string.value); + const maybe_address: ?jetzig.mail.Address = switch (recipient.*) { + .null => null, + .string => |string| .{ .email = string.value }, + .object => |object| .{ + .email = object.getT(.string, "email") orelse return null, + .name = object.getT(.string, "name") orelse return null, + }, + else => unreachable, + }; + if (maybe_address) |address| try to.append(address); } } return if (to.items.len > 0) try to.toOwnedSlice() else null; diff --git a/src/jetzig/mail/Mail.zig b/src/jetzig/mail/Mail.zig index af85e8e..cc96875 100644 --- a/src/jetzig/mail/Mail.zig +++ b/src/jetzig/mail/Mail.zig @@ -30,9 +30,16 @@ pub fn deliver(self: Mail) !void { const data = try self.generateData(); defer self.allocator.free(data); + const to = try self.allocator.alloc(smtp.Message.Address, self.params.to.?.len); + defer self.allocator.free(to); + + for (self.params.to.?, 0..) |address, index| { + to[index] = .{ .address = address.email, .name = address.name }; + } + try smtp.send(.{ - .from = self.params.from.?, - .to = self.params.to.?, + .from = .{ .address = self.params.from.?.email, .name = self.params.from.?.name }, + .to = to, .data = data, }, try self.config.toSMTP(self.allocator, self.env)); } @@ -147,8 +154,8 @@ test "HTML part only" { .config = .{}, .boundary = 123456789, .params = .{ - .from = "user@example.com", - .to = &.{"user@example.com"}, + .from = .{ .name = "Bob", .email = "user@example.com" }, + .to = &.{.{ .name = "Alice", .email = "user@example.com" }}, .subject = "Test subject", .html = "
Hello
", }, @@ -158,7 +165,7 @@ test "HTML part only" { defer std.testing.allocator.free(actual); const expected = try std.mem.replaceOwned(u8, std.testing.allocator, - \\From: user@example.com + \\From: user@example.com \\Subject: Test subject \\MIME-Version: 1.0 \\Content-Type: multipart/alternative; boundary="=_alternative_123456789" @@ -183,8 +190,8 @@ test "text part only" { .config = .{}, .boundary = 123456789, .params = .{ - .from = "user@example.com", - .to = &.{"user@example.com"}, + .from = .{ .name = "Bob", .email = "user@example.com" }, + .to = &.{.{ .name = "Alice", .email = "user@example.com" }}, .subject = "Test subject", .text = "Hello", }, @@ -194,7 +201,7 @@ test "text part only" { defer std.testing.allocator.free(actual); const expected = try std.mem.replaceOwned(u8, std.testing.allocator, - \\From: user@example.com + \\From: user@example.com \\Subject: Test subject \\MIME-Version: 1.0 \\Content-Type: multipart/alternative; boundary="=_alternative_123456789" @@ -219,8 +226,8 @@ test "HTML and text parts" { .config = .{}, .boundary = 123456789, .params = .{ - .from = "user@example.com", - .to = &.{"user@example.com"}, + .from = .{ .name = "Bob", .email = "user@example.com" }, + .to = &.{.{ .name = "Alice", .email = "user@example.com" }}, .subject = "Test subject", .html = "
Hello
", .text = "Hello", @@ -231,7 +238,7 @@ test "HTML and text parts" { defer std.testing.allocator.free(actual); const expected = try std.mem.replaceOwned(u8, std.testing.allocator, - \\From: user@example.com + \\From: user@example.com \\Subject: Test subject \\MIME-Version: 1.0 \\Content-Type: multipart/alternative; boundary="=_alternative_123456789" @@ -262,8 +269,8 @@ test "long content encoding" { .config = .{}, .boundary = 123456789, .params = .{ - .from = "user@example.com", - .to = &.{"user@example.com"}, + .from = .{ .name = "Bob", .email = "user@example.com" }, + .to = &.{.{ .name = "Alice", .email = "user@example.com" }}, .subject = "Test subject", .html = "
Hellooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo!!!
", .text = "Hello", @@ -274,7 +281,7 @@ test "long content encoding" { defer std.testing.allocator.free(actual); const expected = try std.mem.replaceOwned(u8, std.testing.allocator, - \\From: user@example.com + \\From: user@example.com \\Subject: Test subject \\MIME-Version: 1.0 \\Content-Type: multipart/alternative; boundary="=_alternative_123456789" @@ -312,8 +319,8 @@ test "non-latin alphabet encoding" { .config = .{}, .boundary = 123456789, .params = .{ - .from = "user@example.com", - .to = &.{"user@example.com"}, + .from = .{ .name = "Bob", .email = "user@example.com" }, + .to = &.{.{ .name = "Alice", .email = "user@example.com" }}, .subject = "Test subject", .html = "
你爱学习 Zig 吗?
", @@ -325,7 +332,7 @@ test "non-latin alphabet encoding" { defer std.testing.allocator.free(actual); const expected = try std.mem.replaceOwned(u8, std.testing.allocator, - \\From: user@example.com + \\From: user@example.com \\Subject: Test subject \\MIME-Version: 1.0 \\Content-Type: multipart/alternative; boundary="=_alternative_123456789" diff --git a/src/jetzig/mail/MailParams.zig b/src/jetzig/mail/MailParams.zig index deed9e9..044d779 100644 --- a/src/jetzig/mail/MailParams.zig +++ b/src/jetzig/mail/MailParams.zig @@ -1,22 +1,31 @@ subject: ?[]const u8 = null, -from: ?[]const u8 = null, -to: ?[]const []const u8 = null, -cc: ?[]const []const u8 = null, -bcc: ?[]const []const u8 = null, // TODO +from: ?Address = null, +to: ?[]const Address = null, +cc: ?[]const Address = null, +bcc: ?[]const Address = 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 + from: ?Address = null, + to: ?[]const Address = null, + cc: ?[]const Address = null, + bcc: ?[]const Address = null, // TODO html: ?[]const u8 = null, text: ?[]const u8 = null, }; +pub const Address = struct { + name: ?[]const u8 = null, + email: []const u8, + + pub fn format(address: Address, _: anytype, _: anytype, writer: anytype) !void { + try writer.print("<{?s}> {s}", .{ address.name, address.email }); + } +}; + const MailParams = @This(); pub fn get( @@ -24,10 +33,10 @@ pub fn get( 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, + .from => Address, + .to => []const Address, + .cc => []const Address, + .bcc => []const Address, .html => []const u8, .text => []const u8, } { diff --git a/src/jetzig/testing/App.zig b/src/jetzig/testing/App.zig index 09f8c46..246150b 100644 --- a/src/jetzig/testing/App.zig +++ b/src/jetzig/testing/App.zig @@ -349,6 +349,9 @@ fn stubbedRequest( .method = std.enums.nameCast(httpz.Method, @tagName(method)), .protocol = .HTTP11, .params = undefined, + .conn = undefined, + .method_string = undefined, + .unread_body = undefined, .headers = request_headers, .body_buffer = if (options.getBody()) |capture| .{ .data = @constCast(capture), .type = .static }