An SMTP client and server library for Zig implementing RFC 5321.
0

Configure Feed

Select the types of activity you want to include in your feed.

zig-smtp / src / main.zig
28 kB 629 lines
1// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us> 2// SPDX-License-Identifier: MIT 3 4//! Demo CLI for the zig-smtp library. 5//! 6//! zig-smtp send [--tls|--starttls] [--insecure] [--allow-cleartext-auth] 7//! [--chunking] [--smtputf8] [--user <u> --password <p>] 8//! [--auth-method plain|login|cram-md5] 9//! [--ret full|hdrs] [--envid <id>] 10//! [--notify never|success,failure,delay] [--orcpt <address>] 11//! [--submitter <mailbox>|<>] [--requiretls] 12//! [--lmtp] [--binarymime] <host> <port> <from> <to>... 13//! send a message read from stdin; --tls speaks TLS from the first 14//! byte (port 465 style), --starttls upgrades after EHLO (port 587 15//! style), --insecure skips certificate verification, --user/--password 16//! authenticate with the best advertised mechanism (or the one forced 17//! by --auth-method), and --allow-cleartext-auth permits a mechanism 18//! that sends the password over an unencrypted connection; the DSN 19//! options (RFC 3461) are --ret and --envid on the message and 20//! --notify and --orcpt on every recipient; --binarymime sends the 21//! input as BODY=BINARYMIME over BDAT, byte for byte 22//! zig-smtp serve [--tls-cert <pem> --tls-key <pem> [--implicit-tls]] 23//! [--auth <user>:<pass>] [--lmtp [--fail-delivery <address>]] 24//! <port> 25//! run a debug server on 127.0.0.1 that prints received messages; 26//! --auth requires authentication with the given credentials; with a 27//! certificate and key it advertises and accepts STARTTLS, or speaks 28//! TLS from the first byte with --implicit-tls; --lmtp speaks LMTP 29//! instead of SMTP, where --fail-delivery names one recipient to 30//! report as undeliverable at the end of the message 31 32const std = @import("std"); 33const Io = std.Io; 34const smtp = @import("smtp"); 35 36pub fn main(init: std.process.Init) !void { 37 const arena = init.arena.allocator(); 38 const io = init.io; 39 const args = try init.minimal.args.toSlice(arena); 40 41 if (args.len >= 2 and std.mem.eql(u8, args[1], "send")) { 42 var config: SendConfig = .{}; 43 var rest = args[2..]; 44 while (rest.len > 0 and std.mem.startsWith(u8, rest[0], "--")) { 45 if (std.mem.eql(u8, rest[0], "--tls")) { 46 config.mode = .tls; 47 } else if (std.mem.eql(u8, rest[0], "--starttls")) { 48 config.mode = .starttls; 49 } else if (std.mem.eql(u8, rest[0], "--insecure")) { 50 config.insecure = true; 51 } else if (std.mem.eql(u8, rest[0], "--allow-cleartext-auth")) { 52 config.allow_cleartext_auth = true; 53 } else if (std.mem.eql(u8, rest[0], "--chunking")) { 54 config.chunking = true; 55 } else if (std.mem.eql(u8, rest[0], "--smtputf8")) { 56 config.smtputf8 = true; 57 } else if (std.mem.eql(u8, rest[0], "--lmtp")) { 58 config.protocol = .lmtp; 59 } else if (std.mem.eql(u8, rest[0], "--requiretls")) { 60 config.require_tls = true; 61 } else if (std.mem.eql(u8, rest[0], "--binarymime")) { 62 // Binary content can only be framed by BDAT, so this is 63 // chunking plus a declaration of what the chunks hold. 64 config.body = .binary_mime; 65 config.chunking = true; 66 } else if (rest.len >= 2 and std.mem.eql(u8, rest[0], "--submitter")) { 67 // RFC 4954 §5. "<>" is the two characters that say "I do not 68 // know", which is a different claim from not asking. 69 config.submitter = if (std.mem.eql(u8, rest[1], "<>")) 70 .unknown 71 else 72 .{ .mailbox = rest[1] }; 73 rest = rest[1..]; 74 } else if (rest.len >= 2 and std.mem.eql(u8, rest[0], "--ret")) { 75 config.ret = smtp.protocol.Ret.parse(rest[1]) catch return usage(); 76 rest = rest[1..]; 77 } else if (rest.len >= 2 and std.mem.eql(u8, rest[0], "--envid")) { 78 config.envid = rest[1]; 79 rest = rest[1..]; 80 } else if (rest.len >= 2 and std.mem.eql(u8, rest[0], "--notify")) { 81 config.notify = smtp.protocol.Notify.parse(rest[1]) catch return usage(); 82 rest = rest[1..]; 83 } else if (rest.len >= 2 and std.mem.eql(u8, rest[0], "--orcpt")) { 84 // Applied to every recipient, which is all a one-shot 85 // sender can sensibly do with it. 86 config.orcpt = rest[1]; 87 rest = rest[1..]; 88 } else if (rest.len >= 2 and std.mem.eql(u8, rest[0], "--user")) { 89 config.username = rest[1]; 90 rest = rest[1..]; 91 } else if (rest.len >= 2 and std.mem.eql(u8, rest[0], "--password")) { 92 config.password = rest[1]; 93 rest = rest[1..]; 94 } else if (rest.len >= 2 and std.mem.eql(u8, rest[0], "--auth-method")) { 95 config.auth_method = std.meta.stringToEnum( 96 @TypeOf(config.auth_method), 97 rest[1], 98 ) orelse if (std.mem.eql(u8, rest[1], "cram-md5")) .cram_md5 else return usage(); 99 rest = rest[1..]; 100 } else { 101 return usage(); 102 } 103 rest = rest[1..]; 104 } 105 if ((config.username == null) != (config.password == null)) return usage(); 106 // RFC 8689 §4.1 wants a validated certificate before a client may 107 // ask for this, and --insecure is the caller saying it skipped that. 108 // The library cannot see the difference; here it can. 109 if (config.require_tls and config.insecure) { 110 std.log.err("--requiretls needs a validated certificate, so not with --insecure", .{}); 111 return usage(); 112 } 113 if (config.require_tls and config.mode == .plain) { 114 std.log.err("--requiretls needs TLS: use --tls or --starttls", .{}); 115 return usage(); 116 } 117 if (rest.len < 4) return usage(); 118 return send(io, arena, config, rest[0], rest[1], rest[2], rest[3..]); 119 } 120 if (args.len >= 2 and std.mem.eql(u8, args[1], "serve")) { 121 var config: ServeConfig = .{}; 122 var rest = args[2..]; 123 while (rest.len > 0 and std.mem.startsWith(u8, rest[0], "--")) { 124 if (rest.len >= 2 and std.mem.eql(u8, rest[0], "--tls-cert")) { 125 config.cert_path = rest[1]; 126 rest = rest[1..]; 127 } else if (rest.len >= 2 and std.mem.eql(u8, rest[0], "--tls-key")) { 128 config.key_path = rest[1]; 129 rest = rest[1..]; 130 } else if (rest.len >= 2 and std.mem.eql(u8, rest[0], "--auth")) { 131 const sep = std.mem.indexOfScalar(u8, rest[1], ':') orelse return usage(); 132 config.username = rest[1][0..sep]; 133 config.password = rest[1][sep + 1 ..]; 134 rest = rest[1..]; 135 } else if (std.mem.eql(u8, rest[0], "--implicit-tls")) { 136 config.implicit_tls = true; 137 } else if (std.mem.eql(u8, rest[0], "--lmtp")) { 138 config.protocol = .lmtp; 139 } else if (rest.len >= 2 and std.mem.eql(u8, rest[0], "--fail-delivery")) { 140 config.fail_delivery = rest[1]; 141 rest = rest[1..]; 142 } else { 143 return usage(); 144 } 145 rest = rest[1..]; 146 } 147 if (rest.len != 1) return usage(); 148 if ((config.cert_path == null) != (config.key_path == null)) return usage(); 149 if (config.implicit_tls and config.cert_path == null) return usage(); 150 if (config.fail_delivery != null and config.protocol != .lmtp) return usage(); 151 return serve(io, arena, config, rest[0]); 152 } 153 return usage(); 154} 155 156const ServeConfig = struct { 157 cert_path: ?[]const u8 = null, 158 key_path: ?[]const u8 = null, 159 implicit_tls: bool = false, 160 protocol: smtp.Server.Protocol = .smtp, 161 /// Accepted at RCPT time and then failed at the end of the message, 162 /// which only LMTP can say. 163 fail_delivery: ?[]const u8 = null, 164 username: ?[]const u8 = null, 165 password: ?[]const u8 = null, 166}; 167 168const SendConfig = struct { 169 mode: enum { plain, tls, starttls } = .plain, 170 insecure: bool = false, 171 allow_cleartext_auth: bool = false, 172 chunking: bool = false, 173 smtputf8: bool = false, 174 protocol: smtp.Client.Protocol = .smtp, 175 body: ?smtp.protocol.Body = null, 176 submitter: ?smtp.protocol.Submitter = null, 177 require_tls: bool = false, 178 ret: ?smtp.protocol.Ret = null, 179 envid: ?[]const u8 = null, 180 notify: ?smtp.protocol.Notify = null, 181 orcpt: ?[]const u8 = null, 182 username: ?[]const u8 = null, 183 password: ?[]const u8 = null, 184 auth_method: enum { auto, plain, login, cram_md5 } = .auto, 185}; 186 187fn usage() noreturn { 188 std.log.err( 189 \\usage: 190 \\ zig-smtp send [--tls|--starttls] [--insecure] [--allow-cleartext-auth] 191 \\ [--user <u> --password <p>] 192 \\ [--auth-method plain|login|cram-md5] 193 \\ [--ret full|hdrs] [--envid <id>] 194 \\ [--notify never|success,failure,delay] [--orcpt <address>] 195 \\ [--submitter <mailbox>|<>] [--requiretls] 196 \\ [--lmtp] [--binarymime] <host> <port> <from> <to>... 197 \\ (message is read from stdin) 198 \\ zig-smtp serve [--tls-cert <pem> --tls-key <pem> [--implicit-tls]] 199 \\ [--auth <user>:<pass>] [--lmtp [--fail-delivery <address>]] 200 \\ <port> 201 , .{}); 202 std.process.exit(1); 203} 204 205fn send( 206 io: Io, 207 arena: std.mem.Allocator, 208 config: SendConfig, 209 host_arg: []const u8, 210 port_arg: []const u8, 211 from: []const u8, 212 recipients: []const []const u8, 213) !void { 214 const host = try Io.net.HostName.init(host_arg); 215 const port = try std.fmt.parseInt(u16, port_arg, 10); 216 217 var stdin_buf: [4096]u8 = undefined; 218 var stdin: Io.File.Reader = .init(.stdin(), io, &stdin_buf); 219 220 const stream = try host.connect(io, port, .{ .mode = .stream }); 221 defer stream.close(io); 222 // The TLS layer requires stream buffers of at least min_buffer_len. 223 const read_buf = try arena.alloc(u8, smtp.Tls.min_buffer_len); 224 const write_buf = try arena.alloc(u8, smtp.Tls.min_buffer_len); 225 var stream_reader = stream.reader(io, read_buf); 226 var stream_writer = stream.writer(io, write_buf); 227 228 const tls_options: smtp.Tls.Options = .{ 229 .host = host_arg, 230 .ca = if (config.insecure) .insecure else .system, 231 }; 232 var tls: smtp.Tls = undefined; 233 var tls_active = false; 234 defer if (tls_active) { 235 tls.end() catch {}; 236 tls.deinit(arena); 237 }; 238 239 var reply_buf: [1024]u8 = undefined; 240 var client: smtp.Client = .init(&stream_reader.interface, &stream_writer.interface, &reply_buf); 241 client.allow_cleartext_auth = config.allow_cleartext_auth; 242 // The SASL scratch is the caller's; a mechanism never allocates one for 243 // itself and nothing puts one on the stack behind your back. 244 var sasl_scratch: [smtp.Client.sasl_buffer_suggested]u8 = undefined; 245 client.sasl_buffer = &sasl_scratch; 246 client.mode = config.protocol; 247 248 if (config.mode == .tls) { 249 try tls.init(io, arena, &stream_reader.interface, &stream_writer.interface, tls_options); 250 tls_active = true; 251 client.setTransport(tls.reader(), tls.writer(), .encrypted); 252 } 253 254 _ = try client.greet(); 255 var extensions = try client.hello("localhost"); 256 257 if (config.mode == .starttls) { 258 try client.starttls(); 259 try tls.init(io, arena, &stream_reader.interface, &stream_writer.interface, tls_options); 260 tls_active = true; 261 client.setTransport(tls.reader(), tls.writer(), .encrypted); 262 extensions = try client.hello("localhost"); 263 } 264 265 if (config.username) |username| { 266 const password = config.password.?; 267 // The mechanisms come from zig-sasl; what is chosen from them is the 268 // caller's business, and this one lets --auth-method force it. 269 var plain: smtp.sasl.Plain = .init(username, password); 270 var login: smtp.sasl.Login = .init(username, password); 271 var cram_md5: smtp.sasl.CramMd5 = .init(username, password); 272 const offered: []const smtp.sasl.Client = switch (config.auth_method) { 273 // In order of preference, which `selectFromList` reads as such: 274 // PLAIN because every server implements it correctly, CRAM-MD5 275 // last because it is the oldest. On a carrier with no encryption 276 // the first two are skipped and it is the only one left. 277 .auto => &.{ plain.client(), login.client(), cram_md5.client() }, 278 .plain => &.{plain.client()}, 279 .login => &.{login.client()}, 280 .cram_md5 => &.{cram_md5.client()}, 281 }; 282 const mechanism = smtp.sasl.Client.selectFromList( 283 offered, 284 extensions.auth, 285 client.security == .encrypted or client.allow_cleartext_auth, 286 ) orelse { 287 std.log.err( 288 "no usable mechanism; the server offers: {s}{s}", 289 .{ 290 if (extensions.auth.len == 0) "(none)" else extensions.auth, 291 // The common case by far: everything on offer sends the 292 // password, and this connection is not encrypted. 293 if (client.security == .plaintext and !client.allow_cleartext_auth) 294 ", and this connection is not encrypted " ++ 295 "(use --starttls or --tls, or --allow-cleartext-auth)" 296 else 297 "", 298 }, 299 ); 300 return error.NoSupportedMechanism; 301 }; 302 client.authenticate(mechanism) catch |err| { 303 switch (err) { 304 error.AuthenticationFailed => { 305 const reply = client.last_reply.?; 306 // 5.7.8 is "bad credentials" where 4.7.0 is "try again"; 307 // the three-digit code says neither. 308 if (reply.enhanced()) |status| { 309 std.log.err("authentication failed: {d} {f} {s}", .{ 310 reply.code, 311 status, 312 reply.message(), 313 }); 314 } else { 315 std.log.err("authentication failed: {d} {s}", .{ reply.code, reply.text }); 316 } 317 }, 318 error.ServerNotAuthenticated => std.log.err( 319 "the server accepted the login without proving itself; " ++ 320 "this is not the server it claims to be", 321 .{}, 322 ), 323 error.InsecureTransport => std.log.err( 324 "refusing to send credentials over an unencrypted connection; " ++ 325 "use --starttls or --tls, or pass --allow-cleartext-auth", 326 .{}, 327 ), 328 else => {}, 329 } 330 return err; 331 }; 332 } 333 334 if (config.chunking and !extensions.chunking) { 335 std.log.err("server does not advertise CHUNKING", .{}); 336 return error.ChunkingNotAdvertised; 337 } 338 if (config.smtputf8 and !extensions.smtputf8) { 339 std.log.err("server does not advertise SMTPUTF8", .{}); 340 return error.SmtpUtf8NotAdvertised; 341 } 342 if (config.body == .binary_mime and !extensions.binary_mime) { 343 // RFC 3030 is absolute about this one: without the advertisement, 344 // binary must not be sent under any circumstances. 345 std.log.err("server does not advertise BINARYMIME", .{}); 346 return error.BinaryMimeNotAdvertised; 347 } 348 if (config.require_tls and !extensions.requiretls) { 349 // RFC 8689 §4.1: a client that does not see the keyword must not 350 // send the parameter, and a real sender must try another MX rather 351 // than deliver without the guarantee. 352 std.log.err("server does not advertise REQUIRETLS", .{}); 353 return error.RequireTlsNotAdvertised; 354 } 355 if (config.submitter != null and extensions.auth.len == 0) { 356 // RFC 4954 §5 obliges a server to take the parameter only if it 357 // advertised AUTH; one that did not will answer 555. 358 std.log.err("server does not advertise AUTH, so it will not take AUTH=", .{}); 359 return error.AuthNotAdvertised; 360 } 361 const wants_dsn = config.ret != null or config.envid != null or 362 config.notify != null or config.orcpt != null; 363 if (wants_dsn and !extensions.dsn) { 364 // A conforming server answers an unrecognized parameter with 555, 365 // so this is only a clearer way to say the same thing. 366 std.log.err("server does not advertise DSN", .{}); 367 return error.DsnNotAdvertised; 368 } 369 transact(&client, config, from, recipients, &stdin.interface) catch |err| { 370 if (err == error.UnexpectedReply) { 371 const reply = client.last_reply.?; 372 if (reply.enhanced()) |status| { 373 std.log.err("server rejected: {d} {f} {s}", .{ 374 reply.code, 375 status, 376 reply.message(), 377 }); 378 } else { 379 std.log.err("server rejected: {d} {s}", .{ reply.code, reply.text }); 380 } 381 } 382 return err; 383 }; 384 try client.quit(); 385 std.log.info("message sent to {d} recipient(s)", .{recipients.len}); 386} 387 388/// Runs the mail transaction, streaming the message from `message` so 389/// arbitrarily large input never has to fit in memory. 390fn transact( 391 client: *smtp.Client, 392 config: SendConfig, 393 from: []const u8, 394 recipients: []const []const u8, 395 message: *Io.Reader, 396) (smtp.Client.Error || smtp.Client.ArgumentError)!void { 397 try client.mail(from, .{ 398 .smtputf8 = config.smtputf8, 399 .auth = config.submitter, 400 .require_tls = config.require_tls, 401 .body = config.body, 402 .ret = config.ret, 403 .envid = config.envid, 404 }); 405 for (recipients) |recipient| try client.rcpt(recipient, .{ 406 .notify = config.notify, 407 .orcpt = if (config.orcpt) |address| 408 .{ .addr_type = "rfc822", .address = address } 409 else 410 null, 411 }); 412 if (config.chunking) { 413 // BDAT sends the input verbatim (no line-ending normalization). 414 while (true) { 415 const chunk = message.peekGreedy(1) catch |err| switch (err) { 416 error.EndOfStream => break, 417 error.ReadFailed => return error.ReadFailed, 418 }; 419 try client.bdat(chunk, false); 420 message.toss(chunk.len); 421 } 422 try client.bdat("", true); 423 } else { 424 var data_writer = try client.data(); 425 while (true) { 426 const chunk = message.peekGreedy(1) catch |err| switch (err) { 427 error.EndOfStream => break, 428 error.ReadFailed => return error.ReadFailed, 429 }; 430 try data_writer.interface.writeAll(chunk); 431 message.toss(chunk.len); 432 } 433 // In LMTP there is one verdict per recipient rather than one for 434 // the message, and reporting them individually is the only reason 435 // to be speaking it. 436 var verdicts = try data_writer.endResults(); 437 var failed = false; 438 while (try verdicts.next()) |reply| { 439 if (config.protocol == .lmtp) { 440 std.log.info("{s}: {d} {s}", .{ 441 recipients[verdicts.index - 1], 442 reply.code, 443 reply.text, 444 }); 445 } 446 if (!reply.isPositiveCompletion()) failed = true; 447 } 448 // Each verdict was reported above, so the error only has to say 449 // that one of them was a refusal. 450 if (failed) return if (config.protocol == .lmtp) 451 error.RecipientRejected 452 else 453 error.UnexpectedReply; 454 } 455} 456 457fn serve(io: Io, gpa: std.mem.Allocator, config: ServeConfig, port_arg: []const u8) !void { 458 const port = try std.fmt.parseInt(u16, port_arg, 10); 459 const address: Io.net.IpAddress = .{ .ip4 = .loopback(port) }; 460 var listener = try address.listen(io, .{}); 461 defer listener.deinit(io); 462 463 var auth: ?smtp.tls.config.CertKeyPair = if (config.cert_path) |cert_path| 464 try .fromFilePath(gpa, io, .cwd(), cert_path, config.key_path.?) 465 else 466 null; 467 const tls_options: ?smtp.Server.TlsOptions = if (auth) |*a| .{ 468 .io = io, 469 .auth = a, 470 .mode = if (config.implicit_tls) .implicit else .starttls, 471 } else null; 472 std.log.info("listening on 127.0.0.1:{d}{s}", .{ 473 port, 474 if (tls_options) |t| switch (t.mode) { 475 .starttls => " with STARTTLS", 476 .implicit => " with implicit TLS", 477 } else "", 478 }); 479 480 var stdout_buf: [4096]u8 = undefined; 481 var stdout: Io.File.Writer = .init(.stdout(), io, &stdout_buf); 482 483 var printer: MessagePrinter = .{ 484 .out = &stdout.interface, 485 .username = config.username, 486 .password = config.password, 487 .fail_delivery = config.fail_delivery, 488 }; 489 // The credential check, which PLAIN and LOGIN share, and the password 490 // lookup CRAM-MD5 needs instead. Both close over the same one account. 491 const check: smtp.sasl.Server.PasswordCheck = .{ 492 .context = &printer, 493 .verify = MessagePrinter.verify, 494 }; 495 const passwords: smtp.sasl.Server.PasswordLookup = .{ 496 .context = &printer, 497 .lookup = MessagePrinter.lookup, 498 }; 499 500 var connections: usize = 0; 501 while (true) { 502 const stream = try listener.accept(io); 503 defer stream.close(io); 504 connections += 1; 505 506 // A fresh set per connection: the mechanisms hold per-exchange state, 507 // and CRAM-MD5's challenge must not repeat between them. 508 // RFC 2195 wants a challenge that never repeats. A counter and the 509 // clock is what a real server would use, plus its hostname. 510 var challenge_buf: [128]u8 = undefined; 511 const challenge = std.fmt.bufPrint( 512 &challenge_buf, 513 "<{d}.{d}@localhost>", 514 .{ connections, Io.Clock.real.now(io).nanoseconds }, 515 ) catch unreachable; 516 var sasl_scratch: [smtp.Server.sasl_buffer_suggested]u8 = undefined; 517 var plain: smtp.sasl.PlainServer = .init(check); 518 var login: smtp.sasl.LoginServer = .init(check); 519 var cram_md5: smtp.sasl.CramMd5Server = .init(challenge, passwords); 520 const mechanisms: []const smtp.sasl.Server = if (config.username == null) 521 &.{} 522 else 523 &.{ plain.server(), login.server(), cram_md5.server() }; 524 525 // Sized for the TLS handshake, which runs over the raw stream. 526 const read_buf = try gpa.alloc(u8, smtp.tls.input_buffer_len); 527 defer gpa.free(read_buf); 528 const write_buf = try gpa.alloc(u8, smtp.tls.output_buffer_len); 529 defer gpa.free(write_buf); 530 var stream_reader = stream.reader(io, read_buf); 531 var stream_writer = stream.writer(io, write_buf); 532 var session: smtp.Server = .init( 533 &stream_reader.interface, 534 &stream_writer.interface, 535 .{ .context = &printer, .vtable = &.{ 536 .message = MessagePrinter.onMessage, 537 .recipientResult = MessagePrinter.onRecipientResult, 538 } }, 539 .{ 540 .protocol = config.protocol, 541 .hostname = "localhost", 542 .tls = tls_options, 543 .auth_mechanisms = mechanisms, 544 .sasl_buffer = &sasl_scratch, 545 .require_auth = config.username != null, 546 }, 547 ); 548 session.run(gpa) catch |err| { 549 std.log.warn("session ended with error: {t}", .{err}); 550 }; 551 } 552} 553 554const MessagePrinter = struct { 555 out: *Io.Writer, 556 username: ?[]const u8 = null, 557 password: ?[]const u8 = null, 558 fail_delivery: ?[]const u8 = null, 559 560 /// What PLAIN and LOGIN ask: is this password right? The answer is the 561 /// identity to report, which for this one-account server is the username. 562 fn verify( 563 context: ?*anyopaque, 564 authzid: []const u8, 565 authcid: []const u8, 566 password: []const u8, 567 ) ?[]const u8 { 568 const printer: *MessagePrinter = @ptrCast(@alignCast(context.?)); 569 // Acting as somebody else is not a thing this server does. 570 if (authzid.len != 0) return null; 571 if (!std.mem.eql(u8, authcid, printer.username.?)) return null; 572 if (!std.mem.eql(u8, password, printer.password.?)) return null; 573 return printer.username.?; 574 } 575 576 /// What CRAM-MD5 asks instead: the password itself, because it has to 577 /// compute the same HMAC the client did. 578 fn lookup(context: ?*anyopaque, username: []const u8) ?[]const u8 { 579 const printer: *MessagePrinter = @ptrCast(@alignCast(context.?)); 580 if (!std.mem.eql(u8, username, printer.username.?)) return null; 581 return printer.password.?; 582 } 583 584 fn onMessage(context: ?*anyopaque, envelope: smtp.Server.Envelope, data: []const u8) smtp.Server.Decision { 585 const printer: *MessagePrinter = @ptrCast(@alignCast(context.?)); 586 printer.print(envelope, data) catch 587 return .{ .reject = .{ .code = 451, .text = "4.3.0 Local error" } }; 588 return .accept; 589 } 590 591 /// LMTP's per-recipient verdict. Everything was already printed by 592 /// `onMessage`; this only reports the one address `--fail-delivery` 593 /// names as undeliverable, which is the outcome SMTP has no way to 594 /// express for one recipient out of several. 595 fn onRecipientResult( 596 context: ?*anyopaque, 597 envelope: smtp.Server.Envelope, 598 index: usize, 599 ) smtp.Server.Decision { 600 const printer: *MessagePrinter = @ptrCast(@alignCast(context.?)); 601 const failing = printer.fail_delivery orelse return .accept; 602 if (std.mem.eql(u8, envelope.recipients[index].address, failing)) 603 return .{ .reject = .{ .code = 550, .text = "5.2.1 Mailbox disabled" } }; 604 return .accept; 605 } 606 607 fn print(printer: *MessagePrinter, envelope: smtp.Server.Envelope, data: []const u8) !void { 608 try printer.out.print("--- message from <{s}> to", .{envelope.from}); 609 for (envelope.recipients) |recipient| { 610 try printer.out.print(" <{s}>", .{recipient.address}); 611 // DSN parameters, printed so that a session can be checked from 612 // the outside (which is what the interop test does). 613 if (recipient.notify) |notify| try printer.out.print(" NOTIFY={f}", .{notify}); 614 if (recipient.orcpt) |orcpt| try printer.out.print(" ORCPT={f}", .{orcpt}); 615 } 616 if (envelope.ret) |ret| try printer.out.print(" RET={f}", .{ret}); 617 if (envelope.envid) |envid| try printer.out.print(" ENVID={s}", .{envid}); 618 // The decoded mailbox rather than `{f}`, which would print the xtext 619 // that went over the wire. 620 if (envelope.require_tls) try printer.out.writeAll(" REQUIRETLS"); 621 if (envelope.submitter) |who| switch (who) { 622 .unknown => try printer.out.writeAll(" AUTH=<>"), 623 .mailbox => |mailbox| try printer.out.print(" AUTH={s}", .{mailbox}), 624 }; 625 if (envelope.authenticated_as) |who| try printer.out.print(" (authenticated as {s})", .{who}); 626 try printer.out.print(" ({d} bytes)\n{s}---\n", .{ data.len, data }); 627 try printer.out.flush(); 628 } 629};