Initial commit

This commit is contained in:
root
2026-01-04 22:05:37 -05:00
committed by Sebastian Krupinski
commit 4f979ced22
57 changed files with 11076 additions and 0 deletions

View File

@@ -0,0 +1,506 @@
<template>
<div class="jmap-config-panel">
<h3 class="text-h6 mb-4">Connection</h3>
<p class="text-body-2 mb-6">Enter your server and account information then press connect.</p>
<!-- Account Description -->
<v-text-field
v-model="accountLabel"
label="Account Description"
hint="Description for this Account"
persistent-hint
variant="outlined"
prepend-inner-icon="mdi-card-account-details"
class="mb-4"
:rules="[rules.required]"
/>
<!-- Authentication Type Selection -->
<div class="mb-4">
<label class="text-subtitle-2 mb-2 d-block">Authentication Type</label>
<v-btn-toggle
v-model="authType"
color="primary"
variant="outlined"
mandatory
divided
class="mb-4"
>
<v-btn value="BA">Basic</v-btn>
<v-btn value="OA">OAuth</v-btn>
<v-btn value="JB">Json Basic</v-btn>
</v-btn-toggle>
</div>
<!-- Basic Auth / Json Basic Auth Fields -->
<template v-if="authType === 'BA' || authType === 'JB'">
<v-text-field
v-model="bauthId"
label="Account ID"
hint="Authentication ID for your Account"
persistent-hint
variant="outlined"
prepend-inner-icon="mdi-account"
class="mb-4"
autocomplete="off"
autocorrect="off"
autocapitalize="none"
:rules="[rules.required]"
/>
<v-text-field
v-model="bauthSecret"
type="password"
label="Account Secret"
hint="Authentication secret for your Account"
persistent-hint
variant="outlined"
prepend-inner-icon="mdi-lock"
class="mb-4"
autocomplete="off"
autocorrect="off"
autocapitalize="none"
:rules="[rules.required]"
/>
</template>
<!-- OAuth Fields -->
<template v-if="authType === 'OA'">
<v-text-field
v-model="oauthId"
label="Account ID"
hint="Authentication ID for your Account"
persistent-hint
variant="outlined"
prepend-inner-icon="mdi-account"
class="mb-4"
autocomplete="off"
autocorrect="off"
autocapitalize="none"
:rules="[rules.required]"
/>
<v-text-field
v-model="oauthToken"
type="password"
label="Account Token"
hint="OAuth access token for your Account"
persistent-hint
variant="outlined"
prepend-inner-icon="mdi-key"
class="mb-4"
autocomplete="off"
autocorrect="off"
autocapitalize="none"
:rules="[rules.required]"
/>
</template>
<!-- Manual Configuration Toggle -->
<v-switch
v-model="configureManually"
label="Configure server manually"
color="primary"
class="mb-4"
/>
<!-- Manual Configuration Fields -->
<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>
<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 } 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;
}
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>

View File

@@ -0,0 +1,290 @@
<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'
import type { ProviderAuthPanelProps, ProviderAuthPanelEmits } from '@KTXM/MailManager/types/integration'
const props = defineProps<ProviderAuthPanelProps>()
const emit = defineEmits<ProviderAuthPanelEmits>()
// Auth method selection
const authType = ref<'BA' | 'TA' | 'OA'>('BA')
// Basic auth state
const basicIdentity = ref(props.prefilledIdentity || props.emailAddress || '')
const basicSecret = ref(props.prefilledSecret || '')
// Token auth state
const bearerToken = ref('')
// OAuth state
const oauthLoading = ref(false)
const oauthSuccess = ref(false)
const oauthAccessToken = ref('')
const oauthRefreshToken = ref('')
// Validation rules
const rules = {
required: (value: any) => !!value || 'This field is required'
}
// Validation
const isValid = computed(() => {
switch (authType.value) {
case 'BA':
return !!basicIdentity.value && !!basicSecret.value
case 'TA':
return !!bearerToken.value
case 'OA':
return oauthSuccess.value
default:
return false
}
})
// Build ServiceIdentity object
const currentIdentity = computed((): ServiceIdentity | null => {
if (!isValid.value) return null
switch (authType.value) {
case 'BA':
return {
type: 'BA',
identity: basicIdentity.value,
secret: basicSecret.value
}
case 'TA':
return {
type: 'TA',
token: bearerToken.value
}
case 'OA':
return {
type: 'OA',
accessToken: oauthAccessToken.value,
refreshToken: oauthRefreshToken.value,
accessScope: ['mail'],
accessExpiry: Date.now() + 3600000
}
default:
return null
}
})
// Watch and emit changes
watch(
currentIdentity,
(identity) => {
if (identity) {
emit('update:modelValue', identity)
}
},
{ immediate: true, deep: true }
)
watch(
isValid,
(valid) => {
emit('valid', valid)
},
{ immediate: true }
)
// Update local state if modelValue changes externally
watch(
() => props.modelValue,
(newValue) => {
if (newValue) {
authType.value = newValue.type as 'BA' | 'TA' | 'OA'
switch (newValue.type) {
case 'BA':
basicIdentity.value = newValue.identity || ''
basicSecret.value = newValue.secret || ''
break
case 'TA':
bearerToken.value = newValue.token || ''
break
case 'OA':
oauthAccessToken.value = newValue.accessToken || ''
oauthRefreshToken.value = newValue.refreshToken || ''
oauthSuccess.value = !!newValue.accessToken
break
}
}
}
)
// Prefill identity when email address is provided
watch(
() => props.emailAddress,
(email) => {
if (email && !basicIdentity.value) {
basicIdentity.value = email
}
},
{ immediate: true }
)
// OAuth flow (stub for now)
async function initiateOAuth() {
oauthLoading.value = true
try {
// TODO: Implement OAuth flow when backend is ready
emit('error', 'OAuth implementation pending')
throw new Error('OAuth implementation pending')
} catch (error: any) {
emit('error', error.message)
} finally {
oauthLoading.value = false
}
}
</script>
<style scoped>
.jmap-auth-panel {
max-width: 800px;
}
.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>

View 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>