This commit is contained in:
Lizandro Guarnizo
2026-04-25 14:47:20 -05:00
parent c56cd218fa
commit bbf34d08d0
8 changed files with 645 additions and 0 deletions
@@ -35,4 +35,9 @@ class WhatsappController extends Controller
{
return view('whatsapp.logs');
}
public function correo()
{
return view('whatsapp.correo');
}
}
@@ -0,0 +1,99 @@
<?php
namespace App\Http\Livewire\Whatsapp;
use App\Models\WhatsappSystemConfig;
use App\Services\CorreoImapService;
use Livewire\Component;
use Jantinnerezo\LivewireAlert\LivewireAlert;
class ShowCorreoConfig extends Component
{
use LivewireAlert;
// ── Campos de configuración ──────────────────
public string $correo_imap_host = '';
public string $correo_imap_port = '993';
public string $correo_imap_ssl = '1';
public string $correo_imap_user = '';
public string $correo_imap_password = '';
public string $correo_imap_folder = 'INBOX';
public string $correo_imap_enabled = '0';
// ── Estado de prueba ─────────────────────────
public bool $testing = false;
public string $testStatus = ''; // 'ok' | 'error' | ''
public string $testError = '';
public array $emails = [];
protected $rules = [
'correo_imap_host' => 'required|string|max:255',
'correo_imap_port' => 'required|integer|min:1|max:65535',
'correo_imap_user' => 'required|email',
'correo_imap_password' => 'required|string',
'correo_imap_folder' => 'required|string|max:100',
];
public function mount(): void
{
$keys = [
'correo_imap_host', 'correo_imap_port', 'correo_imap_ssl',
'correo_imap_user', 'correo_imap_password', 'correo_imap_folder',
'correo_imap_enabled',
];
foreach ($keys as $key) {
$this->{$key} = WhatsappSystemConfig::get($key, $this->{$key});
}
}
public function save(): void
{
$this->validate();
$keys = [
'correo_imap_host', 'correo_imap_port', 'correo_imap_ssl',
'correo_imap_user', 'correo_imap_password', 'correo_imap_folder',
'correo_imap_enabled',
];
foreach ($keys as $key) {
WhatsappSystemConfig::set($key, (string) $this->{$key});
}
$this->alert('success', 'Configuración guardada correctamente.');
}
public function probar(): void
{
$this->validate();
$this->testing = true;
$this->testStatus = '';
$this->testError = '';
$this->emails = [];
try {
$service = new CorreoImapService(
host: $this->correo_imap_host,
port: (int) $this->correo_imap_port,
useSsl: $this->correo_imap_ssl === '1',
user: $this->correo_imap_user,
password: $this->correo_imap_password,
folder: $this->correo_imap_folder,
timeout: 20
);
$this->emails = $service->fetchLatest(5);
$this->testStatus = 'ok';
} catch (\Throwable $e) {
$this->testStatus = 'error';
$this->testError = $e->getMessage();
} finally {
$this->testing = false;
}
}
public function render()
{
return view('livewire.whatsapp.show-correo-config');
}
}
+284
View File
@@ -0,0 +1,284 @@
<?php
namespace App\Services;
/**
* CorreoImapService
*
* Lee correos vía protocolo IMAP puro usando sockets PHP.
* No requiere la extensión imap de PHP.
*/
class CorreoImapService
{
private $socket = null;
private int $tag = 1;
private string $lastError = '';
public function __construct(
private string $host,
private int $port,
private bool $useSsl,
private string $user,
private string $password,
private string $folder = 'INBOX',
private int $timeout = 15
) {}
// ──────────────────────────────────────────────
// API pública
// ──────────────────────────────────────────────
/**
* Conecta, autentica y devuelve los últimos $limit correos.
* Cada elemento: [uid, from, subject, date, body]
*/
public function fetchLatest(int $limit = 5): array
{
$this->connect();
$this->login();
$total = $this->selectFolder();
if ($total === 0) {
$this->logout();
return [];
}
// Tomamos los últimos $limit números de secuencia
$from = max(1, $total - $limit + 1);
$set = "{$from}:{$total}";
$emails = $this->fetchMessages($set);
$this->logout();
// Devolver en orden descendente (más reciente primero)
return array_reverse($emails);
}
public function getLastError(): string
{
return $this->lastError;
}
// ──────────────────────────────────────────────
// Conexión y autenticación
// ──────────────────────────────────────────────
private function connect(): void
{
$address = $this->useSsl
? "ssl://{$this->host}:{$this->port}"
: "tcp://{$this->host}:{$this->port}";
$context = stream_context_create([
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
]);
$this->socket = stream_socket_client(
$address,
$errno,
$errstr,
$this->timeout,
STREAM_CLIENT_CONNECT,
$context
);
if (! $this->socket) {
throw new \RuntimeException("No se pudo conectar a {$address}: {$errstr} ({$errno})");
}
stream_set_timeout($this->socket, $this->timeout);
// Leer el banner de bienvenida del servidor
$this->readLine();
}
private function login(): void
{
$response = $this->command("LOGIN \"{$this->user}\" \"{$this->password}\"");
if (! str_contains($response, 'OK')) {
throw new \RuntimeException("Autenticación fallida. Respuesta: {$response}");
}
}
private function logout(): void
{
if ($this->socket) {
$this->command('LOGOUT');
fclose($this->socket);
$this->socket = null;
}
}
// ──────────────────────────────────────────────
// Operaciones IMAP
// ──────────────────────────────────────────────
/**
* Selecciona la carpeta y retorna el número total de mensajes.
*/
private function selectFolder(): int
{
$response = $this->command("SELECT \"{$this->folder}\"");
// Buscar "* N EXISTS"
if (preg_match('/\*\s+(\d+)\s+EXISTS/i', $response, $m)) {
return (int) $m[1];
}
return 0;
}
/**
* Descarga cabeceras + cuerpo de un rango de secuencia (ej: "1:5").
*/
private function fetchMessages(string $set): array
{
// Pedimos FLAGS, ENVELOPE y BODY para extraer remitente, asunto y cuerpo
$raw = $this->command("FETCH {$set} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)] BODY.PEEK[TEXT])");
return $this->parseMessages($raw);
}
// ──────────────────────────────────────────────
// Parser de respuesta FETCH
// ──────────────────────────────────────────────
private function parseMessages(string $raw): array
{
$emails = [];
// Dividir por inicio de cada mensaje "* N FETCH"
$blocks = preg_split('/\* \d+ FETCH /i', $raw, -1, PREG_SPLIT_NO_EMPTY);
foreach ($blocks as $block) {
$email = [
'seq' => '',
'from' => '',
'subject' => '',
'date' => '',
'body' => '',
];
// FROM
if (preg_match('/^From:\s*(.+)$/mi', $block, $m)) {
$email['from'] = $this->decodeMimeHeader(trim($m[1]));
}
// SUBJECT
if (preg_match('/^Subject:\s*(.+)$/mi', $block, $m)) {
$email['subject'] = $this->decodeMimeHeader(trim($m[1]));
}
// DATE
if (preg_match('/^Date:\s*(.+)$/mi', $block, $m)) {
$email['date'] = trim($m[1]);
}
// BODY: tomamos el texto entre las dos secciones BODY
// La respuesta tiene dos literales {N}\r\n<contenido>
$parts = preg_split('/\}\r?\n/', $block);
if (count($parts) >= 3) {
// La tercera sección suele ser el cuerpo de texto
$rawBody = $parts[2];
// Limpiar hasta el fin del literal (antes del siguiente *)
$rawBody = preg_replace('/\r?\n\)\s*\*.*$/s', '', $rawBody);
$rawBody = preg_replace('/\r?\n\)\s*[A-Z0-9]+ OK.*/s', '', $rawBody);
$email['body'] = $this->cleanBody(trim($rawBody));
} elseif (count($parts) === 2) {
$rawBody = $parts[1];
$rawBody = preg_replace('/\r?\n\).*$/s', '', $rawBody);
$email['body'] = $this->cleanBody(trim($rawBody));
}
if ($email['from'] || $email['subject']) {
$emails[] = $email;
}
}
return $emails;
}
// ──────────────────────────────────────────────
// Comunicación con el socket
// ──────────────────────────────────────────────
private function command(string $cmd): string
{
$tag = 'A' . str_pad((string) $this->tag++, 4, '0', STR_PAD_LEFT);
fwrite($this->socket, "{$tag} {$cmd}\r\n");
$response = '';
while (! feof($this->socket)) {
$line = fgets($this->socket, 8192);
if ($line === false) {
break;
}
$response .= $line;
// La respuesta termina cuando llega la línea tagged
if (str_starts_with($line, $tag)) {
break;
}
}
return $response;
}
private function readLine(): string
{
if (! $this->socket) {
return '';
}
return (string) fgets($this->socket, 1024);
}
// ──────────────────────────────────────────────
// Helpers
// ──────────────────────────────────────────────
private function decodeMimeHeader(string $value): string
{
if (function_exists('iconv_mime_decode')) {
return iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, 'UTF-8');
}
// Fallback: decodificar manualmente =?charset?encoding?text?=
return preg_replace_callback(
'/=\?([^?]+)\?([BbQq])\?([^?]*)\?=/u',
function ($m) {
$charset = $m[1];
$encoding = strtoupper($m[2]);
$text = $m[3];
$decoded = $encoding === 'B' ? base64_decode($text) : quoted_printable_decode(str_replace('_', ' ', $text));
return mb_convert_encoding($decoded, 'UTF-8', $charset);
},
$value
);
}
private function cleanBody(string $body): string
{
// Quitar quoted-printable si aplica
if (str_contains($body, '=\r\n') || preg_match('/=[0-9A-F]{2}/i', $body)) {
$body = quoted_printable_decode($body);
}
// Quitar líneas de separadores de partes MIME
$body = preg_replace('/--[^\r\n]+\r?\n/m', '', $body);
// Eliminar líneas de cabecera de parte MIME (Content-Type, etc.)
$body = preg_replace('/^(Content-[^\r\n]+\r?\n)+/mi', '', $body);
// Normalizar saltos de línea
$body = str_replace(["\r\n", "\r"], "\n", $body);
// Recortar líneas en blanco excesivas
$body = preg_replace('/\n{3,}/', "\n\n", $body);
return trim($body);
}
}
@@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
// Inserta las claves de configuración del correo en la tabla existente
$keys = [
['config_key' => 'correo_imap_host', 'config_value' => '', 'description' => 'Servidor IMAP (ej: imap.gmail.com)'],
['config_key' => 'correo_imap_port', 'config_value' => '993', 'description' => 'Puerto IMAP (993=SSL, 143=sin SSL)'],
['config_key' => 'correo_imap_ssl', 'config_value' => '1', 'description' => 'Usar SSL/TLS (1=sí, 0=no)'],
['config_key' => 'correo_imap_user', 'config_value' => '', 'description' => 'Usuario/email de la cuenta'],
['config_key' => 'correo_imap_password', 'config_value' => '', 'description' => 'Contraseña o App Password'],
['config_key' => 'correo_imap_folder', 'config_value' => 'INBOX', 'description' => 'Carpeta a leer'],
['config_key' => 'correo_imap_enabled', 'config_value' => '0', 'description' => 'Habilitar lectura de correos (1/0)'],
];
foreach ($keys as $key) {
DB::table('whatsapp_system_config')->insertOrIgnore([
...$key,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
public function down(): void
{
DB::table('whatsapp_system_config')
->whereIn('config_key', [
'correo_imap_host', 'correo_imap_port', 'correo_imap_ssl',
'correo_imap_user', 'correo_imap_password', 'correo_imap_folder',
'correo_imap_enabled',
])->delete();
}
};
@@ -283,6 +283,10 @@
<svg class="w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 0 1 0 3.75H5.625a1.875 1.875 0 0 1 0-3.75Z" /></svg>
<span>Logs Webhook</span>
</a>
<a href="{{ route('whatsapp.correo') }}" class="flex items-center gap-2 py-2 px-3 rounded text-white text-left {{ request()->routeIs('whatsapp.correo') ? 'bg-white/20' : 'hover:bg-white/10' }}">
<svg class="w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" /></svg>
<span>Correo / IMAP</span>
</a>
</div>
</div>
@endif
@@ -0,0 +1,205 @@
<div>
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-gray-800 flex items-center gap-2">
<svg class="w-7 h-7 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
Lector de Correo IMAP
</h1>
</div>
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
{{-- ── Formulario de configuración ─────────────────── --}}
<div class="bg-white rounded-2xl shadow p-6">
<h2 class="text-lg font-semibold text-gray-700 mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
Configuración del servidor
</h2>
<form wire:submit.prevent="save" class="space-y-4">
{{-- Host y puerto --}}
<div class="grid grid-cols-3 gap-3">
<div class="col-span-2">
<label class="block text-xs font-medium text-gray-600 mb-1">Servidor IMAP *</label>
<input wire:model="correo_imap_host" type="text"
placeholder="imap.gmail.com / imap.outlook.com"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400">
@error('correo_imap_host') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">Puerto *</label>
<input wire:model="correo_imap_port" type="number" min="1" max="65535"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400">
@error('correo_imap_port') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
</div>
{{-- SSL --}}
<div class="flex items-center gap-4">
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">Seguridad</label>
<select wire:model="correo_imap_ssl"
class="border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none">
<option value="1">SSL / TLS (puerto 993)</option>
<option value="0">Sin SSL (puerto 143)</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">Carpeta</label>
<input wire:model="correo_imap_folder" type="text" placeholder="INBOX"
class="border border-gray-300 rounded-lg px-3 py-2 text-sm w-36 focus:outline-none focus:ring-2 focus:ring-blue-400">
@error('correo_imap_folder') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
</div>
{{-- Usuario --}}
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">Correo / Usuario *</label>
<input wire:model="correo_imap_user" type="email"
placeholder="pagos@tuempresa.com"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400">
@error('correo_imap_user') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
</div>
{{-- Contraseña --}}
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">Contraseña / App Password *</label>
<input wire:model="correo_imap_password" type="password"
placeholder="••••••••••••"
class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400">
@error('correo_imap_password') <p class="text-red-500 text-xs mt-1">{{ $message }}</p> @enderror
<p class="text-xs text-gray-400 mt-1">
Para Gmail usa una
<a href="https://myaccount.google.com/apppasswords" target="_blank" class="text-blue-500 underline">App Password</a>
(necesitas verificación en 2 pasos activa).
</p>
</div>
{{-- Habilitado --}}
<label class="flex items-center gap-2 text-sm text-gray-700 cursor-pointer">
<input wire:model="correo_imap_enabled" type="checkbox" value="1"
class="rounded border-gray-300 text-blue-500 focus:ring-blue-400">
Habilitar lectura automática de correos
</label>
{{-- Acciones --}}
<div class="flex gap-3 pt-2 border-t border-gray-100">
<button type="submit"
class="bg-blue-600 hover:bg-blue-700 text-white text-sm px-5 py-2 rounded-lg transition flex items-center gap-2">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
Guardar configuración
</button>
<button type="button" wire:click="probar" wire:loading.attr="disabled"
class="bg-green-500 hover:bg-green-600 disabled:opacity-60 text-white text-sm px-5 py-2 rounded-lg transition flex items-center gap-2">
<svg wire:loading wire:target="probar" class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"/>
</svg>
<svg wire:loading.remove wire:target="probar" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span wire:loading.remove wire:target="probar">Probar conexión</span>
<span wire:loading wire:target="probar">Conectando...</span>
</button>
</div>
</form>
{{-- Ayuda rápida --}}
<div class="mt-5 p-4 bg-gray-50 rounded-xl text-xs text-gray-500 space-y-1">
<p class="font-semibold text-gray-600 mb-2">Configuraciones comunes:</p>
<div class="grid grid-cols-2 gap-x-4 gap-y-1">
<span class="font-medium">Gmail</span><span>imap.gmail.com : 993 (SSL)</span>
<span class="font-medium">Outlook/Hotmail</span><span>imap-mail.outlook.com : 993 (SSL)</span>
<span class="font-medium">Yahoo</span><span>imap.mail.yahoo.com : 993 (SSL)</span>
<span class="font-medium">cPanel / Hosting</span><span>mail.tudominio.com : 993 (SSL)</span>
</div>
</div>
</div>
{{-- ── Resultado de la prueba ───────────────────────── --}}
<div>
{{-- Estado de la conexión --}}
@if($testStatus === 'error')
<div class="mb-4 bg-red-50 border border-red-200 rounded-xl p-4 flex gap-3">
<svg class="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div>
<p class="font-semibold text-red-700 text-sm">Error de conexión</p>
<p class="text-red-600 text-xs mt-1 font-mono">{{ $testError }}</p>
</div>
</div>
@elseif($testStatus === 'ok')
<div class="mb-4 bg-green-50 border border-green-200 rounded-xl p-4 flex gap-3">
<svg class="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div>
<p class="font-semibold text-green-700 text-sm">Conexión exitosa</p>
<p class="text-green-600 text-xs mt-1">
Se encontraron <strong>{{ count($emails) }}</strong> correo(s) recientes.
</p>
</div>
</div>
@else
<div class="mb-4 bg-gray-50 border border-dashed border-gray-300 rounded-xl p-6 text-center text-gray-400">
<svg class="w-10 h-10 mx-auto mb-2 opacity-30" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
<p class="text-sm">Configura los datos y pulsa <strong>Probar conexión</strong></p>
<p class="text-xs mt-1">Se mostrarán los últimos 5 correos de la bandeja</p>
</div>
@endif
{{-- Lista de correos --}}
@if(count($emails) > 0)
<div class="space-y-3">
@foreach($emails as $i => $email)
<div x-data="{ open: false }"
class="bg-white rounded-xl shadow border border-gray-100 overflow-hidden">
{{-- Cabecera del correo --}}
<button type="button" @click="open = !open"
class="w-full text-left px-4 py-3 flex items-start justify-between gap-3 hover:bg-gray-50 transition">
<div class="flex items-start gap-3 min-w-0">
<span class="flex-shrink-0 w-7 h-7 rounded-full bg-blue-100 text-blue-700 text-xs font-bold flex items-center justify-center mt-0.5">
{{ $i + 1 }}
</span>
<div class="min-w-0">
<p class="text-sm font-semibold text-gray-800 truncate">
{{ $email['subject'] ?: '(Sin asunto)' }}
</p>
<p class="text-xs text-gray-500 truncate mt-0.5">
<span class="font-medium">De:</span> {{ $email['from'] ?: '—' }}
</p>
@if($email['date'])
<p class="text-xs text-gray-400 mt-0.5">{{ $email['date'] }}</p>
@endif
</div>
</div>
<svg :class="open ? 'rotate-180' : ''"
class="w-4 h-4 text-gray-400 flex-shrink-0 mt-1 transform transition-transform"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
{{-- Cuerpo del correo --}}
<div x-show="open" x-transition class="border-t border-gray-100 px-4 py-3">
@if($email['body'])
<pre class="text-xs text-gray-600 whitespace-pre-wrap font-sans leading-relaxed max-h-64 overflow-y-auto">{{ $email['body'] }}</pre>
@else
<p class="text-xs text-gray-400 italic">(Sin contenido de texto)</p>
@endif
</div>
</div>
@endforeach
</div>
@endif
</div>
</div>
</div>
@@ -0,0 +1,6 @@
<x-app-layout>
<div class="p-4">
<livewire:whatsapp.show-correo-config />
</div>
@include('layouts.footer')
</x-app-layout>
+1
View File
@@ -81,6 +81,7 @@ Route::middleware(["auth", "solo_usuario_administrador"])->group(function () {
Route::get('/programados', [WhatsappController::class, 'programados'])->name('programados');
Route::get('/configuracion', [WhatsappController::class, 'configuracion'])->name('configuracion');
Route::get('/logs', [WhatsappController::class, 'logs'])->name('logs');
Route::get('/correo', [WhatsappController::class, 'correo'])->name('correo');
});
});