Source of file MailerService.php

Size: 27,238 Bytes - Last Modified: 2023-11-16T22:56:03+01:00

/home/websites/teicee/packagist/site/phpdoc/conf/../vendor/teicee/mail-bundle/src/Service/MailerService.php

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
<?php
namespace TIC\MailBundle\Service;

use Doctrine\ORM\EntityManagerInterface;
#use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\HttpFoundation\RequestStack;

use Twig\Loader\ArrayLoader;

use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;

use TIC\MailBundle\Entity\Template;
use TIC\MailBundle\Entity\Maillog;

/**
 * Service d'envoi de notifications par email.
 * Utilisation du transport mailer Symfony et Twig d'après les modèles définis dans le bundle.
 */
class MailerService
{
	protected $em;
#	protected $mailer;
	protected $transport;
	protected $router;
	protected $translator;
	protected $requestStack;
	protected $config;

	protected $locales;
	protected $locale_orig;
	public    $locale;
	public    $twig;
	protected $lastError;  // conserve le dernier message d'erreur
	protected $lastEvent;  // conserve l'id de la dernière entrée du journal

	const ConfKeys = ['maillog','smsdomain','fromdomains','fromname','fromaddr','return','admins','locales','formats'];

	public function __construct(
		EntityManagerInterface  $em,
#		MailerInterface         $mailer,
		TransportInterface      $transport,
		RouterInterface         $router,
		TranslatorInterface     $translator,
		RequestStack            $requestStack,
		array $config)
	{
		$this->em             = $em;
#		$this->mailer         = $mailer;
		$this->transport      = $transport;
		$this->router         = $router;
		$this->translator     = $translator;
		$this->requestStack   = $requestStack;
		$this->config         = \array_merge(\array_fill_keys(self::ConfKeys, null), $config);
		$this->initLocale();  // détection et backup de la locale en cours
	}

	/**
	 * Initialise la locale pour la génération des messages (auto ou forcé).
	 *
	 * @param   string  $locale     Spécifie la locale à utiliser (optionnel)
	 * @return  string              Retourne la valeur de la locale configurée
	 */
	public function initLocale(string $locale = null): ?string
	{
		// récupération de la locale en cours par défaut
		if ($locale === null) {
			$request = $this->requestStack->getCurrentRequest();
			if (\is_object($request)) $locale = $request->getLocale();
#			if ($locale === null) $locale = $this->container->getParameter('kernel.default_locale');
			if ($this->locale_orig === null) $this->locale_orig = $locale;
		}
		
		// màj de la locale pour les filtres I18n (trans...)
		if ($locale !== null) $this->translator->setLocale($locale);
		
		return $this->locale = $locale;
	}

	/**
	 * Réinitialise l'environnement avec la locale d'origine (auto ou forcé).
	 *
	 * @param   string  $locale     Spécifie la locale à restaurer (optionnel)
	 */
	public function restoreLocale(string $locale = null): void
	{
		if ($locale === null) $locale = $this->locale_orig;
		if ($locale !== null) {
			// màj de la locale pour les filtres Intl (localizeddate...)
			\Symfony\Component\Intl\Locale::setDefault($locale);
			// màj de la locale pour les filtres I18n (trans...)
			$this->translator->setLocale($locale);
		}
	}

