Nette Documentation Preview

syntax
Nette Mail
**********

<div class=perex>

¿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

</div>


Instalación
===========

Descargue e instale la biblioteca con [Composer|best-practices:composer]:

```shell
composer require nette/mail
```


Crear correos
=============

Un correo es un objeto [api:Nette\Mail\Message]. Lo creamos así:

```php
$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`. .{data-version:4.2.0}

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:

```php
$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()`:

```php
$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:

```php
// 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?

.[tip]
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:

```php
$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 .{data-version:4.2.0}
-------------------------------------------

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:

```php
$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)`.

```php
// 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):

```php
$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|latte:] es una opción estupenda. ¿Cómo se hace?

```php
$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`:

```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 |dependency-injection:passing-dependencies]:

```php
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.

```latte
<a n:href="Presenter:action">Link</a>
```


Incrustación de CSS
===================

[api: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.

.[note]
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()`:

```php
$inliner = new Nette\Mail\CssInliner;
$html = $inliner->inline($html);
```

Por ejemplo, si el HTML contiene:

```latte
<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):

```latte
<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()`:

```php
$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. .{data-version:4.2.0}

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 [api: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 |#Configuración], que obtiene haciendo que se lo pasen mediante [dependency injection |dependency-injection:passing-dependencies].


SendmailMailer
--------------

El mailer predeterminado es SendmailMailer, que usa la función de PHP [php:mail]. Ejemplo de uso:

```php
$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`.

```php
$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 para `ssl`, 587 para `tls`, en los demás casos 25
* `timeout`: tiempo de espera de la conexión SMTP
* `persistent`: usar una conexión persistente
* `clientHost`: indica la cabecera host del cliente
* `streamOptions`: permite establecer las "opciones de contexto SSL":https://www.php.net/manual/en/context.ssl.php de la conexión


Autenticación OAuth 2.0 .{data-version:4.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:

```php
$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:

```php
$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.

```php
$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. .{data-version:4.2.0}

Puede añadir otro mailer más tarde con `addMailer()` y registrar el evento `$onFailure`, que se llama tras cada intento fallido:

```php
$mailer->onFailure[] = function ($mailer, $exception, $failedMailer, $mail) {
	// p. ej. registra el intento fallido
};
```


FileMailer .{data-version:4.2.0}
--------------------------------

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.

```php
$mailer = new Nette\Mail\FileMailer('/path/to/mails');
$mailer->send($mail);
```


Depurar los correos .{data-version:4.1.2}
=========================================

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":https://mailpit.axllent.org o "MailHog":https://github.com/mailhog/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`:

```neon
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:

```neon
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 |tracy:] que lista todos los correos enviados.

Por dentro de esto se encarga [api: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 |#Configuración]. Si no usa dependency injection, se usa así:

```php
$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":https://datatracker.ietf.org/doc/html/rfc8463) como bytes en bruto codificados en base64; el tipo se detecta de la propia clave. Firmar con Ed25519 requiere la extensión `sodium`. .{data-version:4.2.0}

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`. .{data-version:4.2.0}


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|bootstrap:].

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`:

```neon
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":https://www.php.net/manual/en/openssl.configuration.php.

Para aumentar la fiabilidad, podemos firmar los correos con la [tecnología DKIM |https://blog.nette.org/es/sign-emails-with-dkim]:

```neon
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|#Depurar los correos]:

```neon
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`	  | [api:Nette\Mail\Mailer]   | [clase de envío de correos |#Enviar correos]
| `mail.signer`	  | [api:Nette\Mail\Signer]   | [firma DKIM |#DKIM]


Si está actualizando a una versión más reciente, vea la página de [actualización |upgrading].

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 para ssl, 587 para tls, en los demás casos 25
  • timeout: tiempo de espera de la conexión SMTP
  • persistent: usar una conexión persistente
  • clientHost: indica la cabecera host del cliente
  • streamOptions: 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 MailpitMailHog. 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.