Something went wrong. Try again.
SMTP client for Zig forked from karlseguin.tngl.sh/smtp_client.zig
Something went wrong. Try again.
6.8 kB · 205 lines
Zig
at dev
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206const std = @import("std");
const Address = std.net.Address;const Allocator = std.mem.Allocator;const Bundle = std.crypto.Certificate.Bundle;
pub const client = @import("client.zig");pub const Client = client.Client(Stream);pub const Stream = @import("stream.zig").Stream;pub const Message = @import("message.zig").Message;
pub const Encryption = enum { insecure, none, tls, start_tls,};
pub const Config = struct { port: u16 = 25, host: []const u8, ca_bundle: ?Bundle = null, timeout: i32 = 10_000, encryption: Encryption = .tls, username: ?[]const u8 = null, password: ?[]const u8 = null, local_name: []const u8 = "localhost", allocator: ?Allocator = null, message_id_host: ?[]const u8 = null,};
pub fn connect(config: Config) !Client { const allocator = config.allocator orelse return error.AllocatorRequired; const stream = Stream.init(try std.net.tcpConnectToHost(allocator, config.host, config.port)); return Client.init(stream, config);}
pub fn connectTo(address: Address, config: Config) !Client { const stream = Stream.init(try std.net.tcpConnectToAddress(address)); return Client.init(stream, config);}
pub fn send(message: Message, config: Config) !void { var sent: usize = 0; return sendAll(&[_]Message{message}, config, &sent);}
pub fn sendAll(messages: []const Message, config: Config, sent: *usize) !void { var c = try connect(config); defer c.deinit(); try oneShot(&c, messages, sent);}
pub fn sendTo(address: Address, message: Message, config: Config) !void { var sent: usize = 0; return sendAllTo(address, &[_]Message{message}, config, &sent);}
pub fn sendAllTo(address: Address, messages: []const Message, config: Config, sent: *usize) !void { var c = try connectTo(address, config); defer c.deinit(); try oneShot(&c, messages, sent);}
// client is anytype for testing. It's a client.Client(net.Stream) during normal// execution, but a client.Client(*t.MockServer) during tests.fn oneShot(c: anytype, messages: []const Message, sent: *usize) !void { defer c.quit() catch {};
try c.hello(); try c.auth();
for (messages) |message| { try c.sendMessage(message); sent.* += 1; }}
const t = @import("t.zig");
test { std.testing.refAllDecls(@This());}
test "send: unencrypted single to" { const ms = t.MockServer.init(&.{ .{ .req = "EHLO localhost\r\n", .res = "250\r\n" }, .{ .req = "MAIL FROM:<from-user@localhost.local>\r\n", .res = "250\r\n" }, .{ .req = "RCPT TO:<to-user@localhost.local>\r\n", .res = "250\r\n" }, .{ .req = "DATA\r\n", .res = "354\r\n" }, .{ .req = "This is the data\r\n.\r\n", .res = "250\r\n" }, .{ .req = "QUIT\r\n", .res = null }, });
var sent: usize = 0; var c = try client.Client(t.MockServer).init(ms, .{ .port = 0, .host = "localhost", });
try oneShot(&c, &.{.{ .from = .{.address = "from-user@localhost.local"}, .to = &.{.{.address = "to-user@localhost.local"}}, .data = "This is the data\r\n.\r\n", }}, &sent); try t.expectEqual(1, sent);}
test "send: scram-md5 + multiple to" { const ms = t.MockServer.init(&.{ .{ .req = "EHLO localhost\r\n", .res = "250-Ok\r\n250 AUTH Plain cram-MD5\r\n" }, .{ .req = "AUTH CRAM-MD5\r\n", .res = "235 my secret\r\n" }, .{ .req = "leto 9103a6f589ce8c5a3b775dd878b5ac3a\r\n", .res = "235 my secret\r\n" }, .{ .req = "MAIL FROM:<from-user@localhost.local>\r\n", .res = "250\r\n" }, .{ .req = "RCPT TO:<to-user1@localhost.local>\r\n", .res = "250\r\n" }, .{ .req = "RCPT TO:<to-user2@localhost.local>\r\n", .res = "250\r\n" }, .{ .req = "DATA\r\n", .res = "354\r\n" }, .{ .req = "hi\r\n", .res = "250\r\n" }, .{ .req = "QUIT\r\n", .res = null }, });
var sent: usize = 0; var c = try client.Client(t.MockServer).init(ms, .{ .port = 0, .host = "localhost", .username = "leto", .password = "ghanima", });
try oneShot(&c, &.{.{ .from = .{.address = "from-user@localhost.local"}, .to = &.{ .{.address = "to-user1@localhost.local" }, .{.address = "to-user2@localhost.local"}}, .data = "hi\r\n", }}, &sent); try t.expectEqual(1, sent);}
test "sendAll: success" { const ms = t.MockServer.init(&.{ .{ .req = "EHLO localhost\r\n", .res = "250 Ok\r\n" }, .{ .req = "MAIL FROM:<from-user1@localhost.local>\r\n", .res = "250\r\n" }, .{ .req = "RCPT TO:<to-user1@localhost.local>\r\n", .res = "250\r\n" }, .{ .req = "DATA\r\n", .res = "354\r\n" }, .{ .req = "hi1\r\n", .res = "250\r\n" }, .{ .req = "MAIL FROM:<from-user2@localhost.local>\r\n", .res = "250\r\n" }, .{ .req = "RCPT TO:<to-user2@localhost.local>\r\n", .res = "250\r\n" }, .{ .req = "DATA\r\n", .res = "354\r\n" }, .{ .req = "hi2\r\n", .res = "250\r\n" }, .{ .req = "QUIT\r\n", .res = null }, });
var sent: usize = 0; var c = try client.Client(t.MockServer).init(ms, .{ .port = 0, .host = "localhost", });
try oneShot(&c, &.{ .{ .from = .{.address = "from-user1@localhost.local"}, .to = &.{.{.address = "to-user1@localhost.local"}}, .data = "hi1\r\n", }, .{ .from = .{.address = "from-user2@localhost.local"}, .to = &.{.{.address = "to-user2@localhost.local"}}, .data = "hi2\r\n", }, }, &sent);
try t.expectEqual(2, sent);}
test "sendAll: partial" { const ms = t.MockServer.init(&.{ .{ .req = "EHLO localhost\r\n", .res = "250 Ok\r\n" }, .{ .req = "MAIL FROM:<from-user1@localhost.local>\r\n", .res = "250\r\n" }, .{ .req = "RCPT TO:<to-user1@localhost.local>\r\n", .res = "250\r\n" }, .{ .req = "DATA\r\n", .res = "354\r\n" }, .{ .req = "hi1\r\n", .res = "250\r\n" }, .{ .req = "MAIL FROM:<from-user2@localhost.local>\r\n", .res = "550\r\n" }, .{ .req = "QUIT\r\n", .res = null }, });
var sent: usize = 0; var c = try client.Client(t.MockServer).init(ms, .{ .port = 0, .host = "localhost", }); const err = oneShot(&c, &.{ .{ .from = .{.address = "from-user1@localhost.local"}, .to = &.{.{.address = "to-user1@localhost.local"}}, .data = "hi1\r\n", }, .{ .from = .{.address = "from-user2@localhost.local"}, .to = &.{.{.address = "to-user2@localhost.local"}}, .data = "hi2\r\n", }, }, &sent);
try t.expectEqual(error.MailboxNotAvailable, err); try t.expectEqual(1, sent);}