	/**
	 * Définition de l'environnement Twig (contenus des templates & ajout des extensions utiles).
	 * @TODO transformations/adaptations dans les contenus des templates ?
	 *
	 * @param   array   $contents   Contenus des vues twig pour 'subject', 'bodyText', 'bodyHtml' et 'bodySms'
	 */
	protected function initTwig(array $contents): void
	{
		// vérification que les 4 vues twig attendues existent (vide par défaut)
		foreach (array('subject', 'bodyText', 'bodyHtml', 'bodySms') as $view) {
			if (! isset($contents[$view])) $contents[$view] = '';
		}
		
		// création de l'environnement twig avec les contenus
		$this->twig = new \Twig\Environment(new ArrayLoader($contents));
		
		// ajout des extensions twig utilisables
		$this->twig->addExtension(new \Twig\Extra\Intl\IntlExtension());
#		$this->twig->addExtension(new \Twig\Extensions\Extension\I18n());
#		$this->twig->addExtension(new \Twig\Extensions\Extension\Text());
		
		$this->twig->addExtension(new \Symfony\Bridge\Twig\Extension\TranslationExtension($this->translator));
		$this->twig->addExtension(new \Symfony\Bridge\Twig\Extension\RoutingExtension($this->router->getGenerator()));
#		$this->twig->addExtension(new \Symfony\Bridge\Twig\Extension\AssetExtension($this->container->get('assets.packages')));
#		$this->twig->addExtension(new \Symfony\Bridge\Twig\Extension\HttpFoundationExtension($this->requestStack));
		
		// TIC Core extensions
		$this->twig->addExtension(new \TIC\TwigBundle\Extension\DatetimeExtension());
		$this->twig->addExtension(new \TIC\TwigBundle\Extension\FormatExtension());
#		$this->twig->addExtension(new \TIC\TwigBundle\Extension\RouterExtension());
		
		// màj de la locale pour les filtres Intl (localizeddate...)
		\Symfony\Component\Intl\Locale::setDefault($this->locale);
	}

	/**
	 * Préparation d'un message à envoyer (objet Symfony Email & environnement Twig).
	 *
	 * @param   string          $ref        Référence du modèle de notification (cf tic_mail.templates dans services.yaml)
	 * @param   string|array    $to         Adresses mail des destinataires (chaine avec virgule en séparateur acceptée)
	 * @param   string|array    $pjs        Pièces-jointes (chemin ou structure {'name':, 'type':, 'data':}, seul ou en liste)
	 * @param   string          $locale     Spécifie la locale à utiliser (optionnel)
	 * @param   boolean         $sms        Indique s'il s'agit d'un envoi par SMS plutôt que par SMTP
	 * @return  Email           $message    Instance du message (Symfony Email) initialisée selon le modèle de notification
	 *                                      (ou null si aucun modèle de notification correspondant actif ou exception)
	 */
	public function prepare(string $ref, mixed $to = array(), mixed $pjs = array(), string $locale = null, bool $sms = false): ?Email
	{
		$this->lastError = null;
		$this->lastEvent = null;
		$message = null;
		try {
			// nouvelle instance de message
			$message = new Email();
			$message->getHeaders()->addTextHeader('X-TIC-Template', $ref);
			
			// recherche du modèle de notification
			$template = $this->em->getRepository(Template::class)->findOneByRef($ref);
			if (! \is_object($template)) return null;
			if (! $template->getEnabled()) return null;
			
			// initialisation de l'environnement twig
			if ($locale !== null) $this->initLocale($locale);
			$this->initTwig($template->getContents($this->locale));
			
			// nom et adresse de l'expéditeur (bdd sinon parameters)
			$sender = $template->getSender();
			if (! empty($sender) && \preg_match('/^("?([^"]+)"?\s+)?<?\s*([^\s<@]+@[^@>\s]+)\s*>?$/', $sender, $match)) {
				$from_name = isset($match[1]) ? $match[2] : null;
				$from_addr = $match[3];
			} else {
				$from_name = $this->config['fromname'];
				$from_addr = $this->config['fromaddr'];
			}
			$message->from(($from_name === null) ? $from_addr : new Address($from_addr, $from_name));
			
			// spécification d'une adresse de retour
			$return = $template->getReturn();
			if (empty($return)) $return = $this->config['return'];
			if (! empty($return)) $message->returnPath($return);
			
			// ajout des destinataires
			$this->addDest($message, $to, false, $sms);
			
			// ajout des destinataires en copie cachée (sauf envoi sms)
			if (empty($sms)) {
				// copie aux administrateurs ?
				if ($template->getBccAdmins()) $this->addDest($message, $this->config['admins'], true);
				// copies supplémentaires ?
				$this->addDest($message, $template->getBccMore(), true);
			}
			
			// attachement de l'éventuelle pièce jointe
			if (empty($sms)) {
				if (! \is_array($pjs) || isset($pjs['data'])) $pjs = array($pjs);
				foreach ($pjs as $pj) $this->attach($message, $pj);
			}
			
			return $message;
		}
		catch (\Exception $e) {
			$this->logger($ref, $message, -3, $e->getMessage(), $sms);
			return null;
		}
	}

