From c8f0f8328bbdae3469112c16135b2b458e3e7166 Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Tue, 23 Jun 2026 14:23:56 -0400 Subject: [PATCH] refactor: use split socket location class Signed-off-by: Sebastian Krupinski --- lib/Console/ConnectCommand.php | 11 +- lib/Console/DisconnectCommand.php | 2 +- lib/Console/DiscoverCommand.php | 8 +- lib/Console/TestCommand.php | 2 +- lib/Providers/ServiceLocation.php | 170 ++++++++++++------- lib/Service/Discovery.php | 11 +- src/components/ImapProtocolPanel.vue | 236 ++++++++++++++++++++------- 7 files changed, 310 insertions(+), 130 deletions(-) diff --git a/lib/Console/ConnectCommand.php b/lib/Console/ConnectCommand.php index fa1340b..0080613 100644 --- a/lib/Console/ConnectCommand.php +++ b/lib/Console/ConnectCommand.php @@ -150,12 +150,11 @@ class ConnectCommand extends Command // ── Build service object ──────────────────────────────────────────── $location = new ServiceLocation( - host: $host, - port: $port > 0 ? $port : 993, - encryption: $encryption, - verifyPeer: !$noVerify, - verifyPeerName: !$noVerify, - allowSelfSigned: $noVerify, + inboundHost: $host, + inboundPort: $port > 0 ? $port : 993, + inboundEncryption: $encryption, + inboundVerifyPeer: !$noVerify, + inboundVerifyHost: !$noVerify, ); $identity = (new ServiceIdentityBasic())->jsonDeserialize([ diff --git a/lib/Console/DisconnectCommand.php b/lib/Console/DisconnectCommand.php index 7314654..5ea6a88 100644 --- a/lib/Console/DisconnectCommand.php +++ b/lib/Console/DisconnectCommand.php @@ -139,7 +139,7 @@ class DisconnectCommand extends Command } $label = $service->getLabel() ?? $serviceId; - $host = $service->getLocation()?->getHost() ?? 'unknown'; + $host = $service->getLocation()?->getInboundHost() ?? 'unknown'; $io->title('Disconnect IMAP Service'); $io->definitionList( diff --git a/lib/Console/DiscoverCommand.php b/lib/Console/DiscoverCommand.php index 1b51f42..9b716a8 100644 --- a/lib/Console/DiscoverCommand.php +++ b/lib/Console/DiscoverCommand.php @@ -151,7 +151,7 @@ class DiscoverCommand extends Command /** @var array $choiceMap */ $choiceMap = []; foreach ($candidates as $c) { - $label = sprintf('%s : %d [%s]', $c->getHost(), $c->getPort(), $encLabel($c->getEncryption())); + $label = sprintf('%s : %d [%s]', $c->getInboundHost(), $c->getInboundPort(), $encLabel($c->getInboundEncryption())); $choiceMap[$label] = $c; } @@ -170,9 +170,9 @@ class DiscoverCommand extends Command $io->success('Server selected:'); $io->definitionList( - ['Host' => $location->getHost()], - ['Port' => (string) $location->getPort()], - ['Encryption' => $encLabel($location->getEncryption())], + ['Host' => $location->getInboundHost()], + ['Port' => (string) $location->getInboundPort()], + ['Encryption' => $encLabel($location->getInboundEncryption())], ); if (!$save) { diff --git a/lib/Console/TestCommand.php b/lib/Console/TestCommand.php index 807dd92..2b4196c 100644 --- a/lib/Console/TestCommand.php +++ b/lib/Console/TestCommand.php @@ -308,7 +308,7 @@ class TestCommand extends Command return 'unknown'; } - return sprintf('%s://%s:%d', $location->getEncryption(), $location->getHost(), $location->getPort()); + return sprintf('%s://%s:%d', $location->getInboundEncryption(), $location->getInboundHost(), $location->getInboundPort()); } private function formatSender(Message $message): string diff --git a/lib/Providers/ServiceLocation.php b/lib/Providers/ServiceLocation.php index c8cc197..27e75f3 100644 --- a/lib/Providers/ServiceLocation.php +++ b/lib/Providers/ServiceLocation.php @@ -11,22 +11,29 @@ namespace KTXM\ProviderImap\Providers; use KTXM\ProviderImap\Client\ConnectionConfig; use KTXM\ProviderImap\Client\ConnectionSecurity; -use KTXF\Resource\Provider\ResourceServiceLocationInterface; +use KTXM\ProviderImap\Smtp\ConnectionConfig as SmtpConnectionConfig; +use KTXM\ProviderImap\Smtp\ConnectionSecurity as SmtpConnectionSecurity; +use KTXF\Resource\Provider\ResourceServiceLocationSocketSplit; /** - * IMAP Service Location + * IMAP/SMTP Service Location * - * Connection details for an IMAP server (host / port / encryption). + * Split socket location: the inbound side describes the IMAP server, the + * outbound side describes the SMTP submission server. */ -class ServiceLocation implements ResourceServiceLocationInterface +class ServiceLocation implements ResourceServiceLocationSocketSplit { public function __construct( - private string $host = '', - private int $port = 993, - private string $encryption = 'ssl', // ssl | tls | starttls | none - private bool $verifyPeer = true, - private bool $verifyPeerName = true, - private bool $allowSelfSigned = false, + private string $inboundHost = '', + private int $inboundPort = 993, + private string $inboundEncryption = 'ssl', // none | ssl | tls | starttls + private bool $inboundVerifyPeer = true, + private bool $inboundVerifyHost = true, + private string $outboundHost = '', + private int $outboundPort = 587, + private string $outboundEncryption = 'starttls', // none | ssl | tls | starttls + private bool $outboundVerifyPeer = true, + private bool $outboundVerifyHost = true, ) {} // ── Serialisation ──────────────────────────────────────────────────────── @@ -43,15 +50,19 @@ class ServiceLocation implements ResourceServiceLocationInterface public function jsonSerialize(): array { - return array_filter([ - 'type' => self::TYPE_URI, - 'host' => $this->host, - 'port' => $this->port, - 'encryption' => $this->encryption, - 'verifyPeer' => $this->verifyPeer, - 'verifyPeerName' => $this->verifyPeerName, - 'allowSelfSigned' => $this->allowSelfSigned, - ], fn($v) => $v !== null && $v !== ''); + return [ + 'type' => self::TYPE_SOCKET_SPLIT, + 'inboundHost' => $this->inboundHost, + 'inboundPort' => $this->inboundPort, + 'inboundEncryption' => $this->inboundEncryption, + 'inboundVerifyPeer' => $this->inboundVerifyPeer, + 'inboundVerifyHost' => $this->inboundVerifyHost, + 'outboundHost' => $this->outboundHost, + 'outboundPort' => $this->outboundPort, + 'outboundEncryption' => $this->outboundEncryption, + 'outboundVerifyPeer' => $this->outboundVerifyPeer, + 'outboundVerifyHost' => $this->outboundVerifyHost, + ]; } public function jsonDeserialize(array|string $data): static @@ -60,70 +71,119 @@ class ServiceLocation implements ResourceServiceLocationInterface $data = json_decode($data, true); } - $this->host = $data['host'] ?? ''; - $this->port = (int)($data['port'] ?? 993); - $this->encryption = $data['encryption'] ?? 'ssl'; - $this->verifyPeer = $data['verifyPeer'] ?? true; - $this->verifyPeerName = $data['verifyPeerName'] ?? true; - $this->allowSelfSigned = $data['allowSelfSigned'] ?? false; + $this->inboundHost = $data['inboundHost'] ?? ''; + $this->inboundPort = (int) ($data['inboundPort'] ?? 993); + $this->inboundEncryption = $data['inboundEncryption'] ?? 'ssl'; + $this->inboundVerifyPeer = $data['inboundVerifyPeer'] ?? true; + $this->inboundVerifyHost = $data['inboundVerifyHost'] ?? true; + + $this->outboundHost = $data['outboundHost'] ?? ''; + $this->outboundPort = (int) ($data['outboundPort'] ?? 587); + $this->outboundEncryption = $data['outboundEncryption'] ?? 'starttls'; + $this->outboundVerifyPeer = $data['outboundVerifyPeer'] ?? true; + $this->outboundVerifyHost = $data['outboundVerifyHost'] ?? true; return $this; } - // ── ResourceServiceLocationInterface ───────────────────────────────────── + // ── ResourceServiceLocationSocketSplit ─────────────────────────────────── public function type(): string { - return self::TYPE_URI; + return self::TYPE_SOCKET_SPLIT; } - public function location(): string + public function locationInbound(): string { - return $this->encryption . '://' . $this->host . ':' . $this->port; + return $this->inboundEncryption . '://' . $this->inboundHost . ':' . $this->inboundPort; } - // ── Accessors ──────────────────────────────────────────────────────────── + public function locationOutbound(): string + { + return $this->outboundEncryption . '://' . $this->outboundHost . ':' . $this->outboundPort; + } - public function getHost(): string { return $this->host; } - public function setHost(string $v): void { $this->host = $v; } + public function getInboundHost(): string { return $this->inboundHost; } + public function setInboundHost(string $value): void { $this->inboundHost = $value; } - public function getPort(): int { return $this->port; } - public function setPort(int $v): void { $this->port = $v; } + public function getOutboundHost(): string { return $this->outboundHost; } + public function setOutboundHost(string $value): void { $this->outboundHost = $value; } - public function getEncryption(): string { return $this->encryption; } - public function setEncryption(string $v): void { $this->encryption = $v; } + public function getInboundPort(): int { return $this->inboundPort; } + public function setInboundPort(int $value): void { $this->inboundPort = $value; } - public function getVerifyPeer(): bool { return $this->verifyPeer; } - public function setVerifyPeer(bool $v): void { $this->verifyPeer = $v; } + public function getOutboundPort(): int { return $this->outboundPort; } + public function setOutboundPort(int $value): void { $this->outboundPort = $value; } - public function getVerifyPeerName(): bool { return $this->verifyPeerName; } - public function setVerifyPeerName(bool $v): void { $this->verifyPeerName = $v; } + public function getInboundEncryption(): string { return $this->inboundEncryption; } + public function setInboundEncryption(string $value): void { $this->inboundEncryption = $value; } - public function getAllowSelfSigned(): bool { return $this->allowSelfSigned; } - public function setAllowSelfSigned(bool $v): void { $this->allowSelfSigned = $v; } + public function getOutboundEncryption(): string { return $this->outboundEncryption; } + public function setOutboundEncryption(string $value): void { $this->outboundEncryption = $value; } + + public function getInboundVerifyPeer(): bool { return $this->inboundVerifyPeer; } + public function setInboundVerifyPeer(bool $value): void { $this->inboundVerifyPeer = $value; } + + public function getInboundVerifyHost(): bool { return $this->inboundVerifyHost; } + public function setInboundVerifyHost(bool $value): void { $this->inboundVerifyHost = $value; } + + public function getOutboundVerifyPeer(): bool { return $this->outboundVerifyPeer; } + public function setOutboundVerifyPeer(bool $value): void { $this->outboundVerifyPeer = $value; } + + public function getOutboundVerifyHost(): bool { return $this->outboundVerifyHost; } + public function setOutboundVerifyHost(bool $value): void { $this->outboundVerifyHost = $value; } // ── Client helpers ─────────────────────────────────────────────────────── /** - * Build a standalone IMAP client ConnectionConfig from this location. + * Build an IMAP ConnectionConfig from the inbound side. */ public function toConnectionConfig(?string $username = null, ?string $password = null): ConnectionConfig { - $security = match ($this->encryption) { - 'ssl', 'tls' => ConnectionSecurity::Tls, - 'starttls' => ConnectionSecurity::StartTls, - default => ConnectionSecurity::Plain, - }; - return new ConnectionConfig( - host: $this->host, - port: $this->port, - security: $security, + host: $this->inboundHost, + port: $this->inboundPort, + security: $this->imapSecurity($this->inboundEncryption), username: $username, password: $password, - verifyPeer: $this->verifyPeer, - verifyPeerName: $this->verifyPeerName, - allowSelfSigned: $this->allowSelfSigned, + verifyPeer: $this->inboundVerifyPeer, + verifyPeerName: $this->inboundVerifyHost, + allowSelfSigned: !$this->inboundVerifyPeer, ); } + + /** + * Build an SMTP submission ConnectionConfig from the outbound side. + */ + public function toSmtpConnectionConfig(?string $username = null, ?string $password = null): SmtpConnectionConfig + { + return new SmtpConnectionConfig( + host: $this->outboundHost, + port: $this->outboundPort, + security: $this->smtpSecurity($this->outboundEncryption), + username: $username, + password: $password, + verifyPeer: $this->outboundVerifyPeer, + verifyPeerName: $this->outboundVerifyHost, + allowSelfSigned: !$this->outboundVerifyPeer, + ); + } + + private function imapSecurity(string $encryption): ConnectionSecurity + { + return match ($encryption) { + 'ssl', 'tls' => ConnectionSecurity::Tls, + 'starttls' => ConnectionSecurity::StartTls, + default => ConnectionSecurity::Plain, + }; + } + + private function smtpSecurity(string $encryption): SmtpConnectionSecurity + { + return match ($encryption) { + 'ssl', 'tls' => SmtpConnectionSecurity::Tls, + 'starttls' => SmtpConnectionSecurity::StartTls, + default => SmtpConnectionSecurity::Plain, + }; + } } diff --git a/lib/Service/Discovery.php b/lib/Service/Discovery.php index 9ffec5f..f0dd574 100644 --- a/lib/Service/Discovery.php +++ b/lib/Service/Discovery.php @@ -110,7 +110,7 @@ class Discovery ]; $seen = []; foreach ($results as $r) { - $seen[$r->getHost()] = true; + $seen[$r->getInboundHost()] = true; } foreach ($candidates as $host) { if (isset($seen[$host])) { @@ -203,10 +203,11 @@ class Discovery $loc = new ServiceLocation(); $loc->jsonDeserialize([ - 'host' => $host, - 'port' => $port, - 'encryption' => $encryption, - 'verifyPeer' => $verifySSL, + 'inboundHost' => $host, + 'inboundPort' => $port, + 'inboundEncryption' => $encryption, + 'inboundVerifyPeer' => $verifySSL, + 'inboundVerifyHost' => $verifySSL, ]); return $loc; } catch (\Throwable) { diff --git a/src/components/ImapProtocolPanel.vue b/src/components/ImapProtocolPanel.vue index 3fd7837..d76808f 100644 --- a/src/components/ImapProtocolPanel.vue +++ b/src/components/ImapProtocolPanel.vue @@ -1,26 +1,34 @@ @@ -232,4 +352,4 @@ function defaultPortFor(nextEncryption: ImapEncryption): number { letter-spacing: 0.0178571429em; color: rgba(var(--v-theme-on-surface), 0.7); } - \ No newline at end of file +