netgescon-day0/app/Services/Posta/ImapClient.php

212 lines
6.1 KiB
PHP

<?php
namespace App\Services\Posta;
use Exception;
class ImapClient
{
private mixed $socket = null;
private int $tagIndex = 0;
public function testConnection(string $host, int $port, string $username, string $password, string $encryption = 'ssl'): array
{
try {
$this->connect($host, $port, $username, $password, $encryption);
$this->disconnect();
return ['success' => true, 'message' => 'Connessione e autenticazione IMAP riuscite con successo.'];
} catch (\Throwable $e) {
return ['success' => false, 'message' => 'Errore IMAP: ' . $e->getMessage()];
}
}
public function connect(string $host, int $port, string $username, string $password, string $encryption = 'ssl'): void
{
$prefix = strtolower($encryption) === 'ssl' ? 'ssl://' : '';
$target = $prefix . $host;
$timeout = 15;
$context = stream_context_create([
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
],
]);
$this->socket = @stream_socket_client(
$target . ':' . $port,
$errno,
$errstr,
$timeout,
STREAM_CLIENT_CONNECT,
$context
);
if (! $this->socket) {
throw new Exception("Impossibile connettersi al server IMAP {$host}:{$port} ({$errstr})");
}
stream_set_timeout($this->socket, $timeout);
// Leggi il banner iniziale
$greeting = $this->readLine();
if (! str_starts_with($greeting, '* OK')) {
throw new Exception("Risposta iniziale inattesa dal server IMAP: {$greeting}");
}
// Login
$userEscaped = addcslashes($username, '\\"');
$passEscaped = addcslashes($password, '\\"');
$res = $this->sendCommand("LOGIN \"{$userEscaped}\" \"{$passEscaped}\"");
if (! $res['ok']) {
throw new Exception("Autenticazione IMAP fallita per l'utente {$username}: " . ($res['response'] ?? ''));
}
}
public function selectFolder(string $folder = 'INBOX'): array
{
$folderEscaped = addcslashes($folder, '\\"');
$res = $this->sendCommand("SELECT \"{$folderEscaped}\"");
if (! $res['ok']) {
throw new Exception("Impossibile aprire la cartella IMAP '{$folder}'");
}
$exists = 0;
foreach ($res['lines'] as $line) {
if (preg_match('/^\*\s+(\d+)\s+EXISTS/i', $line, $m)) {
$exists = (int) $m[1];
}
}
return ['ok' => true, 'exists' => $exists];
}
/**
* @return array<int, int>
*/
public function search(string $criteria = 'ALL'): array
{
$res = $this->sendCommand("SEARCH {$criteria}");
if (! $res['ok']) {
return [];
}
$ids = [];
foreach ($res['lines'] as $line) {
if (str_starts_with($line, '* SEARCH')) {
$parts = explode(' ', trim(substr($line, 8)));
foreach ($parts as $p) {
if (is_numeric($p) && (int) $p > 0) {
$ids[] = (int) $p;
}
}
}
}
return $ids;
}
public function fetchRawEml(int $msgId): string
{
$tag = $this->nextTag();
$cmd = "{$tag} FETCH {$msgId} (BODY.PEEK[])\r\n";
fwrite($this->socket, $cmd);
$firstLine = $this->readLine();
$expectedLength = null;
if (preg_match('/\{(\d+)\}$/', trim($firstLine), $m)) {
$expectedLength = (int) $m[1];
}
$rawEml = '';
if ($expectedLength !== null && $expectedLength > 0) {
$bytesRead = 0;
while ($bytesRead < $expectedLength && ! feof($this->socket)) {
$chunk = fread($this->socket, min(8192, $expectedLength - $bytesRead));
if ($chunk === false || $chunk === '') {
break;
}
$rawEml .= $chunk;
$bytesRead += strlen($chunk);
}
}
// Leggi fino al tag di completamento
while (! feof($this->socket)) {
$line = $this->readLine();
if (str_starts_with($line, $tag . ' OK')) {
break;
}
if (str_starts_with($line, $tag . ' NO') || str_starts_with($line, $tag . ' BAD')) {
break;
}
}
return $rawEml;
}
public function disconnect(): void
{
if ($this->socket) {
try {
$this->sendCommand('LOGOUT');
} catch (\Throwable) {
// ignore
}
@fclose($this->socket);
$this->socket = null;
}
}
private function nextTag(): string
{
$this->tagIndex++;
return 'A' . str_pad((string) $this->tagIndex, 4, '0', STR_PAD_LEFT);
}
private function sendCommand(string $command): array
{
if (! $this->socket) {
throw new Exception("Socket IMAP non connesso.");
}
$tag = $this->nextTag();
$payload = "{$tag} {$command}\r\n";
fwrite($this->socket, $payload);
$lines = [];
$ok = false;
$response = '';
while (! feof($this->socket)) {
$line = $this->readLine();
$lines[] = $line;
if (str_starts_with($line, $tag . ' OK')) {
$ok = true;
$response = $line;
break;
}
if (str_starts_with($line, $tag . ' NO') || str_starts_with($line, $tag . ' BAD')) {
$ok = false;
$response = $line;
break;
}
}
return [
'ok' => $ok,
'response' => $response,
'lines' => $lines,
];
}
private function readLine(): string
{
$line = fgets($this->socket);
return $line !== false ? trim($line, "\r\n") : '';
}
}