Merge pull request 'refactor: code cleanup' (#5) from refactor/code-cleanup into main
Some checks failed
Renovate / renovate (push) Failing after 1m39s
Some checks failed
Renovate / renovate (push) Failing after 1m39s
Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
@@ -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>
|
<template>
|
||||||
<div class="jmap-config-panel">
|
<div class="jmap-config-panel">
|
||||||
<h3 class="text-h6 mb-4">Connection</h3>
|
<h3 class="text-h6 mb-4">Connection</h3>
|
||||||
@@ -243,232 +469,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<style scoped>
|
||||||
.jmap-config-panel {
|
.jmap-config-panel {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
|
|||||||
@@ -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">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import type { ServiceIdentity } from '@KTXM/MailManager/types/service'
|
import type { ServiceIdentity } from '@KTXM/MailManager/types/service'
|
||||||
@@ -261,6 +140,127 @@ async function initiateOAuth() {
|
|||||||
}
|
}
|
||||||
</script>
|
</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>
|
<style scoped>
|
||||||
.jmap-auth-panel {
|
.jmap-auth-panel {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
|
|||||||
@@ -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">
|
<script setup lang="ts">
|
||||||
import { ref, watch, computed } from 'vue'
|
import { ref, watch, computed } from 'vue'
|
||||||
import type { ServiceLocationUri, ServiceLocation } from '@KTXM/MailManager/types/service'
|
import type { ServiceLocationUri, ServiceLocation } from '@KTXM/MailManager/types/service'
|
||||||
@@ -326,6 +173,159 @@ const jmapCapabilities = [
|
|||||||
]
|
]
|
||||||
</script>
|
</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>
|
<style scoped>
|
||||||
.jmap-config-panel {
|
.jmap-config-panel {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { ModuleIntegrations } from "@KTXC/types/moduleTypes";
|
import type { ModuleIntegrations } from "@KTXC/types/moduleTypes";
|
||||||
import type { ProviderMetadata } from "@KTXM/MailManager/types/provider";
|
|
||||||
import type { ServiceInterface } from "@KTXM/MailManager/types/service";
|
import type { ServiceInterface } from "@KTXM/MailManager/types/service";
|
||||||
import { JmapServiceObject } from './models/JmapServiceObject'
|
import { JmapServiceObject } from './models/JmapServiceObject'
|
||||||
|
|
||||||
@@ -32,25 +31,7 @@ const integrations: ModuleIntegrations = {
|
|||||||
label: 'JMAP',
|
label: 'JMAP',
|
||||||
description: 'Modern JSON-based mail API protocol',
|
description: 'Modern JSON-based mail API protocol',
|
||||||
icon: 'mdi-api',
|
icon: 'mdi-api',
|
||||||
auth: {
|
}
|
||||||
methods: ['BA', 'OA', 'TA'],
|
|
||||||
default: 'BA',
|
|
||||||
allowMethodSelection: true,
|
|
||||||
oauth: {
|
|
||||||
// OAuth config will be provider-specific
|
|
||||||
// Some JMAP providers use OAuth (e.g., Fastmail)
|
|
||||||
authorizeUrl: '', // Configured per-instance
|
|
||||||
tokenUrl: '',
|
|
||||||
scopes: ['mail'],
|
|
||||||
flowType: 'authorization_code'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
supportsDiscovery: true,
|
|
||||||
meta: {
|
|
||||||
protocol: 'JMAP',
|
|
||||||
wellKnownPath: '/.well-known/jmap'
|
|
||||||
}
|
|
||||||
} as ProviderMetadata
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
import { fetchWrapper } from '@KTXC/utils/helpers/fetch-wrapper';
|
|
||||||
import type {
|
|
||||||
Service,
|
|
||||||
ConnectionTestRequest,
|
|
||||||
ConnectionTestResponse,
|
|
||||||
CollectionsResponse,
|
|
||||||
DiscoverResponse
|
|
||||||
} from '@/models/service';
|
|
||||||
|
|
||||||
const BASE_PATH = '/m/provider_jmapc';
|
|
||||||
|
|
||||||
export const serviceService = {
|
|
||||||
/**
|
|
||||||
* List all JMAP services for current user
|
|
||||||
*/
|
|
||||||
async list(capability?: string): Promise<{ services: Service[] }> {
|
|
||||||
const params = capability ? `?capability=${encodeURIComponent(capability)}` : '';
|
|
||||||
return fetchWrapper.get(`${BASE_PATH}/services${params}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a single service by ID
|
|
||||||
*/
|
|
||||||
async fetch(id: string): Promise<Service> {
|
|
||||||
return fetchWrapper.get(`${BASE_PATH}/services/${id}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new service
|
|
||||||
*/
|
|
||||||
async create(service: Partial<Service>): Promise<Service> {
|
|
||||||
return fetchWrapper.post(`${BASE_PATH}/services`, service);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update an existing service
|
|
||||||
*/
|
|
||||||
async update(id: string, service: Partial<Service>): Promise<Service> {
|
|
||||||
return fetchWrapper.put(`${BASE_PATH}/services/${id}`, service);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a service
|
|
||||||
*/
|
|
||||||
async destroy(id: string): Promise<{ success: boolean }> {
|
|
||||||
return fetchWrapper.delete(`${BASE_PATH}/services/${id}`);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test JMAP connection
|
|
||||||
*/
|
|
||||||
async test(request: ConnectionTestRequest): Promise<ConnectionTestResponse> {
|
|
||||||
return fetchWrapper.post(`${BASE_PATH}/services/test`, request);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Auto-discover JMAP endpoint from hostname
|
|
||||||
*/
|
|
||||||
async discover(hostname: string, protocol?: string, port?: number, path?: string): Promise<DiscoverResponse> {
|
|
||||||
return fetchWrapper.post(`${BASE_PATH}/services/discover`, {
|
|
||||||
hostname,
|
|
||||||
protocol,
|
|
||||||
port,
|
|
||||||
path
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch collections for a service
|
|
||||||
*/
|
|
||||||
async fetchCollections(id: string): Promise<CollectionsResponse> {
|
|
||||||
return fetchWrapper.get(`${BASE_PATH}/services/${id}/collections`);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh collections for a service (re-query remote server)
|
|
||||||
*/
|
|
||||||
async refreshCollections(id: string): Promise<CollectionsResponse> {
|
|
||||||
return fetchWrapper.post(`${BASE_PATH}/services/${id}/collections/refresh`, {});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
import { defineStore } from 'pinia';
|
|
||||||
import { serviceService } from '@/services/serviceService';
|
|
||||||
import { ServiceModel } from '@/models/service';
|
|
||||||
import type { Service } from '@/models/service';
|
|
||||||
|
|
||||||
export const useServicesStore = defineStore('jmapc_services', {
|
|
||||||
state: () => ({
|
|
||||||
services: [] as ServiceModel[],
|
|
||||||
loading: false,
|
|
||||||
error: null as string | null,
|
|
||||||
}),
|
|
||||||
|
|
||||||
getters: {
|
|
||||||
/**
|
|
||||||
* Get services filtered by capability
|
|
||||||
*/
|
|
||||||
byCapability: (state) => (capability: string) => {
|
|
||||||
return state.services.filter(s => s.capabilities.includes(capability));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get mail services
|
|
||||||
*/
|
|
||||||
mailServices: (state) => {
|
|
||||||
return state.services.filter(s => s.hasMail());
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get contact services
|
|
||||||
*/
|
|
||||||
contactServices: (state) => {
|
|
||||||
return state.services.filter(s => s.hasContacts());
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get calendar services
|
|
||||||
*/
|
|
||||||
calendarServices: (state) => {
|
|
||||||
return state.services.filter(s => s.hasCalendars());
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get service by ID
|
|
||||||
*/
|
|
||||||
getById: (state) => (id: string) => {
|
|
||||||
return state.services.find(s => s.id === id);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
/**
|
|
||||||
* Load all services from API
|
|
||||||
*/
|
|
||||||
async loadServices(capability?: string) {
|
|
||||||
this.loading = true;
|
|
||||||
this.error = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await serviceService.list(capability);
|
|
||||||
this.services = response.services.map(s => new ServiceModel(s));
|
|
||||||
} catch (error: any) {
|
|
||||||
this.error = error.message || 'Failed to load services';
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load a single service
|
|
||||||
*/
|
|
||||||
async loadService(id: string) {
|
|
||||||
this.loading = true;
|
|
||||||
this.error = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const service = await serviceService.fetch(id);
|
|
||||||
const model = new ServiceModel(service);
|
|
||||||
|
|
||||||
// Update or add to store
|
|
||||||
const index = this.services.findIndex(s => s.id === id);
|
|
||||||
if (index >= 0) {
|
|
||||||
this.services[index] = model;
|
|
||||||
} else {
|
|
||||||
this.services.push(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
return model;
|
|
||||||
} catch (error: any) {
|
|
||||||
this.error = error.message || 'Failed to load service';
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new service
|
|
||||||
*/
|
|
||||||
async createService(service: Partial<Service>) {
|
|
||||||
this.loading = true;
|
|
||||||
this.error = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const created = await serviceService.create(service);
|
|
||||||
const model = new ServiceModel(created);
|
|
||||||
this.services.push(model);
|
|
||||||
return model;
|
|
||||||
} catch (error: any) {
|
|
||||||
this.error = error.message || 'Failed to create service';
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update an existing service
|
|
||||||
*/
|
|
||||||
async updateService(id: string, service: Partial<Service>) {
|
|
||||||
this.loading = true;
|
|
||||||
this.error = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const updated = await serviceService.update(id, service);
|
|
||||||
const model = new ServiceModel(updated);
|
|
||||||
|
|
||||||
const index = this.services.findIndex(s => s.id === id);
|
|
||||||
if (index >= 0) {
|
|
||||||
this.services[index] = model;
|
|
||||||
}
|
|
||||||
|
|
||||||
return model;
|
|
||||||
} catch (error: any) {
|
|
||||||
this.error = error.message || 'Failed to update service';
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a service
|
|
||||||
*/
|
|
||||||
async deleteService(id: string) {
|
|
||||||
this.loading = true;
|
|
||||||
this.error = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await serviceService.destroy(id);
|
|
||||||
this.services = this.services.filter(s => s.id !== id);
|
|
||||||
} catch (error: any) {
|
|
||||||
this.error = error.message || 'Failed to delete service';
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test connection with provided settings
|
|
||||||
*/
|
|
||||||
async testConnection(config: any) {
|
|
||||||
return serviceService.test(config);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Auto-discover JMAP endpoint
|
|
||||||
*/
|
|
||||||
async discover(hostname: string, protocol?: string, port?: number, path?: string) {
|
|
||||||
return serviceService.discover(hostname, protocol, port, path);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch collections for a service
|
|
||||||
*/
|
|
||||||
async fetchCollections(id: string) {
|
|
||||||
return serviceService.fetchCollections(id);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh collections for a service
|
|
||||||
*/
|
|
||||||
async refreshCollections(id: string) {
|
|
||||||
const result = await serviceService.refreshCollections(id);
|
|
||||||
|
|
||||||
// Update local service with fresh collection data
|
|
||||||
const service = this.getById(id);
|
|
||||||
if (service && result.success) {
|
|
||||||
service.collections = {
|
|
||||||
contacts: result.contacts || [],
|
|
||||||
calendars: result.calendars || []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all services
|
|
||||||
*/
|
|
||||||
reset() {
|
|
||||||
this.services = [];
|
|
||||||
this.error = null;
|
|
||||||
this.loading = false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user