141 / Networking

An email with a text AND an HTML version

An ERP used to send its invoices as HTML only, and spam filters punished them: a message with no text part is a classic signal (SpamAssassin calls it MIME_HTML_ONLY). The fix is multipart/alternative — the same information in two versions, with the mail client showing the best one it understands. The recipe opens no connection, so it runs on its own.

141-smtp-texto-y-html.nxSource →
// 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
}
Outputstdout
— 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

How it works

smtp_message_alternative(from, to, subject, text, html) builds it. It combines with smtp_attach, and the structure becomes multipart/mixed containing the alternative, then the attachment, as the first lines of output show. The boundaries carry a timestamp and random digits, so they change on every run.

The text is NOT invented for you: if there is only HTML, mime_html_to_text (from std/mime) produces an approximate one, and whether to use it is the caller's decision. The structure that travels is shown with smtp_render, a pure function.