Files
provider_imap/src/components/ImapProtocolPanel.vue
2026-04-23 22:03:17 -04:00

234 lines
6.4 KiB
Vue

<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { LocationSocketSole } from '@KTXM/MailManager/models/location'
import { ServiceObject } from '@KTXM/MailManager/models/service'
import type {
ServiceLocation,
ServiceLocationSocketSole,
} from '@KTXM/MailManager/types/service'
import type {
ProviderProtocolPanelProps,
ProviderProtocolPanelEmits,
} from '@KTXM/MailManager/types/integration'
type ImapEncryption = '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)
const encryptionOptions = [
{ title: 'Implicit TLS (SSL)', value: 'ssl' },
{ title: 'TLS', value: 'tls' },
{ title: 'STARTTLS', value: 'starttls' },
{ title: 'None', value: 'none' },
]
const rules = {
required: (value: unknown) => !!value || 'This field is required',
port: (value: string) => {
const numericValue = Number(value)
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 currentLocation = computed((): ServiceLocationSocketSole | 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,
}
})
watch(
() => [props.service, props.discoveredLocation] as const,
([service, discoveredLocation]) => {
syncFromLocation(service?.location?.toJson() ?? discoveredLocation ?? null)
},
{ immediate: true }
)
watch(
currentLocation,
location => {
const existingLocation = props.service?.location?.toJson() ?? null
if (sameLocation(existingLocation, location)) {
return
}
const nextService = createServiceObject(props.service)
nextService.location = location ? LocationSocketSole.fromJson(location) : null
emit('update:service', nextService)
},
{ immediate: true, deep: true }
)
watch(encryption, (next, previous) => {
const previousDefault = defaultPortFor(previous)
if (!port.value || Number(port.value) === previousDefault) {
port.value = String(defaultPortFor(next))
}
})
function syncFromLocation(location: ServiceLocation | null) {
const socketLocation = getSocketLocation(location)
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
}
function createServiceObject(service?: ServiceObject): ServiceObject {
const nextService = new ServiceObject()
if (service) {
nextService.fromJson(service.toJson())
}
return nextService
}
function getSocketLocation(location?: ServiceLocation | null): ServiceLocationSocketSole | null {
return location?.type === 'SOCKET_SOLE' ? location : null
}
function sameLocation(a: ServiceLocation | null, b: ServiceLocation | null): boolean {
if (a === null || b === null) {
return a === b
}
if (a.type !== 'SOCKET_SOLE' || b.type !== 'SOCKET_SOLE') {
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)
}
function defaultPortFor(nextEncryption: ImapEncryption): number {
return nextEncryption === 'ssl' || nextEncryption === 'tls' ? 993 : 143
}
</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>
<v-text-field
v-model="host"
label="Server Host"
hint="For example: imap.example.com"
persistent-hint
variant="outlined"
prepend-inner-icon="mdi-server"
class="mb-4"
autocomplete="off"
autocorrect="off"
autocapitalize="none"
:rules="[rules.required]"
/>
<v-select
v-model="encryption"
:items="encryptionOptions"
label="Security"
variant="outlined"
prepend-inner-icon="mdi-shield-lock"
class="mb-4"
/>
<v-text-field
v-model="port"
label="Port"
hint="Defaults to 993 for TLS/SSL and 143 for plain or STARTTLS"
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 class="mt-4">
<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="verifyPeer"
label="Verify TLS certificate"
color="primary"
hint="Disable only for trusted internal or test environments"
persistent-hint
class="mb-4"
/>
<v-switch
v-model="verifyHost"
label="Verify certificate hostname"
color="primary"
hint="Checks that the certificate matches the IMAP host"
persistent-hint
class="mb-4"
/>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<v-alert type="info" variant="tonal" density="compact" class="mt-4">
<template #prepend>
<v-icon>mdi-information</v-icon>
</template>
<div class="text-caption">
STARTTLS is accepted for compatibility, but the current IMAP client transport does not perform STARTTLS negotiation. Prefer TLS on port 993 when available.
</div>
</v-alert>
</div>
</template>
<style scoped>
.imap-protocol-panel {
max-width: 800px;
}
.text-h6 {
font-size: 1.25rem;
font-weight: 500;
line-height: 2rem;
letter-spacing: 0.0125em;
}
.text-body-2 {
font-size: 0.875rem;
font-weight: 400;
line-height: 1.25rem;
letter-spacing: 0.0178571429em;
color: rgba(var(--v-theme-on-surface), 0.7);
}
</style>