adminEmail; } public function getAdminFrom(): string { return 'Association E-Cosplay <'.$this->adminEmail.'>'; } public function send(Email $email): void { $publicKeyPath = $this->projectDir.'/key.asc'; if (file_exists($publicKeyPath)) { $email->attachFromPath($publicKeyPath, 'public_key.asc', 'application/pgp-keys'); } $certificate = $this->projectDir.'/config/cert/smime/certificate.pem'; $privateKey = $this->projectDir.'/config/cert/smime/private-key.pem'; // @codeCoverageIgnoreStart if (file_exists($certificate) && file_exists($privateKey)) { $signer = new SMimeSigner($certificate, $privateKey, $this->smimePassphrase); $email = $signer->sign($email); } // @codeCoverageIgnoreEnd $this->bus->dispatch(new SendEmailMessage($email)); } /** * @param array|null $attachments */ public function sendEmail(string $to, string $subject, string $content, ?string $from = null, ?string $replyTo = null, bool $withUnsubscribe = true, ?array $attachments = null, int $priority = 3): void { $from ??= $this->getAdminFrom(); $canUnsubscribe = $withUnsubscribe && !$this->isWhitelisted($to); if ($canUnsubscribe && $this->unsubscribeManager->isUnsubscribed($to)) { return; } $email = (new Email()) ->from($from) ->to($to) ->subject($subject) ->html($content) ->priority($priority); if ($replyTo) { $email->replyTo($replyTo); } if ($attachments) { $processedAttachments = []; foreach ($attachments as $attachment) { $name = $attachment['name'] ?? basename($attachment['path']); $email->attachFromPath($attachment['path'], $name); $processedAttachments[] = [ 'path' => $attachment['path'], 'name' => $name, ]; } $attachments = $processedAttachments; } $messageId = bin2hex(random_bytes(16)); $email->getHeaders()->addIdHeader('Message-ID', $messageId.'@e-cosplay.fr'); $trackingUrl = $this->urlGenerator->generate('app_email_track', [ 'messageId' => $messageId, ], UrlGeneratorInterface::ABSOLUTE_URL); $viewUrl = $this->urlGenerator->generate('app_email_view', [ 'messageId' => $messageId, ], UrlGeneratorInterface::ABSOLUTE_URL); $html = $email->getHtmlBody(); $html = str_replace('https://crm.e-cosplay.fr/logo.jpg', $trackingUrl, $html); $html = str_replace('__VIEW_URL__', $viewUrl, $html); $dnsReportUrl = $this->urlGenerator->generate('app_dns_report', [ 'token' => $messageId, ], UrlGeneratorInterface::ABSOLUTE_URL); $html = str_replace('__DNS_REPORT_URL__', $dnsReportUrl, $html); // Injection du bloc liste des pieces jointes (hors .asc, .p7z, smime) if ($attachments) { $html = $this->injectAttachmentsList($html, $attachments); } $email->html($html); $tracking = new EmailTracking($messageId, $to, $subject, $html, $attachments); $this->em->persist($tracking); $this->em->flush(); // Ajout automatique du fichier VCF (fiche contact E-Cosplay) $vcfPath = $this->generateVcf(); if (null !== $vcfPath) { $email->attachFromPath($vcfPath, 'Association-E-Cosplay.vcf', 'text/vcard'); } if ($canUnsubscribe) { $this->addUnsubscribeHeaders($email, $to); } $this->send($email); // Nettoyage du fichier VCF temporaire if (null !== $vcfPath) { @unlink($vcfPath); } } private function isWhitelisted(string $email): bool { return strtolower(trim($email)) === strtolower($this->adminEmail); } /** * Injecte un bloc HTML listant les pieces jointes dans le corps du mail, * juste avant le footer dark (#111827). Exclut .asc, .p7z et smime. * * @param array $attachments */ private function injectAttachmentsList(string $html, array $attachments): string { $excluded = ['.asc', '.p7z']; $filtered = []; foreach ($attachments as $a) { $name = $a['name'] ?? basename($a['path']); $path = $a['path'] ?? ''; $ext = strtolower(pathinfo($name, \PATHINFO_EXTENSION)); if (\in_array('.'.$ext, $excluded, true) || str_contains(strtolower($name), 'smime')) { continue; } $size = file_exists($path) ? filesize($path) : 0; $filtered[] = ['name' => $name, 'size' => $size]; } if ([] === $filtered) { return $html; } $items = ''; foreach ($filtered as $f) { $sizeStr = $this->formatFileSize($f['size']); $items .= '' .'' .'' .'' .'' .'
📎' .'

'.htmlspecialchars($f['name'], \ENT_QUOTES, 'UTF-8').'

' .'

Piece jointe ('.$sizeStr.')

' .'
' .'' .''; } $block = '' .'

Pieces jointes

' .'' .$items .'
' .''; // Injecte avant le footer dark $marker = '= 1048576) { return number_format($bytes / 1048576, 1, ',', ' ').' Mo'; } if ($bytes >= 1024) { return number_format($bytes / 1024, 0, ',', ' ').' Ko'; } return $bytes.' o'; } private function addUnsubscribeHeaders(Email $email, string $to): void { $token = $this->unsubscribeManager->generateToken($to); $unsubscribeUrl = $this->urlGenerator->generate('app_unsubscribe', [ 'email' => $to, 'token' => $token, ], UrlGeneratorInterface::ABSOLUTE_URL); $email->getHeaders()->addTextHeader( 'List-Unsubscribe', sprintf('<%s>, ', $unsubscribeUrl, urlencode($to)) ); $email->getHeaders()->addTextHeader('List-Unsubscribe-Post', 'List-Unsubscribe=One-Click'); } }