add SMTP sender

This commit is contained in:
2026-05-27 07:29:29 +02:00
parent 982d754857
commit b0865368c8
19 changed files with 1384 additions and 35 deletions
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\EmailSetting;
use App\Models\SenderAccount;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
@@ -13,7 +14,8 @@ class EmailSettingsController extends Controller
{
$this->authorizeWrite('settings');
$settings = EmailSetting::first() ?? new EmailSetting();
return view('admin.email-settings.index', compact('settings'));
$senderAccounts = SenderAccount::orderBy('email_address')->get();
return view('admin.email-settings.index', compact('settings', 'senderAccounts'));
}
public function save(Request $request)
@@ -183,4 +185,91 @@ class EmailSettingsController extends Controller
return back()->with('error', 'Errore sync: ' . $e->getMessage());
}
}
public function senderStore(Request $request)
{
$this->authorizeWrite('settings');
$data = $request->validate([
'email_address' => 'required|email|max:255|unique:sender_accounts,email_address',
'email_name' => 'nullable|string|max:255',
'smtp_host' => 'nullable|string|max:255',
'smtp_port' => 'nullable|integer|min:1|max:65535',
'smtp_encryption' => 'nullable|in:tls,ssl,none',
'smtp_username' => 'nullable|string|max:255',
'smtp_password' => 'nullable|string',
'reply_to' => 'nullable|email|max:255',
'verify_email' => 'nullable|email|max:255',
'note' => 'nullable|string|max:500',
'is_active' => 'boolean',
]);
if (!empty($data['smtp_password'])) {
$data['smtp_password'] = Crypt::encryptString($data['smtp_password']);
} else {
unset($data['smtp_password']);
}
$sender = SenderAccount::create($data);
return back()->with('success', 'Mittente "' . $sender->email_address . '" creato con successo.');
}
public function senderUpdate(Request $request, int $id)
{
$this->authorizeWrite('settings');
$sender = SenderAccount::findOrFail($id);
$data = $request->validate([
'email_address' => 'required|email|max:255|unique:sender_accounts,email_address,' . $id,
'email_name' => 'nullable|string|max:255',
'smtp_host' => 'nullable|string|max:255',
'smtp_port' => 'nullable|integer|min:1|max:65535',
'smtp_encryption' => 'nullable|in:tls,ssl,none',
'smtp_username' => 'nullable|string|max:255',
'smtp_password' => 'nullable|string',
'reply_to' => 'nullable|email|max:255',
'verify_email' => 'nullable|email|max:255',
'note' => 'nullable|string|max:500',
'is_active' => 'boolean',
]);
if (!empty($data['smtp_password'])) {
$data['smtp_password'] = Crypt::encryptString($data['smtp_password']);
} else {
unset($data['smtp_password']);
}
$sender->update($data);
return back()->with('success', 'Mittente "' . $sender->email_address . '" aggiornato.');
}
public function senderDestroy(int $id)
{
$this->authorizeDelete('settings');
$sender = SenderAccount::findOrFail($id);
$sender->delete();
return back()->with('success', 'Mittente eliminato.');
}
public function senderTestSmtp(Request $request, int $id)
{
$this->authorizeWrite('settings');
$request->validate(['email' => 'required|email']);
$sender = SenderAccount::findOrFail($id);
try {
$sender->sendEmail(
$request->email,
'Test Configurazione SMTP - ' . config('app.name'),
'Test email da ' . config('app.name') . ' - Configurazione SMTP mittente "' . $sender->email_address . '" funziona!'
);
return response()->json(['success' => true, 'message' => 'Email di test inviata a ' . $request->email]);
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => 'Errore invio: ' . $e->getMessage()]);
}
}
}
+107 -21
View File
@@ -6,6 +6,7 @@ use App\Models\EmailFolder;
use App\Models\EmailMessage;
use App\Models\EmailAttachment;
use App\Models\EmailSetting;
use App\Models\SenderAccount;
use App\Models\MailingList;
use App\Models\Gruppo;
use App\Models\Individuo;
@@ -90,6 +91,7 @@ class EmailController extends Controller
$mailingLists = MailingList::where('attiva', true)->get();
$gruppi = Gruppo::orderBy('nome')->get();
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$senderAccounts = SenderAccount::active()->get();
$prefill = [];
if ($request->reply_to) {
@@ -108,7 +110,7 @@ class EmailController extends Controller
}
}
return view('email.compose', compact('folders', 'mailingLists', 'gruppi', 'individui', 'prefill'));
return view('email.compose', compact('folders', 'mailingLists', 'gruppi', 'individui', 'prefill', 'senderAccounts'));
}
public function send(Request $request)
@@ -125,11 +127,6 @@ class EmailController extends Controller
$validated = $request->validate($rules);
$settings = EmailSetting::getActive();
if (!$settings) {
return back()->with('error', 'Account email non configurato.');
}
$recipients = $this->resolveRecipients($request);
\Illuminate\Support\Facades\Log::info('Email send attempt', [
@@ -137,44 +134,94 @@ class EmailController extends Controller
'recipients_count' => count($recipients),
'subject' => $validated['subject'],
'destinatario_tipo' => $request->destinatario_tipo,
'to' => $validated['to'] ?? null,
'individui' => $request->individui,
'gruppo_id' => $request->gruppo_id,
'mailing_list_id' => $request->mailing_list_id,
'mittente_id' => $request->mittente_id,
]);
if (empty($recipients)) {
return back()->with('error', 'Nessun destinatario trovato. Seleziona almeno un destinatario dal menu a tendina o inserisci un indirizzo email manuale.')->withInput();
}
$useSender = $request->filled('mittente_id') && $request->mittente_id > 0;
if ($useSender) {
return $this->sendWithSender($request, $validated, $recipients);
}
$settings = EmailSetting::getActive();
if (!$settings) {
return back()->with('error', 'Account email non configurato.');
}
$imapSuccess = false;
$imapError = null;
$attachments = $this->processAttachments($request);
foreach ($recipients as $recipient) {
try {
$this->sendViaImap($settings, $recipient, $validated['subject'], $validated['body'], $request->cc);
$bodyWithSig = $this->appendSignature($validated['body'], $settings->getSignature(), $settings->signature_enabled ?? false);
$this->sendViaImap($settings, $recipient, $validated['subject'], $bodyWithSig, $request->cc, $attachments);
$imapSuccess = true;
} catch (\Exception $e) {
$imapError = $e->getMessage();
\Illuminate\Support\Facades\Log::error('IMAP send error', ['error' => $e->getMessage()]);
\Illuminate\Support\Facades\Log::error('SMTP send error', ['error' => $e->getMessage()]);
}
}
try {
$this->storeSentMessage($settings, $recipients, $validated['subject'], $validated['body'], $request->cc);
\Illuminate\Support\Facades\Log::info('Stored sent message');
$bodyWithSig = $this->appendSignature($validated['body'], $settings->getSignature(), $settings->signature_enabled ?? false);
$this->storeSentMessage($settings, $recipients, $validated['subject'], $bodyWithSig, $request->cc);
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Store sent message error', ['error' => $e->getMessage()]);
return back()->with('error', 'Errore nel salvataggio: ' . $e->getMessage())->withInput();
}
if (!$imapSuccess && $imapError) {
return redirect()->route('email.index', 'sent')->with('warning', 'Email salvata in Invio ma non inviata via IMAP: ' . $imapError);
return redirect()->route('email.index', 'sent')->with('warning', 'Email salvata in Invio ma non inviata via SMTP: ' . $imapError);
}
return redirect()->route('email.index', 'sent')->with('success', 'Email inviata con successo.');
}
private function sendWithSender(Request $request, array $validated, array $recipients)
{
$sender = SenderAccount::findOrFail($request->mittente_id);
$attachmentPaths = $this->resolveAttachmentPaths($request);
$ok = 0;
$errors = [];
foreach ($recipients as $recipient) {
try {
$sender->sendEmail($recipient, $validated['subject'], $validated['body'], $attachmentPaths);
$ok++;
} catch (\Exception $e) {
$errors[] = $recipient . ': ' . $e->getMessage();
}
}
if ($sender->verify_email) {
$reportSubject = '✅ Invio: ' . $validated['subject'] . ' - ' . $sender->email_address;
$reportBody = "Report invio mittente: {$sender->email_address}\n"
. "Destinatari totali: " . count($recipients) . "\n"
. "Inviate con successo: {$ok}\n"
. "Fallite: " . count($errors) . "\n";
if (!empty($errors)) {
$reportBody .= "\nDettaglio errori:\n" . implode("\n", $errors);
}
$sender->sendReport($reportSubject, $reportBody);
}
if ($ok === 0 && !empty($errors)) {
return redirect()->route('email.index', 'sent')->with('error', 'Invio fallito: ' . implode('; ', $errors));
}
$message = $ok . ' email inviate con successo tramite "' . $sender->email_address . '".';
if (!empty($errors)) {
$message .= ' ' . count($errors) . ' fallite.';
}
return redirect()->route('email.index', 'sent')->with('success', $message);
}
public function storeDraft(Request $request)
{
$this->authorizeWrite('email');
@@ -454,10 +501,8 @@ class EmailController extends Controller
return $body . "\n\n---\n" . $signature;
}
private function sendViaImap($settings, $to, $subject, $body, $cc = null)
private function sendViaImap($settings, $to, $subject, $body, $cc = null, array $attachmentPaths = [])
{
$bodyWithSignature = $this->appendSignature($body, $settings->getSignature(), $settings->signature_enabled ?? false);
\Illuminate\Support\Facades\Log::info('sendViaSMTP start', ['to' => $to, 'subject' => $subject]);
$smtpPassword = $settings->getDecryptedSmtpPassword() ?? $settings->getDecryptedPassword();
@@ -475,8 +520,6 @@ class EmailController extends Controller
$encryption
);
\Illuminate\Support\Facades\Log::info('SMTP DSN', ['dsn' => $dsn]);
try {
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
@@ -498,11 +541,15 @@ class EmailController extends Controller
->from($fromAddress)
->to($to)
->subject($subject)
->text($bodyWithSignature);
->text($body);
if ($cc) {
$email->cc($cc);
}
foreach ($attachmentPaths as $attachmentPath) {
$email->attachFromPath($attachmentPath);
}
$mailer->send($email);
@@ -514,6 +561,45 @@ class EmailController extends Controller
}
}
private function processAttachments(Request $request): array
{
$paths = [];
if ($request->hasFile('allegati')) {
foreach ($request->file('allegati') as $file) {
if ($file->isValid()) {
$paths[] = $file->getRealPath();
}
}
}
return $paths;
}
private function resolveAttachmentPaths(Request $request): array
{
$paths = [];
if ($request->hasFile('allegati')) {
foreach ($request->file('allegati') as $file) {
if ($file->isValid()) {
$paths[] = $file->getRealPath();
}
}
}
$documentIds = $request->input('documenti_selezionati');
if (!empty($documentIds)) {
$ids = is_array($documentIds) ? $documentIds : explode(',', $documentIds);
$docs = Documento::whereIn('id', $ids)->get();
foreach ($docs as $doc) {
if ($doc->file_path && \Illuminate\Support\Facades\Storage::exists($doc->file_path)) {
$paths[] = \Illuminate\Support\Facades\Storage::path($doc->file_path);
}
}
}
return $paths;
}
private function storeSentMessage($settings, $recipients, $subject, $body, $cc = null)
{
$sentFolder = EmailFolder::where('type', 'sent')->first();
+243 -9
View File
@@ -1,12 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\MailingList;
use App\Models\MailingContact;
use App\Models\MailingMessaggio;
use App\Models\SenderAccount;
use App\Models\Individuo;
use App\Models\Documento;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class MailingController extends Controller
@@ -24,6 +29,7 @@ class MailingController extends Controller
$liste = MailingList::all();
$documenti = Documento::orderBy('nome_file')->get();
$senderAccounts = SenderAccount::active()->get();
$documentiSelezionati = collect();
if ($request->has('documenti_selezionati')) {
@@ -31,7 +37,7 @@ class MailingController extends Controller
$documentiSelezionati = Documento::whereIn('id', $docIds)->get();
}
return view('mailing.nuovo', compact('individui', 'liste', 'documenti', 'documentiSelezionati'));
return view('mailing.nuovo', compact('individui', 'liste', 'documenti', 'documentiSelezionati', 'senderAccounts'));
}
public function invia(Request $request)
@@ -44,8 +50,14 @@ class MailingController extends Controller
'destinatari_ids' => 'nullable|string',
'documenti_selezionati' => 'nullable|string',
'allegato' => 'nullable|file|max:10240',
'mittente_id' => 'nullable|integer|exists:sender_accounts,id',
]);
$sender = null;
if (!empty($data['mittente_id'])) {
$sender = SenderAccount::findOrFail($data['mittente_id']);
}
$documentiAllegati = [];
if ($request->hasFile('allegato')) {
@@ -72,13 +84,90 @@ class MailingController extends Controller
$documentiAllegati = array_merge($documentiAllegati, $docIds);
}
$destinatari = [];
$destinatari = collect();
if (!empty($data['destinatari_ids'])) {
$ids = array_filter(array_map('trim', explode(',', $data['destinatari_ids'])));
$destinatari = Individuo::with('contatti')->whereIn('id', $ids)->get();
} elseif (!empty($data['lista_id'])) {
$lista = MailingList::with('contatti.individuo.contatti')->find($data['lista_id']);
if ($lista) {
$destinatari = $lista->contatti->filter(fn($c) => $c->opt_in)->map(fn($c) => $c->individuo)->filter();
}
}
return back()->with('success', 'Messaggio preparato per ' . count($destinatari) . ' destinatari con ' . count($documentiAllegati) . ' allegato(i).');
$emails = $destinatari->flatMap(fn($ind) => $ind->contatti->where('tipo', 'email')->pluck('valore'))
->filter()->unique()->values();
if ($emails->isEmpty()) {
return back()->with('error', 'Nessun destinatario email valido trovato.')->withInput();
}
$attachmentPaths = $this->resolveMailingAttachmentPaths($documentiAllegati);
$messaggio = MailingMessaggio::create([
'mailing_list_id' => $data['lista_id'] ?? null,
'user_id' => auth()->id(),
'oggetto' => $data['oggetto'],
'corpo' => $data['corpo'],
'stato' => 'in_coda',
'totale_destinatari' => $emails->count(),
'mittente_nome' => $sender?->email_name,
'mittente_email' => $sender?->email_address,
]);
$ok = 0;
$fail = 0;
$logFalliti = [];
foreach ($emails as $email) {
try {
if ($sender) {
$sender->sendEmail($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
} else {
$this->sendViaSystem($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
}
$ok++;
} catch (\Throwable $e) {
$fail++;
$logFalliti[] = ['email' => $email, 'errore' => $e->getMessage()];
Log::error('Mailing send failed', ['email' => $email, 'error' => $e->getMessage()]);
}
}
$stato = $fail === 0 ? 'inviato' : ($ok > 0 ? 'parziale' : 'fallito');
$messaggio->update([
'stato' => $stato,
'invii_ok' => $ok,
'invii_falliti' => $fail,
'log_falliti' => $logFalliti,
'inviato_al' => now(),
]);
$senderLabel = $sender ? '"' . $sender->email_address . '"' : 'sistema predefinito';
if ($sender && $sender->verify_email && ($ok > 0 || $stato === 'fallito')) {
$reportSubject = '✅ Report Invio - ' . $data['oggetto'];
$reportBody = "Report invio mailing\n"
. "Mittente: {$sender->email_name} <{$sender->email_address}>\n"
. "Oggetto: {$data['oggetto']}\n"
. "Totale destinatari: {$emails->count()}\n"
. "Inviate con successo: {$ok}\n"
. "Fallite: {$fail}\n";
if (!empty($logFalliti)) {
$reportBody .= "\nDettaglio errori:\n";
foreach ($logFalliti as $f) {
$reportBody .= "- {$f['email']}: {$f['errore']}\n";
}
}
$sender->sendReport($reportSubject, $reportBody);
}
$msg = "Invio completato tramite {$senderLabel}: {$ok} inviate, {$fail} fallite.";
if ($ok > 0) {
return redirect('/mailing-liste')->with('success', $msg);
}
return back()->with('error', $msg);
}
public function invio(Request $request)
@@ -86,8 +175,9 @@ class MailingController extends Controller
$this->authorizeWrite('mailing');
$liste = MailingList::where('attiva', true)->orderBy('nome')->get();
$documenti = Documento::orderBy('nome_file')->get();
$senderAccounts = SenderAccount::active()->get();
return view('mailing.invio', compact('liste', 'documenti'));
return view('mailing.invio', compact('liste', 'documenti', 'senderAccounts'));
}
public function invioElabora(Request $request)
@@ -101,8 +191,14 @@ class MailingController extends Controller
'documenti_selezionati' => 'nullable|array',
'documenti_selezionati.*' => 'exists:documenti,id',
'allegato' => 'nullable|file|max:10240',
'mittente_id' => 'nullable|integer|exists:sender_accounts,id',
]);
$sender = null;
if (!empty($data['mittente_id'])) {
$sender = SenderAccount::findOrFail($data['mittente_id']);
}
$documentiAllegati = [];
if ($request->hasFile('allegato')) {
@@ -133,10 +229,148 @@ class MailingController extends Controller
->with('individuo.contatti')
->get();
$destinatari = $contatti->map(function($c) {
return $c->individuo->contatti->where('tipo', 'email')->first()?->valore;
})->filter()->unique()->values();
$emails = $contatti->map(fn($c) => $c->individuo?->contatti->where('tipo', 'email')->first()?->valore)
->filter()->unique()->values();
return back()->with('success', 'Messaggio pronto per l\'invio a ' . $destinatari->count() . ' destinatari unici da ' . count($data['liste']) . ' lista(e).');
if ($emails->isEmpty()) {
return back()->with('error', 'Nessun destinatario email valido trovato nelle liste selezionate.');
}
$attachmentPaths = $this->resolveMailingAttachmentPaths($documentiAllegati);
$listeNomi = MailingList::whereIn('id', $data['liste'])->pluck('nome')->implode(', ');
$messaggio = MailingMessaggio::create([
'mailing_list_id' => null,
'user_id' => auth()->id(),
'oggetto' => $data['oggetto'],
'corpo' => $data['corpo'],
'stato' => 'in_coda',
'totale_destinatari' => $emails->count(),
'mittente_nome' => $sender?->email_name,
'mittente_email' => $sender?->email_address,
]);
$ok = 0;
$fail = 0;
$logFalliti = [];
foreach ($emails as $email) {
try {
if ($sender) {
$sender->sendEmail($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
} else {
$this->sendViaSystem($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
}
$ok++;
} catch (\Throwable $e) {
$fail++;
$logFalliti[] = ['email' => $email, 'errore' => $e->getMessage()];
Log::error('Mailing bulk send failed', ['email' => $email, 'error' => $e->getMessage()]);
}
}
$stato = $fail === 0 ? 'inviato' : ($ok > 0 ? 'parziale' : 'fallito');
$messaggio->update([
'stato' => $stato,
'invii_ok' => $ok,
'invii_falliti' => $fail,
'log_falliti' => $logFalliti,
'inviato_al' => now(),
]);
$senderLabel = $sender ? '"' . $sender->email_address . '"' : 'sistema predefinito';
if ($sender && $sender->verify_email && ($ok > 0 || $stato === 'fallito')) {
$reportSubject = '✅ Report Invio Massivo - ' . $data['oggetto'];
$reportBody = "Report invio massivo\n"
. "Mittente: {$sender->email_name} <{$sender->email_address}>\n"
. "Liste: {$listeNomi}\n"
. "Oggetto: {$data['oggetto']}\n"
. "Totale destinatari: {$emails->count()}\n"
. "Inviate con successo: {$ok}\n"
. "Fallite: {$fail}\n";
if (!empty($logFalliti)) {
$reportBody .= "\nDettaglio errori:\n";
foreach ($logFalliti as $f) {
$reportBody .= "- {$f['email']}: {$f['errore']}\n";
}
}
$sender->sendReport($reportSubject, $reportBody);
}
$msg = "Invio massivo completato tramite {$senderLabel}: {$ok} inviate, {$fail} fallite.";
if ($ok > 0) {
return redirect('/')->with('success', $msg);
}
return back()->with('error', $msg);
}
}
private function sendViaSystem(string $to, string $subject, string $body, array $attachmentPaths = []): void
{
$settings = \App\Models\EmailSetting::getActive();
if (!$settings) {
throw new \Exception('Account email principale non configurato.');
}
$smtpPassword = $settings->getDecryptedSmtpPassword() ?? $settings->getDecryptedPassword();
$encryption = $settings->smtp_encryption ?? 'tls';
$scheme = ($encryption === 'ssl') ? 'smtps' : 'smtp';
$dsn = sprintf(
'%s://%s:%s@%s:%d?encryption=%s',
$scheme,
rawurlencode($settings->smtp_username ?? $settings->email_address),
rawurlencode($smtpPassword),
$settings->smtp_host ?? 'smtp.gmail.com',
$settings->smtp_port ?? 587,
$encryption
);
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
$fromName = $settings->email_name ?? '';
if (filter_var($fromName, FILTER_VALIDATE_EMAIL)) {
$fromName = 'Glastree';
} else {
$fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName);
$fromName = trim($fromName);
if (empty($fromName) || strlen($fromName) > 50) {
$fromName = 'Glastree';
}
}
$fromAddress = \Symfony\Component\Mime\Address::create($settings->email_address, $fromName);
$email = (new \Symfony\Component\Mime\Email())
->from($fromAddress)
->to($to)
->subject($subject)
->text($body);
foreach ($attachmentPaths as $path) {
$email->attachFromPath($path);
}
$mailer->send($email);
}
private function resolveMailingAttachmentPaths(array $documentIds): array
{
$paths = [];
if (empty($documentIds)) {
return $paths;
}
$docs = Documento::whereIn('id', $documentIds)->get();
foreach ($docs as $doc) {
if ($doc->file_path && Storage::exists($doc->file_path)) {
$paths[] = Storage::path($doc->file_path);
}
}
return $paths;
}
}
+3 -1
View File
@@ -10,11 +10,13 @@ class MailingMessaggio extends Model
protected $table = 'mailing_messaggi';
protected $fillable = [
'tenant_id', 'mailing_list_id', 'user_id', 'oggetto', 'corpo',
'canale', 'stato', 'inviato_al', 'totale_destinatari', 'invii_ok', 'invii_falliti'
'canale', 'stato', 'inviato_al', 'totale_destinatari', 'invii_ok', 'invii_falliti',
'log_falliti', 'mittente_nome', 'mittente_email',
];
protected $casts = [
'inviato_al' => 'datetime',
'log_falliti' => 'array',
];
public function tenant(): BelongsTo
+146
View File
@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Crypt;
class SenderAccount extends Model
{
protected $table = 'sender_accounts';
protected $fillable = [
'email_address',
'email_name',
'smtp_host',
'smtp_port',
'smtp_encryption',
'smtp_username',
'smtp_password',
'reply_to',
'verify_email',
'note',
'is_active',
];
protected $casts = [
'is_active' => 'boolean',
'smtp_port' => 'integer',
];
public function scopeActive(Builder $query): Builder
{
return $query->where('is_active', true);
}
public function getDecryptedPassword(): string
{
if (empty($this->smtp_password)) {
return '';
}
try {
return Crypt::decryptString($this->smtp_password);
} catch (\Exception $e) {
return $this->smtp_password;
}
}
public function sanitizeName(): string
{
$name = $this->email_name ?? '';
if (filter_var($name, FILTER_VALIDATE_EMAIL)) {
return 'Glastree';
}
$name = preg_replace('/[^\p{L}\p{N}\s]/u', '', $name);
$name = trim($name);
return empty($name) ? 'Glastree' : $name;
}
public function getReplyTo(): ?string
{
if ($this->reply_to) {
return $this->reply_to;
}
if ($this->verify_email) {
return $this->verify_email;
}
return null;
}
public function buildDsn(): string
{
$password = $this->getDecryptedPassword();
$username = $this->smtp_username ?? $this->email_address;
$encryption = $this->smtp_encryption ?? 'tls';
$scheme = ($encryption === 'ssl') ? 'smtps' : 'smtp';
return sprintf(
'%s://%s:%s@%s:%d?encryption=%s',
$scheme,
rawurlencode($username),
rawurlencode($password),
$this->smtp_host ?? 'smtp.gmail.com',
$this->smtp_port ?? 587,
$encryption
);
}
public function buildSymfonyEmail(string $to, string $subject, string $body): \Symfony\Component\Mime\Email
{
$fromName = $this->sanitizeName();
$fromAddress = \Symfony\Component\Mime\Address::create($this->email_address, $fromName);
$email = (new \Symfony\Component\Mime\Email())
->from($fromAddress)
->to($to)
->subject($subject)
->text($body);
$replyTo = $this->getReplyTo();
if ($replyTo) {
$email->replyTo($replyTo);
}
return $email;
}
public function sendEmail(string $to, string $subject, string $body, array $attachmentPaths = []): void
{
$email = $this->buildSymfonyEmail($to, $subject, $body);
foreach ($attachmentPaths as $path) {
$email->attachFromPath($path);
}
$transport = \Symfony\Component\Mailer\Transport::fromDsn($this->buildDsn());
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
$mailer->send($email);
}
public function sendReport(string $subject, string $body): bool
{
if (empty($this->verify_email)) {
return false;
}
try {
$this->sendEmail($this->verify_email, $subject, $body);
return true;
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Sender report email failed', [
'sender' => $this->email_address,
'error' => $e->getMessage(),
]);
return false;
}
}
}