	/**
	 * Indique les informations de l'expéditeur sur un message.
	 * (adresse du "From:" et "Return-Path:" préservée si le domaine du $sender n'est pas dans $config['fromdomains'])
	 *
	 * @param   Email           $message    Instance du message (Symfony Email) auquel ajouter les destinataires
	 * @param   string          $sender     Adresse de l'expéditeur à utiliser (peut contenir une partie "nom" devant)
	 */
	public function setSender(Email &$message, string $sender): void
	{
		if (! \preg_match('/^("?([^"]+)"?\s+)?<?\s*([^\s<@]+@[^@>\s]+)\s*>?$/', $sender, $match)) return;
		$from_name = isset($match[1]) ? $match[2] : "";
		$from_addr = $match[3];
		list($from_user, $from_host) = \explode('@', $from_addr . '@');
		
		// domaine connu : utilisation totale de l'expéditeur en remplacement (entêtes "From:" et "Return-Path:")
		if (\is_array($this->config['fromdomains']) && \in_array(\strtolower($from_host), $this->config['fromdomains'])) {
			$message->from(new Address($from_addr, $from_name));
			$message->returnPath($from_addr);
		}
		// domaine autre : remplace uniquement la partie "nom" du "From:" (mais pas l'adresse) et ajout sur "Reply-to:"
		else {
			$message->replyTo(new Address($from_addr, $from_name));
			$from_orig = \current($message->getFrom());
			if ($from_orig) $message->from(new Address($from_orig->getAddress(), empty($from_name) ? $from_user : $from_name));
		}
	}

	/**
	 * Ajout de destinataires sur un message.
	 * @see https://symfony.com/doc/current/mailer.html#email-addresses
	 *
	 * @param   Email           $message    Instance du message (Symfony Email) auquel ajouter les destinataires
	 * @param   array|string    $dest       Liste d'adresses email (si chaine, virgule en séparateur)
	 * @param   boolean         $bcc        Ajout des destinataires en copie cachée (Bcc, sinon To)
	 * @param   boolean         $sms        Destinataires pour SMS (numéros de téléphone avec nom de domaine spécial)
	 */
	public function addDest(Email &$message, mixed $dest, bool $bcc = false, bool $sms = false): void
	{
		if ($dest === null) return;
		if (! \is_array($dest)) $dest = \explode(',', $dest);
		
		if ($sms) $smsdomain = $this->config['smsdomain'];
		
		foreach ($dest as $addr) {
			$addr = \trim($addr);
			if ($sms) $addr = \preg_replace('/[^\d\w\.\-@]/', '', \preg_replace('/^\+/', 'p', $addr));
			if ($addr === '') continue;
			if (isset($smsdomain) && (\strpos($addr, '@') === FALSE)) $addr.= '@' . $smsdomain;
			
			if ($bcc)
				$message->addBcc($addr);
			else
				$message->addTo($addr);
		}
	}

	/**
	 * Ajout d'un fichier en pièce-jointe d'un message.
	 * @see https://symfony.com/doc/current/mailer.html#file-attachments
	 *
	 * @param   Email           $message    Instance du message (Symfony Email) à compléter
	 * @param   array|string    $pj         Pièce-jointe (structure {'name':, 'type':, 'data':} ou simple chemin)
	 */
	public function attach(Email &$message, mixed $pj): void
	{
		if (\is_array($pj)) {
			$message->attach( $pj['data'], $pj['name'], $pj['type'] );
		} elseif (! empty($pj)) {
			$message->attachFromPath($pj, null, 'application/octet-stream');
		}
	}

