correo
This commit is contained in:
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user