Initial commit

This commit is contained in:
root
2025-12-21 09:55:58 -05:00
committed by Sebastian Krupinski
commit 169b7b4c91
57 changed files with 10105 additions and 0 deletions
+158
View File
@@ -0,0 +1,158 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
const props = defineProps<{
address: string
secret?: string | null
hostname?: string | null
}>()
const emit = defineEmits<{
'update:address': [value: string]
'update:secret': [value: string | null]
'update:hostname': [value: string | null]
'discover': []
'manual': []
}>()
const localAddress = computed({
get: () => props.address,
set: (value) => emit('update:address', value?.trim())
})
const localSecret = computed({
get: () => props.secret,
set: (value) => emit('update:secret', value?.trim() || null)
})
const localHostname = computed({
get: () => props.hostname || '',
set: (value) => emit('update:hostname', value?.trim() || null)
})
const showSecret = ref(false)
const rules = {
required: (v: string) => !!v || 'Required',
email: (v: string) => /.+@.+\..+/.test(v) || 'Invalid email address'
}
</script>
<template>
<div class="identity-entry-step">
<h3 class="text-h6 mb-2">Email Account Setup</h3>
<p class="text-body-2 text-medium-emphasis mb-6">
Enter your email address to automatically discover your mail server settings.
</p>
<v-text-field
v-model="localAddress"
label="Email Address"
type="email"
prepend-inner-icon="mdi-email"
variant="outlined"
required
autocomplete="off"
:rules="[rules.required, rules.email]"
class="mb-4"
/>
<!-- Advanced Options -->
<v-expansion-panels variant="accordion" class="mb-6">
<v-expansion-panel>
<v-expansion-panel-title>
<v-icon start>mdi-cog</v-icon>
Advanced Options (Optional)
</v-expansion-panel-title>
<v-expansion-panel-text>
<v-text-field
v-model="localSecret"
:type="showSecret ? 'text' : 'password'"
label="Password (Optional)"
hint="Provide password to validate credentials during discovery"
persistent-hint
prepend-inner-icon="mdi-lock"
variant="outlined"
autocomplete="new-password"
class="mb-4"
>
<template #append-inner>
<v-btn
:icon="showSecret ? 'mdi-eye-off' : 'mdi-eye'"
variant="text"
size="small"
@click="showSecret = !showSecret"
/>
</template>
</v-text-field>
<!-- Info -->
<v-alert
type="info"
variant="tonal"
density="compact"
class="mt-6"
>
<template #prepend>
<v-icon>mdi-information</v-icon>
</template>
<div class="text-caption">
Your credentials are used only to discover and test server settings.
They are transmitted securely and not stored during discovery.
</div>
</v-alert>
<v-text-field
v-model="localHostname"
label="Server Hostname"
hint="If you know your mail server hostname, enter it here to skip DNS lookup (e.g., mail.example.com)"
persistent-hint
prepend-inner-icon="mdi-server"
variant="outlined"
placeholder="mail.example.com"
clearable
/>
<v-alert type="info" variant="tonal" density="compact" class="mt-3">
<template #prepend>
<v-icon size="small">mdi-information</v-icon>
</template>
<div class="text-caption">
Providing a hostname will skip DNS SRV lookups and test the server directly.
Leave blank for automatic discovery.
</div>
</v-alert>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<!-- Actions -->
<div class="d-flex flex-column gap-3">
<v-btn
color="primary"
size="large"
block
:disabled="!localAddress || !rules.email(localAddress)"
@click="$emit('discover')"
>
<v-icon start>mdi-magnify</v-icon>
Discover Settings
</v-btn>
<v-btn
variant="text"
block
@click="$emit('manual')"
>
Manual Configuration
</v-btn>
</div>
</div>
</template>
<style scoped>
.gap-3 {
gap: 12px;
}
</style>
@@ -0,0 +1,220 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { ProviderDiscoveryStatus } from '@MailManager/types/service'
const props = defineProps<{
address: string
status: Record<string, ProviderDiscoveryStatus>
}>()
const emit = defineEmits<{
'select': [providerId: string]
'advanced': [providerId: string]
'manual': []
'back': []
}>()
const sortedStatus = computed(() => {
const statusArray = Object.values(props.status)
const order = { success: 0, discovering: 1, pending: 2, failed: 3 }
return statusArray.sort((a, b) => order[a.status] - order[b.status])
})
const progressPercent = computed(() => {
const total = Object.keys(props.status).length
if (total === 0) return 0
const completed = Object.values(props.status).filter(
s => s.status === 'success' || s.status === 'failed'
).length
return (completed / total) * 100
})
const isDiscovering = computed(() => {
return Object.values(props.status).some(
s => s.status === 'discovering' || s.status === 'pending'
)
})
const successCount = computed(() => {
return Object.values(props.status).filter(s => s.status === 'success').length
})
function getStatusColor(status: string): string {
const colors: Record<string, string> = {
success: 'success',
failed: 'error',
discovering: 'primary',
pending: 'grey'
}
return colors[status] || 'grey'
}
function getProviderLabel(providerId: string): string {
const labels: Record<string, string> = {
jmap: 'JMAP',
smtp: 'SMTP/IMAP',
imap: 'IMAP',
exchange: 'Microsoft Exchange'
}
return labels[providerId] || providerId.toUpperCase()
}
</script>
<template>
<div class="discovery-status-step">
<h3 class="text-h6 mb-2">Discovering Mail Servers</h3>
<p class="text-body-2 text-medium-emphasis mb-6">
Testing {{ address }} across multiple providers...
</p>
<!-- Overall Progress -->
<v-progress-linear
:model-value="progressPercent"
color="primary"
height="8"
rounded
class="mb-6"
/>
<!-- Provider Status List -->
<v-list class="provider-status-list">
<v-list-item
v-for="status in sortedStatus"
:key="status.provider"
class="provider-status-item mb-2"
:class="`status-${status.status}`"
>
<template #prepend>
<v-avatar :color="getStatusColor(status.status)" size="40">
<v-icon v-if="status.status === 'success'" color="white">
mdi-check
</v-icon>
<v-icon v-else-if="status.status === 'failed'" color="white">
mdi-close
</v-icon>
<v-progress-circular
v-else-if="status.status === 'discovering'"
indeterminate
size="24"
width="3"
color="white"
/>
<v-icon v-else color="white">
mdi-clock-outline
</v-icon>
</v-avatar>
</template>
<v-list-item-title class="d-flex align-center gap-2">
<span class="font-weight-medium">{{ getProviderLabel(status.provider) }}</span>
<v-chip
v-if="status.status === 'success'"
size="x-small"
color="success"
variant="flat"
>
Found
</v-chip>
<v-chip
v-else-if="status.status === 'failed'"
size="x-small"
color="error"
variant="flat"
>
{{ status.error || 'Failed' }}
</v-chip>
<v-chip
v-else-if="status.status === 'discovering'"
size="x-small"
color="primary"
variant="flat"
>
Testing...
</v-chip>
</v-list-item-title>
<v-list-item-subtitle v-if="status.status === 'success' && status.metadata">
<div class="text-caption">
<v-icon size="14" class="mr-1">mdi-server</v-icon>
{{ status.metadata.host }}{{ status.metadata.port ? ':' + status.metadata.port : '' }}
<span v-if="status.metadata.protocol" class="ml-2">
<v-icon size="14" class="mr-1">mdi-shield-lock</v-icon>
{{ status.metadata.protocol.toUpperCase() }}
</span>
</div>
</v-list-item-subtitle>
<template #append>
<div v-if="status.status === 'success'" class="d-flex gap-2">
<v-btn
size="small"
variant="tonal"
color="primary"
prepend-icon="mdi-check"
@click="$emit('select', status.provider)"
>
Select
</v-btn>
<v-tooltip text="Advanced configuration">
<template #activator="{ props }">
<v-btn
v-bind="props"
size="small"
variant="outlined"
icon="mdi-tune"
@click="$emit('advanced', status.provider)"
/>
</template>
</v-tooltip>
</div>
</template>
</v-list-item>
</v-list>
<!-- No Results Message -->
<v-alert
v-if="!isDiscovering && successCount === 0"
type="warning"
variant="tonal"
class="mt-6"
>
<template #prepend>
<v-icon>mdi-alert</v-icon>
</template>
<div>
<div class="font-weight-medium mb-1">No configurations found</div>
<div class="text-caption">
We couldn't automatically discover server settings for {{ address }}.
You can try manual configuration instead.
</div>
</div>
</v-alert>
</div>
</template>
<style scoped>
.provider-status-item {
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
border-radius: 8px;
transition: all 0.2s;
}
.provider-status-item.status-success {
background-color: rgba(var(--v-theme-success), 0.05);
}
.provider-status-item.status-discovering {
background-color: rgba(var(--v-theme-primary), 0.05);
}
.gap-2 {
gap: 8px;
}
.gap-3 {
gap: 12px;
}
</style>
+163
View File
@@ -0,0 +1,163 @@
<template>
<div class="provider-auth-step">
<h3 class="text-h6 mb-2">Authentication</h3>
<p class="text-body-2 text-medium-emphasis mb-6">
Configure authentication for {{ providerLabel }}
</p>
<!-- Loading State -->
<div v-if="loadingPanel" class="text-center py-8">
<v-progress-circular indeterminate color="primary" />
<p class="text-caption text-medium-emphasis mt-2">
Loading authentication panel...
</p>
</div>
<!-- Dynamic Provider Auth Panel -->
<component
v-else-if="currentAuthPanel"
:is="currentAuthPanel"
:email-address="emailAddress"
:discovered-location="discoveredLocation"
:prefilled-identity="prefilledIdentity"
:prefilled-secret="prefilledSecret"
v-model="localIdentity"
@update:model-value="handleIdentityUpdate"
@valid="handleValidChange"
@error="handleAuthError"
/>
<!-- No Panel Available -->
<v-alert v-else type="error" variant="tonal">
<v-icon start>mdi-alert-circle</v-icon>
No authentication method available for this provider.
Please contact support.
</v-alert>
<!-- Error Display -->
<v-alert
v-if="authError"
type="error"
variant="tonal"
class="mt-4"
closable
@click:close="authError = ''"
>
<v-icon start>mdi-alert</v-icon>
{{ authError }}
</v-alert>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useIntegrationStore } from '@KTXC/stores/integrationStore'
import type { ServiceIdentity, ServiceLocation } from '@MailManager/types/service'
const props = defineProps<{
providerId: string
providerLabel: string
emailAddress: string
discoveredLocation?: ServiceLocation
prefilledIdentity?: string
prefilledSecret?: string
modelValue?: ServiceIdentity | null
}>()
const emit = defineEmits<{
'update:modelValue': [value: ServiceIdentity]
'valid': [value: boolean]
}>()
const integrationStore = useIntegrationStore()
const loadedPanels = new Map<string, any>()
const currentAuthPanel = ref<any>(null)
const loadingPanel = ref(false)
const localIdentity = ref<ServiceIdentity | undefined>(props.modelValue)
const authError = ref('')
// The full integration ID (e.g., "jmap")
const effectiveIntegrationId = computed(() => {
return props.providerId
})
// Load provider auth panel dynamically
async function loadAuthPanel(integrationId: string) {
if (loadedPanels.has(integrationId)) {
currentAuthPanel.value = loadedPanels.get(integrationId)
return
}
loadingPanel.value = true
// Try to find panel - integration IDs are prefixed with module handle
// so we need to search for panels that match the provider ID
const panels = integrationStore.getItems('mail_account_auth_panels')
const panelConfig = panels.find((panel: any) => {
// Check if the ID ends with the provider ID (e.g., "provider_jmapc.jmap" contains "jmap")
return panel.id === integrationId || panel.id.endsWith(`.${integrationId}`)
})
if (!panelConfig?.component) {
console.error(`No auth panel found for provider ID: ${integrationId}`)
console.error(`Available panels:`, panels.map((p: any) => p.id))
currentAuthPanel.value = null
loadingPanel.value = false
return
}
try {
const module = await panelConfig.component()
const component = module.default || module
loadedPanels.set(integrationId, component)
currentAuthPanel.value = component
} catch (error) {
console.error(`Failed to load auth panel for ${integrationId}:`, error)
currentAuthPanel.value = null
authError.value = `Failed to load authentication panel: ${error}`
} finally {
loadingPanel.value = false
}
}
// Load panel when provider changes
watch(
effectiveIntegrationId,
(newIntegrationId, oldIntegrationId) => {
if (newIntegrationId && newIntegrationId !== oldIntegrationId) {
loadAuthPanel(newIntegrationId)
}
},
{ immediate: true }
)
function handleIdentityUpdate(identity: ServiceIdentity) {
localIdentity.value = identity
emit('update:modelValue', identity)
}
function handleValidChange(valid: boolean) {
emit('valid', valid)
}
function handleAuthError(error: string) {
authError.value = error
}
// Watch for prop changes
watch(
() => props.modelValue,
(newValue) => {
if (newValue) {
localIdentity.value = newValue
}
}
)
</script>
<style scoped>
.provider-auth-step {
max-width: 800px;
}
</style>
+124
View File
@@ -0,0 +1,124 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { useIntegrationStore } from '@KTXC/stores/integrationStore'
import type { ServiceLocation } from '@MailManager/types/service'
const props = defineProps<{
providerId: string
discoveredLocation?: ServiceLocation
modelValue?: ServiceLocation | null
}>()
const emit = defineEmits<{
'update:modelValue': [value: ServiceLocation]
'valid': [value: boolean]
}>()
const integrationStore = useIntegrationStore()
const loadedPanels = new Map<string, any>()
const currentProviderPanel = ref<any>(null)
const loadingPanel = ref(false)
const localLocation = ref<ServiceLocation | undefined>(props.modelValue || props.discoveredLocation)
// The full integration ID (e.g., "provider_jmapc.jmap")
const effectiveIntegrationId = computed(() => {
return props.providerId
})
// Load provider panel dynamically using the integration ID
async function loadProviderPanel(integrationId: string) {
if (loadedPanels.has(integrationId)) {
currentProviderPanel.value = loadedPanels.get(integrationId)
return
}
loadingPanel.value = true
// Try to find panel - integration IDs are prefixed with module handle
// so we need to search for panels that match the provider ID
const panels = integrationStore.getItems('mail_account_config_panels')
const panelConfig = panels.find((panel: any) => {
// Check if the ID ends with the provider ID (e.g., "provider_jmapc.jmap" contains "jmap")
return panel.id === integrationId || panel.id.endsWith(`.${integrationId}`)
})
if (!panelConfig?.component) {
console.warn(`No config panel found for provider ID: ${integrationId}`)
console.warn(`Available panels:`, panels.map((p: any) => p.id))
currentProviderPanel.value = null
loadingPanel.value = false
return
}
try {
const module = await panelConfig.component()
const component = module.default || module
loadedPanels.set(integrationId, component)
currentProviderPanel.value = component
} catch (error) {
console.error(`Failed to load panel for ${integrationId}:`, error)
currentProviderPanel.value = null
} finally {
loadingPanel.value = false
}
}
watch(effectiveIntegrationId, (newIntegrationId, oldIntegrationId) => {
if (newIntegrationId && newIntegrationId !== oldIntegrationId) {
loadProviderPanel(newIntegrationId)
}
}, { immediate: true })
function handleLocationUpdate(location: ServiceLocation) {
localLocation.value = location
emit('update:modelValue', location)
// Emit valid when location is provided
emit('valid', !!location)
}
// Watch for prop changes
watch(() => props.modelValue, (newValue) => {
if (newValue) {
localLocation.value = newValue
}
})
watch(() => props.discoveredLocation, (newValue) => {
if (newValue && !props.modelValue) {
localLocation.value = newValue
emit('update:modelValue', newValue)
emit('valid', true)
}
})
</script>
<template>
<div class="provider-config-step">
<h3 class="text-h6 mb-2">Protocol Configuration</h3>
<p class="text-body-2 text-medium-emphasis mb-6">
Configure the connection settings for your mail service
</p>
<!-- Dynamic Provider Panel -->
<component
v-if="currentProviderPanel"
:is="currentProviderPanel"
v-model="localLocation"
:discovered-location="discoveredLocation"
@update:model-value="handleLocationUpdate"
/>
<!-- Loading state for panel -->
<div v-else-if="loadingPanel" class="text-center py-8">
<v-progress-circular indeterminate color="primary" />
<p class="text-caption text-medium-emphasis mt-2">Loading configuration panel...</p>
</div>
<!-- No panel available -->
<v-alert v-else type="info" variant="tonal">
<v-icon start>mdi-information</v-icon>
No configuration panel available for this provider
</v-alert>
</div>
</template>
@@ -0,0 +1,125 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useProvidersStore } from '@MailManager/stores/providersStore'
import { useIntegrationStore } from '@KTXC/stores/integrationStore'
const emit = defineEmits<{
'select': [providerId: string]
'back': []
}>()
const providersStore = useProvidersStore()
const integrationStore = useIntegrationStore()
const selected = ref<string | null>(null)
// Get provider metadata from integrations
const providerMetadata = computed(() => {
const metadata = integrationStore.getItems('mail_provider_metadata')
return metadata.reduce((acc: any, meta: any) => {
acc[meta.id] = meta
return acc
}, {})
})
// Color palette for providers without specific colors
const defaultColors = ['blue', 'green', 'orange', 'purple', 'teal', 'indigo', 'pink', 'cyan']
// Combine provider data with metadata
const availableProviders = computed(() => {
return providersStore.providers.map((provider, index) => {
const metadata = providerMetadata.value[provider.identifier] || {}
return {
id: provider.identifier,
name: provider.label || metadata.label || provider.identifier,
description: metadata.description || `${provider.label} mail provider`,
icon: metadata.icon || 'mdi-email',
color: metadata.color || defaultColors[index % defaultColors.length]
}
})
})
function selectProvider(providerId: string) {
selected.value = providerId
emit('select', providerId)
}
</script>
<template>
<div class="provider-selection-step">
<h3 class="text-h6 mb-2">Select Provider</h3>
<p class="text-body-2 text-medium-emphasis mb-6">
Choose the mail provider you want to configure manually.
</p>
<!-- Loading State -->
<div v-if="providersStore.transceiving" class="text-center py-8">
<v-progress-circular indeterminate color="primary" />
<p class="text-caption text-medium-emphasis mt-2">
Loading providers...
</p>
</div>
<!-- No Providers Available -->
<v-alert
v-else-if="availableProviders.length === 0"
type="warning"
variant="tonal"
class="mb-4"
>
<v-icon start>mdi-alert</v-icon>
No mail providers are currently available. Please contact your administrator.
</v-alert>
<!-- Provider Grid -->
<v-row v-else>
<v-col
v-for="provider in availableProviders"
:key="provider.id"
cols="12"
sm="6"
md="4"
>
<v-card
variant="outlined"
hover
class="provider-card"
:class="{ 'provider-card--selected': selected === provider.id }"
@click="selectProvider(provider.id)"
>
<v-card-text class="text-center pa-6">
<v-avatar :color="provider.color" size="64" class="mb-4">
<v-icon :icon="provider.icon" size="32" color="white" />
</v-avatar>
<h4 class="text-h6 mb-2">{{ provider.name }}</h4>
<p class="text-caption text-medium-emphasis">
{{ provider.description }}
</p>
</v-card-text>
</v-card>
</v-col>
</v-row>
</div>
</template>
<style scoped>
.provider-card {
cursor: pointer;
transition: all 0.2s;
}
.provider-card:hover {
border-color: rgb(var(--v-theme-primary));
transform: translateY(-2px);
}
.provider-card--selected {
border-color: rgb(var(--v-theme-primary));
background-color: rgba(var(--v-theme-primary), 0.05);
}
.gap-3 {
gap: 12px;
}
</style>
+295
View File
@@ -0,0 +1,295 @@
<template>
<div class="test-and-save-step">
<h3 class="text-h6 mb-2">Test & Save</h3>
<p class="text-body-2 text-medium-emphasis mb-6">
Test your connection and save the account configuration
</p>
<!-- Configuration Summary -->
<v-card variant="outlined" class="mb-4">
<v-card-subtitle>Configuration Summary</v-card-subtitle>
<v-card-text>
<v-list density="compact" class="bg-transparent">
<!-- Account Label -->
<v-list-item>
<template #prepend>
<v-icon>mdi-label</v-icon>
</template>
<v-list-item-title>Account Name</v-list-item-title>
<v-list-item-subtitle>{{ accountLabel }}</v-list-item-subtitle>
</v-list-item>
<!-- Email Address -->
<v-list-item>
<template #prepend>
<v-icon>mdi-email</v-icon>
</template>
<v-list-item-title>Email Address</v-list-item-title>
<v-list-item-subtitle>{{ emailAddress }}</v-list-item-subtitle>
</v-list-item>
<!-- Provider -->
<v-list-item>
<template #prepend>
<v-icon>mdi-cloud</v-icon>
</template>
<v-list-item-title>Provider</v-list-item-title>
<v-list-item-subtitle>{{ providerLabel }}</v-list-item-subtitle>
</v-list-item>
<!-- Location Details -->
<template v-if="location">
<v-divider class="my-2" />
<v-list-item v-if="location.type === 'URI'">
<template #prepend>
<v-icon>mdi-web</v-icon>
</template>
<v-list-item-title>Service URL</v-list-item-title>
<v-list-item-subtitle>
{{ location.scheme }}://{{ location.host }}:{{ location.port }}{{ location.path || '' }}
</v-list-item-subtitle>
</v-list-item>
<template v-if="location.type === 'SOCKET_SOLE'">
<v-list-item>
<template #prepend>
<v-icon>mdi-server</v-icon>
</template>
<v-list-item-title>Server</v-list-item-title>
<v-list-item-subtitle>{{ location.host }}:{{ location.port }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template #prepend>
<v-icon>mdi-shield-lock</v-icon>
</template>
<v-list-item-title>Security</v-list-item-title>
<v-list-item-subtitle>{{ location.encryption.toUpperCase() }}</v-list-item-subtitle>
</v-list-item>
</template>
<template v-if="location.type === 'SOCKET_SPLIT'">
<v-list-item>
<template #prepend>
<v-icon>mdi-inbox-arrow-down</v-icon>
</template>
<v-list-item-title>Incoming Mail</v-list-item-title>
<v-list-item-subtitle>
{{ location.inbound.protocol.toUpperCase() }} - {{ location.inbound.host }}:{{ location.inbound.port }} ({{ location.inbound.encryption.toUpperCase() }})
</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template #prepend>
<v-icon>mdi-send</v-icon>
</template>
<v-list-item-title>Outgoing Mail</v-list-item-title>
<v-list-item-subtitle>
{{ location.outbound.protocol.toUpperCase() }} - {{ location.outbound.host }}:{{ location.outbound.port }} ({{ location.outbound.encryption.toUpperCase() }})
</v-list-item-subtitle>
</v-list-item>
</template>
</template>
<!-- Authentication Method -->
<v-divider class="my-2" />
<v-list-item>
<template #prepend>
<v-icon>{{ getAuthIcon(identity?.type) }}</v-icon>
</template>
<v-list-item-title>Authentication</v-list-item-title>
<v-list-item-subtitle>{{ getAuthLabel(identity?.type) }}</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
<!-- Account Label Input -->
<v-text-field
v-model="localAccountLabel"
label="Account Name"
variant="outlined"
hint="A friendly name for this account (e.g., Work Email)"
persistent-hint
prepend-inner-icon="mdi-label"
class="mb-4"
/>
<!-- Enable Account Toggle -->
<v-switch
v-model="accountEnabled"
label="Enable this account"
color="primary"
class="mb-4"
/>
<!-- Test Connection -->
<div class="mb-4">
<v-btn
color="primary"
variant="outlined"
size="large"
block
:loading="testing"
:disabled="testSuccess"
prepend-icon="mdi-connection"
@click="handleTest"
>
{{ testSuccess ? 'Connection Tested Successfully' : 'Test Connection' }}
</v-btn>
<!-- Test Result -->
<v-alert
v-if="testResult"
:type="testResult.success ? 'success' : 'error'"
variant="tonal"
class="mt-4"
>
<div class="d-flex align-center">
<v-icon
:icon="testResult.success ? 'mdi-check-circle' : 'mdi-alert-circle'"
class="mr-2"
/>
<div class="flex-grow-1">
<div class="font-weight-bold">{{ testResult.message }}</div>
<div v-if="testResult.details?.latency" class="text-caption">
Response time: {{ testResult.details.latency }}ms
</div>
<div v-if="testResult.details?.protocols" class="text-caption">
Protocols: {{ Object.keys(testResult.details.protocols).join(', ') }}
</div>
<div v-if="testResult.details?.capabilities" class="text-caption mt-1">
Capabilities: {{ formatCapabilities(testResult.details.capabilities) }}
</div>
</div>
</div>
</v-alert>
</div>
<!-- Save Warning -->
<v-alert
v-if="!testSuccess"
type="warning"
variant="tonal"
density="compact"
>
<v-icon start>mdi-alert</v-icon>
Please test the connection before saving
</v-alert>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import type { ServiceIdentity, ServiceLocation } from '@MailManager/types/service'
const props = defineProps<{
providerId: string
providerLabel: string
emailAddress: string
location: ServiceLocation | null
identity: ServiceIdentity | null
prefilledLabel?: string
onTest: () => Promise<any>
}>()
const emit = defineEmits<{
'update:label': [value: string]
'update:enabled': [value: boolean]
'tested': [success: boolean]
'valid': [value: boolean]
}>()
// Local state
const localAccountLabel = ref(props.prefilledLabel || props.emailAddress || '')
const accountEnabled = ref(true)
const testing = ref(false)
const testResult = ref<any>(null)
// Computed
const accountLabel = computed(() => localAccountLabel.value || props.emailAddress || 'New Account')
const testSuccess = computed(() => testResult.value?.success === true)
const isValid = computed(() => {
return testSuccess.value && !!localAccountLabel.value
})
// Helper functions
function getAuthIcon(type?: string): string {
switch (type) {
case 'NA': return 'mdi-lock-open-variant'
case 'BA': return 'mdi-account-key'
case 'TA': return 'mdi-key'
case 'OA': return 'mdi-shield-account'
case 'CC': return 'mdi-certificate'
default: return 'mdi-help-circle'
}
}
function getAuthLabel(type?: string): string {
switch (type) {
case 'NA': return 'No Authentication'
case 'BA': return 'Username & Password'
case 'TA': return 'API Token'
case 'OA': return 'OAuth 2.0'
case 'CC': return 'Client Certificate'
default: return 'Unknown'
}
}
function formatCapabilities(capabilities: any): string {
if (!capabilities || typeof capabilities !== 'object') return 'N/A'
const caps = Object.entries(capabilities)
.filter(([_, value]) => value === true)
.map(([key]) => key)
.slice(0, 5)
const total = caps.length
const display = caps.slice(0, 3).join(', ')
if (total > 3) {
return `${display}, +${total - 3} more`
}
return display
}
async function handleTest() {
if (!props.location || !props.identity) {
testResult.value = {
success: false,
message: 'Missing configuration'
}
return
}
testing.value = true
testResult.value = null
try {
const result = await props.onTest()
testResult.value = result
emit('tested', result.success)
} catch (error: any) {
testResult.value = {
success: false,
message: error.message || 'Connection test failed'
}
emit('tested', false)
} finally {
testing.value = false
}
}
// Watch for changes and emit
watch(localAccountLabel, (value) => {
emit('update:label', value)
})
watch(accountEnabled, (value) => {
emit('update:enabled', value)
})
watch(isValid, (value) => {
emit('valid', value)
}, { immediate: true })
</script>