Mail

Invio
Email

Nette Mail

State pensando di inviare email, per esempio newsletter o conferme d'ordine? Il Nette Framework offre gli strumenti necessari con un'API molto amichevole. Vi mostreremo:

  • come creare un'email, allegati compresi
  • come inviarla
  • come combinare email e template

Installazione

La libreria si scarica e si installa con Composer:

composer require nette/mail

Creare le email

Un'email è un oggetto Nette\Mail\Message. Creiamone uno così:

$mail = new Nette\Mail\Message;
$mail->setFrom('John <john@example.com>')
	->addTo('peter@example.com')
	->addTo('jack@example.com')
	->setSubject('Ordine confermato')
	->setBody("Buongiorno,\nil vostro ordine è stato accettato.");

Tutti i parametri indicati devono essere nella codifica UTF-8.

Gli indirizzi con un dominio internazionalizzato, per esempio jan@příklad.cz, vengono convertiti automaticamente nella forma ASCII detta punycode, che i server di posta richiedono; per questo serve l'estensione intl.

Oltre a indicare i destinatari con addTo(), potete indicare i destinatari in copia con addCc(), oppure i destinatari in copia nascosta con addBcc(). Tutti questi metodi, setFrom() compreso, accettano il destinatario in tre modi:

$mail->setFrom('john.doe@example.com');
$mail->setFrom('john.doe@example.com', 'John Doe');
$mail->setFrom('John Doe <john.doe@example.com>');

Il corpo di un'email scritta in HTML si passa con il metodo setHtmlBody():

$mail->setHtmlBody('<p>Buongiorno,</p><p>il vostro ordine è stato accettato.</p>');

Non dovete creare l'alternativa testuale, la genera automaticamente Nette per voi. E se l'email non ha un oggetto impostato, cercherà di prenderlo dall'elemento <title>.

Anche inserire le immagini nel corpo HTML è eccezionalmente facile. Basta passare come secondo parametro il percorso in cui le immagini si trovano fisicamente e Nette le includerà automaticamente nell'email:

// aggiunge automaticamente /path/to/images/background.gif all'email
$mail->setHtmlBody(
	'<b>Buongiorno</b> <img src="background.gif">',
	'/path/to/images',
);

L'algoritmo di inserimento delle immagini cerca questi schemi: <img src=...>, <body background=...>, url(...) dentro l'attributo HTML style e la sintassi speciale [[...]].

Inviare email potrebbe essere ancora più facile?

Le email sono come le cartoline. Non inviate mai per email password o altre credenziali.

Altre opzioni

L'oggetto Message vi permette anche di impostare un indirizzo di risposta, un percorso di ritorno per i messaggi respinti e la priorità del messaggio:

$mail->addReplyTo('reply@example.com', 'Support')
	->setReturnPath('bounces@example.com')
	->setPriority(Nette\Mail\Message::High);

La priorità è una delle costanti Message::High, Message::Normal oppure Message::Low.

Disiscrizione con un clic

Gmail e Yahoo richiedono che la posta di massa come le newsletter offra la disiscrizione con un solo clic direttamente nel client di posta. Se ne occupa una coppia di header definiti dalla RFC 8058, che il metodo setUnsubscribe() imposta correttamente per voi:

$mail->setUnsubscribe('https://example.com/unsubscribe?token=xyz');

L'URL deve disiscrivere il destinatario in risposta a una semplice richiesta HTTP POST, senza altre conferme. Il secondo parametro può fornire un indirizzo email come ripiego per i client che non sanno inviare una POST; funziona anche da solo: $mail->setUnsubscribe(email: 'unsubscribe@example.com').

Allegati

All'email potete naturalmente allegare dei file. A questo serve il metodo addAttachment(string $file, ?string $content = null, ?string $contentType = null).

// allega all'email il file /path/to/example.zip con il nome example.zip
$mail->addAttachment('/path/to/example.zip');

// allega il file /path/to/example.zip con il nome info.zip
$mail->addAttachment('info.zip', file_get_contents('/path/to/example.zip'));

// allega il file example.txt con il contenuto "Buongiorno John!"
$mail->addAttachment('example.txt', 'Buongiorno John!');

Potete anche inserire un file direttamente nel corpo HTML con addEmbeddedFile(). Restituisce la parte MIME creata, il cui Content-ID referenziate nell'HTML (è esattamente il meccanismo che l'inserimento automatico delle immagini usa internamente):