	/**
	 * Détection des images intégrées en base64 dans du contenu HTML pour extraction et conversion en attachements).
	 * @see https://symfony.com/doc/current/mailer.html#embedding-images
	 *
	 * @param   Email           $message    Instance du message (Symfony Email) à compléter
	 * @param   string          $html       Contenu HTML à analyser pour traiter les images à extraire et attacher
	 * @return  string                      Contenu HTML avec les tag <img> modifiés (data base 64 => cid interne)
	 */
	public function embedBase64HtmlImages(Email &$message, string $html): string
	{
		if (empty($html)) return '';
		$cache_img = array();
		return preg_replace_callback(
			'|<img([^>]*) src="data:image/(\w+);base64,([a-zA-Z0-9/+]+=*)"|i',
			function($matches) use ($message, &$cache_img){
				$sign = \md5($matches[3]);
				if (! \array_key_exists($sign, $cache_img)) {
					$type = $matches[2];
					$name = \sprintf('internal_img_%03d.%s', \count($cache_img) + 1, $type);
					$message->embed(\base64_decode($matches[3]), $name, 'image/'.$type);
					$cache_img[$sign] = $name;
				}
				return \sprintf('<img%s src="%s"', $matches[1], $cache_img[$sign]);
			},
			$html
		);
	}

	/**
	 * Composition du contenu textuel d'un message (rendu des vues Twig).
	 * @see https://symfony.com/doc/current/mailer.html#message-contents
	 *
	 * @param   Email           $message    Instance du message (Symfony Email) à compléter
	 * @param   array           $data       Liste clé/valeur pour les substitutions des templates
	 * @param   string          $ref        Référence du modèle de notification (pour info dans les logs ; false pour désactiver les logs)
	 * @return  boolean                     Vrai en cas de succès, faux si une exception a été attrapée
	 */
	public function compose(Email &$message, array $data = array(), string $ref = null): bool
	{
		$this->lastError = null;
		$this->lastEvent = null;
		try {
			// génération du sujet
			$subject  = $this->twig->render('subject', $data);
			$subject  = \html_entity_decode($subject, ENT_QUOTES, 'UTF-8');
			$message->subject($subject);
			
			// liste des formats autorisés par la configuration (HTML et TEXT par défaut)
			$formats = $this->config['formats'];
			if (empty($formats)) $formats = array('HTML', 'TEXT');
			
			// génération du contenu text/plain
			$bodyText = \in_array('TEXT', $formats) ? $this->twig->render('bodyText', $data) : '';
			$bodyText = \html_entity_decode($bodyText, ENT_QUOTES, 'UTF-8');
			
			// génération du contenu text/html (avec conversion des tags img base64 en attachements)
			$bodyHtml = \in_array('HTML', $formats) ? $this->twig->render('bodyHtml', $data) : '';
			$bodyHtml = $this->embedBase64HtmlImages($message, $bodyHtml);
			
			// intégration du ou des contenus multipart
			if (empty($bodyHtml)) {
				$message->text($bodyText);
			} else {
				$message->html($bodyHtml);
				if (! empty($bodyText)) $message->text($bodyText);
			}
			return true;
		}
		catch (\Exception $e) {
			$this->logger($ref, $message, -2, $e->getMessage(), false);
			return false;
		}
	}

	/**
	 * Composition du contenu SMS d'un message (rendu des vues Twig).
	 *
	 * @param   Email           $message    Instance du message (Symfony Email) à compléter
	 * @param   array           $data       Liste clé/valeur pour les substitutions des templates
	 * @param   string          $ref        Référence du modèle de notification (pour info dans les logs ; false pour désactiver les logs)
	 * @return  boolean                     Vrai en cas de succès, faux si une exception a été attrapée
	 */
	public function composeSMS(Email &$message, array $data = array(), string $ref = null): bool
	{
		$this->lastError = null;
		$this->lastEvent = null;
		try {
			// vérification que le format SMS est autorisé par la configuration
			$formats = $this->config['formats'];
			if (! \in_array('SMS', $formats)) throw new \Exception("SMS format not available!");
			
			// génération du sujet
			$subject = $this->twig->render('subject', $data);
			$subject = \html_entity_decode($subject, ENT_QUOTES, 'UTF-8');
			$message->subject($subject);
			
			// génération du texte
			$bodySms = $this->twig->render('bodySms', $data);
			$bodySms = \html_entity_decode($bodySms, ENT_QUOTES, 'UTF-8');
			$message->text($bodySms);
			
			return true;
		}
		catch (\Exception $e) {
			$this->logger($ref, $message, -2, $e->getMessage(), true);
			return false;
		}
	}

