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 perssl, 587 pertls, altrimenti 25timeout– timeout della connessione SMTPpersistent– usa una connessione persistenteclientHost– indica l'header host del clientstreamOptions– 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.