refactor: code cleanup
All checks were successful
JS Unit Tests / test (pull_request) Successful in 29s
Build Test / test (pull_request) Successful in 33s
PHP Unit Tests / test (pull_request) Successful in 54s

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-03-06 22:51:32 -05:00
parent 07fab0873d
commit 006c303917
6 changed files with 501 additions and 809 deletions

View File

@@ -1,3 +1,229 @@
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import type { ServiceLocationUri } from '@KTXM/MailManager/types/service'
// Types
interface ServiceConfiguration {
label?: string
auth?: 'BA' | 'OA' | 'JB'
bauth_id?: string
bauth_secret?: string
oauth_id?: string
oauth_access_token?: string
location_host?: string
location_protocol?: string
location_security?: boolean
location_port?: string
location_path?: string
}
const props = defineProps<{
modelValue?: ServiceLocationUri
discoveredLocation?: ServiceLocationUri
service?: ServiceConfiguration
}>()
const emit = defineEmits<{
'update:modelValue': [value: ServiceLocationUri]
'update:service': [value: ServiceConfiguration]
}>()
// Initialize from discovered location or create new
function buildSessionUrl(location?: ServiceLocationUri): 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}`
}
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
}
}
}
// Account configuration fields
const accountLabel = ref(props.service?.label || 'New Connection')
const authType = ref<'BA' | 'OA' | 'JB'>(props.service?.auth || 'BA')
const bauthId = ref(props.service?.bauth_id || '')
const bauthSecret = ref(props.service?.bauth_secret || '')
const oauthId = ref(props.service?.oauth_id || '')
const oauthToken = ref(props.service?.oauth_access_token || '')
// Manual configuration toggle and fields
const configureManually = ref(false)
const serviceHost = ref(props.service?.location_host || '')
const serviceProtocol = ref(props.service?.location_protocol || 'https')
const servicePort = ref(props.service?.location_port || '')
const servicePath = ref(props.service?.location_path || '')
// 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(
props.service?.location_security ?? props.modelValue?.verifyPeer ?? props.discoveredLocation?.verifyPeer ?? true
)
const verifyHost = ref(
props.modelValue?.verifyHost ?? props.discoveredLocation?.verifyHost ?? true
)
// 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 as 'http' | 'https',
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)
}
})
// Build service configuration from current state
const currentService = computed((): ServiceConfiguration => {
return {
label: accountLabel.value,
auth: authType.value,
bauth_id: bauthId.value,
bauth_secret: bauthSecret.value,
oauth_id: oauthId.value,
oauth_access_token: oauthToken.value,
location_host: serviceHost.value,
location_protocol: serviceProtocol.value,
location_security: verifyPeer.value,
location_port: servicePort.value,
location_path: servicePath.value
}
})
// Emit location whenever it changes
watch(
[sessionUrl, serviceHost, serviceProtocol, servicePort, servicePath, verifyPeer, verifyHost, configureManually],
() => {
if (currentLocation.value) {
emit('update:modelValue', currentLocation.value)
}
},
{ immediate: true }
)
// Emit service configuration whenever it changes
watch(
[accountLabel, authType, bauthId, bauthSecret, oauthId, oauthToken, serviceHost, serviceProtocol, servicePort, servicePath, verifyPeer],
() => {
emit('update:service', currentService.value)
},
{ immediate: true }
)
// Update local state when props change
watch(
() => props.modelValue,
(newValue) => {
if (newValue) {
sessionUrl.value = buildSessionUrl(newValue)
verifyPeer.value = newValue.verifyPeer ?? true
verifyHost.value = newValue.verifyHost ?? true
}
}
)
watch(
() => props.discoveredLocation,
(newValue) => {
if (newValue && !props.modelValue) {
sessionUrl.value = buildSessionUrl(newValue)
verifyPeer.value = newValue.verifyPeer ?? true
verifyHost.value = newValue.verifyHost ?? true
}
},
{ immediate: true }
)
watch(
() => props.service,
(newValue) => {
if (newValue) {
accountLabel.value = newValue.label || 'New Connection'
authType.value = newValue.auth || 'BA'
bauthId.value = newValue.bauth_id || ''
bauthSecret.value = newValue.bauth_secret || ''
oauthId.value = newValue.oauth_id || ''
oauthToken.value = newValue.oauth_access_token || ''
serviceHost.value = newValue.location_host || ''
serviceProtocol.value = newValue.location_protocol || 'https'
servicePort.value = newValue.location_port || ''
servicePath.value = newValue.location_path || ''
verifyPeer.value = newValue.location_security ?? 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>
<template>
<div class="jmap-config-panel">
<h3 class="text-h6 mb-4">Connection</h3>
@@ -243,232 +469,6 @@
</div>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import type { ServiceLocationUri } from '@KTXM/MailManager/types/service'
// Types
interface ServiceConfiguration {
label?: string
auth?: 'BA' | 'OA' | 'JB'
bauth_id?: string
bauth_secret?: string
oauth_id?: string
oauth_access_token?: string
location_host?: string
location_protocol?: string
location_security?: boolean
location_port?: string
location_path?: string
}
const props = defineProps<{
modelValue?: ServiceLocationUri
discoveredLocation?: ServiceLocationUri
service?: ServiceConfiguration
}>()
const emit = defineEmits<{
'update:modelValue': [value: ServiceLocationUri]
'update:service': [value: ServiceConfiguration]
}>()
// Initialize from discovered location or create new
function buildSessionUrl(location?: ServiceLocationUri): 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}`
}
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
}
}
}
// Account configuration fields
const accountLabel = ref(props.service?.label || 'New Connection')
const authType = ref<'BA' | 'OA' | 'JB'>(props.service?.auth || 'BA')
const bauthId = ref(props.service?.bauth_id || '')
const bauthSecret = ref(props.service?.bauth_secret || '')
const oauthId = ref(props.service?.oauth_id || '')
const oauthToken = ref(props.service?.oauth_access_token || '')
// Manual configuration toggle and fields
const configureManually = ref(false)
const serviceHost = ref(props.service?.location_host || '')
const serviceProtocol = ref(props.service?.location_protocol || 'https')
const servicePort = ref(props.service?.location_port || '')
const servicePath = ref(props.service?.location_path || '')
// 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(
props.service?.location_security ?? props.modelValue?.verifyPeer ?? props.discoveredLocation?.verifyPeer ?? true
)
const verifyHost = ref(
props.modelValue?.verifyHost ?? props.discoveredLocation?.verifyHost ?? true
)
// 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 as 'http' | 'https',
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)
}
})
// Build service configuration from current state
const currentService = computed((): ServiceConfiguration => {
return {
label: accountLabel.value,
auth: authType.value,
bauth_id: bauthId.value,
bauth_secret: bauthSecret.value,
oauth_id: oauthId.value,
oauth_access_token: oauthToken.value,
location_host: serviceHost.value,
location_protocol: serviceProtocol.value,
location_security: verifyPeer.value,
location_port: servicePort.value,
location_path: servicePath.value
}
})
// Emit location whenever it changes
watch(
[sessionUrl, serviceHost, serviceProtocol, servicePort, servicePath, verifyPeer, verifyHost, configureManually],
() => {
if (currentLocation.value) {
emit('update:modelValue', currentLocation.value)
}
},
{ immediate: true }
)
// Emit service configuration whenever it changes
watch(
[accountLabel, authType, bauthId, bauthSecret, oauthId, oauthToken, serviceHost, serviceProtocol, servicePort, servicePath, verifyPeer],
() => {
emit('update:service', currentService.value)
},
{ immediate: true }
)
// Update local state when props change
watch(
() => props.modelValue,
(newValue) => {
if (newValue) {
sessionUrl.value = buildSessionUrl(newValue)
verifyPeer.value = newValue.verifyPeer ?? true
verifyHost.value = newValue.verifyHost ?? true
}
}
)
watch(
() => props.discoveredLocation,
(newValue) => {
if (newValue && !props.modelValue) {
sessionUrl.value = buildSessionUrl(newValue)
verifyPeer.value = newValue.verifyPeer ?? true
verifyHost.value = newValue.verifyHost ?? true
}
},
{ immediate: true }
)
watch(
() => props.service,
(newValue) => {
if (newValue) {
accountLabel.value = newValue.label || 'New Connection'
authType.value = newValue.auth || 'BA'
bauthId.value = newValue.bauth_id || ''
bauthSecret.value = newValue.bauth_secret || ''
oauthId.value = newValue.oauth_id || ''
oauthToken.value = newValue.oauth_access_token || ''
serviceHost.value = newValue.location_host || ''
serviceProtocol.value = newValue.location_protocol || 'https'
servicePort.value = newValue.location_port || ''
servicePath.value = newValue.location_path || ''
verifyPeer.value = newValue.location_security ?? 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;

View File

@@ -1,124 +1,3 @@
<template>
<div class="jmap-auth-panel">
<h3 class="text-h6 mb-4">Authentication</h3>
<p class="text-body-2 mb-6">Choose your authentication method and enter your credentials.</p>
<v-alert type="info" variant="tonal" class="mb-4">
<template #prepend>
<v-icon>mdi-information</v-icon>
</template>
<div class="text-caption">
JMAP supports multiple authentication methods. Choose the one your server uses.
</div>
</v-alert>
<!-- Authentication Type Selection -->
<div class="mb-4">
<label class="text-subtitle-2 mb-2 d-block">Authentication Method</label>
<v-btn-toggle
v-model="authType"
color="primary"
variant="outlined"
mandatory
divided
class="mb-4"
>
<v-btn value="BA">
<v-icon start>mdi-account-key</v-icon>
Basic Auth
</v-btn>
<v-btn value="TA">
<v-icon start>mdi-key</v-icon>
Bearer Token
</v-btn>
<v-btn value="OA">
<v-icon start>mdi-shield-account</v-icon>
OAuth 2.0
</v-btn>
</v-btn-toggle>
</div>
<!-- Basic Authentication -->
<template v-if="authType === 'BA'">
<v-text-field
v-model="basicIdentity"
label="Username / Email"
hint="Your account username or email address"
persistent-hint
variant="outlined"
prepend-inner-icon="mdi-account"
class="mb-4"
autocomplete="username"
autocorrect="off"
autocapitalize="none"
:rules="[rules.required]"
/>
<v-text-field
v-model="basicSecret"
type="password"
label="Password"
hint="Your account password"
persistent-hint
variant="outlined"
prepend-inner-icon="mdi-lock"
class="mb-4"
autocomplete="current-password"
:rules="[rules.required]"
/>
</template>
<!-- Bearer Token Authentication -->
<template v-else-if="authType === 'TA'">
<v-textarea
v-model="bearerToken"
label="Bearer Token"
hint="Enter your API token or bearer token"
persistent-hint
variant="outlined"
prepend-inner-icon="mdi-key"
class="mb-4"
rows="3"
autocomplete="off"
autocorrect="off"
autocapitalize="none"
:rules="[rules.required]"
/>
</template>
<!-- OAuth 2.0 -->
<template v-else-if="authType === 'OA'">
<v-alert type="warning" variant="tonal" class="mb-4">
<template #prepend>
<v-icon>mdi-alert</v-icon>
</template>
<div class="text-caption">
OAuth 2.0 implementation is pending. This will launch a browser window
for secure authentication with your JMAP provider.
</div>
</v-alert>
<v-btn
v-if="!oauthSuccess"
color="primary"
size="large"
block
@click="initiateOAuth"
:loading="oauthLoading"
:disabled="true"
>
<v-icon start>mdi-login</v-icon>
Authorize with OAuth 2.0
</v-btn>
<div v-else class="text-center py-4">
<v-icon color="success" size="64">mdi-check-circle</v-icon>
<p class="text-subtitle-1 mt-2">OAuth Authorized Successfully</p>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import type { ServiceIdentity } from '@KTXM/MailManager/types/service'
@@ -261,6 +140,127 @@ async function initiateOAuth() {
}
</script>
<template>
<div class="jmap-auth-panel">
<h3 class="text-h6 mb-4">Authentication</h3>
<p class="text-body-2 mb-6">Choose your authentication method and enter your credentials.</p>
<v-alert type="info" variant="tonal" class="mb-4">
<template #prepend>
<v-icon>mdi-information</v-icon>
</template>
<div class="text-caption">
JMAP supports multiple authentication methods. Choose the one your server uses.
</div>
</v-alert>
<!-- Authentication Type Selection -->
<div class="mb-4">
<label class="text-subtitle-2 mb-2 d-block">Authentication Method</label>
<v-btn-toggle
v-model="authType"
color="primary"
variant="outlined"
mandatory
divided
class="mb-4"
>
<v-btn value="BA">
<v-icon start>mdi-account-key</v-icon>
Basic Auth
</v-btn>
<v-btn value="TA">
<v-icon start>mdi-key</v-icon>
Bearer Token
</v-btn>
<v-btn value="OA">
<v-icon start>mdi-shield-account</v-icon>
OAuth 2.0
</v-btn>
</v-btn-toggle>
</div>
<!-- Basic Authentication -->
<template v-if="authType === 'BA'">
<v-text-field
v-model="basicIdentity"
label="Username / Email"
hint="Your account username or email address"
persistent-hint
variant="outlined"
prepend-inner-icon="mdi-account"
class="mb-4"
autocomplete="username"
autocorrect="off"
autocapitalize="none"
:rules="[rules.required]"
/>
<v-text-field
v-model="basicSecret"
type="password"
label="Password"
hint="Your account password"
persistent-hint
variant="outlined"
prepend-inner-icon="mdi-lock"
class="mb-4"
autocomplete="current-password"
:rules="[rules.required]"
/>
</template>
<!-- Bearer Token Authentication -->
<template v-else-if="authType === 'TA'">
<v-textarea
v-model="bearerToken"
label="Bearer Token"
hint="Enter your API token or bearer token"
persistent-hint
variant="outlined"
prepend-inner-icon="mdi-key"
class="mb-4"
rows="3"
autocomplete="off"
autocorrect="off"
autocapitalize="none"
:rules="[rules.required]"
/>
</template>
<!-- OAuth 2.0 -->
<template v-else-if="authType === 'OA'">
<v-alert type="warning" variant="tonal" class="mb-4">
<template #prepend>
<v-icon>mdi-alert</v-icon>
</template>
<div class="text-caption">
OAuth 2.0 implementation is pending. This will launch a browser window
for secure authentication with your JMAP provider.
</div>
</v-alert>
<v-btn
v-if="!oauthSuccess"
color="primary"
size="large"
block
@click="initiateOAuth"
:loading="oauthLoading"
:disabled="true"
>
<v-icon start>mdi-login</v-icon>
Authorize with OAuth 2.0
</v-btn>
<div v-else class="text-center py-4">
<v-icon color="success" size="64">mdi-check-circle</v-icon>
<p class="text-subtitle-1 mt-2">OAuth Authorized Successfully</p>
</div>
</template>
</div>
</template>
<style scoped>
.jmap-auth-panel {
max-width: 800px;

View File

@@ -1,156 +1,3 @@
<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'
@@ -326,6 +173,159 @@ const jmapCapabilities = [
]
</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="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>
<style scoped>
.jmap-config-panel {
max-width: 800px;