Rev 612 | Rev 614 | Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?php
declare(strict_types = 1);
namespace LeadersLinked\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Laminas\Db\Adapter\AdapterInterface;
use Laminas\Log\LoggerInterface;
use LeadersLinked\Mapper\EmailMapper;
use PHPMailer\PHPMailer\PHPMailer;
use LeadersLinked\Model\Email;
use Laminas\Mvc\I18n\Translator;
use LeadersLinked\Cache\CacheInterface;
use Laminas\Db\Adapter\Exception\InvalidQueryException; // Importar para manejar excepciones de BD
class ProcessQueueEmailCommand extends Command
{
/**
*
* @var \Laminas\Db\Adapter\AdapterInterface
*/
private $adapter;
/**
*
* @var \LeadersLinked\Cache\CacheInterface
*/
private $cache;
/**
*
* @var \Laminas\Log\LoggerInterface
*/
private $logger;
/**
*
* @var array
*/
private $config;
/**
*
* @var \Laminas\Mvc\I18n\Translator
*/
private $translator;
/**
*
* @param \Laminas\Db\Adapter\AdapterInterface $adapter
* @param \LeadersLinked\Cache\CacheInterface $cache
* @param \Laminas\Log\LoggerInterface $logger
* @param array $config
* @param \Laminas\Mvc\I18n\Translator $translator
*/
public function __construct($adapter, $cache, $logger, $config, $translator)
{
$this->adapter = $adapter;
$this->cache = $cache;
$this->logger = $logger;
$this->config = $config;
$this->translator = $translator;
parent::__construct();
}
protected function execute($input, $output)
{
$sandbox = $this->config['leaderslinked.runmode.sandbox'];
if ($sandbox) {
$batch_size = $this->config['leaderslinked.email.sandbox_batch_size'];
$from_address = $this->config['leaderslinked.email.sandbox_from_address'];
$from_name = $this->config['leaderslinked.email.sandbox_from_name'];
$host = $this->config['leaderslinked.email.sandbox_host'];
$port = $this->config['leaderslinked.email.sandbox_port'];
$username = $this->config['leaderslinked.email.sandbox_username'];
$password = $this->config['leaderslinked.email.sandbox_password'];
} else {
$batch_size = $this->config['leaderslinked.email.production_batch_size'];
$from_address = $this->config['leaderslinked.email.production_from_address'];
$from_name = $this->config['leaderslinked.email.production_from_name'];
$host = $this->config['leaderslinked.email.production_host'];
$port = $this->config['leaderslinked.email.production_port'];
$username = $this->config['leaderslinked.email.production_username'];
$password = $this->config['leaderslinked.email.production_password'];
}
$this->logger->info('Inicio del proceso de la cola de Email');
$output->writeln('<info>Iniciando el procesamiento de la cola de emails...</info>');
$emailCompleted = 0;
$emailError = 0; // Para correos con errores que se descartan
$emailRetried = 0; // Para correos que se reintentan
$emailMapper = EmailMapper::getInstance($this->adapter);
$emails = $emailMapper->fetchBatch($batch_size);
if (!$emails) {
$this->logger->info('No hay emails en la cola');
$output->writeln('<comment>No hay emails en la cola para procesar.</comment>');
return Command::SUCCESS;
}
foreach ($emails as $email) {
// Iniciar transacción para cada email para asegurar atomicidad
$this->adapter->getDriver()->getConnection()->beginTransaction();
try {
$content = json_decode($email->content, true);
// Validación básica de los datos del email
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \RuntimeException('Error al decodificar el contenido JSON del email: ' . json_last_error_msg());
}
if (!isset($content['to_address'], $content['subject'], $content['message'])) {
throw new \RuntimeException('Contenido del email JSON incompleto: faltan datos esenciales.');
}
$to_address = $content['to_address'];
$to_name = $content['to_name'] ?? ''; // Valor por defecto
$cc = $content['cc'] ?? []; // Valor por defecto
$bcc = $content['bcc'] ?? []; // Valor por defecto
$subject = $content['subject'];
$message = $content['message'];
// Normalización de codificación
$subject = $this->normalizeEncoding($subject);
$message = $this->normalizeEncoding($message);
$phpMailer = new PHPMailer(true); // true para habilitar excepciones
$phpMailer->isSMTP();
$phpMailer->SMTPDebug = 0; // 0 para no depurar, 2 para mensajes detallados en desarrollo
$phpMailer->Host = $host;
$phpMailer->SMTPAuth = true;
$phpMailer->SMTPSecure = 'tls'; // o 'ssl' si el puerto es 465
$phpMailer->Username = $username;
$phpMailer->Password = $password;
$phpMailer->Port = $port;
$phpMailer->CharSet = 'UTF-8';
$phpMailer->setFrom($from_address, $from_name);
$phpMailer->addAddress($to_address, $to_name);
foreach ($cc as $address => $name) {
$phpMailer->addCC($address, $name);
}
foreach ($bcc as $address => $name) {
$phpMailer->addBCC($address, $name);
}
$phpMailer->IsHTML(true);
$phpMailer->Subject = $subject;
$phpMailer->Body = $message;
$phpMailer->AltBody = strip_tags($message); // Versión de texto plano
$phpMailer->send();
// Si el envío es exitoso
$emailCompleted++;
$email->status = Email::STATUS_COMPLETED;
$email->tried = $email->tried + 1; // Aunque exitoso, es bueno registrar el intento
$this->logger->info('Email enviado correctamente: ' . $email->id);
$output->writeln(" <info>✔ Email #{$email->id} enviado a {$to_address}</info>");
// Actualizar el estado del email en la BD y confirmar la transacción
$emailMapper->update($email);
$this->adapter->getDriver()->getConnection()->commit();
} catch (\Exception $e) {
$error_message = 'Error de PHPMailer al enviar email ' . $email->id . ': ' . $e->getMessage() . ' Debug: ' . $phpMailer->ErrorInfo;
$this->logger->error($error_message);
$output->writeln(" <error>✗ Error al enviar email #{$email->id}: {$e->getMessage()}</error>");
$email->tried = $email->tried + 1;
if ($email->tried >= $this->config['leaderslinked.email.max_retries']) { // Usar una configuración para el número máximo de reintentos
$emailError++;
$email->status = Email::STATUS_ERROR;
$this->logger->info('Email descartado después de ' . $email->tried . ' intentos fallidos: ' . $email->id);
$output->writeln(" <error>Email #{$email->id} descartado (máx. reintentos alcanzado).</error>");
} else {
$emailRetried++;
$email->status = Email::STATUS_PENDING; // Mantener como pendiente para reintentar
$this->logger->warning('Email ' . $email->id . ' falló. Intentos: ' . $email->tried . '. Reintentando más tarde.');
}
// Actualizar el estado del email en la BD y revertir si algo sale mal con la actualización (raro en este punto)
try {
$emailMapper->update($email);
$this->adapter->getDriver()->getConnection()->commit(); // Confirmar la transacción
} catch (\Exception $dbE) {
$this->adapter->getDriver()->getConnection()->rollback();
$this->logger->critical('Error CRÍTICO al actualizar el estado del email #'.$email->id.' en la BD después de fallo de envío: ' . $dbE->getMessage());
$output->writeln("<error> ERROR CRÍTICO: No se pudo actualizar el estado del email #{$email->id} en la BD.</error>");
}
} catch (\RuntimeException $e) {
// Errores de lógica de negocio o validación (ej. JSON malformado)
$error_message = 'Error de lógica/validación para email ' . $email->id . ': ' . $e->getMessage();
$this->logger->error($error_message);
$output->writeln(" <error>✗ Error de procesamiento interno para email #{$email->id}: {$e->getMessage()}</error>");
$emailError++; // Generalmente estos errores son irrecuperables por reintentos
$email->status = Email::STATUS_ERROR;
$email->tried = $email->tried + 1; // Registrar el intento fallido
try {
$emailMapper->update($email);
$this->adapter->getDriver()->getConnection()->commit(); // Confirmar la transacción
} catch (\Exception $dbE) {
$this->adapter->getDriver()->getConnection()->rollback();
$this->logger->critical('Error CRÍTICO al actualizar el estado del email #'.$email->id.' en la BD después de error de procesamiento: ' . $dbE->getMessage());
$output->writeln("<error> ERROR CRÍTICO: No se pudo actualizar el estado del email #{$email->id} en la BD.</error>");
}
}
catch (\Exception $e) {
// Cualquier otra excepción no capturada
$error_message = 'Error desconocido al procesar email ' . $email->id . ': ' . $e->getMessage();
$this->logger->critical($error_message);
$output->writeln(" <error>✗ Error inesperado al procesar email #{$email->id}: {$e->getMessage()}</error>");
$emailError++;
$email->status = Email::STATUS_ERROR; // Marcar como error irrecuperable
$email->tried = $email->tried + 1;
try {
$emailMapper->update($email);
$this->adapter->getDriver()->getConnection()->commit(); // Confirmar la transacción
} catch (\Exception $dbE) {
$this->adapter->getDriver()->getConnection()->rollback();
$this->logger->critical('Error CRÍTICO al actualizar el estado del email #'.$email->id.' en la BD después de error desconocido: ' . $dbE->getMessage());
$output->writeln("<error> ERROR CRÍTICO: No se pudo actualizar el estado del email #{$email->id} en la BD.</error>");
}
}
}
$this->logger->info('Email con Errores descartados: ' . $emailError);
$this->logger->info('Email pendientes para reintento: ' . $emailRetried);
$this->logger->info('Email enviados correctamente: ' . $emailCompleted);
$this->logger->info('Fin del proceso de la cola de Email');
$output->writeln('');
$output->writeln("<info>Resumen de la ejecución:</info>");
$output->writeln(" - Correos enviados: <info>{$emailCompleted}</info>");
$output->writeln(" - Correos con error y descartados: <error>{$emailError}</error>");
$output->writeln(" - Correos a reintentar: <comment>{$emailRetried}</comment>");
$output->writeln('<info>Proceso de la cola de emails finalizado.</info>');
return Command::SUCCESS; // Usar constantes de Symfony para el código de salida
}
/**
* Normaliza la codificación de una cadena a UTF-8.
*
* @param string $string
* @return string
*/
private function normalizeEncoding(string $string): string
{
$encoding = mb_detect_encoding($string, mb_detect_order(), true);
if ($encoding && $encoding != 'UTF-8') {
return mb_convert_encoding($string, 'UTF-8', $encoding);
}
return $string;
}
}