std/smtp connects both real ways — 587 with STARTTLS and 465 with TLS from the greeting —, authenticates with AUTH PLAIN/LOGIN, and builds the message with headers, body and binary attachments base64-encoded. This recipe does not open a connection on purpose — a real SMTP server needs credentials —, so it runs on its own under make test-examples; the calls that connect are left commented out, ready to copy.
// Nyx by Example: mandar un correo con una factura adjunta. // // El caso real: un ERP perdió la contraseña de su instalación de demostración // y la única salida que el sistema pudo ofrecer fue crear un usuario nuevo, // porque no había forma de mandar un correo de recuperación. El campo `correo` // estaba guardado en el usuario y no se usaba para nada. // // Lo que esta receta muestra: // 1. el mensaje armado tal como viaja — headers, MIME y adjunto en base64; // 2. el DOT-STUFFING, que es lo que evita que el mensaje llegue cortado; // 3. cómo se distingue un rechazo de auth de uno de destinatario; // 4. la forma de las dos conexiones reales (587 con STARTTLS y 465). // // NO abre una conexión a propósito: un servidor SMTP de verdad necesita // credenciales, y esta receta corre sola bajo `make test-examples`. Las dos // llamadas que conectan quedan como comentario, listas para copiar. import "std/smtp" import "std/error" fn main() -> int { // ── 1. El mensaje ──────────────────────────────────────────────────── // El cuerpo se escribe con \n normal: std/smtp lo normaliza a CRLF, que es // lo único que el protocolo admite. let cuerpo: String = "Estimado cliente,\n\nAdjuntamos la factura del mes.\n\nSaludos" var msg: Array = smtp_message( "facturacion@miempresa.com", // de ["cliente@ejemplo.com"], // para (varios: agregar a la lista) "Factura de septiembre", // asunto cuerpo) // El adjunto son los BYTES del archivo. read_file es binary-safe, así que // un PDF viaja entero; la codificación base64 la hace smtp_attach. // let pdf: String = read_file("factura.pdf") let pdf: String = "%PDF-1.4 (contenido de ejemplo)" msg = smtp_attach(msg, "factura.pdf", "application/pdf", pdf) // ── 2. Lo que se manda de verdad ───────────────────────────────────── // smtp_render es una función PURA: se puede inspeccionar el mensaje sin // abrir un socket. Útil para depurar y para testear. let crudo: String = smtp_render(msg) print("— headers del mensaje —") let corte: int = crudo.indexOf("\r\n\r\n") print(crudo.substring(0, corte)) print("") print("¿lleva el adjunto en base64? " + __si_no(crudo.indexOf("Content-Transfer-Encoding: base64") >= 0)) print("¿lleva Content-Disposition? " + __si_no(crudo.indexOf("Content-Disposition: attachment") >= 0)) // ── 3. DOT-STUFFING: por qué la factura no llega cortada ───────────── // El cuerpo de un mensaje termina con una línea que dice solo ".". Si una // línea del CONTENIDO empieza con punto y no se duplica, el servidor da el // mensaje por terminado ahí y entrega la mitad — sin error de ningún lado. // std/smtp lo hace solo. let con_punto: Array = smtp_message("a@x.com", ["b@x.com"], "Importe", "Total:\n.50 de descuento\nGracias") let r: String = smtp_render(con_punto) print("") print("línea que empieza con punto, ya protegida: " + __si_no(r.indexOf("..50 de descuento") >= 0)) // ── 4. Cómo se conecta de verdad ───────────────────────────────────── // 587 con STARTTLS (lo habitual): // // match smtp_connect_plain("smtp.proveedor.com", 587, "miempresa.com") { // Result.Ok(c0) => { // match smtp_starttls(c0, "miempresa.com") { // Result.Ok(c) => { // match smtp_auth_plain(c, "usuario", "clave") { // Result.Ok(_x) => { // match smtp_send(c, msg) { // Result.Ok(_y) => { print("enviado") } // Result.Err(e) => { print(explicar(e)) } // } // let _q: int = smtp_quit(c) // } // Result.Err(e) => { print(explicar(e)) } // } // } // Result.Err(e) => { print(explicar(e)) } // } // } // Result.Err(e) => { print(explicar(e)) } // } // // 465 con TLS desde el saludo: smtp_connect_tls(host, 465, "miempresa.com") // y después igual, sin el paso de smtp_starttls. // // Las dos VERIFICAN el certificado. Para un servidor de desarrollo con // certificado autofirmado existen smtp_starttls_insecure y // smtp_connect_tls_insecure, que dicen en el nombre lo que hacen. // // Y AUTH nunca viaja en claro: si el canal no está cifrado, smtp_auth_* // falla ANTES de mandar las credenciales. No hay flag para saltearlo. print("") print("— qué hacer con cada error —") print(explicar(err_new(535, "auth", "autenticación rechazada"))) print(explicar(err_new(550, "recipient", "destinatario rechazado"))) print(explicar(err_new(0, "connection", "no se pudo conectar"))) print(explicar(err_new(451, "protocol", "fallo transitorio"))) return 0 } // Cada `kind` pide una acción distinta. Eso es lo que hace que valga la pena // que el error sea tipado: un Err genérico no dice qué arreglar. fn explicar(e: Error) -> String { if e.kind == "auth" { return "auth → revisar usuario y clave del proveedor" } if e.kind == "recipient" { return "recipient → la dirección del destinatario está mal" } if e.kind == "sender" { return "sender → el proveedor no te deja mandar desde ese remitente" } if e.kind == "connection" { return "connection → red, host o puerto; o el certificado no validó" } if e.kind == "protocol" { return "protocol → el servidor cortó o contestó algo inesperado" } return "desconocido" } fn __si_no(b: bool) -> String { if b { return "sí" } return "no" }
— headers del mensaje — From: facturacion@miempresa.com To: cliente@ejemplo.com Subject: Factura de septiembre Date: Wed, 23 Sep 2026 17:04:58 +0000 Message-ID: <...@miempresa.com> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=_nyx_..." ¿lleva el adjunto en base64? sí ¿lleva Content-Disposition? sí línea que empieza con punto, ya protegida: sí — qué hacer con cada error — auth → revisar usuario y clave del proveedor recipient → la dirección del destinatario está mal connection → red, host o puerto; o el certificado no validó protocol → el servidor cortó o contestó algo inesperado
How it works
smtp_render is a PURE function: it builds the whole message — headers, MIME, attachment — without opening any socket, so it can be inspected and tested before anything is sent. smtp_attach takes a file's raw bytes (from read_file, say, which is binary-safe) and base64-encodes them, wrapped at 76 columns.
DOT-STUFFING is what keeps a message from arriving cut in half: the protocol ends the body with a line that says just ".", so if a content line starts with a dot and it is not doubled, the server considers the message finished right there — with no error from anywhere. std/smtp handles it on its own, along with normalizing to CRLF.
Each Error kind calls for a different action: "auth" is credentials, "recipient" is the address, "connection" is network or certificate. And AUTH never travels in the clear — if the channel is not encrypted, authentication fails BEFORE sending the credentials, with no flag to force it; the certificate is verified by default, with _insecure variants that say so in the name, for a development server.