	/**
	 * Envoi un message préparé.
	 * @see https://symfony.com/doc/current/mailer.html#debugging-emails
	 *
	 * @param   Email           $message    Instance du message (Symfony Email) à envoyer
	 * @param   mixed           $ref        Référence du modèle de notification (pour info dans les logs)
	 *                                      Null (défaut) pour la récupérer automatiquement d'après l'entête X-TIC-Template
	 *                                      False pour désactiver les logs en cas de succès (mais toujours sur Exception)
	 * @param   boolean         $sms        Indique s'il s'agit d'un envoi par SMS plutôt que par SMTP
	 * @return  integer                     Nombre de destinataires ou -1 si exception du TransportMailer
	 */
	public function send(Email &$message, mixed $ref = null, bool $sms = false): int
	{
		$this->lastError = null;
		$this->lastEvent = null;
		try {
#			$this->mailer->send($message);
			$sentMessage = $this->transport->send($message);
#			dump($sentMessage->getDebug());
			$message->messageId = $sentMessage->getMessageId();
			$rc = \count($sentMessage->getEnvelope()->getRecipients());
			if ($ref !== false) $this->logger($ref, $message, $rc, null, $sms);
		} catch (TransportExceptionInterface $e) {
#			dump($e->getDebug());
			$rc = -1;
			$this->logger($ref, $message, $rc, $e->getMessage(), $sms);
		} catch (\Exception $e) {
			$rc = -1;
			$this->logger($ref, $message, $rc, $e->getMessage(), $sms);
		}
		return $rc;
	}

	/**
	 * Préparation, composition puis envoi d'un message PLAIN et/ou HTML (méthode "combo").
	 *
	 * @param   string          $ref        Référence du modèle de notification (cf tic_mail.templates dans services.yaml)
	 * @param   string|array    $to         Adresses mail des destinataires (chaine avec virgule en séparateur acceptée)
	 * @param   array           $data       Liste clé/valeur pour les substitutions des templates (peut contenir 'locale')
	 * @param   string|array    $pjs        Pièces-jointes (chemin ou structure {'name':, 'type':, 'data':}, seul ou en liste)
	 * @param   boolean         $return     Si vrai retournera l'objet Symfony Email au lieu de tenter de l'envoyer
	 * @param   String          $sender     Adresse d'expéditeur (From, Reply-to ou Return-path selon $config['fromdomains'])
	 * @return  mixed                       Nombre de destinaires du message envoyé (peut être 0) ou objet Email généré
	 *                                      Null    sur échec aux étapes "prepare" ou "compose"
	 *                                      False   sur échec à l'envoi par le Transport Mailer
	 */
	public function notify(string $ref, mixed $to = array(), array $data = array(), mixed $pjs = array(), bool $return = false, string $sender = null): mixed
	{
		$locale = \array_key_exists('locale', $data) ? $data['locale'] : null;
		
		// préparation...
		$message = $this->prepare($ref, $to, $pjs, $locale);
		if ($message === null) return null;
		
		// forcer les informations de l'expéditeur ?
		if ($sender !== null) $this->setSender($message, $sender);
		
		// composition...
		$rc = $this->compose($message, $data, $ref);
		if (! $rc) return null;
		
		$this->restoreLocale();
		
		// retour ou envoi...
		if ($return) return $message;
		$rc = $this->send($message, $ref);
		return ($rc < 0) ? false : $rc;
	}

