generated from Nodarx/template
feat: implement provider
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
131
src/components/ImapAuthPanel.vue
Normal file
131
src/components/ImapAuthPanel.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, 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>()
|
||||
|
||||
const identity = ref(props.prefilledIdentity || props.emailAddress || '')
|
||||
const secret = ref(props.prefilledSecret || '')
|
||||
|
||||
const rules = {
|
||||
required: (value: unknown) => !!value || 'This field is required'
|
||||
}
|
||||
|
||||
const isValid = computed(() => !!identity.value && !!secret.value)
|
||||
|
||||
const currentIdentity = computed((): ServiceIdentity | null => {
|
||||
if (!isValid.value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'BA',
|
||||
identity: identity.value,
|
||||
secret: secret.value,
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
currentIdentity,
|
||||
value => {
|
||||
if (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
isValid,
|
||||
value => {
|
||||
emit('valid', value)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
value => {
|
||||
if (value?.type === 'BA') {
|
||||
identity.value = value.identity || ''
|
||||
secret.value = value.secret || ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.emailAddress,
|
||||
value => {
|
||||
if (value && !identity.value) {
|
||||
identity.value = value
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="imap-auth-panel">
|
||||
<h3 class="text-h6 mb-4">Authentication</h3>
|
||||
<p class="text-body-2 mb-6">Provide the username and password your IMAP server expects.</p>
|
||||
|
||||
<v-alert type="info" variant="tonal" class="mb-4">
|
||||
<template #prepend>
|
||||
<v-icon>mdi-information</v-icon>
|
||||
</template>
|
||||
<div class="text-caption">
|
||||
Most IMAP servers use your full email address as the username. Use an app password if your mail host requires one.
|
||||
</div>
|
||||
</v-alert>
|
||||
|
||||
<v-text-field
|
||||
v-model="identity"
|
||||
label="Username / Email"
|
||||
hint="Account login used by the IMAP server"
|
||||
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="secret"
|
||||
type="password"
|
||||
label="Password"
|
||||
hint="Password or app-specific password"
|
||||
persistent-hint
|
||||
variant="outlined"
|
||||
prepend-inner-icon="mdi-lock"
|
||||
class="mb-4"
|
||||
autocomplete="current-password"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.imap-auth-panel {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.text-h6 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
line-height: 2rem;
|
||||
letter-spacing: 0.0125em;
|
||||
}
|
||||
|
||||
.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>
|
||||
277
src/components/ImapConfigPanel.vue
Normal file
277
src/components/ImapConfigPanel.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type {
|
||||
ServiceLocation,
|
||||
ServiceLocationSocketSole,
|
||||
ServiceLocationUri,
|
||||
} from '@KTXM/MailManager/types/service'
|
||||
import type { ProviderConfigPanelProps, ProviderConfigPanelEmits } from '@KTXM/MailManager/types/integration'
|
||||
|
||||
type ImapEncryption = 'none' | 'ssl' | 'tls' | 'starttls'
|
||||
|
||||
type ImapLocation = ServiceLocationSocketSole & {
|
||||
type: 'SOCKET_SOLE' | 'URI'
|
||||
verifyPeerName?: boolean
|
||||
allowSelfSigned?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<ProviderConfigPanelProps>()
|
||||
const emit = defineEmits<ProviderConfigPanelEmits>()
|
||||
|
||||
function asImapLocation(location?: ServiceLocation): ImapLocation | null {
|
||||
if (!location) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (location.type === 'SOCKET_SOLE') {
|
||||
return location as ImapLocation
|
||||
}
|
||||
|
||||
if (location.type === 'URI') {
|
||||
const uriLocation = location as ServiceLocationUri & {
|
||||
encryption?: ImapEncryption
|
||||
verifyPeerName?: boolean
|
||||
allowSelfSigned?: boolean
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'URI',
|
||||
host: uriLocation.host || '',
|
||||
port: uriLocation.port || 993,
|
||||
encryption: uriLocation.encryption || 'ssl',
|
||||
verifyPeer: uriLocation.verifyPeer ?? true,
|
||||
verifyHost: uriLocation.verifyPeerName ?? uriLocation.verifyHost ?? true,
|
||||
verifyPeerName: uriLocation.verifyPeerName ?? uriLocation.verifyHost ?? true,
|
||||
allowSelfSigned: uriLocation.allowSelfSigned ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function defaultPortFor(encryption: ImapEncryption): number {
|
||||
return encryption === 'ssl' || encryption === 'tls' ? 993 : 143
|
||||
}
|
||||
|
||||
const sourceLocation = computed(() => asImapLocation(props.modelValue || props.discoveredLocation))
|
||||
|
||||
const host = ref(sourceLocation.value?.host || '')
|
||||
const encryption = ref<ImapEncryption>(sourceLocation.value?.encryption || 'ssl')
|
||||
const port = ref(String(sourceLocation.value?.port || defaultPortFor(encryption.value)))
|
||||
const verifyPeer = ref(sourceLocation.value?.verifyPeer ?? true)
|
||||
const verifyHost = ref(sourceLocation.value?.verifyPeerName ?? sourceLocation.value?.verifyHost ?? true)
|
||||
const allowSelfSigned = ref(sourceLocation.value?.allowSelfSigned ?? false)
|
||||
|
||||
const encryptionOptions = [
|
||||
{ title: 'Implicit TLS (SSL)', value: 'ssl' },
|
||||
{ title: 'TLS', value: 'tls' },
|
||||
{ title: 'STARTTLS', value: 'starttls' },
|
||||
{ title: 'None', value: 'none' },
|
||||
]
|
||||
|
||||
const rules = {
|
||||
required: (value: unknown) => !!value || 'This field is required',
|
||||
port: (value: string) => {
|
||||
const numericValue = Number(value)
|
||||
return Number.isInteger(numericValue) && numericValue >= 1 && numericValue <= 65535
|
||||
? true
|
||||
: 'Port must be between 1 and 65535'
|
||||
}
|
||||
}
|
||||
|
||||
const isValid = computed(() => !!host.value && rules.port(port.value) === true)
|
||||
|
||||
const currentLocation = computed((): ServiceLocation | null => {
|
||||
if (!isValid.value) {
|
||||
return null
|
||||
}
|
||||
|
||||
const numericPort = Number(port.value)
|
||||
const location: ImapLocation = {
|
||||
type: 'SOCKET_SOLE',
|
||||
host: host.value,
|
||||
port: numericPort,
|
||||
encryption: encryption.value,
|
||||
verifyPeer: verifyPeer.value,
|
||||
verifyHost: verifyHost.value,
|
||||
verifyPeerName: verifyHost.value,
|
||||
allowSelfSigned: allowSelfSigned.value,
|
||||
}
|
||||
|
||||
return location as ServiceLocation
|
||||
})
|
||||
|
||||
watch(
|
||||
currentLocation,
|
||||
value => {
|
||||
if (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
isValid,
|
||||
value => {
|
||||
emit('valid', value)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
value => {
|
||||
const next = asImapLocation(value)
|
||||
if (!next) {
|
||||
return
|
||||
}
|
||||
|
||||
host.value = next.host || ''
|
||||
encryption.value = next.encryption || 'ssl'
|
||||
port.value = String(next.port || defaultPortFor(encryption.value))
|
||||
verifyPeer.value = next.verifyPeer ?? true
|
||||
verifyHost.value = next.verifyPeerName ?? next.verifyHost ?? true
|
||||
allowSelfSigned.value = next.allowSelfSigned ?? false
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.discoveredLocation,
|
||||
value => {
|
||||
if (props.modelValue) {
|
||||
return
|
||||
}
|
||||
|
||||
const next = asImapLocation(value)
|
||||
if (!next) {
|
||||
return
|
||||
}
|
||||
|
||||
host.value = next.host || ''
|
||||
encryption.value = next.encryption || 'ssl'
|
||||
port.value = String(next.port || defaultPortFor(encryption.value))
|
||||
verifyPeer.value = next.verifyPeer ?? true
|
||||
verifyHost.value = next.verifyPeerName ?? next.verifyHost ?? true
|
||||
allowSelfSigned.value = next.allowSelfSigned ?? false
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(encryption, (next, previous) => {
|
||||
const previousDefault = defaultPortFor(previous)
|
||||
if (!port.value || Number(port.value) === previousDefault) {
|
||||
port.value = String(defaultPortFor(next))
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="imap-config-panel">
|
||||
<h3 class="text-h6 mb-4">IMAP Connection Settings</h3>
|
||||
<p class="text-body-2 mb-6">Configure the server address, transport security, and certificate verification for your IMAP mailbox.</p>
|
||||
|
||||
<v-text-field
|
||||
v-model="host"
|
||||
label="Server Host"
|
||||
hint="For example: imap.example.com"
|
||||
persistent-hint
|
||||
variant="outlined"
|
||||
prepend-inner-icon="mdi-server"
|
||||
class="mb-4"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
|
||||
<v-select
|
||||
v-model="encryption"
|
||||
:items="encryptionOptions"
|
||||
label="Security"
|
||||
variant="outlined"
|
||||
prepend-inner-icon="mdi-shield-lock"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="port"
|
||||
label="Port"
|
||||
hint="Defaults to 993 for TLS/SSL and 143 for plain or STARTTLS"
|
||||
persistent-hint
|
||||
variant="outlined"
|
||||
prepend-inner-icon="mdi-numeric"
|
||||
class="mb-4"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
:rules="[rules.required, rules.port]"
|
||||
/>
|
||||
|
||||
<v-expansion-panels class="mt-4">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-title>
|
||||
<v-icon start>mdi-cog</v-icon>
|
||||
Security Options
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-switch
|
||||
v-model="verifyPeer"
|
||||
label="Verify TLS certificate"
|
||||
color="primary"
|
||||
hint="Disable only for trusted internal or test environments"
|
||||
persistent-hint
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-switch
|
||||
v-model="verifyHost"
|
||||
label="Verify certificate hostname"
|
||||
color="primary"
|
||||
hint="Checks that the certificate matches the IMAP host"
|
||||
persistent-hint
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<v-switch
|
||||
v-model="allowSelfSigned"
|
||||
label="Allow self-signed certificates"
|
||||
color="primary"
|
||||
hint="Use only when your server is intentionally deployed with a self-signed certificate"
|
||||
persistent-hint
|
||||
/>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
|
||||
<v-alert type="info" variant="tonal" density="compact" class="mt-4">
|
||||
<template #prepend>
|
||||
<v-icon>mdi-information</v-icon>
|
||||
</template>
|
||||
<div class="text-caption">
|
||||
STARTTLS is accepted for compatibility, but the current IMAP client transport does not perform STARTTLS negotiation. Prefer TLS on port 993 when available.
|
||||
</div>
|
||||
</v-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.imap-config-panel {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.text-h6 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
line-height: 2rem;
|
||||
letter-spacing: 0.0125em;
|
||||
}
|
||||
|
||||
.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>
|
||||
38
src/integrations.ts
Normal file
38
src/integrations.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { ModuleIntegrations } from '@KTXC/types/moduleTypes'
|
||||
import type { ServiceInterface } from '@KTXM/MailManager/types/service'
|
||||
import { ServiceObject } from '@KTXM/MailManager/models/service'
|
||||
|
||||
const integrations: ModuleIntegrations = {
|
||||
mail_account_config_panels: [
|
||||
{
|
||||
id: 'imap',
|
||||
label: 'IMAP',
|
||||
icon: 'mdi-email',
|
||||
caption: 'Internet Message Access Protocol',
|
||||
component: () => import('@/components/ImapConfigPanel.vue'),
|
||||
priority: 20,
|
||||
}
|
||||
],
|
||||
mail_account_auth_panels: [
|
||||
{
|
||||
id: 'imap',
|
||||
component: () => import('@/components/ImapAuthPanel.vue'),
|
||||
}
|
||||
],
|
||||
mail_service_factory: [
|
||||
{
|
||||
id: 'imap',
|
||||
factory: (data: ServiceInterface) => new ServiceObject().fromJson(data)
|
||||
}
|
||||
],
|
||||
mail_provider_metadata: [
|
||||
{
|
||||
id: 'imap',
|
||||
label: 'IMAP',
|
||||
description: 'Classic mailbox access over IMAP',
|
||||
icon: 'mdi-email',
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default integrations
|
||||
12
src/main.ts
Normal file
12
src/main.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import routes from '@/routes'
|
||||
import integrations from '@/integrations'
|
||||
import type { App as VueApp } from 'vue'
|
||||
|
||||
export const css = ['__CSS_FILENAME_PLACEHOLDER__']
|
||||
|
||||
export { routes, integrations }
|
||||
|
||||
export default {
|
||||
install(_app: VueApp) {
|
||||
}
|
||||
}
|
||||
3
src/routes.ts
Normal file
3
src/routes.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
const routes = []
|
||||
|
||||
export default routes
|
||||
1
src/style.css
Normal file
1
src/style.css
Normal file
@@ -0,0 +1 @@
|
||||
/* imap provider module styles */
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user