correo
This commit is contained in:
@@ -35,4 +35,9 @@ class WhatsappController extends Controller
|
|||||||
{
|
{
|
||||||
return view('whatsapp.logs');
|
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>
|
<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>
|
<span>Logs Webhook</span>
|
||||||
</a>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@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('/programados', [WhatsappController::class, 'programados'])->name('programados');
|
||||||
Route::get('/configuracion', [WhatsappController::class, 'configuracion'])->name('configuracion');
|
Route::get('/configuracion', [WhatsappController::class, 'configuracion'])->name('configuracion');
|
||||||
Route::get('/logs', [WhatsappController::class, 'logs'])->name('logs');
|
Route::get('/logs', [WhatsappController::class, 'logs'])->name('logs');
|
||||||
|
Route::get('/correo', [WhatsappController::class, 'correo'])->name('correo');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user