$file = $mail->addEmbeddedFile('/path/to/logo.png');
$mail->setHtmlBody('<img src="cid:' . trim($file->getHeader('Content-ID'), '<>') . '">');

Template

Se inviate email HTML, scriverle nel sistema di template Latte è un'ottima possibilità. Come si fa?

$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',
	);

File email.latte:

<html>
<head>
	<meta charset="utf-8">
	<title>Conferma d'ordine</title>
	<style>
	body {
		background: url("background.png")
	}
	</style>
</head>
<body>
	<p>Buongiorno,</p>

	<p>il vostro ordine numero {$orderId} è stato accettato.</p>
</body>
</html>

Nette inserisce automaticamente tutte le immagini, imposta l'oggetto in base all'elemento <title> e genera l'alternativa testuale dell'HTML.

Uso in Nette Application

Se usate le email insieme a Nette Application, cioè con i presenter, potreste voler creare nei template dei link con l'attributo n:href oppure con il tag {link}. Latte non li conosce per impostazione predefinita, ma è molto facile aggiungerli. L'oggetto Nette\Application\LinkGenerator sa creare i link e lo ottenete facendovelo passare con la 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;
	}
}

Nel template create poi i link come siete abituati. Tutti i link creati tramite LinkGenerator saranno assoluti.

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

Inlining del CSS

Nette\Mail\CssInliner converte le regole CSS in attributi style inline, così che le email si rendano in modo coerente in tutti i client. Genera inoltre attributi HTML per la compatibilità con Outlook.

Richiede PHP 8.4 o superiore e l'estensione dom.

La maggior parte dei client di posta supporta i tag <style> in modo limitato o li ignora del tutto. Per garantire un rendering corretto, le regole CSS vanno convertite in attributi style inline sui singoli elementi. Basta passare il vostro HTML a inline():

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

Se per esempio l'HTML contiene:

<style>
p { margin: 0; color: #333; }
a { color: #a0704e; }
</style>
<p>Buongiorno <a href="#">mondo</a></p>

Il risultato dopo l'inlining sarà (il tag <style> viene conservato, ma qui è omesso per brevità):

<p style="margin: 0; color: #333">Buongiorno <a href="#" style="color: #a0704e">mondo</a></p>

Il tag <style> viene sempre conservato nell'output, così le @media query e le altre regole che non si possono rendere inline continuano a funzionare.

Oltre a estrarre gli stili dai tag <style>, potete fornire il CSS anche con il metodo addCss(). Dovete fare l'inlining del CSS prima di passare l'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);

Quando più regole puntano alla stessa proprietà di un elemento, il vincitore è deciso dalla cascata CSS, proprio come in un browser: le dichiarazioni !important battono quelle normali, un attributo style inline già presente batte qualsiasi selettore, un selettore più specifico batte uno meno specifico e a parità vince la regola successiva. Le regole dei tag <style> vengono elaborate prima di quelle aggiunte con addCss() e viene scritto solo il valore vincente.

Le at-rule come @media o @font-face vengono saltate durante l'inlining. Notate che le pseudo-classi come :hover non si possono rendere inline in modo sensato, perché gli stili inline non supportano gli stati dinamici.

Attributi HTML per Outlook

Le versioni desktop di Microsoft Outlook usano il motore di rendering di Word, che non comprende molte proprietà CSS. Per garantire la compatibilità, CssInliner genera automaticamente dalle regole CSS i corrispondenti attributi HTML accanto agli stili inline:

Proprietà CSS Attributo HTML Applicato 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>

Per width, height e cellspacing l'unità px viene rimossa automaticamente (per esempio width: 600px diventa width="600"), una percentuale mantiene il suo % e i valori che un attributo non può esprimere, come auto o calc(), non producono alcun attributo. Vengono impostati insieme sia lo stile inline sia l'attributo HTML, così l'email si rende correttamente sia nei client moderni sia in Outlook.

Gli attributi HTML vengono generati solo dalle regole CSS elaborate da CssInliner, non dagli attributi style già presenti nell'HTML originale.

Inviare le email

Il mailer è la classe responsabile dell'invio delle email. Implementa l'interfaccia Nette\Mail\Mailer e sono disponibili diversi mailer già pronti, che presenteremo.

Il framework aggiunge automaticamente al container DI un servizio Nette\Mail\Mailer in base alla configurazione, che ottenete facendovelo passare con la dependency injection.

SendmailMailer

Il mailer predefinito è SendmailMailer, che usa la funzione PHP mail. Esempio d'uso:

$mailer = new Nette\Mail\SendmailMailer;
$mailer->send($mail);

Se volete impostare returnPath e il vostro server continua a sovrascriverlo, usate $mailer->commandArgs = '-fmy@email.com'.

Per impostazione predefinita SendmailMailer passa alla funzione mail() l'indirizzo del mittente come envelope sender (l'argomento -f). Lo potete disattivare con $mailer->setEnvelopeSender(false).

