Initial commit
This commit is contained in:
363
src/components/JmapConfigPanel.vue
Normal file
363
src/components/JmapConfigPanel.vue
Normal file
@@ -0,0 +1,363 @@
|
||||
<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="sessionUrl"
|
||||
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>
|
||||
|
||||
<!-- Advanced Settings -->
|
||||
<v-expansion-panels class="mt-4">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-title>
|
||||
<v-icon start>mdi-cog</v-icon>
|
||||
Advanced Settings
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-select
|
||||
v-model="capabilities"
|
||||
:items="jmapCapabilities"
|
||||
label="Enabled Capabilities"
|
||||
multiple
|
||||
chips
|
||||
variant="outlined"
|
||||
hint="Select which JMAP capabilities to enable"
|
||||
persistent-hint
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model.number="timeout"
|
||||
type="number"
|
||||
label="Timeout (seconds)"
|
||||
variant="outlined"
|
||||
class="mt-4"
|
||||
hint="Connection timeout in seconds"
|
||||
persistent-hint
|
||||
:min="5"
|
||||
:max="300"
|
||||
/>
|
||||
|
||||
<v-switch
|
||||
v-model="verifyHost"
|
||||
label="Verify SSL Hostname"
|
||||
color="primary"
|
||||
hint="Verify the certificate matches the hostname"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
|
||||
<!-- Info Alert -->
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mt-4"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-information</v-icon>
|
||||
</template>
|
||||
<div class="text-caption">
|
||||
JMAP is a modern protocol for mail access. Most JMAP servers use
|
||||
<code>/.well-known/jmap</code> for autodiscovery.
|
||||
</div>
|
||||
</v-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import type { ServiceLocationUri, ServiceLocation } from '@KTXM/MailManager/types/service'
|
||||
import type { ProviderConfigPanelProps, ProviderConfigPanelEmits } from '@KTXM/MailManager/types/integration'
|
||||
|
||||
const props = defineProps<ProviderConfigPanelProps>()
|
||||
const emit = defineEmits<ProviderConfigPanelEmits>()
|
||||
|
||||
// Helper to build session URL from location
|
||||
function buildSessionUrl(location?: ServiceLocation): 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): boolean {
|
||||
return (location?.type === 'URI' ? location.verifyPeer : undefined) ?? true
|
||||
}
|
||||
|
||||
function getUriVerifyHost(location?: ServiceLocation): boolean {
|
||||
return (location?.type === 'URI' ? location.verifyHost : undefined) ?? 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('')
|
||||
|
||||
// Local state - protocol settings only
|
||||
const sessionUrl = ref(buildSessionUrl(props.modelValue || props.discoveredLocation))
|
||||
const capabilities = ref<string[]>(['urn:ietf:params:jmap:mail'])
|
||||
const timeout = ref(30)
|
||||
const verifyPeer = ref(getUriVerifyPeer(props.modelValue || props.discoveredLocation))
|
||||
const verifyHost = ref(getUriVerifyHost(props.modelValue || props.discoveredLocation))
|
||||
|
||||
// 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 (!sessionUrl.value) return null
|
||||
return parseSessionUrl(sessionUrl.value)
|
||||
}
|
||||
})
|
||||
|
||||
// Validation state
|
||||
const isValid = computed(() => {
|
||||
if (configureManually.value) {
|
||||
return !!serviceHost.value
|
||||
} else {
|
||||
return !!sessionUrl.value && rules.url(sessionUrl.value) === true
|
||||
}
|
||||
})
|
||||
|
||||
// Emit location whenever it changes
|
||||
watch(
|
||||
currentLocation,
|
||||
(newLocation) => {
|
||||
if (newLocation) {
|
||||
emit('update:modelValue', newLocation)
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
// Emit validation state
|
||||
watch(
|
||||
isValid,
|
||||
(valid) => {
|
||||
emit('valid', valid)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Update local state when props change
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
if (newValue && newValue.type === 'URI') {
|
||||
sessionUrl.value = buildSessionUrl(newValue)
|
||||
verifyPeer.value = newValue.verifyPeer ?? true
|
||||
verifyHost.value = newValue.verifyHost ?? true
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.discoveredLocation,
|
||||
(newValue) => {
|
||||
if (newValue && newValue.type === 'URI' && !props.modelValue) {
|
||||
sessionUrl.value = buildSessionUrl(newValue)
|
||||
verifyPeer.value = newValue.verifyPeer ?? true
|
||||
verifyHost.value = newValue.verifyHost ?? true
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const jmapCapabilities = [
|
||||
{ title: 'Mail', value: 'urn:ietf:params:jmap:mail' },
|
||||
{ title: 'Contacts', value: 'urn:ietf:params:jmap:contacts' },
|
||||
{ title: 'Calendars', value: 'urn:ietf:params:jmap:calendars' },
|
||||
{ title: 'Tasks', value: 'urn:ietf:params:jmap:tasks' },
|
||||
{ title: 'Notes', value: 'urn:ietf:params:jmap:notes' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<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>
|
||||
Reference in New Issue
Block a user