341 lines
9.4 KiB
Vue
341 lines
9.4 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, watch } from 'vue'
|
|
import { LocationUri } from '@KTXM/MailManager/models/location'
|
|
import { ServiceObject } from '@KTXM/MailManager/models/service'
|
|
import type { ServiceLocationUri, ServiceLocation } from '@KTXM/MailManager/types/service'
|
|
import type { ProviderProtocolPanelProps, ProviderProtocolPanelEmits } from '@KTXM/MailManager/types/integration'
|
|
|
|
const props = defineProps<ProviderProtocolPanelProps>()
|
|
const emit = defineEmits<ProviderProtocolPanelEmits>()
|
|
|
|
const serviceLocationUrl = ref('')
|
|
const verifyPeer = ref(true)
|
|
const verifyHost = ref(true)
|
|
|
|
// Manual configuration toggle and fields
|
|
const configureManually = ref(false)
|
|
const serviceHost = ref('')
|
|
const serviceProtocol = ref<'http' | 'https'>('https')
|
|
const servicePort = ref('')
|
|
const servicePath = ref('')
|
|
|
|
// Validation rules
|
|
const rules = {
|
|
required: (value: any) => !!value || 'This field is required',
|
|
url: (value: string) => {
|
|
try {
|
|
new URL(value)
|
|
return true
|
|
} catch {
|
|
return 'Please enter a valid URL'
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build location from current state
|
|
const currentLocation = computed((): ServiceLocationUri | null => {
|
|
if (configureManually.value) {
|
|
// Build from manual fields
|
|
if (!serviceHost.value) return null
|
|
|
|
const port = servicePort.value
|
|
? parseInt(servicePort.value)
|
|
: (serviceProtocol.value === 'https' ? 443 : 80)
|
|
|
|
return {
|
|
type: 'URI',
|
|
scheme: serviceProtocol.value,
|
|
host: serviceHost.value,
|
|
port,
|
|
path: servicePath.value || '/.well-known/jmap',
|
|
verifyPeer: verifyPeer.value,
|
|
verifyHost: verifyHost.value
|
|
}
|
|
} else {
|
|
// Build from session URL
|
|
if (!serviceLocationUrl.value) return null
|
|
return parseSessionUrl(serviceLocationUrl.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 = props.service ?? new ServiceObject()
|
|
|
|
if (location === null) {
|
|
nextService.location = null
|
|
emit('update:service', nextService)
|
|
return
|
|
}
|
|
|
|
if (nextService.location instanceof LocationUri) {
|
|
nextService.location.scheme = location.scheme
|
|
nextService.location.host = location.host
|
|
nextService.location.port = location.port
|
|
nextService.location.path = location.path
|
|
nextService.location.verifyPeer = location.verifyPeer ?? true
|
|
nextService.location.verifyHost = location.verifyHost ?? true
|
|
} else {
|
|
nextService.location = LocationUri.fromJson(location)
|
|
}
|
|
|
|
emit('update:service', nextService)
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
function syncFromLocation(location: ServiceLocation | null) {
|
|
const uriLocation = getUriLocation(location)
|
|
|
|
serviceLocationUrl.value = buildSessionUrl(uriLocation)
|
|
verifyPeer.value = getUriVerifyPeer(uriLocation)
|
|
verifyHost.value = getUriVerifyHost(uriLocation)
|
|
serviceProtocol.value = uriLocation?.scheme === 'http' ? 'http' : 'https'
|
|
serviceHost.value = uriLocation?.host ?? ''
|
|
servicePort.value = normalizePort(uriLocation)
|
|
servicePath.value = uriLocation?.path ?? ''
|
|
}
|
|
|
|
function sameLocation(a: ServiceLocation | null, b: ServiceLocation | null): boolean {
|
|
if (a === null || b === null) {
|
|
return a === b
|
|
}
|
|
|
|
if (a.type !== 'URI' || b.type !== 'URI') {
|
|
return false
|
|
}
|
|
|
|
return a.scheme === b.scheme
|
|
&& a.host === b.host
|
|
&& a.port === b.port
|
|
&& (a.path ?? '') === (b.path ?? '')
|
|
&& getUriVerifyPeer(a) === getUriVerifyPeer(b)
|
|
&& getUriVerifyHost(a) === getUriVerifyHost(b)
|
|
}
|
|
|
|
function getUriLocation(location?: ServiceLocation | null): ServiceLocationUri | null {
|
|
return location?.type === 'URI' ? location : null
|
|
}
|
|
|
|
function normalizePort(location: ServiceLocationUri | null): string {
|
|
if (!location) {
|
|
return ''
|
|
}
|
|
|
|
const defaultPort = location.scheme === 'http' ? 80 : 443
|
|
return location.port === defaultPort ? '' : String(location.port)
|
|
}
|
|
|
|
// Helper to build session URL from location
|
|
function buildSessionUrl(location?: ServiceLocation | null): string {
|
|
if (!location || location.type !== 'URI') return ''
|
|
|
|
const protocol = location.scheme || 'https'
|
|
const host = location.host || ''
|
|
const port = location.port || (protocol === 'https' ? 443 : 80)
|
|
const path = location.path || '/.well-known/jmap'
|
|
|
|
// Don't include port if it's the default for the protocol
|
|
const portStr = (protocol === 'https' && port === 443) || (protocol === 'http' && port === 80)
|
|
? ''
|
|
: `:${port}`
|
|
|
|
return `${protocol}://${host}${portStr}${path}`
|
|
}
|
|
|
|
// Helper to parse session URL into location
|
|
function parseSessionUrl(url: string): ServiceLocationUri {
|
|
try {
|
|
const parsed = new URL(url)
|
|
return {
|
|
type: 'URI',
|
|
scheme: parsed.protocol.replace(':', '') as 'http' | 'https',
|
|
host: parsed.hostname,
|
|
port: parsed.port ? parseInt(parsed.port) : (parsed.protocol === 'https:' ? 443 : 80),
|
|
path: parsed.pathname || '/.well-known/jmap',
|
|
verifyPeer: verifyPeer.value,
|
|
verifyHost: verifyHost.value
|
|
}
|
|
} catch {
|
|
return {
|
|
type: 'URI',
|
|
scheme: 'https',
|
|
host: '',
|
|
port: 443,
|
|
path: '/.well-known/jmap',
|
|
verifyPeer: verifyPeer.value,
|
|
verifyHost: verifyHost.value
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper to extract URI properties safely
|
|
function getUriVerifyPeer(location?: ServiceLocation | null): boolean {
|
|
return (location?.type === 'URI' ? location.verifyPeer : undefined) ?? true
|
|
}
|
|
|
|
function getUriVerifyHost(location?: ServiceLocation | null): boolean {
|
|
return (location?.type === 'URI' ? location.verifyHost : undefined) ?? true
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="jmap-config-panel">
|
|
<h3 class="text-h6 mb-4">JMAP Connection Settings</h3>
|
|
<p class="text-body-2 mb-6">Configure how to connect to your JMAP server.</p>
|
|
|
|
<!-- Manual Configuration Toggle -->
|
|
<v-switch
|
|
v-model="configureManually"
|
|
label="Configure server manually"
|
|
color="primary"
|
|
class="mb-4"
|
|
/>
|
|
|
|
<!-- Session URL (Simple Mode) -->
|
|
<template v-if="!configureManually">
|
|
<v-text-field
|
|
v-model="serviceLocationUrl"
|
|
label="JMAP Session URL"
|
|
hint="e.g., https://jmap.example.com/.well-known/jmap"
|
|
persistent-hint
|
|
variant="outlined"
|
|
prepend-inner-icon="mdi-link"
|
|
class="mb-4"
|
|
:rules="[rules.required, rules.url]"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Manual Configuration Fields -->
|
|
<template v-if="configureManually">
|
|
<v-text-field
|
|
v-model="serviceHost"
|
|
label="Service Address"
|
|
hint="Domain or IP Address"
|
|
persistent-hint
|
|
variant="outlined"
|
|
prepend-inner-icon="mdi-server"
|
|
class="mb-4"
|
|
autocomplete="off"
|
|
autocorrect="off"
|
|
autocapitalize="none"
|
|
:rules="[rules.required]"
|
|
/>
|
|
|
|
<div class="mb-4">
|
|
<label class="text-subtitle-2 mb-2 d-block">Service Protocol</label>
|
|
<v-btn-toggle
|
|
v-model="serviceProtocol"
|
|
color="primary"
|
|
variant="outlined"
|
|
mandatory
|
|
divided
|
|
>
|
|
<v-btn value="http">http</v-btn>
|
|
<v-btn value="https">https</v-btn>
|
|
</v-btn-toggle>
|
|
</div>
|
|
|
|
<v-switch
|
|
v-model="verifyPeer"
|
|
label="Secure Transport Verification (SSL Certificate Verification)"
|
|
color="primary"
|
|
class="mb-4"
|
|
hint="Should always be ON, unless connecting to a service over a secure internal network"
|
|
persistent-hint
|
|
/>
|
|
|
|
<v-text-field
|
|
v-model="servicePort"
|
|
label="Service Port"
|
|
hint="Leave empty for default. http (80) https (443)"
|
|
persistent-hint
|
|
variant="outlined"
|
|
prepend-inner-icon="mdi-numeric"
|
|
class="mb-4"
|
|
autocomplete="off"
|
|
autocorrect="off"
|
|
autocapitalize="none"
|
|
/>
|
|
|
|
<v-text-field
|
|
v-model="servicePath"
|
|
label="Service Path"
|
|
hint="Leave empty for default path (/.well-known/jmap)"
|
|
persistent-hint
|
|
variant="outlined"
|
|
prepend-inner-icon="mdi-folder"
|
|
class="mb-4"
|
|
autocomplete="off"
|
|
autocorrect="off"
|
|
autocapitalize="none"
|
|
/>
|
|
</template>
|
|
|
|
<v-switch
|
|
v-model="verifyHost"
|
|
label="Verify SSL Hostname"
|
|
color="primary"
|
|
class="mt-4"
|
|
hint="Verify the certificate matches the hostname"
|
|
persistent-hint
|
|
/>
|
|
|
|
<!-- Info Alert -->
|
|
<v-alert type="info" variant="tonal">
|
|
JMAP is a modern protocol for mail access. Most JMAP servers use
|
|
<code>/.well-known/jmap</code> for autodiscovery.
|
|
</v-alert>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.jmap-config-panel {
|
|
max-width: 800px;
|
|
}
|
|
|
|
code {
|
|
background-color: rgba(var(--v-theme-surface-variant), 0.3);
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.text-h6 {
|
|
font-size: 1.25rem;
|
|
font-weight: 500;
|
|
line-height: 2rem;
|
|
letter-spacing: 0.0125em;
|
|
}
|
|
|
|
.text-subtitle-2 {
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
line-height: 1.375rem;
|
|
letter-spacing: 0.00714em;
|
|
}
|
|
|
|
.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>
|