Nette Mail
¿Está pensando en enviar correos electrónicos, como boletines o confirmaciones de pedidos? Nette Framework le proporciona las herramientas necesarias con una API muy cómoda. Le mostraremos:
- cómo crear un correo, incluidos los archivos adjuntos
- cómo enviarlo
- cómo combinar los correos con las plantillas
Instalación
Descargue e instale la biblioteca con Composer:
composer require nette/mail
Crear correos
Un correo es un objeto Nette\Mail\Message. Lo creamos así:
$mail = new Nette\Mail\Message;
$mail->setFrom('John <john@example.com>')
->addTo('peter@example.com')
->addTo('jack@example.com')
->setSubject('Order Confirmation')
->setBody("Hello,\nYour order has been accepted.");
Todos los parámetros indicados tienen que estar en codificación UTF-8.
Las direcciones con dominio internacionalizado, como jan@příklad.cz, se convierten
automáticamente a la forma ASCII conocida como punycode, que exigen los servidores de correo; para eso hace falta la extensión
intl.
Además de indicar los destinatarios con addTo(), también puede indicar destinatarios en copia con
addCc(), o destinatarios en copia oculta con addBcc(). Todos estos métodos, incluido
setFrom(), aceptan al destinatario de tres formas:
$mail->setFrom('john.doe@example.com');
$mail->setFrom('john.doe@example.com', 'John Doe');
$mail->setFrom('John Doe <john.doe@example.com>');
El cuerpo de un correo escrito en HTML se pasa con el método setHtmlBody():
$mail->setHtmlBody('<p>Hello,</p><p>Your order has been accepted.</p>');
No hace falta que cree la alternativa en texto; Nette se la genera automáticamente. Y si el correo no tiene asunto, intentará
tomarlo del elemento <title>.
Las imágenes también se pueden incrustar en el cuerpo HTML con una facilidad excepcional. Basta con pasar como segundo parámetro la ruta donde están físicamente las imágenes y Nette las incluirá automáticamente en el correo:
// añade automáticamente /path/to/images/background.gif al correo
$mail->setHtmlBody(
'<b>Hello</b> <img src="background.gif">',
'/path/to/images',
);
El algoritmo de incrustación de imágenes busca estos patrones: <img src=...>,
<body background=...>, url(...) dentro del atributo HTML style, y la sintaxis
especial [[...]].
¿Podría ser aún más fácil enviar correos?
Los correos son como postales. Nunca envíe contraseñas ni otras credenciales por correo.
Otras opciones
El objeto Message también le permite establecer una dirección de respuesta, una ruta de retorno para los
mensajes rebotados y la prioridad del mensaje:
$mail->addReplyTo('reply@example.com', 'Support')
->setReturnPath('bounces@example.com')
->setPriority(Nette\Mail\Message::High);
La prioridad es una de las constantes Message::High, Message::Normal o Message::Low.
Baja con un solo clic
Gmail y Yahoo exigen que el correo masivo, como los boletines, ofrezca darse de baja con un solo clic directamente en el
cliente de correo. De eso se ocupa un par de cabeceras definidas por la RFC 8058, que el método setUnsubscribe()
configura correctamente por usted:
$mail->setUnsubscribe('https://example.com/unsubscribe?token=xyz');
La URL tiene que dar de baja al destinatario en respuesta a una simple petición HTTP POST, sin ninguna confirmación
adicional. El segundo parámetro puede proporcionar una dirección de correo como alternativa para los clientes que no pueden
enviar un POST; también funciona por sí solo: $mail->setUnsubscribe(email: 'unsubscribe@example.com').
Archivos adjuntos
Por supuesto, a los correos se les pueden adjuntar archivos. Para eso sirve el método
addAttachment(string $file, ?string $content = null, ?string $contentType = null).
// adjunta al correo el archivo /path/to/example.zip con el nombre example.zip
$mail->addAttachment('/path/to/example.zip');
// adjunta el archivo /path/to/example.zip con el nombre info.zip
$mail->addAttachment('info.zip', file_get_contents('/path/to/example.zip'));
// adjunta el archivo example.txt con el contenido "Hello John!"
$mail->addAttachment('example.txt', 'Hello John!');
También puede incrustar un archivo directamente en el cuerpo HTML con addEmbeddedFile(). Devuelve la parte MIME
creada, cuyo Content-ID se referencia en el HTML (este es exactamente el mecanismo que usa por dentro la
incrustación automática de imágenes):
$file = $mail->addEmbeddedFile('/path/to/logo.png');
$mail->setHtmlBody('<img src="cid:' . trim($file->getHeader('Content-ID'), '<>') . '">');
Plantillas
Si envía correos en HTML, escribirlos en el sistema de plantillas Latte es una opción estupenda. ¿Cómo se hace?
$latte = new Latte\Engine;
$params = [
'orderId' => 123,
];
$mail = new Nette\Mail\Message;
$mail->setFrom('John <john@example.com>')
->addTo('jack@example.com')
->setHtmlBody(
$latte->renderToString('/path/to/email.latte', $params),
'/path/to/images',
);
Archivo email.latte:
<html>
<head>
<meta charset="utf-8">
<title>Order Confirmation</title>
<style>
body {
background: url("background.png")
}
</style>
</head>
<body>
<p>Hello,</p>
<p>Your order number {$orderId} has been accepted.</p>
</body>
</html>
Nette incrusta automáticamente todas las imágenes, establece el asunto a partir del elemento <title> y
genera la alternativa en texto del HTML.
Uso en Nette Application
Si usa los correos junto con Nette Application, es decir, con presenters, quizá quiera crear enlaces en las plantillas con el
atributo n:href o la etiqueta {link}. Latte no los conoce de forma predeterminada, pero es muy fácil
añadirlos. El objeto Nette\Application\LinkGenerator puede crear enlaces y lo obtiene haciendo que se lo pasen
mediante dependency injection:
use Nette;
class MailSender
{
public function __construct(
private Nette\Application\LinkGenerator $linkGenerator,
private Nette\Bridges\ApplicationLatte\TemplateFactory $templateFactory,
) {
}
private function createTemplate(): Nette\Application\UI\Template
{
$template = $this->templateFactory->createTemplate();
$template->getLatte()->addProvider('uiControl', $this->linkGenerator);
return $template;
}
public function createEmail(): Nette\Mail\Message
{
$template = $this->createTemplate();
$html = $template->renderToString('/path/to/email.latte', $params);
$mail = new Nette\Mail\Message;
$mail->setHtmlBody($html);
// ...
return $mail;
}
}
En la plantilla crea después los enlaces como está acostumbrado. Todos los enlaces creados con LinkGenerator serán absolutos.
<a n:href="Presenter:action">Link</a>
Incrustación de CSS
Nette\Mail\CssInliner convierte las reglas CSS en
atributos style en línea para que los correos se rendericen de forma consistente en todos los clientes. También
genera atributos HTML para la compatibilidad con Outlook.
Requiere PHP 8.4 o posterior y la extensión dom.
La mayoría de los clientes de correo tienen un soporte limitado de las etiquetas <style> o las ignoran por
completo. Para asegurar un renderizado correcto, las reglas CSS hay que convertirlas en atributos style en línea en
cada elemento. Basta con pasar su HTML por inline():
$inliner = new Nette\Mail\CssInliner;
$html = $inliner->inline($html);
Por ejemplo, si el HTML contiene:
<style>
p { margin: 0; color: #333; }
a { color: #a0704e; }
</style>
<p>Hello <a href="#">world</a></p>
El resultado tras la incrustación será (la etiqueta <style> se conserva, pero aquí se omite por
brevedad):
<p style="margin: 0; color: #333">Hello <a href="#" style="color: #a0704e">world</a></p>
La etiqueta <style> se conserva siempre en la salida, así que las @media queries y otras
reglas que no se pueden incrustar siguen funcionando.
Además de extraer los estilos de las etiquetas <style>, también puede proporcionar CSS con el método
addCss(). Tiene que incrustar el CSS antes de pasar el HTML a setHtmlBody():
$latte = new Latte\Engine;
$params = [
'orderId' => 123,
];
$html = $latte->renderToString('/path/to/email.latte', $params);
$html = (new Nette\Mail\CssInliner)
->addCss(file_get_contents('/path/to/email.css'))
->inline($html);
$mail = new Nette\Mail\Message;
$mail->setHtmlBody($html);
Cuando varias reglas apuntan a la misma propiedad de un elemento, el ganador lo decide la cascada CSS,
igual que en un navegador: las declaraciones !important ganan a las normales, un atributo style en
línea ya existente gana a cualquier selector, un selector más específico gana a uno menos específico, y en caso de empate gana
la regla posterior. Las reglas de las etiquetas <style> se procesan antes que las añadidas con
addCss(), y solo se escribe el valor ganador.
Las at-rules como @media o @font-face se saltan durante la incrustación. Tenga en cuenta que las
pseudoclases como :hover no se pueden incrustar de forma significativa, ya que los estilos en línea no soportan
estados dinámicos.
Atributos HTML para Outlook
Las versiones de escritorio de Microsoft Outlook usan el motor de renderizado de Word, que no entiende muchas propiedades CSS.
Para asegurar la compatibilidad, CssInliner genera automáticamente, junto a los estilos en línea, los atributos
HTML correspondientes a partir de las reglas CSS:
| Propiedad CSS | Atributo HTML | Se aplica a |
|---|---|---|
background-color |
bgcolor |
<table>, <td>, <th>,
<body>, <tr> |
width |
width |
<table>, <td>, <th>, <img> |
height |
height |
<table>, <td>, <th>, <img> |
border-spacing |
cellspacing |
<table> |
En width, height y cellspacing se elimina automáticamente la unidad px (p.
ej. width: 600px se convierte en width="600"), un porcentaje conserva su %, y los valores
que un atributo no puede expresar, como auto o calc(), no producen ningún atributo. Se establecen a la
vez el estilo en línea y el atributo HTML, así que el correo se renderiza correctamente tanto en los clientes modernos como en
Outlook.
Los atributos HTML se generan solo a partir de las reglas CSS que procesa CssInliner, no de los atributos
style que ya están en el HTML original.
Enviar correos
Mailer es una clase que se encarga de enviar los correos. Implementa la interfaz Nette\Mail\Mailer y hay disponibles varios mailers ya hechos, que le presentaremos.
El framework añade automáticamente al contenedor DI un servicio Nette\Mail\Mailer según la configuración, que obtiene haciendo que se lo pasen mediante dependency injection.
SendmailMailer
El mailer predeterminado es SendmailMailer, que usa la función de PHP mail. Ejemplo de uso:
$mailer = new Nette\Mail\SendmailMailer;
$mailer->send($mail);
Si quiere establecer el returnPath y su servidor lo sobrescribe igualmente, use
$mailer->commandArgs = '-fmy@email.com'.
De forma predeterminada, SendmailMailer pasa la dirección del remitente a la función mail() como
remitente del sobre (el argumento -f). Eso se puede desactivar con
$mailer->setEnvelopeSender(false).
SmtpMailer
Para enviar el correo a través de un servidor SMTP, use SmtpMailer.
$mailer = new Nette\Mail\SmtpMailer(
host: 'smtp.gmail.com',
username: 'john@gmail.com',
password: '*****', // su contraseña
encryption: 'ssl', // o 'tls'
);
$mailer->send($mail);
Al constructor se le pueden pasar los siguientes parámetros adicionales:
port: si no se establece, se usa el predeterminado: 465 parassl, 587 paratls, en los demás casos 25timeout: tiempo de espera de la conexión SMTPpersistent: usar una conexión persistenteclientHost: indica la cabecera host del clientestreamOptions: permite establecer las opciones de contexto SSL de la conexión
Autenticación OAuth 2.0
Gmail y Microsoft 365 están retirando la autenticación por contraseña en SMTP y exigen en su lugar un token de acceso OAuth
2.0 (el mecanismo XOAUTH2). Pase el token con el método setAccessToken(); el nombre de usuario se mantiene, la
contraseña se deja vacía:
$mailer = new Nette\Mail\SmtpMailer(
host: 'smtp.gmail.com',
username: 'john@gmail.com',
password: '',
encryption: 'tls',
);
$mailer->setAccessToken($accessToken);
Como los tokens de acceso caducan, en su lugar puede pasar un callback; se llama en cada conexión, así que siempre puede proporcionar un token fresco. Obtener y refrescar el token sigue siendo cosa suya o de su biblioteca de OAuth:
$mailer->setAccessToken(fn() => $oauthProvider->getFreshToken());
FallbackMailer
Este mailer no envía los correos directamente, sino que media el envío a través de un conjunto de mailers. Si un mailer falla, lo reintenta con el siguiente. Si falla el último, empieza otra vez por el primero.
$mailer = new Nette\Mail\FallbackMailer([
$smtpMailer,
$backupSmtpMailer,
$sendmailMailer,
]);
$mailer->send($mail);
Los demás parámetros del constructor son el número de reintentos (de forma predeterminada 3) y el tiempo de
espera entre ellos en milisegundos (de forma predeterminada 1000). Si todos los mailers fallan en todos los intentos,
se lanza una Nette\Mail\FallbackMailerException, cuya propiedad $failures contiene las excepciones
recogidas.
Un mailer cuyo fallo es permanente, como que el servidor SMTP rechace las credenciales, se descarta de los siguientes intentos: reintentarlo no puede cambiar el resultado.
Puede añadir otro mailer más tarde con addMailer() y registrar el evento $onFailure, que se llama
tras cada intento fallido:
$mailer->onFailure[] = function ($mailer, $exception, $failedMailer, $mail) {
// p. ej. registra el intento fallido
};
FileMailer
Este mailer no envía nada: escribe cada mensaje como un archivo .eml en el directorio dado. Los archivos se abren
en cualquier cliente de correo, así que puede comprobar exactamente qué se habría enviado; práctico en las pruebas y durante
el desarrollo.
$mailer = new Nette\Mail\FileMailer('/path/to/mails');
$mailer->send($mail);
Depurar los correos
Al desarrollar o al ejecutar un servidor de staging, no quiere que un correo de prueba se escape a un cliente real. Hay dos formas de asegurarse de que eso no ocurra nunca.
La configuración local recomendada es ejecutar en su máquina un capturador SMTP ligero como Mailpit o MailHog. Aceptan todos los
mensajes, los muestran en una interfaz web y nunca reenvían nada; usted solo apunta Nette Mail a 127.0.0.1:1025:
mail:
smtp: true
host: 127.0.0.1
port: 1025
Para staging o para los entornos donde no puede ejecutar un capturador local, Nette Mail trae una redirección integrada.
Establezca el destino en la configuración y todos los destinatarios To, Cc y Bcc se
sustituirán por él. Nette Mail conserva los originales en las cabeceras X-Original-* para que pueda ver a quién
iba destinado el correo, y puede anteponer un marcador al asunto:
mail:
redirect:
to: dev@example.com
subjectPrefix: '[debug]' # opcional
La forma abreviada redirect: dev@example.com sirve cuando no necesita un prefijo en el asunto. En modo de
depuración se adjunta automáticamente un panel de la Tracy Bar que lista todos los
correos enviados.
Por dentro de esto se encarga Nette\Mail\Interceptor, que además expone un evento
$onSent para listeners propios (registros de auditoría, métricas, …).
DKIM
DKIM (DomainKeys Identified Mail) es una tecnología para aumentar la fiabilidad de los correos, que además ayuda a detectar los mensajes falsificados. El mensaje enviado se firma con la clave privada del dominio del remitente y esa firma se guarda en la cabecera del correo. El servidor del destinatario compara esa firma con la clave pública guardada en los registros DNS del dominio. Si la firma coincide, queda demostrado que el correo procede realmente del dominio del remitente y que el mensaje no se modificó durante la transmisión.
Puede configurar el mailer para que firme los correos directamente en la configuración. Si no usa dependency injection, se usa así:
$signer = new Nette\Mail\DkimSigner(
domain: 'yourdomain.com',
selector: 'dkim', // selector del registro DNS
privateKey: file_get_contents('/path/to/dkim.key'), // ruta a su clave privada
passPhrase: 'your_passphrase', // frase de contraseña de la clave privada, si la hay
);
$mailer = new Nette\Mail\SendmailMailer; // o SmtpMailer
$mailer->setSigner($signer);
$mailer->send($mail);
La clave privada puede ser una clave RSA en formato PEM, o una clave Ed25519 (RFC 8463) como bytes en bruto codificados en base64; el tipo se detecta
de la propia clave. Firmar con Ed25519 requiere la extensión sodium.
En el parámetro oversignHeaders puede listar las cabeceras que quiere proteger contra que se
añada una segunda copia al mensaje ya firmado, un truco que usan los correos falsificados; el candidato habitual es
From.
Configuración
Resumen de las opciones de configuración de Nette Mail. Si no usa todo el framework, sino solo esta biblioteca, lea cómo cargar la configuración.
De forma predeterminada, para enviar los correos se usa Nette\Mail\SendmailMailer, que no requiere ninguna
configuración más. Pero podemos cambiarlo a Nette\Mail\SmtpMailer:
mail:
# usa SmtpMailer
smtp: true # (bool) el valor predeterminado es false
host: ... # (string) hostname del servidor SMTP
port: ... # (int) puerto del servidor SMTP
username: ... # (string) nombre de usuario para la autenticación SMTP
password: ... # (string) contraseña para la autenticación SMTP
timeout: ... # (int) tiempo de espera de la conexión SMTP
encryption: ... # (ssl|tls|null) el valor predeterminado es null (alias 'secure')
clientHost: ... # (string) hostname del cliente, de forma predeterminada $_SERVER['HTTP_HOST'] o 'localhost'
persistent: ... # (bool) usa una conexión persistente, de forma predeterminada false
# opciones de contexto de flujo de la conexión SMTP, de forma predeterminada stream_context_get_default()
context:
ssl: # todas las opciones en https://www.php.net/manual/en/context.ssl.php
allow_self_signed: ...
...
http: # lista de opciones en https://www.php.net/manual/en/context.http.php
header: ...
...
Puede desactivar la verificación de los certificados SSL con la opción context › ssl › verify_peer: false.
Le desaconsejamos encarecidamente hacerlo, porque hace la aplicación vulnerable. En su lugar, añada los certificados al almacén de confianza.
Para aumentar la fiabilidad, podemos firmar los correos con la tecnología DKIM:
mail:
dkim:
domain: myweb.com # su dominio
selector: lovenette # selector DKIM
privateKey: %appDir%/cert/dkim.key # ruta al archivo de su clave privada
passPhrase: ... # frase de contraseña de la clave privada, si hace falta
Las opciones para redirigir todos los correos y activar el panel de depuración se describen en la sección Depurar los correos:
mail:
# redirige todos los correos a una única dirección
redirect: dev@example.com
# activa (true) o desactiva (false) el panel de Tracy y la interceptación de los correos
debugger: ... # (bool) el valor predeterminado es null, es decir, auto en modo de depuración
Servicios DI
Estos servicios se añaden al contenedor DI:
| Nombre | Tipo | Descripción |
|---|---|---|
mail.mailer |
Nette\Mail\Mailer | clase de envío de correos |
mail.signer |
Nette\Mail\Signer | firma DKIM |
Si está actualizando a una versión más reciente, vea la página de actualización.