generated from Nodarx/template
feat: lots more improvements
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { IdentityBasic } from '@KTXM/MailManager/models/identity'
|
||||
import { ServiceObject } from '@KTXM/MailManager/models/service'
|
||||
import type { ServiceIdentity } from '@KTXM/MailManager/types/service'
|
||||
import type { ServiceIdentity, ServiceIdentityBasic } from '@KTXM/MailManager/types/service'
|
||||
import type { ProviderAuthPanelProps, ProviderAuthPanelEmits } from '@KTXM/MailManager/types/integration'
|
||||
|
||||
const props = defineProps<ProviderAuthPanelProps>()
|
||||
@@ -15,7 +15,7 @@ const rules = {
|
||||
required: (value: unknown) => !!value || 'This field is required'
|
||||
}
|
||||
|
||||
const currentIdentity = computed((): ServiceIdentity | null => {
|
||||
const currentIdentity = computed((): ServiceIdentityBasic | null => {
|
||||
if (!identity.value || !secret.value) {
|
||||
return null
|
||||
}
|
||||
@@ -53,8 +53,21 @@ watch(
|
||||
return
|
||||
}
|
||||
|
||||
const nextService = createServiceObject(props.service)
|
||||
nextService.identity = value ? new IdentityBasic(value.identity, value.secret) : null
|
||||
const nextService = props.service ?? new ServiceObject()
|
||||
|
||||
if (value === null) {
|
||||
nextService.identity = null
|
||||
emit('update:service', nextService)
|
||||
return
|
||||
}
|
||||
|
||||
if (nextService.identity instanceof IdentityBasic) {
|
||||
nextService.identity.identity = value.identity
|
||||
nextService.identity.secret = value.secret
|
||||
} else {
|
||||
nextService.identity = new IdentityBasic(value.identity, value.secret)
|
||||
}
|
||||
|
||||
emit('update:service', nextService)
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
@@ -73,16 +86,6 @@ function syncFromService(service?: ServiceObject) {
|
||||
secret.value = props.prefilledSecret || ''
|
||||
}
|
||||
|
||||
function createServiceObject(service?: ServiceObject): ServiceObject {
|
||||
const nextService = new ServiceObject()
|
||||
|
||||
if (service) {
|
||||
nextService.fromJson(service.toJson())
|
||||
}
|
||||
|
||||
return nextService
|
||||
}
|
||||
|
||||
function sameIdentity(a: ServiceIdentity | null, b: ServiceIdentity | null): boolean {
|
||||
if (a === null || b === null) {
|
||||
return a === b
|
||||
@@ -100,13 +103,8 @@ function sameIdentity(a: ServiceIdentity | null, b: ServiceIdentity | null): boo
|
||||
<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 type="info" variant="tonal">
|
||||
Most IMAP servers use your full email address as the username. Use an app password if your mail host requires one.
|
||||
</v-alert>
|
||||
|
||||
<v-text-field
|
||||
|
||||
268
src/components/ImapAuxiliaryPanel.vue
Normal file
268
src/components/ImapAuxiliaryPanel.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="imap-auxiliary-panel">
|
||||
<div class="imap-auxiliary-shell">
|
||||
<v-tabs
|
||||
v-model="activeTab"
|
||||
direction="vertical"
|
||||
color="primary"
|
||||
class="imap-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="imap-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="imap-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="imap-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>
|
||||
.imap-auxiliary-shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 220px) minmax(0, 1fr);
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.imap-auxiliary-tabs {
|
||||
border-right: 1px solid rgba(var(--v-theme-outline), 0.16);
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.imap-settings-card {
|
||||
padding: 4px 4px 4px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.imap-auxiliary-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.imap-auxiliary-tabs {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid rgba(var(--v-theme-outline), 0.16);
|
||||
padding-right: 0;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -72,8 +72,24 @@ watch(
|
||||
return
|
||||
}
|
||||
|
||||
const nextService = createServiceObject(props.service)
|
||||
nextService.location = location ? LocationSocketSole.fromJson(location) : null
|
||||
const nextService = props.service ?? new ServiceObject()
|
||||
|
||||
if (location === null) {
|
||||
nextService.location = null
|
||||
emit('update:service', nextService)
|
||||
return
|
||||
}
|
||||
|
||||
if (nextService.location instanceof LocationSocketSole) {
|
||||
nextService.location.host = location.host
|
||||
nextService.location.port = location.port
|
||||
nextService.location.encryption = location.encryption
|
||||
nextService.location.verifyPeer = location.verifyPeer ?? true
|
||||
nextService.location.verifyHost = location.verifyHost ?? true
|
||||
} else {
|
||||
nextService.location = LocationSocketSole.fromJson(location)
|
||||
}
|
||||
|
||||
emit('update:service', nextService)
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
@@ -96,16 +112,6 @@ function syncFromLocation(location: ServiceLocation | null) {
|
||||
verifyHost.value = socketLocation?.verifyHost ?? true
|
||||
}
|
||||
|
||||
function createServiceObject(service?: ServiceObject): ServiceObject {
|
||||
const nextService = new ServiceObject()
|
||||
|
||||
if (service) {
|
||||
nextService.fromJson(service.toJson())
|
||||
}
|
||||
|
||||
return nextService
|
||||
}
|
||||
|
||||
function getSocketLocation(location?: ServiceLocation | null): ServiceLocationSocketSole | null {
|
||||
return location?.type === 'SOCKET_SOLE' ? location : null
|
||||
}
|
||||
@@ -201,13 +207,8 @@ function defaultPortFor(nextEncryption: ImapEncryption): number {
|
||||
</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 type="info" variant="tonal">
|
||||
STARTTLS is accepted for compatibility, but the current IMAP client transport does not perform STARTTLS negotiation. Prefer TLS on port 993 when available.
|
||||
</v-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user