feat: lots more improvements
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -175,8 +175,6 @@ class Provider implements ProviderServiceMutateInterface, ProviderServiceDiscove
|
|||||||
?string $secret = null
|
?string $secret = null
|
||||||
): ResourceServiceLocationInterface|null {
|
): ResourceServiceLocationInterface|null {
|
||||||
$discovery = new Discovery();
|
$discovery = new Discovery();
|
||||||
|
|
||||||
// TODO: Make SSL verification configurable based on tenant/user settings
|
|
||||||
$verifySSL = true;
|
$verifySSL = true;
|
||||||
|
|
||||||
return $discovery->discover($identity, $location, $secret, $verifySSL);
|
return $discovery->discover($identity, $location, $secret, $verifySSL);
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
|||||||
], fn($v) => $v !== null);
|
], fn($v) => $v !== null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function jsonDeserialize(array|string $data): static {
|
public function jsonDeserialize(array|string $data, bool $delta = false): static {
|
||||||
if (is_string($data)) {
|
if (is_string($data)) {
|
||||||
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
|
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
|
||||||
}
|
}
|
||||||
@@ -193,13 +193,11 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
|||||||
$this->setIdentity($this->freshIdentity(null, $data[self::JSON_PROPERTY_IDENTITY]));
|
$this->setIdentity($this->freshIdentity(null, $data[self::JSON_PROPERTY_IDENTITY]));
|
||||||
}
|
}
|
||||||
if (isset($data[self::JSON_PROPERTY_PRIMARY_ADDRESS]) && is_string($data[self::JSON_PROPERTY_PRIMARY_ADDRESS])) {
|
if (isset($data[self::JSON_PROPERTY_PRIMARY_ADDRESS]) && is_string($data[self::JSON_PROPERTY_PRIMARY_ADDRESS])) {
|
||||||
if (is_array($data[self::JSON_PROPERTY_PRIMARY_ADDRESS]) && isset($data[self::JSON_PROPERTY_PRIMARY_ADDRESS]['address'])) {
|
$this->setPrimaryAddress(new Address($data[self::JSON_PROPERTY_PRIMARY_ADDRESS]));
|
||||||
$this->setPrimaryAddress(new Address($data[self::JSON_PROPERTY_PRIMARY_ADDRESS]));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (isset($data[self::JSON_PROPERTY_SECONDARY_ADDRESSES]) && is_array($data[self::JSON_PROPERTY_SECONDARY_ADDRESSES])) {
|
if (isset($data[self::JSON_PROPERTY_SECONDARY_ADDRESSES]) && is_array($data[self::JSON_PROPERTY_SECONDARY_ADDRESSES])) {
|
||||||
$this->setSecondaryAddresses(array_map(
|
$this->setSecondaryAddresses(array_map(
|
||||||
fn($addr) => new Address($addr['address']),
|
fn($addr) => new Address(is_array($addr) ? ($addr['address'] ?? $addr) : $addr),
|
||||||
$data[self::JSON_PROPERTY_SECONDARY_ADDRESSES]
|
$data[self::JSON_PROPERTY_SECONDARY_ADDRESSES]
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,506 +0,0 @@
|
|||||||
<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>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
@@ -5,10 +5,9 @@ import {
|
|||||||
IdentityOAuth,
|
IdentityOAuth,
|
||||||
IdentityToken,
|
IdentityToken,
|
||||||
} from '@KTXM/MailManager/models/identity'
|
} from '@KTXM/MailManager/models/identity'
|
||||||
import type { ServiceObject } from '@KTXM/MailManager/models/service'
|
import { ServiceObject } from '@KTXM/MailManager/models/service'
|
||||||
import type { ServiceIdentity } from '@KTXM/MailManager/types/service'
|
import type { ServiceIdentity } from '@KTXM/MailManager/types/service'
|
||||||
import type { ProviderAuthPanelProps, ProviderAuthPanelEmits } from '@KTXM/MailManager/types/integration'
|
import type { ProviderAuthPanelProps, ProviderAuthPanelEmits } from '@KTXM/MailManager/types/integration'
|
||||||
import { JmapServiceObject } from '@/models/JmapServiceObject'
|
|
||||||
|
|
||||||
const props = defineProps<ProviderAuthPanelProps>()
|
const props = defineProps<ProviderAuthPanelProps>()
|
||||||
const emit = defineEmits<ProviderAuthPanelEmits>()
|
const emit = defineEmits<ProviderAuthPanelEmits>()
|
||||||
@@ -95,8 +94,30 @@ watch(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextService = createServiceObject(props.service)
|
const nextService = props.service ?? new ServiceObject()
|
||||||
nextService.identity = createIdentityModel(identity)
|
const nextIdentity = createIdentityModel(identity)
|
||||||
|
|
||||||
|
if (nextIdentity === null) {
|
||||||
|
nextService.identity = null
|
||||||
|
emit('update:service', nextService)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextIdentity instanceof IdentityBasic && nextService.identity instanceof IdentityBasic) {
|
||||||
|
nextService.identity.identity = nextIdentity.identity
|
||||||
|
nextService.identity.secret = nextIdentity.secret
|
||||||
|
} else if (nextIdentity instanceof IdentityToken && nextService.identity instanceof IdentityToken) {
|
||||||
|
nextService.identity.token = nextIdentity.token
|
||||||
|
} else if (nextIdentity instanceof IdentityOAuth && nextService.identity instanceof IdentityOAuth) {
|
||||||
|
nextService.identity.accessToken = nextIdentity.accessToken
|
||||||
|
nextService.identity.accessScope = nextIdentity.accessScope
|
||||||
|
nextService.identity.accessExpiry = nextIdentity.accessExpiry
|
||||||
|
nextService.identity.refreshToken = nextIdentity.refreshToken
|
||||||
|
nextService.identity.refreshLocation = nextIdentity.refreshLocation
|
||||||
|
} else {
|
||||||
|
nextService.identity = nextIdentity
|
||||||
|
}
|
||||||
|
|
||||||
emit('update:service', nextService)
|
emit('update:service', nextService)
|
||||||
},
|
},
|
||||||
{ immediate: true, deep: true }
|
{ immediate: true, deep: true }
|
||||||
@@ -146,16 +167,6 @@ function syncFromService(service?: ServiceObject) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createServiceObject(service?: ServiceObject): JmapServiceObject {
|
|
||||||
const nextService = new JmapServiceObject()
|
|
||||||
|
|
||||||
if (service) {
|
|
||||||
nextService.fromJson(service.toJson())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextService
|
|
||||||
}
|
|
||||||
|
|
||||||
function createIdentityModel(identity: ServiceIdentity | null) {
|
function createIdentityModel(identity: ServiceIdentity | null) {
|
||||||
if (identity === null) {
|
if (identity === null) {
|
||||||
return null
|
return null
|
||||||
@@ -174,6 +185,8 @@ function createIdentityModel(identity: ServiceIdentity | null) {
|
|||||||
identity.refreshToken,
|
identity.refreshToken,
|
||||||
identity.refreshLocation
|
identity.refreshLocation
|
||||||
)
|
)
|
||||||
|
default:
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,13 +234,8 @@ async function initiateOAuth() {
|
|||||||
<h3 class="text-h6 mb-4">Authentication</h3>
|
<h3 class="text-h6 mb-4">Authentication</h3>
|
||||||
<p class="text-body-2 mb-6">Choose your authentication method and enter your credentials.</p>
|
<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">
|
<v-alert type="info" variant="tonal">
|
||||||
<template #prepend>
|
JMAP supports multiple authentication methods. Choose the one your server uses.
|
||||||
<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>
|
</v-alert>
|
||||||
|
|
||||||
<!-- Authentication Type Selection -->
|
<!-- Authentication Type Selection -->
|
||||||
|
|||||||
268
src/components/JmapAuxiliaryPanel.vue
Normal file
268
src/components/JmapAuxiliaryPanel.vue
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { ServiceObject } from '@KTXM/MailManager/models/service'
|
||||||
|
|
||||||
|
type AuxiliaryTab = 'addresses' | 'messages' | 'sync'
|
||||||
|
type DeleteMode = 'soft' | 'hard'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
service?: ServiceObject
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:service': [value: ServiceObject]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const activeTab = ref<AuxiliaryTab>('addresses')
|
||||||
|
const deleteMode = ref<DeleteMode>('soft')
|
||||||
|
const deleteDestination = ref('Trash')
|
||||||
|
const primaryAddress = ref('')
|
||||||
|
const secondaryAddresses = ref('')
|
||||||
|
|
||||||
|
const settingGroups = [
|
||||||
|
{
|
||||||
|
title: 'Addresses',
|
||||||
|
value: 'addresses' as const,
|
||||||
|
icon: 'mdi-at',
|
||||||
|
description: 'Configure the primary mailbox identity and any sender aliases exposed by this service.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Messages',
|
||||||
|
value: 'messages' as const,
|
||||||
|
icon: 'mdi-email-outline',
|
||||||
|
description: 'Control how message actions should be translated to operations.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Sync',
|
||||||
|
value: 'sync' as const,
|
||||||
|
icon: 'mdi-sync',
|
||||||
|
description: 'Reserved for future synchronization settings.'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const deleteModeOptions = [
|
||||||
|
{
|
||||||
|
title: 'Move to another mailbox',
|
||||||
|
value: 'soft' as const,
|
||||||
|
subtitle: 'Marks delete as a move operation and keeps the message in a destination mailbox.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Permanently delete',
|
||||||
|
value: 'hard' as const,
|
||||||
|
subtitle: 'Removes the message immediately without moving it first.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const destinationHint = computed(() => {
|
||||||
|
if (deleteMode.value === 'hard') {
|
||||||
|
return 'Not used when messages are deleted permanently.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Mailbox identifier or well-known role target, for example Trash.'
|
||||||
|
})
|
||||||
|
|
||||||
|
const secondaryAddressesHint = computed(() => {
|
||||||
|
return 'Use one address per line. Commas are also accepted.'
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.service,
|
||||||
|
service => {
|
||||||
|
syncFromService(service)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[deleteMode, deleteDestination, primaryAddress, secondaryAddresses],
|
||||||
|
() => {
|
||||||
|
const nextService = props.service ?? new ServiceObject()
|
||||||
|
const nextAuxiliary = {
|
||||||
|
...(nextService.auxiliary ?? {}),
|
||||||
|
deleteMode: deleteMode.value,
|
||||||
|
deleteDestination: deleteMode.value === 'soft'
|
||||||
|
? normalizeDeleteDestination(deleteDestination.value)
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sameAuxiliary(nextService.auxiliary ?? {}, nextAuxiliary)) {
|
||||||
|
if (sameAddresses(nextService, primaryAddress.value, secondaryAddresses.value)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextService.primaryAddress = normalizePrimaryAddress(primaryAddress.value)
|
||||||
|
nextService.secondaryAddresses = normalizeSecondaryAddresses(secondaryAddresses.value)
|
||||||
|
nextService.auxiliary = nextAuxiliary
|
||||||
|
emit('update:service', nextService)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
function syncFromService(service?: ServiceObject) {
|
||||||
|
const auxiliary = service?.auxiliary ?? {}
|
||||||
|
deleteMode.value = auxiliary.deleteMode === 'hard' ? 'hard' : 'soft'
|
||||||
|
deleteDestination.value = typeof auxiliary.deleteDestination === 'string' && auxiliary.deleteDestination.length > 0
|
||||||
|
? auxiliary.deleteDestination
|
||||||
|
: 'Trash'
|
||||||
|
primaryAddress.value = service?.primaryAddress ?? ''
|
||||||
|
secondaryAddresses.value = (service?.secondaryAddresses ?? []).join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDeleteDestination(value: string): string {
|
||||||
|
const trimmedValue = value.trim()
|
||||||
|
return trimmedValue.length > 0 ? trimmedValue : 'Trash'
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePrimaryAddress(value: string): string | null {
|
||||||
|
const trimmedValue = value.trim()
|
||||||
|
return trimmedValue.length > 0 ? trimmedValue : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSecondaryAddresses(value: string): string[] {
|
||||||
|
return value
|
||||||
|
.split(/\r?\n|,/)
|
||||||
|
.map(entry => entry.trim())
|
||||||
|
.filter((entry, index, entries) => entry.length > 0 && entries.indexOf(entry) === index)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sameAuxiliary(current: Record<string, any>, next: Record<string, any>): boolean {
|
||||||
|
return (current.deleteMode === 'hard' ? 'hard' : 'soft') === next.deleteMode
|
||||||
|
&& (current.deleteDestination ?? undefined) === (next.deleteDestination ?? undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sameAddresses(service: ServiceObject, nextPrimaryAddress: string, nextSecondaryAddresses: string): boolean {
|
||||||
|
return (service.primaryAddress ?? null) === normalizePrimaryAddress(nextPrimaryAddress)
|
||||||
|
&& JSON.stringify(service.secondaryAddresses) === JSON.stringify(normalizeSecondaryAddresses(nextSecondaryAddresses))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="jmap-auxiliary-panel">
|
||||||
|
<div class="jmap-auxiliary-shell">
|
||||||
|
<v-tabs
|
||||||
|
v-model="activeTab"
|
||||||
|
direction="vertical"
|
||||||
|
color="primary"
|
||||||
|
class="jmap-auxiliary-tabs"
|
||||||
|
>
|
||||||
|
<v-tab
|
||||||
|
v-for="group in settingGroups"
|
||||||
|
:key="group.value"
|
||||||
|
:value="group.value"
|
||||||
|
class="justify-start"
|
||||||
|
>
|
||||||
|
<v-icon start>{{ group.icon }}</v-icon>
|
||||||
|
{{ group.title }}
|
||||||
|
</v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
|
||||||
|
<v-window v-model="activeTab" class="flex-1-1">
|
||||||
|
<v-window-item value="addresses">
|
||||||
|
<div class="jmap-settings-card">
|
||||||
|
<h3 class="text-h6 mb-2">Addresses</h3>
|
||||||
|
<p class="text-body-2 text-medium-emphasis mb-6">
|
||||||
|
Configure the primary mailbox identity and any additional sender aliases exposed by this service.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="primaryAddress"
|
||||||
|
label="Primary Address"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-inner-icon="mdi-email-outline"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-textarea
|
||||||
|
v-model="secondaryAddresses"
|
||||||
|
label="Secondary Addresses"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-inner-icon="mdi-email-multiple-outline"
|
||||||
|
rows="4"
|
||||||
|
:hint="secondaryAddressesHint"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-window-item>
|
||||||
|
|
||||||
|
<v-window-item value="messages">
|
||||||
|
<div class="jmap-settings-card">
|
||||||
|
<h3 class="text-h6 mb-2">Message Deletion</h3>
|
||||||
|
<p class="text-body-2 text-medium-emphasis mb-6">
|
||||||
|
Choose how the mail system should react when a delete command is issued.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<v-radio-group
|
||||||
|
v-model="deleteMode"
|
||||||
|
color="primary"
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
<v-radio
|
||||||
|
v-for="option in deleteModeOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<div>
|
||||||
|
<div class="text-body-1">{{ option.title }}</div>
|
||||||
|
<div class="text-caption text-medium-emphasis">{{ option.subtitle }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-radio>
|
||||||
|
</v-radio-group>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
v-model="deleteDestination"
|
||||||
|
label="Delete Destination"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-inner-icon="mdi-folder-move-outline"
|
||||||
|
:disabled="deleteMode === 'hard'"
|
||||||
|
:hint="destinationHint"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</v-window-item>
|
||||||
|
|
||||||
|
<v-window-item value="sync">
|
||||||
|
<div class="jmap-settings-card">
|
||||||
|
<h3 class="text-h6 mb-2">Sync Settings</h3>
|
||||||
|
<p class="text-body-2 text-medium-emphasis mb-0">
|
||||||
|
Additional synchronization controls can be added here as the provider grows.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</v-window-item>
|
||||||
|
</v-window>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.jmap-auxiliary-shell {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(180px, 220px) minmax(0, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jmap-auxiliary-tabs {
|
||||||
|
border-right: 1px solid rgba(var(--v-theme-outline), 0.16);
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jmap-settings-card {
|
||||||
|
padding: 4px 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.jmap-auxiliary-shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jmap-auxiliary-tabs {
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 1px solid rgba(var(--v-theme-outline), 0.16);
|
||||||
|
padding-right: 0;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { LocationUri } from '@KTXM/MailManager/models/location'
|
import { LocationUri } from '@KTXM/MailManager/models/location'
|
||||||
import type { ServiceObject } from '@KTXM/MailManager/models/service'
|
import { ServiceObject } from '@KTXM/MailManager/models/service'
|
||||||
import type { ServiceLocationUri, ServiceLocation } from '@KTXM/MailManager/types/service'
|
import type { ServiceLocationUri, ServiceLocation } from '@KTXM/MailManager/types/service'
|
||||||
import type { ProviderProtocolPanelProps, ProviderProtocolPanelEmits } from '@KTXM/MailManager/types/integration'
|
import type { ProviderProtocolPanelProps, ProviderProtocolPanelEmits } from '@KTXM/MailManager/types/integration'
|
||||||
import { JmapServiceObject } from '@/models/JmapServiceObject'
|
|
||||||
|
|
||||||
const props = defineProps<ProviderProtocolPanelProps>()
|
const props = defineProps<ProviderProtocolPanelProps>()
|
||||||
const emit = defineEmits<ProviderProtocolPanelEmits>()
|
const emit = defineEmits<ProviderProtocolPanelEmits>()
|
||||||
@@ -75,8 +74,25 @@ watch(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextService = createServiceObject(props.service)
|
const nextService = props.service ?? new ServiceObject()
|
||||||
nextService.location = location ? LocationUri.fromJson(location) : null
|
|
||||||
|
if (location === null) {
|
||||||
|
nextService.location = null
|
||||||
|
emit('update:service', nextService)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextService.location instanceof LocationUri) {
|
||||||
|
nextService.location.scheme = location.scheme
|
||||||
|
nextService.location.host = location.host
|
||||||
|
nextService.location.port = location.port
|
||||||
|
nextService.location.path = location.path
|
||||||
|
nextService.location.verifyPeer = location.verifyPeer ?? true
|
||||||
|
nextService.location.verifyHost = location.verifyHost ?? true
|
||||||
|
} else {
|
||||||
|
nextService.location = LocationUri.fromJson(location)
|
||||||
|
}
|
||||||
|
|
||||||
emit('update:service', nextService)
|
emit('update:service', nextService)
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
@@ -94,16 +110,6 @@ function syncFromLocation(location: ServiceLocation | null) {
|
|||||||
servicePath.value = uriLocation?.path ?? ''
|
servicePath.value = uriLocation?.path ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function createServiceObject(service?: ServiceObject): JmapServiceObject {
|
|
||||||
const nextService = new JmapServiceObject()
|
|
||||||
|
|
||||||
if (service) {
|
|
||||||
nextService.fromJson(service.toJson())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextService
|
|
||||||
}
|
|
||||||
|
|
||||||
function sameLocation(a: ServiceLocation | null, b: ServiceLocation | null): boolean {
|
function sameLocation(a: ServiceLocation | null, b: ServiceLocation | null): boolean {
|
||||||
if (a === null || b === null) {
|
if (a === null || b === null) {
|
||||||
return a === b
|
return a === b
|
||||||
@@ -290,19 +296,9 @@ function getUriVerifyHost(location?: ServiceLocation | null): boolean {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Info Alert -->
|
<!-- Info Alert -->
|
||||||
<v-alert
|
<v-alert type="info" variant="tonal">
|
||||||
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
|
JMAP is a modern protocol for mail access. Most JMAP servers use
|
||||||
<code>/.well-known/jmap</code> for autodiscovery.
|
<code>/.well-known/jmap</code> for autodiscovery.
|
||||||
</div>
|
|
||||||
</v-alert>
|
</v-alert>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,31 +3,39 @@ import type { ServiceInterface } from "@KTXM/MailManager/types/service";
|
|||||||
import { JmapServiceObject } from './models/JmapServiceObject'
|
import { JmapServiceObject } from './models/JmapServiceObject'
|
||||||
|
|
||||||
const integrations: ModuleIntegrations = {
|
const integrations: ModuleIntegrations = {
|
||||||
mail_account_protocol_panels: [
|
mail_provider_panels_auxiliary: [
|
||||||
{
|
{
|
||||||
id: 'jmap',
|
id: 'jmap',
|
||||||
label: 'JMAP Protocol Panel',
|
label: 'JMAP Settings',
|
||||||
|
component: () => import('@/components/JmapAuxiliaryPanel.vue'),
|
||||||
|
priority: 10,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
mail_provider_panels_protocol: [
|
||||||
|
{
|
||||||
|
id: 'jmap',
|
||||||
|
label: 'JMAP Protocol',
|
||||||
component: () => import('@/components/JmapProtocolPanel.vue'),
|
component: () => import('@/components/JmapProtocolPanel.vue'),
|
||||||
priority: 10,
|
priority: 10,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
mail_account_auth_panels: [
|
mail_provider_panels_auth: [
|
||||||
{
|
{
|
||||||
id: 'jmap',
|
id: 'jmap',
|
||||||
label: 'JMAP Authentication Panel',
|
label: 'JMAP Authentication',
|
||||||
component: () => import('@/components/JmapAuthPanel.vue'),
|
component: () => import('@/components/JmapAuthPanel.vue'),
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
mail_service_factory: [
|
mail_provider_factory_service: [
|
||||||
{
|
{
|
||||||
id: 'jmap',
|
id: 'jmap',
|
||||||
factory: (data: ServiceInterface) => new JmapServiceObject().fromJson(data)
|
factory: (data: ServiceInterface) => new JmapServiceObject().fromJson(data)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
mail_provider_metadata: [
|
mail_provider_details: [
|
||||||
{
|
{
|
||||||
id: 'jmap',
|
id: 'jmap',
|
||||||
label: 'JMAP',
|
label: 'JMAP Protocol',
|
||||||
description: 'Modern JSON-based mail API protocol',
|
description: 'Modern JSON-based mail API protocol',
|
||||||
icon: 'mdi-api',
|
icon: 'mdi-api',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,16 @@ export default defineConfig({
|
|||||||
'vue',
|
'vue',
|
||||||
'vue-router',
|
'vue-router',
|
||||||
'pinia',
|
'pinia',
|
||||||
|
/^@KTXM\/MailManager\//,
|
||||||
],
|
],
|
||||||
output: {
|
output: {
|
||||||
|
paths: (id) => {
|
||||||
|
if (id.startsWith('@KTXM/MailManager/')) {
|
||||||
|
return '/modules/mail_manager/static/module.mjs'
|
||||||
|
}
|
||||||
|
|
||||||
|
return id
|
||||||
|
},
|
||||||
assetFileNames: (assetInfo) => {
|
assetFileNames: (assetInfo) => {
|
||||||
if (assetInfo.name?.endsWith('.css')) {
|
if (assetInfo.name?.endsWith('.css')) {
|
||||||
return 'provider_jmapc-[hash].css'
|
return 'provider_jmapc-[hash].css'
|
||||||
|
|||||||
Reference in New Issue
Block a user