generated from Nodarx/template
refactor: use custom imap client
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Transport;
|
||||
|
||||
use Exception;
|
||||
|
||||
interface Connection
|
||||
{
|
||||
public function isOpen(): bool;
|
||||
|
||||
public function open(): void;
|
||||
|
||||
public function close(): void;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function send(string $data): void;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function receive(): ResponseStream;
|
||||
}
|
||||
13
lib/Client/Transport/ConnectionFactoryInterface.php
Normal file
13
lib/Client/Transport/ConnectionFactoryInterface.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ProviderImap\Client\Transport;
|
||||
|
||||
use KTXM\ProviderImap\Client\ConnectionConfig;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
interface ConnectionFactoryInterface
|
||||
{
|
||||
public function create(ConnectionConfig $config, ?LoggerInterface $logger = null): ConnectionInterface;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Transport;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class ConnectionFailed extends RuntimeException
|
||||
{
|
||||
}
|
||||
24
lib/Client/Transport/ConnectionInterface.php
Normal file
24
lib/Client/Transport/ConnectionInterface.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ProviderImap\Client\Transport;
|
||||
|
||||
use KTXM\ProviderImap\Client\ConnectionConfig;
|
||||
|
||||
interface ConnectionInterface
|
||||
{
|
||||
public function connect(ConnectionConfig $config): void;
|
||||
|
||||
public function disconnect(): void;
|
||||
|
||||
public function isConnected(): bool;
|
||||
|
||||
public function write(string $payload): void;
|
||||
|
||||
public function readLine(): string;
|
||||
|
||||
public function readBytes(int $length): string;
|
||||
|
||||
public function upgradeToTls(): void;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Transport;
|
||||
|
||||
interface ResponseStream
|
||||
{
|
||||
public function read(int $bytes): string;
|
||||
|
||||
public function readLine(): string;
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Transport\Socket;
|
||||
|
||||
use Exception;
|
||||
use Gricob\IMAP\Transport\Connection;
|
||||
use Gricob\IMAP\Transport\ConnectionFailed;
|
||||
use Gricob\IMAP\Transport\ResponseStream;
|
||||
|
||||
class SocketConnection implements Connection
|
||||
{
|
||||
private string $transport;
|
||||
private string $host;
|
||||
private int $port;
|
||||
private float $timeout;
|
||||
private bool $verifyPeer;
|
||||
private bool $allowSelfSigned;
|
||||
private bool $verifyPeerName;
|
||||
|
||||
/**
|
||||
* @var resource|false
|
||||
*/
|
||||
private $stream = false;
|
||||
|
||||
public function __construct(
|
||||
string $transport,
|
||||
string $host,
|
||||
int $port,
|
||||
float $timeout,
|
||||
bool $verifyPeer = true,
|
||||
bool $verifyPeerName = true,
|
||||
bool $allowSelfSigned = false,
|
||||
) {
|
||||
$this->port = $port;
|
||||
$this->host = $host;
|
||||
$this->transport = $transport;
|
||||
$this->timeout = $timeout;
|
||||
$this->verifyPeer = $verifyPeer;
|
||||
$this->verifyPeerName = $verifyPeerName;
|
||||
$this->allowSelfSigned = $allowSelfSigned;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
|
||||
public function isOpen(): bool
|
||||
{
|
||||
return false !== $this->stream;
|
||||
}
|
||||
|
||||
public function open(): void
|
||||
{
|
||||
if ($this->isOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->stream = @stream_socket_client(
|
||||
sprintf('%s://%s:%s', $this->transport, $this->host, $this->port),
|
||||
$errorCode,
|
||||
$errorMessage,
|
||||
$this->timeout,
|
||||
context: stream_context_create([
|
||||
'ssl' => [
|
||||
'verify_peer' => $this->verifyPeer,
|
||||
'verify_peer_name' => $this->verifyPeerName,
|
||||
'allow_self_signed' => $this->allowSelfSigned,
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
if (false === $this->stream) {
|
||||
throw new ConnectionFailed(
|
||||
sprintf('SocketConnection failed [%s]: %s', $errorCode, $errorMessage)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
{
|
||||
if (!$this->stream) {
|
||||
return;
|
||||
}
|
||||
|
||||
fclose($this->stream);
|
||||
|
||||
$this->stream = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade an open plain-TCP socket to TLS in-place (STARTTLS patch).
|
||||
*
|
||||
* Must be called after the server has responded OK to a STARTTLS command.
|
||||
*
|
||||
* @throws ConnectionFailed
|
||||
*/
|
||||
public function upgradeTls(): void
|
||||
{
|
||||
if (!$this->stream) {
|
||||
throw new ConnectionFailed('Cannot upgrade TLS: connection is not open');
|
||||
}
|
||||
|
||||
stream_context_set_option($this->stream, [
|
||||
'ssl' => [
|
||||
'peer_name' => $this->host,
|
||||
'verify_peer' => $this->verifyPeer,
|
||||
'verify_peer_name' => $this->verifyPeerName,
|
||||
'allow_self_signed' => $this->allowSelfSigned,
|
||||
],
|
||||
]);
|
||||
|
||||
$cryptoMethod = STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
|
||||
| STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT;
|
||||
|
||||
$result = @stream_socket_enable_crypto(
|
||||
$this->stream,
|
||||
true,
|
||||
$cryptoMethod,
|
||||
);
|
||||
|
||||
if ($result !== true) {
|
||||
$last = error_get_last();
|
||||
$detail = $last['message'] ?? 'unknown error';
|
||||
throw new ConnectionFailed('STARTTLS upgrade failed: ' . $detail);
|
||||
}
|
||||
}
|
||||
|
||||
public function send(string $data): void
|
||||
{
|
||||
if (!$this->stream) {
|
||||
throw new Exception('Unable to send data. SocketConnection is not open');
|
||||
}
|
||||
|
||||
fwrite($this->stream, $data);
|
||||
}
|
||||
|
||||
public function receive(): ResponseStream
|
||||
{
|
||||
if (!$this->stream) {
|
||||
throw new Exception('Unable to receive data. SocketConnection is not open');
|
||||
}
|
||||
|
||||
return new SocketResponseStream($this->stream);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Transport\Socket;
|
||||
|
||||
use Gricob\IMAP\Transport\ResponseStream;
|
||||
|
||||
final class SocketResponseStream implements ResponseStream
|
||||
{
|
||||
/**
|
||||
* @param resource $stream
|
||||
*/
|
||||
public function __construct(private $stream)
|
||||
{
|
||||
}
|
||||
|
||||
public function read(int $bytes): string
|
||||
{
|
||||
if ($bytes <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$remainingBytes = $bytes;
|
||||
$data = '';
|
||||
do {
|
||||
$data .= fread($this->stream, $remainingBytes);
|
||||
$remainingBytes = $bytes - strlen($data);
|
||||
} while ($remainingBytes > 0);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function readLine(): string
|
||||
{
|
||||
$line = '';
|
||||
|
||||
while ("\n" !== ($char = fread($this->stream, 1))) {
|
||||
$line .= $char;
|
||||
}
|
||||
|
||||
return $line."\n";
|
||||
}
|
||||
}
|
||||
162
lib/Client/Transport/SocketConnection.php
Normal file
162
lib/Client/Transport/SocketConnection.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ProviderImap\Client\Transport;
|
||||
|
||||
use KTXM\ProviderImap\Client\ConnectionConfig;
|
||||
use KTXM\ProviderImap\Client\ImapException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class SocketConnection implements ConnectionInterface
|
||||
{
|
||||
/** @var resource|null */
|
||||
private $stream = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly ConnectionConfig $config,
|
||||
private readonly ?LoggerInterface $logger = null,
|
||||
) {}
|
||||
|
||||
public function connect(ConnectionConfig $config): void
|
||||
{
|
||||
if ($this->isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$context = stream_context_create($config->streamContextOptions());
|
||||
$errorCode = 0;
|
||||
$errorMessage = '';
|
||||
|
||||
$stream = @stream_socket_client(
|
||||
$config->endpoint(),
|
||||
$errorCode,
|
||||
$errorMessage,
|
||||
$config->timeout(),
|
||||
STREAM_CLIENT_CONNECT,
|
||||
$context,
|
||||
);
|
||||
|
||||
if (!is_resource($stream)) {
|
||||
$this->logger?->error('IMAP socket connection failed', [
|
||||
'endpoint' => $config->endpoint(),
|
||||
'code' => $errorCode,
|
||||
'message' => $errorMessage ?: 'unknown error',
|
||||
]);
|
||||
|
||||
throw new ImapException(sprintf(
|
||||
'Unable to connect to %s (%d: %s)',
|
||||
$config->endpoint(),
|
||||
$errorCode,
|
||||
$errorMessage ?: 'unknown error',
|
||||
));
|
||||
}
|
||||
|
||||
stream_set_timeout($stream, (int) $config->timeout());
|
||||
$this->stream = $stream;
|
||||
|
||||
$this->logger?->info('IMAP socket connected to {endpoint} (timeout={timeout})', [
|
||||
'endpoint' => $config->endpoint(),
|
||||
'timeout' => $config->timeout(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function disconnect(): void
|
||||
{
|
||||
if (!is_resource($this->stream)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fclose($this->stream);
|
||||
$this->stream = null;
|
||||
|
||||
$this->logger?->info('IMAP socket disconnected');
|
||||
}
|
||||
|
||||
public function isConnected(): bool
|
||||
{
|
||||
return is_resource($this->stream);
|
||||
}
|
||||
|
||||
public function write(string $payload): void
|
||||
{
|
||||
$stream = $this->stream();
|
||||
$written = fwrite($stream, $payload);
|
||||
|
||||
if ($written === false || $written !== strlen($payload)) {
|
||||
$this->logger?->error('IMAP socket write failed (bytes={bytes})', [
|
||||
'bytes' => strlen($payload),
|
||||
]);
|
||||
|
||||
throw new ImapException('Failed to write complete payload to IMAP socket.');
|
||||
}
|
||||
}
|
||||
|
||||
public function readLine(): string
|
||||
{
|
||||
$stream = $this->stream();
|
||||
$line = fgets($stream);
|
||||
|
||||
if ($line === false) {
|
||||
$this->logger?->error('IMAP socket read failed');
|
||||
throw new ImapException('Failed to read line from IMAP socket.');
|
||||
}
|
||||
|
||||
return $line;
|
||||
}
|
||||
|
||||
public function readBytes(int $length): string
|
||||
{
|
||||
if ($length < 0) {
|
||||
throw new ImapException('IMAP socket cannot read a negative number of bytes.');
|
||||
}
|
||||
|
||||
if ($length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$stream = $this->stream();
|
||||
$buffer = '';
|
||||
|
||||
while (strlen($buffer) < $length) {
|
||||
$chunk = fread($stream, $length - strlen($buffer));
|
||||
|
||||
if ($chunk === false || $chunk === '') {
|
||||
$this->logger?->error('IMAP socket literal read failed (bytes={bytes})', [
|
||||
'bytes' => $length,
|
||||
]);
|
||||
|
||||
throw new ImapException('Failed to read literal payload from IMAP socket.');
|
||||
}
|
||||
|
||||
$buffer .= $chunk;
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
}
|
||||
|
||||
public function upgradeToTls(): void
|
||||
{
|
||||
$stream = $this->stream();
|
||||
$result = stream_socket_enable_crypto($stream, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
|
||||
|
||||
if ($result !== true) {
|
||||
$this->logger?->error('IMAP TLS upgrade failed');
|
||||
throw new ImapException('Failed to enable TLS on IMAP socket.');
|
||||
}
|
||||
|
||||
$this->logger?->info('IMAP socket upgraded to TLS');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return resource
|
||||
*/
|
||||
private function stream()
|
||||
{
|
||||
if (!is_resource($this->stream)) {
|
||||
throw new ImapException('IMAP socket is not connected.');
|
||||
}
|
||||
|
||||
return $this->stream;
|
||||
}
|
||||
}
|
||||
16
lib/Client/Transport/SocketConnectionFactory.php
Normal file
16
lib/Client/Transport/SocketConnectionFactory.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ProviderImap\Client\Transport;
|
||||
|
||||
use KTXM\ProviderImap\Client\ConnectionConfig;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final class SocketConnectionFactory implements ConnectionFactoryInterface
|
||||
{
|
||||
public function create(ConnectionConfig $config, ?LoggerInterface $logger = null): ConnectionInterface
|
||||
{
|
||||
return new SocketConnection($config, $logger);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Transport\Traceable;
|
||||
|
||||
use Gricob\IMAP\Transport\Connection;
|
||||
use Gricob\IMAP\Transport\ResponseStream;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final readonly class TraceableConnection implements Connection
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
public function isOpen(): bool
|
||||
{
|
||||
return $this->connection->isOpen();
|
||||
}
|
||||
|
||||
public function open(): void
|
||||
{
|
||||
$this->connection->open();
|
||||
}
|
||||
|
||||
public function close(): void
|
||||
{
|
||||
$this->connection->close();
|
||||
}
|
||||
|
||||
public function send(string $data): void
|
||||
{
|
||||
$this->debug(addslashes($data));
|
||||
|
||||
$this->connection->send($data);
|
||||
}
|
||||
|
||||
public function receive(): ResponseStream
|
||||
{
|
||||
return new TraceableResponseStream(
|
||||
$this->connection->receive(),
|
||||
$this->logger,
|
||||
);
|
||||
}
|
||||
|
||||
private function debug(string $data): void
|
||||
{
|
||||
$data = str_replace("\r\n", "\\r\\n", $data);
|
||||
|
||||
$this->logger->debug($data);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP\Transport\Traceable;
|
||||
|
||||
use Gricob\IMAP\Transport\ResponseStream;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final readonly class TraceableResponseStream implements ResponseStream
|
||||
{
|
||||
public function __construct(
|
||||
private ResponseStream $responseStream,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
public function read(int $bytes): string
|
||||
{
|
||||
$data = $this->responseStream->read($bytes);
|
||||
|
||||
$this->debug($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function readLine(): string
|
||||
{
|
||||
$line = $this->responseStream->readLine();
|
||||
|
||||
$this->debug($line);
|
||||
|
||||
return $line;
|
||||
}
|
||||
|
||||
private function debug(string $data): void
|
||||
{
|
||||
// $data = addslashes($data);
|
||||
// $data = str_replace("\r\n", "\\r\\n", $data);
|
||||
|
||||
$this->logger->debug($data);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user