SmtpMailer

Per inviare la posta tramite un server SMTP usate SmtpMailer.

$mailer = new Nette\Mail\SmtpMailer(
	host: 'smtp.gmail.com',
	username: 'john@gmail.com',
	password: '*****', // la vostra password
	encryption: 'ssl', // oppure 'tls'
);
$mailer->send($mail);

Al costruttore si possono passare questi parametri aggiuntivi:

  • port – se non è impostato, si usa quello predefinito: 465 per ssl, 587 per tls, altrimenti 25
  • timeout – timeout della connessione SMTP
  • persistent – usa una connessione persistente
  • clientHost – indica l'header host del client
  • streamOptions – permette di impostare le opzioni del contesto SSL per la connessione

Autenticazione OAuth 2.0

Gmail e Microsoft 365 stanno abbandonando l'autenticazione con password per SMTP e richiedono invece un token di accesso OAuth 2.0 (il meccanismo XOAUTH2). Passate il token con il metodo setAccessToken(); il nome utente resta, la password si lascia vuota:

$mailer = new Nette\Mail\SmtpMailer(
	host: 'smtp.gmail.com',
	username: 'john@gmail.com',
	password: '',
	encryption: 'tls',
);
$mailer->setAccessToken($accessToken);

Poiché i token di accesso scadono, potete passare invece un callback; viene chiamato a ogni connessione, così può fornire sempre un token fresco. Ottenere e rinnovare il token resta compito vostro o della vostra libreria OAuth:

$mailer->setAccessToken(fn() => $oauthProvider->getFreshToken());

FallbackMailer

Questo mailer non invia le email direttamente, ma media l'invio attraverso un insieme di mailer. Se un mailer fallisce, riprova con il successivo. Se fallisce l'ultimo, ricomincia dal primo.

$mailer = new Nette\Mail\FallbackMailer([
	$smtpMailer,
	$backupSmtpMailer,
	$sendmailMailer,
]);
$mailer->send($mail);

Gli altri parametri del costruttore sono il numero di tentativi (predefinito 3) e il tempo di attesa tra di essi in millisecondi (predefinito 1000). Se tutti i mailer falliscono a ogni tentativo, viene lanciata una Nette\Mail\FallbackMailerException, la cui proprietà $failures contiene le eccezioni raccolte.

Un mailer il cui fallimento è permanente, per esempio il server SMTP che rifiuta le credenziali, viene escluso dai tentativi successivi: riprovare non può cambiare l'esito.

Potete aggiungere un altro mailer in seguito con addMailer() e registrare l'evento $onFailure, che viene chiamato dopo ogni tentativo fallito:

$mailer->onFailure[] = function ($mailer, $exception, $failedMailer, $mail) {
	// per esempio registriamo il tentativo fallito
};

FileMailer

Questo mailer non invia nulla: scrive ogni messaggio come file .eml nella directory indicata. I file si aprono in qualsiasi client di posta, così potete controllare esattamente che cosa sarebbe stato inviato: comodo nei test e durante lo sviluppo.

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

Debug delle email

Quando sviluppate o fate girare un server di staging, non volete che un'email di prova sfugga a un cliente reale. Ci sono due modi per essere certi che non accada mai.

L'impostazione locale consigliata è far girare sulla vostra macchina un leggero catcher SMTP come Mailpit oppure MailHog. Accettano ogni messaggio, lo mostrano in un'interfaccia web e non inoltrano nulla: basta puntare Nette Mail su 127.0.0.1:1025:

mail:
	smtp: true
	host: 127.0.0.1
	port: 1025

Per lo staging o per gli ambienti in cui non potete far girare un catcher locale, Nette Mail ha un reindirizzamento integrato. Impostate la destinazione nella configurazione e ogni destinatario To, Cc e Bcc viene sostituito con essa. Nette Mail conserva gli originali negli header X-Original-*, così potete vedere a chi era destinata l'email, e potete anteporre un marcatore all'oggetto:

mail:
	redirect:
		to: dev@example.com
		subjectPrefix: '[debug]'   # facoltativo