	/**
	 * Préparation, composition puis envoi d'un message SMS (méthode "combo").
	 *
	 * @param   string          $ref        Référence du modèle de notification (cf tic_mail.templates dans services.yaml)
	 * @param   string|array    $to         Adresses mail des destinataires (chaine avec virgule en séparateur acceptée)
	 * @param   array           $data       Liste clé/valeur pour les substitutions des templates (peut contenir 'locale')
	 * @param   boolean         $return     Si vrai retournera l'objet Symfony Email au lieu de tenter de l'envoyer
	 * @return  mixed                       Nombre de destinaires du message envoyé (peut être 0) ou objet Email généré
	 *                                      Null    sur échec aux étapes "prepare" ou "compose"
	 *                                      False   sur échec à l'envoi par le Transport Mailer
	 */
	public function notifySMS(string $ref, mixed $to = array(), array $data = array(), bool $return = false): mixed
	{
		$locale = \array_key_exists('locale', $data) ? $data['locale'] : null;
		
		// préparation...
		$message = $this->prepare($ref, $to, array(), $locale, true);
		if ($message === null) return null;
		
		// composition...
		$rc = $this->composeSMS($message, $data, $ref);
		if (! $rc) return null;
		
		$this->restoreLocale();
		
		// retour ou envoi...
		if ($return) return $message;
		$rc = $this->send($message, $ref, true);
		return ($rc < 0) ? false : $rc;
	}

	/**
	 * Préparation, composition puis envoi de messages pour chaque destinataire (méthode "combo").
	 *
	 * @param   string          $ref        Référence du modèle de notification (cf tic_mail.templates dans services.yaml)
	 * @param   string|array    $to         Adresses mail des destinataires (chaine avec virgule en séparateur acceptée)
	 * @param   array           $data       Liste clé/valeur pour les substitutions des templates
	 * @param   string|array    $pjs        Pièces-jointes (chemin ou structure {'name':, 'type':, 'data':}, seul ou en liste)
	 * @return  integer                     Nombre de messages envoyés (succès sur destinataire)
	 *                                      Null sur échec aux étapes "prepare" ou "compose"
	 */
	public function batch(string $ref, mixed $to, array $data = array(), mixed $pjs = array()): ?int
	{
		$locale = \array_key_exists('locale', $data) ? $data['locale'] : null;
		
		// préparation...
		$message = $this->prepare($ref, null, $pjs, $locale);
		if ($message === null) return null;
		
		// composition...
		$rc = $this->compose($message, $data, $ref);
		if (! $rc) return null;
		
		$this->restoreLocale();
		
		// envois...
		$counter = 0;
		if (! \is_array($to)) $to = \explode(',', $to);
		foreach ($to as $addr) {
			$addr = \trim($addr);
			if (empty($addr)) continue;
			$message->to($addr);
			
			$rc = $this->send($message, $ref);
			if ($rc >= 0) $counter++;
		}
		return $counter;
	}

