refactor: use split socket location class

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-06-23 14:23:56 -04:00
parent db45636bd4
commit c8f0f8328b
7 changed files with 310 additions and 130 deletions
+5 -6
View File
@@ -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([
+1 -1
View File
@@ -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(
+4 -4
View File
@@ -151,7 +151,7 @@ class DiscoverCommand extends Command
/** @var array<string, ServiceLocation> $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) {
+1 -1
View File
@@ -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
+114 -54
View File
@@ -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) {
return new ConnectionConfig(
host: $this->inboundHost,
port: $this->inboundPort,
security: $this->imapSecurity($this->inboundEncryption),
username: $username,
password: $password,
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,
};
}
return new ConnectionConfig(
host: $this->host,
port: $this->port,
security: $security,
username: $username,
password: $password,
verifyPeer: $this->verifyPeer,
verifyPeerName: $this->verifyPeerName,
allowSelfSigned: $this->allowSelfSigned,
);
private function smtpSecurity(string $encryption): SmtpConnectionSecurity
{
return match ($encryption) {
'ssl', 'tls' => SmtpConnectionSecurity::Tls,
'starttls' => SmtpConnectionSecurity::StartTls,
default => SmtpConnectionSecurity::Plain,
};
}
}
+6 -5
View File
@@ -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) {
+177 -57
View File
@@ -1,26 +1,34 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { LocationSocketSole } from '@KTXM/MailManager/models/location'
import { LocationSocketSplit } from '@KTXM/MailManager/models/location'
import { ServiceObject } from '@KTXM/MailManager/models/service'
import type {
ServiceLocation,
ServiceLocationSocketSole,
ServiceLocationSocketSplit,
} from '@KTXM/MailManager/types/service'
import type {
ProviderProtocolPanelProps,
ProviderProtocolPanelEmits,
} from '@KTXM/MailManager/types/integration'
type ImapEncryption = 'none' | 'ssl' | 'tls' | 'starttls'
type Encryption = 'none' | 'ssl' | 'tls' | 'starttls'
const props = defineProps<ProviderProtocolPanelProps>()
const emit = defineEmits<ProviderProtocolPanelEmits>()
const host = ref('')
const encryption = ref<ImapEncryption>('ssl')
const port = ref('993')
const verifyPeer = ref(true)
const verifyHost = ref(true)
// ── Inbound (IMAP) ──────────────────────────────────────────────────────────
const inboundHost = ref('')
const inboundEncryption = ref<Encryption>('ssl')
const inboundPort = ref('993')
const inboundVerifyPeer = ref(true)
const inboundVerifyHost = ref(true)
// ── Outbound (SMTP) ─────────────────────────────────────────────────────────
const outboundHost = ref('')
const outboundEncryption = ref<Encryption>('starttls')
const outboundPort = ref('587')
const outboundVerifyPeer = ref(true)
const outboundVerifyHost = ref(true)
const encryptionOptions = [
{ title: 'Implicit TLS (SSL)', value: 'ssl' },
@@ -36,23 +44,30 @@ const rules = {
return Number.isInteger(numericValue) && numericValue >= 1 && numericValue <= 65535
? true
: 'Port must be between 1 and 65535'
}
},
}
const isValid = computed(() => !!host.value && rules.port(port.value) === true)
const inboundValid = computed(() => !!inboundHost.value && rules.port(inboundPort.value) === true)
const outboundValid = computed(() => !!outboundHost.value && rules.port(outboundPort.value) === true)
const isValid = computed(() => inboundValid.value && outboundValid.value)
const currentLocation = computed((): ServiceLocationSocketSole | null => {
const currentLocation = computed((): ServiceLocationSocketSplit | null => {
if (!isValid.value) {
return null
}
return {
type: 'SOCKET_SOLE',
host: host.value,
port: Number(port.value),
encryption: encryption.value,
verifyPeer: verifyPeer.value,
verifyHost: verifyHost.value,
type: 'SOCKET_SPLIT',
inboundHost: inboundHost.value,
inboundPort: Number(inboundPort.value),
inboundEncryption: inboundEncryption.value,
inboundVerifyPeer: inboundVerifyPeer.value,
inboundVerifyHost: inboundVerifyHost.value,
outboundHost: outboundHost.value,
outboundPort: Number(outboundPort.value),
outboundEncryption: outboundEncryption.value,
outboundVerifyPeer: outboundVerifyPeer.value,
outboundVerifyHost: outboundVerifyHost.value,
}
})
@@ -80,14 +95,20 @@ watch(
return
}
if (nextService.location instanceof LocationSocketSole) {
nextService.location.host = location.host
nextService.location.port = location.port
nextService.location.encryption = location.encryption
nextService.location.verifyPeer = location.verifyPeer ?? true
nextService.location.verifyHost = location.verifyHost ?? true
if (nextService.location instanceof LocationSocketSplit) {
const target = nextService.location
target.inboundHost = location.inboundHost
target.inboundPort = location.inboundPort
target.inboundEncryption = location.inboundEncryption
target.inboundVerifyPeer = location.inboundVerifyPeer ?? true
target.inboundVerifyHost = location.inboundVerifyHost ?? true
target.outboundHost = location.outboundHost
target.outboundPort = location.outboundPort
target.outboundEncryption = location.outboundEncryption
target.outboundVerifyPeer = location.outboundVerifyPeer ?? true
target.outboundVerifyHost = location.outboundVerifyHost ?? true
} else {
nextService.location = LocationSocketSole.fromJson(location)
nextService.location = LocationSocketSplit.fromJson(location)
}
emit('update:service', nextService)
@@ -95,25 +116,34 @@ watch(
{ immediate: true, deep: true }
)
watch(encryption, (next, previous) => {
const previousDefault = defaultPortFor(previous)
if (!port.value || Number(port.value) === previousDefault) {
port.value = String(defaultPortFor(next))
watch(inboundEncryption, (next, previous) => {
const previousDefault = defaultInboundPort(previous)
if (!inboundPort.value || Number(inboundPort.value) === previousDefault) {
inboundPort.value = String(defaultInboundPort(next))
}
})
watch(outboundEncryption, (next, previous) => {
const previousDefault = defaultOutboundPort(previous)
if (!outboundPort.value || Number(outboundPort.value) === previousDefault) {
outboundPort.value = String(defaultOutboundPort(next))
}
})
function syncFromLocation(location: ServiceLocation | null) {
const socketLocation = getSocketLocation(location)
const split = location?.type === 'SOCKET_SPLIT' ? location : null
host.value = socketLocation?.host ?? ''
encryption.value = socketLocation?.encryption ?? 'ssl'
port.value = String(socketLocation?.port ?? defaultPortFor(encryption.value))
verifyPeer.value = socketLocation?.verifyPeer ?? true
verifyHost.value = socketLocation?.verifyHost ?? true
}
inboundHost.value = split?.inboundHost ?? ''
inboundEncryption.value = split?.inboundEncryption ?? 'ssl'
inboundPort.value = String(split?.inboundPort ?? defaultInboundPort(inboundEncryption.value))
inboundVerifyPeer.value = split?.inboundVerifyPeer ?? true
inboundVerifyHost.value = split?.inboundVerifyHost ?? true
function getSocketLocation(location?: ServiceLocation | null): ServiceLocationSocketSole | null {
return location?.type === 'SOCKET_SOLE' ? location : null
outboundHost.value = split?.outboundHost ?? ''
outboundEncryption.value = split?.outboundEncryption ?? 'starttls'
outboundPort.value = String(split?.outboundPort ?? defaultOutboundPort(outboundEncryption.value))
outboundVerifyPeer.value = split?.outboundVerifyPeer ?? true
outboundVerifyHost.value = split?.outboundVerifyHost ?? true
}
function sameLocation(a: ServiceLocation | null, b: ServiceLocation | null): boolean {
@@ -121,30 +151,51 @@ function sameLocation(a: ServiceLocation | null, b: ServiceLocation | null): boo
return a === b
}
if (a.type !== 'SOCKET_SOLE' || b.type !== 'SOCKET_SOLE') {
if (a.type !== 'SOCKET_SPLIT' || b.type !== 'SOCKET_SPLIT') {
return false
}
return a.host === b.host
&& a.port === b.port
&& a.encryption === b.encryption
&& (a.verifyPeer ?? true) === (b.verifyPeer ?? true)
&& (a.verifyHost ?? true) === (b.verifyHost ?? true)
return a.inboundHost === b.inboundHost
&& a.inboundPort === b.inboundPort
&& a.inboundEncryption === b.inboundEncryption
&& (a.inboundVerifyPeer ?? true) === (b.inboundVerifyPeer ?? true)
&& (a.inboundVerifyHost ?? true) === (b.inboundVerifyHost ?? true)
&& a.outboundHost === b.outboundHost
&& a.outboundPort === b.outboundPort
&& a.outboundEncryption === b.outboundEncryption
&& (a.outboundVerifyPeer ?? true) === (b.outboundVerifyPeer ?? true)
&& (a.outboundVerifyHost ?? true) === (b.outboundVerifyHost ?? true)
}
function defaultPortFor(nextEncryption: ImapEncryption): number {
return nextEncryption === 'ssl' || nextEncryption === 'tls' ? 993 : 143
function defaultInboundPort(encryption: Encryption): number {
return encryption === 'ssl' || encryption === 'tls' ? 993 : 143
}
function defaultOutboundPort(encryption: Encryption): number {
if (encryption === 'ssl') {
return 465
}
if (encryption === 'none') {
return 25
}
return 587
}
</script>
<template>
<div class="imap-protocol-panel">
<h3 class="text-h6 mb-4">IMAP Connection Settings</h3>
<p class="text-body-2 mb-6">Configure the server address, transport security, and certificate verification for your IMAP mailbox.</p>
<h3 class="text-h6 mb-4">Connection Settings</h3>
<p class="text-body-2 mb-6">Configure the incoming (IMAP) and outgoing (SMTP) servers, transport security, and certificate verification for this mailbox.</p>
<!-- Incoming (IMAP) -->
<div class="d-flex align-center mb-4">
<v-icon start color="primary">mdi-email-arrow-left</v-icon>
<span class="text-subtitle-1 font-weight-medium">Incoming mail (IMAP)</span>
</div>
<v-text-field
v-model="host"
label="Server Host"
v-model="inboundHost"
label="IMAP Server Host"
hint="For example: imap.example.com"
persistent-hint
variant="outlined"
@@ -157,7 +208,7 @@ function defaultPortFor(nextEncryption: ImapEncryption): number {
/>
<v-select
v-model="encryption"
v-model="inboundEncryption"
:items="encryptionOptions"
label="Security"
variant="outlined"
@@ -166,7 +217,7 @@ function defaultPortFor(nextEncryption: ImapEncryption): number {
/>
<v-text-field
v-model="port"
v-model="inboundPort"
label="Port"
hint="Defaults to 993 for TLS/SSL and 143 for plain or STARTTLS"
persistent-hint
@@ -179,7 +230,7 @@ function defaultPortFor(nextEncryption: ImapEncryption): number {
:rules="[rules.required, rules.port]"
/>
<v-expansion-panels class="mt-4">
<v-expansion-panels class="mb-6">
<v-expansion-panel>
<v-expansion-panel-title>
<v-icon start>mdi-cog</v-icon>
@@ -187,7 +238,7 @@ function defaultPortFor(nextEncryption: ImapEncryption): number {
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-switch
v-model="verifyPeer"
v-model="inboundVerifyPeer"
label="Verify TLS certificate"
color="primary"
hint="Disable only for trusted internal or test environments"
@@ -196,7 +247,7 @@ function defaultPortFor(nextEncryption: ImapEncryption): number {
/>
<v-switch
v-model="verifyHost"
v-model="inboundVerifyHost"
label="Verify certificate hostname"
color="primary"
hint="Checks that the certificate matches the IMAP host"
@@ -207,9 +258,78 @@ function defaultPortFor(nextEncryption: ImapEncryption): number {
</v-expansion-panel>
</v-expansion-panels>
<v-alert type="info" variant="tonal">
STARTTLS is accepted for compatibility, but the current IMAP client transport does not perform STARTTLS negotiation. Prefer TLS on port 993 when available.
</v-alert>
<v-divider class="mb-6" />
<!-- Outgoing (SMTP) -->
<div class="d-flex align-center mb-4">
<v-icon start color="primary">mdi-email-arrow-right</v-icon>
<span class="text-subtitle-1 font-weight-medium">Outgoing mail (SMTP)</span>
</div>
<v-text-field
v-model="outboundHost"
label="SMTP Server Host"
hint="For example: smtp.example.com"
persistent-hint
variant="outlined"
prepend-inner-icon="mdi-server-network"
class="mb-4"
autocomplete="off"
autocorrect="off"
autocapitalize="none"
:rules="[rules.required]"
/>
<v-select
v-model="outboundEncryption"
:items="encryptionOptions"
label="Security"
variant="outlined"
prepend-inner-icon="mdi-shield-lock"
class="mb-4"
/>
<v-text-field
v-model="outboundPort"
label="Port"
hint="Defaults to 465 for implicit TLS, 587 for STARTTLS, and 25 for plain"
persistent-hint
variant="outlined"
prepend-inner-icon="mdi-numeric"
class="mb-4"
type="number"
min="1"
max="65535"
:rules="[rules.required, rules.port]"
/>
<v-expansion-panels>
<v-expansion-panel>
<v-expansion-panel-title>
<v-icon start>mdi-cog</v-icon>
Security Options
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-switch
v-model="outboundVerifyPeer"
label="Verify TLS certificate"
color="primary"
hint="Disable only for trusted internal or test environments"
persistent-hint
class="mb-4"
/>
<v-switch
v-model="outboundVerifyHost"
label="Verify certificate hostname"
color="primary"
hint="Checks that the certificate matches the SMTP host"
persistent-hint
class="mb-4"
/>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</div>
</template>