La forma abbreviata redirect: dev@example.com funziona quando non vi serve un prefisso nell'oggetto. In modalità debug si aggancia automaticamente un pannello della Tracy Bar che elenca tutte le email inviate.

Internamente se ne occupa Nette\Mail\Interceptor, che espone anche l'evento $onSent per i listener personalizzati (log di audit, metriche, …).

DKIM

DKIM (DomainKeys Identified Mail) è una tecnologia per aumentare l'affidabilità delle email, che aiuta anche a rilevare i messaggi contraffatti. Il messaggio inviato viene firmato con la chiave privata del dominio del mittente e questa firma viene conservata nell'header dell'email. Il server del destinatario confronta questa firma con la chiave pubblica conservata nei record DNS del dominio. Se la firma corrisponde, è provato che l'email proviene davvero dal dominio del mittente e che il messaggio non è stato modificato durante la trasmissione.

Potete impostare il mailer perché firmi le email direttamente nella configurazione. Se non usate la dependency injection, si usa così:

$signer = new Nette\Mail\DkimSigner(
	domain: 'yourdomain.com',
	selector: 'dkim', // selettore dal record DNS
	privateKey: file_get_contents('/path/to/dkim.key'), // percorso della vostra chiave privata
	passPhrase: 'your_passphrase', // passphrase della chiave privata, se c'è
);

$mailer = new Nette\Mail\SendmailMailer; // oppure SmtpMailer
$mailer->setSigner($signer);
$mailer->send($mail);

La chiave privata può essere una chiave RSA in formato PEM, oppure una chiave Ed25519 (RFC 8463) come byte grezzi codificati in base64; il tipo viene rilevato dalla chiave stessa. La firma Ed25519 richiede l'estensione sodium.

Nel parametro oversignHeaders potete elencare gli header da proteggere contro l'aggiunta di una seconda copia al messaggio già firmato, un trucco usato dalle email contraffatte; il candidato consueto è From.

Configurazione

Panoramica delle opzioni di configurazione di Nette Mail. Se non usate tutto il framework, ma solo questa libreria, leggete come caricare la configurazione.

Per impostazione predefinita, per inviare le email si usa Nette\Mail\SendmailMailer, che non richiede altra configurazione. Possiamo però passare a Nette\Mail\SmtpMailer:

mail:
	# usa SmtpMailer
	smtp: true       # (bool) predefinito false

	host: ...        # (string) hostname del server SMTP
	port: ...        # (int) porta del server SMTP
	username: ...    # (string) nome utente per l'autenticazione SMTP
	password: ...    # (string) password per l'autenticazione SMTP
	timeout: ...     # (int) timeout della connessione SMTP
	encryption: ...  # (ssl|tls|null) predefinito null (alias 'secure')
	clientHost: ...  # (string) hostname del client, predefinito $_SERVER['HTTP_HOST'] oppure 'localhost'
	persistent: ...  # (bool) usa una connessione persistente, predefinito false

	# opzioni del contesto stream per la connessione SMTP, predefinito stream_context_get_default()
	context:
		ssl:         # tutte le opzioni su https://www.php.net/manual/en/context.ssl.php
			allow_self_signed: ...
			...
		http:        # elenco delle opzioni su https://www.php.net/manual/en/context.http.php
			header: ...
			...

Potete disattivare la verifica del certificato SSL con l'opzione context › ssl › verify_peer: false. Sconsigliamo vivamente di farlo, perché rende l'applicazione vulnerabile. Piuttosto, aggiungete i certificati al trust store.

Per aumentare l'affidabilità possiamo firmare le email con la tecnologia DKIM:

mail:
	dkim:
		domain: myweb.com                  # il vostro dominio
		selector: lovenette                # selettore DKIM
		privateKey: %appDir%/cert/dkim.key # percorso del file della vostra chiave privata
		passPhrase: ...                    # passphrase della chiave privata, se serve

Le opzioni per reindirizzare tutte le email e per attivare il pannello di debug sono descritte nella sezione Debug delle email:

mail:
	# reindirizza tutte le email a un unico indirizzo
	redirect: dev@example.com

	# attiva (true) o disattiva (false) il pannello di Tracy e l'intercettazione delle email
	debugger: ...    # (bool) predefinito null, cioè auto in modalità debug

Servizi DI

Al container DI vengono aggiunti questi servizi:

Nome Tipo Descrizione
mail.mailer Nette\Mail\Mailer classe per inviare le email
mail.signer Nette\Mail\Signer firma DKIM

Se state aggiornando a una versione più recente, guardate la pagina aggiornamento.

versione: 4.x