212 lines
6.1 KiB
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") : '';
|
|
}
|
|
}
|