	/**
	 * Enregistrement dans le journal des envois (envoyé avec succès ou échec d'une étape).
	 *
	 * @param   string          $ref        Référence du modèle de notification (par défaut récupéré depuis les entêtes)
	 * @param   Email           $message    Instance du message traité (Symfony Email)
	 * @param   integer         $rc         Nombre de destinataires si succès, code d'erreur négatif sur exception
	 * @param   string          $error      Message de l'exception en cas d'erreur
	 * @param   boolean         $sms        Indique s'il s'agit d'un envoi par SMS plutôt que par SMTP
	 * @return  integer                     Identifiant de l'objet Maillog généré
	 */
	public function logger(?string $ref, Email $message = null, int $rc = null, string $error = null, bool $sms = false): ?int
	{
		$this->lastError = $error;
		$this->lastEvent = 0;
		try {
			if (! $this->config['maillog']) return null;
			
			if ($message instanceof Email) {
				// récupération de la ref du template dans les headers
				if ($ref === null) {
					$header = $message->getHeaders()->getHeaderBody('X-TIC-Template');
					if ($header) $ref = $header->getValue();
				}
				// récupération du contenu et de son type (plain|html|sms)
				if ($sms) {
					$contentBody = $message->getTextBody();
					$contentType = 'text/sms';
				} else {
					$contentBody = $message->getHtmlBody();
					$contentType = 'text/html';
					if (empty($contentBody)) {
						$contentBody = $message->getTextBody();
						$contentType = 'text/plain';
					}
				}
				$return = $message->getReturnPath();
				
				$this->lastEvent = $this->em->getRepository(Maillog::class)->createEventLog(array(
					'template'   => $ref,
					'locale'     => $this->locale,
					'returnPath' => isset($return) ? $return->getAddress() : null,
					'mailFrom'   => \array_map(function($a){ return $a->getAddress(); }, $message->getFrom()),
					'mailRcpt'   => \array_map(function($a){ return $a->getAddress(); }, $message->getTo()),
					'mailBcc'    => \array_map(function($a){ return $a->getAddress(); }, $message->getBcc()),
					'subject'    => $message->getSubject(),
					'body'       => $contentBody,
					'contentType'=> $contentType,
					'messageId'  => isset($message->messageId) ? $message->messageId : null,
					'sendCode'   => $rc,
					'errorMsg'   => $error,
				));
			} else {
				$this->lastEvent = $this->em->getRepository(Maillog::class)->createEventLog(array(
					'template'   => $ref,
					'locale'     => $this->locale,
					'contentType'=> ($sms) ? 'text/sms' : '',
					'sendCode'   => $rc,
					'errorMsg'   => $error,
				));
			}
			return $this->lastEvent;
		} catch (\Exception $e) {
			\printf("MAILER LOGGER FAILED: %s\n", $e->getMessage());
			return null;
		}
	}

	/**
	 * Retourne le message de la dernière erreur rencontrée (exception interceptée).
	 *
	 * @return  string
	 */
	public function getLastError()
	{
		return $this->lastError;
	}

	/**
	 * Retourne l'entrée du journal correspondant au dernier envoi (uniquement son id par défaut).
	 *
	 * @param   boolean             Vrai pour obtenir l'entité complète ; Faux pour avoir une référence Doctrine.
	 * @return  integer|Maillog     Clé primaire du Maillog, référence du Maillog ou objet Maillog
	 */
	public function getLastEvent(bool $retrieve_entity = null): mixed
	{
		if (empty($this->lastEvent)) return null;
		if ($retrieve_entity === null) return $this->lastEvent;
		if (! $retrieve_entity) return $this->em->getReference(Maillog::class, $this->lastEvent);
		return $this->em->getRepository(Maillog::class)->find($this->lastEvent);
	}

	/**
	 * Ajout dans une entité donnée de l'entrée du journal du dernier envoi (via méthode addMaillog ou addNotification).
	 */
	public function logLastEvent(object &$entity): MailerService
	{
		$event = $this->getLastEvent(false);
		if ($event !== null) {
			foreach (array('addMaillog', 'addNotification') as $method) {
				if (! \method_exists($entity, $method)) continue;
				\call_user_func(array($entity, $method), $event);
				break;
			}
		}
		return $this;
	}

	/**
	 * Retourne la liste des langues définies (gestion des messages multilingues).
	 * Note: les locales possibles sont utilisées dans l'admin, mais pas à l'envoi.
	 *
	 * @return  array   Liste des locales possibles
	 */
	public function getLocales(): array
	{
		if ($this->locales !== null) return $this->locales;
		
		// recherche dans les paramètres du bundle
		$this->locales = $this->config['locales'];
		if ($this->locales !== null) return $this->locales;
		
		// recherche dans les attributs de la requête
		$request = $this->requestStack->getCurrentRequest();
		if ($request) $this->locales = $request->attributes->get('locales');
		if ($this->locales !== null) return $this->locales;
		
		// recherche dans les données de la session
#		$session = $this->container->get('session');
#		if ($session) $this->locales = $session->get('locales');
#		if ($this->locales !== null) return $this->locales;
		
		// liste avec la locale par défaut en dernier recours
		$this->locales = array();
		return $this->locales;
	}

}