Un ERP mandaba sus facturas solo en HTML y los filtros de spam las castigaban: un mensaje sin parte de texto es una señal clásica (SpamAssassin la llama MIME_HTML_ONLY). La salida es multipart/alternative — la misma información en dos versiones, y el cliente de correo muestra la mejor que entiende. La receta no abre ninguna conexión, así que corre sola.
// Nyx by Example: un correo con versión de texto Y versión HTML. // // El caso real: un ERP mandaba sus facturas solo en HTML y los filtros de spam // las castigaban. Un mensaje sin parte de texto es una señal clásica // (SpamAssassin: MIME_HTML_ONLY). La salida es multipart/alternative: la misma // información en dos versiones, y el cliente de correo muestra la mejor que // entiende. // // Lo que muestra esta receta: // 1. smtp_message_alternative(de, para, asunto, texto, html); // 2. que se combina con smtp_attach: queda multipart/mixed > alternative; // 3. que el texto NO se inventa solo: si solo hay HTML, mime_html_to_text // (std/mime) arma uno aproximado, y es decisión de quien llama usarlo; // 4. la estructura que viaja, vista con smtp_render (función pura). // // NO abre ninguna conexión: corre sola bajo `make test-examples`. import "std/smtp" import "std/mime" fn main() -> int { let html: String = "<h1>Factura de septiembre</h1>\n<p>Estimado cliente: adjuntamos su factura.</p>\n<p>Total: <b>$ 1.250</b></p>" // Texto escrito a mano: lo mejor. Si no se tiene, una aproximación: // let texto: String = mime_html_to_text(html) let texto: String = "Factura de septiembre\n\nEstimado cliente: adjuntamos su factura.\nTotal: $ 1.250" var msg: Array = smtp_message_alternative( "facturacion@miempresa.com", ["cliente@ejemplo.com"], "Factura de septiembre", texto, html) msg = smtp_attach(msg, "factura.pdf", "application/pdf", "%PDF-1.4 (ejemplo)") let raw: String = smtp_render(msg) print("— estructura MIME que viaja —") var i: int = 0 var lineas: int = 0 while i < raw.length() { let fin: int = raw.indexOf("\r\n", i) if fin < 0 { break } let linea: String = raw.substring(i, fin) if linea.startsWith("Content-Type:") || linea.startsWith("Content-Transfer-Encoding:") { print(" " + linea) lineas = lineas + 1 } i = fin + 2 } // Lo que un cliente ve al leerlo: las dos partes y el adjunto. match mime_parse(raw, mime_limits()) { Result.Ok(p) => { print("") print("texto : " + int_to_string(p.text.length()) + " bytes") print("html : " + int_to_string(p.html.length()) + " bytes") print("adjuntos: " + int_to_string(p.attachments.length())) } Result.Err(e) => { print("no se pudo leer el mensaje armado") } } // Enviarlo es igual que cualquier otro mensaje: smtp_send(c, msg). Un texto // vacío se rechaza con Err("invalid") antes de abrir el diálogo SMTP. return 0 }
— estructura MIME que viaja — Content-Type: multipart/mixed; boundary="=_nyx_1792350867_3523985490351435988" Content-Type: multipart/alternative; boundary="=_nyx_alt_1792350867_6362569340132166581" Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: quoted-printable Content-Type: text/html; charset=utf-8 Content-Transfer-Encoding: quoted-printable Content-Type: application/pdf Content-Transfer-Encoding: base64 texto : 78 bytes html : 107 bytes adjuntos: 1
Cómo funciona
smtp_message_alternative(de, para, asunto, texto, html) lo arma. Se combina con smtp_attach, y la estructura queda multipart/mixed que contiene la alternative y después el adjunto, como muestran las primeras líneas de la salida. Los boundaries llevan una marca de tiempo y dígitos aleatorios, así que cambian en cada ejecución.
El texto NO se inventa solo: si solo hay HTML, mime_html_to_text (de std/mime) arma uno aproximado, y usarlo es decisión de quien llama. La estructura que viaja se ve con smtp_render, una función pura.