chore: bunch of improvements #13
@@ -160,7 +160,7 @@ class DefaultController extends ControllerAbstract {
|
|||||||
'entity.extant' => $this->entityExtant($tenantId, $userId, $data),
|
'entity.extant' => $this->entityExtant($tenantId, $userId, $data),
|
||||||
'entity.create' => throw new InvalidArgumentException('Operation not implemented: ' . $operation),
|
'entity.create' => throw new InvalidArgumentException('Operation not implemented: ' . $operation),
|
||||||
'entity.update' => throw new InvalidArgumentException('Operation not implemented: ' . $operation),
|
'entity.update' => throw new InvalidArgumentException('Operation not implemented: ' . $operation),
|
||||||
'entity.delete' => throw new InvalidArgumentException('Operation not implemented: ' . $operation),
|
'entity.delete' => $this->entityDelete($tenantId, $userId, $data),
|
||||||
'entity.stream' => $this->entityStream($tenantId, $userId, $data, $version, $transaction),
|
'entity.stream' => $this->entityStream($tenantId, $userId, $data, $version, $transaction),
|
||||||
'entity.delta' => $this->entityDelta($tenantId, $userId, $data),
|
'entity.delta' => $this->entityDelta($tenantId, $userId, $data),
|
||||||
'entity.move' => $this->entityMove($tenantId, $userId, $data),
|
'entity.move' => $this->entityMove($tenantId, $userId, $data),
|
||||||
@@ -640,6 +640,24 @@ class DefaultController extends ControllerAbstract {
|
|||||||
return $this->mailManager->entityDelta($tenantId, $userId, $sources);
|
return $this->mailManager->entityDelta($tenantId, $userId, $sources);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function entityDelete(string $tenantId, string $userId, array $data): mixed {
|
||||||
|
if (!isset($data['sources'])) {
|
||||||
|
throw new InvalidArgumentException(self::ERR_MISSING_SOURCES);
|
||||||
|
}
|
||||||
|
if (!is_array($data['sources'])) {
|
||||||
|
throw new InvalidArgumentException(self::ERR_INVALID_SOURCES);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sources = ResourceIdentifiers::fromArray($data['sources']);
|
||||||
|
foreach ($sources as $source) {
|
||||||
|
if (!$source instanceof EntityIdentifier) {
|
||||||
|
throw new InvalidArgumentException('Invalid parameter: sources must contain provider:service:collection:entity identifiers');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->mailManager->entityDelete($tenantId, $userId, $sources);
|
||||||
|
}
|
||||||
|
|
||||||
private function entityMove(string $tenantId, string $userId, array $data): mixed {
|
private function entityMove(string $tenantId, string $userId, array $data): mixed {
|
||||||
if (!isset($data['target'])) {
|
if (!isset($data['target'])) {
|
||||||
throw new InvalidArgumentException(self::ERR_MISSING_TARGET);
|
throw new InvalidArgumentException(self::ERR_MISSING_TARGET);
|
||||||
|
|||||||
@@ -981,24 +981,105 @@ class Manager {
|
|||||||
return $responseData;
|
return $responseData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function entityDelete(string $tenantId, string $userId, ResourceIdentifiers $sources): array {
|
||||||
|
$operationOutcome = [];
|
||||||
|
|
||||||
|
foreach ($sources->providers() as $providerName) {
|
||||||
|
$providerSources = $sources->byProvider($providerName);
|
||||||
|
foreach ($providerSources->services() as $serviceName) {
|
||||||
|
$serviceSources = $providerSources->byService($serviceName);
|
||||||
|
|
||||||
|
$service = null;
|
||||||
|
try {
|
||||||
|
$service = $this->serviceFetch($tenantId, $userId, $providerName, $serviceName);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Service not found, mark all identifiers as failed
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($service === null) {
|
||||||
|
foreach ($serviceSources as $identifier) {
|
||||||
|
$operationOutcome[(string)$identifier] = [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'serviceNotFound',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporarily disabled check until all methods are properly implemented from ServiceEntityMutableInterface
|
||||||
|
/*
|
||||||
|
if (!($service instanceof ServiceEntityMutableInterface)) {
|
||||||
|
foreach ($serviceSources as $identifier) {
|
||||||
|
$operationOutcome[(string)$identifier] = [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'serviceNotEntityMutable',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$service->capable(ServiceEntityMutableInterface::CAPABILITY_ENTITY_DELETE)) {
|
||||||
|
foreach ($serviceSources as $identifier) {
|
||||||
|
$operationOutcome[(string)$identifier] = [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'serviceCannotDeleteEntities',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
try {
|
||||||
|
$operationResult = $service->entityDelete(...$serviceSources->all());
|
||||||
|
|
||||||
|
foreach ($serviceSources as $identifier) {
|
||||||
|
$sourceIdentifier = (string)$identifier;
|
||||||
|
$entityIdentifier = $identifier->entity();
|
||||||
|
$result = $operationResult[$entityIdentifier] ?? null;
|
||||||
|
|
||||||
|
if ($result === true) {
|
||||||
|
$operationOutcome[$sourceIdentifier] = [
|
||||||
|
'success' => true,
|
||||||
|
];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$operationOutcome[$sourceIdentifier] = [
|
||||||
|
'success' => false,
|
||||||
|
'error' => is_string($result) && $result !== '' ? $result : 'unknownError',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
foreach ($serviceSources as $identifier) {
|
||||||
|
$operationOutcome[(string)$identifier] = [
|
||||||
|
'success' => false,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $operationOutcome;
|
||||||
|
}
|
||||||
|
|
||||||
public function entityMove(string $tenantId, string $userId, CollectionIdentifier $target, ResourceIdentifiers $sources): array {
|
public function entityMove(string $tenantId, string $userId, CollectionIdentifier $target, ResourceIdentifiers $sources): array {
|
||||||
|
|
||||||
$targetService = $this->serviceFetch($tenantId, $userId, $target->provider(), $target->service());
|
$targetService = $this->serviceFetch($tenantId, $userId, $target->provider(), $target->service());
|
||||||
|
|
||||||
// Check if service supports entity move
|
// Check if service supports entity move
|
||||||
|
// Temporarily disabled check until all methods are properly implemented from ServiceEntityMutableInterface
|
||||||
|
/*
|
||||||
if ($targetService instanceof ServiceEntityMutableInterface === false) {
|
if ($targetService instanceof ServiceEntityMutableInterface === false) {
|
||||||
//return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
$operationOutcome = [];
|
$operationOutcome = [];
|
||||||
|
|
||||||
$destinationSources = $sources->byProvider($targetService->provider())->byService((string)$targetService->identifier());
|
$destinationSources = $sources->byProvider($targetService->provider())->byService((string)$targetService->identifier());
|
||||||
if (!$destinationSources->isEmpty()) {
|
if (!$destinationSources->isEmpty()) {
|
||||||
$entitiesToMove = [];
|
$operationResult = $targetService->entityMove($target, ...$destinationSources->all());
|
||||||
foreach ($destinationSources as $identifier) {
|
|
||||||
$entitiesToMove[$identifier->collection()][] = $identifier->entity();
|
|
||||||
}
|
|
||||||
$operationResult = $targetService->entityMove($target->collection(), $entitiesToMove);
|
|
||||||
|
|
||||||
foreach ($destinationSources as $identifier) {
|
foreach ($destinationSources as $identifier) {
|
||||||
$sourceIdentifier = (string)$identifier;
|
$sourceIdentifier = (string)$identifier;
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import { useIntegrationStore } from '@KTXC/stores/integrationStore'
|
||||||
import { useServicesStore } from '@MailManager/stores/servicesStore'
|
import { useServicesStore } from '@MailManager/stores/servicesStore'
|
||||||
import { useProvidersStore } from '@MailManager/stores/providersStore'
|
import { useProvidersStore } from '@MailManager/stores/providersStore'
|
||||||
import type { ProviderDiscoveryStatus, ServiceLocation, ServiceIdentity } from '@MailManager/types'
|
import { ServiceObject, type ProviderObject } from '@MailManager/models'
|
||||||
import type { ServiceObject } from '@MailManager/models/service'
|
import type { ProviderDiscoveryStatus, ServiceInterface, ServiceLocation } from '@MailManager/types'
|
||||||
import DiscoveryStatusStep from '@MailManager/components/steps/DiscoveryStatusStep.vue'
|
import DiscoveryEntryPanel from '@MailManager/components/steps/DiscoveryEntryPanel.vue'
|
||||||
import ProviderSelectionStep from '@MailManager/components/steps/ProviderSelectionStep.vue'
|
import DiscoveryStatusPanel from '@MailManager/components/steps/DiscoveryStatusPanel.vue'
|
||||||
import ProviderConfigStep from '@MailManager/components/steps/ProviderConfigStep.vue'
|
import ProviderSelectionPanel from '@MailManager/components/steps/ProviderSelectionPanel.vue'
|
||||||
import ProviderAuthStep from '@MailManager/components/steps/ProviderAuthStep.vue'
|
import ProviderProtocolPanel from '@MailManager/components/steps/ProviderProtocolPanel.vue'
|
||||||
import TestAndSaveStep from '@MailManager/components/steps/TestAndSaveStep.vue'
|
import ProviderAuthPanel from '@MailManager/components/steps/ProviderAuthPanel.vue'
|
||||||
import DiscoveryEntryStep from '@MailManager/components/steps/DiscoveryEntryStep.vue'
|
import TestAndSavePanel from '@MailManager/components/steps/TestAndSavePanel.vue'
|
||||||
|
|
||||||
// ==================== Step Constants ====================
|
// ==================== Step Constants ====================
|
||||||
// Discovery flow: Entry → Discovery → Auth → Test
|
// Discovery flow: Entry → Discovery → Auth → Test
|
||||||
@@ -38,6 +39,7 @@ const emit = defineEmits<{
|
|||||||
'saved': []
|
'saved': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const integrationStore = useIntegrationStore()
|
||||||
const servicesStore = useServicesStore()
|
const servicesStore = useServicesStore()
|
||||||
const providersStore = useProvidersStore()
|
const providersStore = useProvidersStore()
|
||||||
|
|
||||||
@@ -56,19 +58,10 @@ const discoverSecret = ref<string | null>(null)
|
|||||||
const discoverHostname = ref<string | null>(null)
|
const discoverHostname = ref<string | null>(null)
|
||||||
|
|
||||||
// Step 2: Discovery Status / Provider Selection
|
// Step 2: Discovery Status / Provider Selection
|
||||||
const selectedProviderId = ref<string | undefined>(undefined)
|
const selectedProvider = ref<ProviderObject | null>(null)
|
||||||
const selectedProviderLabel = ref<string>('')
|
const selectedService = ref<ServiceObject | null>(null)
|
||||||
|
|
||||||
// Step 3: Config (manual only) OR Auth (both paths)
|
|
||||||
const configuredLocation = ref<ServiceLocation | null>(null)
|
|
||||||
|
|
||||||
// Step 4: Auth (both paths)
|
|
||||||
const configuredIdentity = ref<ServiceIdentity | null>(null)
|
|
||||||
const authValid = ref(false)
|
|
||||||
|
|
||||||
// Step 5: Test & Save
|
// Step 5: Test & Save
|
||||||
const accountLabel = ref<string>('')
|
|
||||||
const accountEnabled = ref(true)
|
|
||||||
const testAndSaveValid = ref(false)
|
const testAndSaveValid = ref(false)
|
||||||
|
|
||||||
// Local discovery state (not stored in global store)
|
// Local discovery state (not stored in global store)
|
||||||
@@ -137,19 +130,61 @@ const showSaveButton = computed(() => {
|
|||||||
const canProceedToNext = computed(() => {
|
const canProceedToNext = computed(() => {
|
||||||
if (isManualMode.value) {
|
if (isManualMode.value) {
|
||||||
if (currentStep.value === MANUAL_STEPS.CONFIG) {
|
if (currentStep.value === MANUAL_STEPS.CONFIG) {
|
||||||
return !!configuredLocation.value
|
return !!selectedService.value?.location
|
||||||
}
|
}
|
||||||
if (currentStep.value === MANUAL_STEPS.AUTH) {
|
if (currentStep.value === MANUAL_STEPS.AUTH) {
|
||||||
return authValid.value
|
return !!selectedService.value?.identity
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (currentStep.value === DISCOVERY_STEPS.AUTH) {
|
if (currentStep.value === DISCOVERY_STEPS.AUTH) {
|
||||||
return authValid.value
|
return !!selectedService.value?.identity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function createServiceObject(
|
||||||
|
providerId: string,
|
||||||
|
data: Partial<ServiceInterface> = {}
|
||||||
|
): ServiceObject {
|
||||||
|
const model: ServiceInterface = {
|
||||||
|
'@type': 'mail:service',
|
||||||
|
version: 1,
|
||||||
|
provider: providerId,
|
||||||
|
identifier: null,
|
||||||
|
label: data.label ?? null,
|
||||||
|
enabled: data.enabled ?? true,
|
||||||
|
primaryAddress: data.primaryAddress ?? (discoverAddress.value || null),
|
||||||
|
secondaryAddresses: data.secondaryAddresses ?? null,
|
||||||
|
location: data.location ?? null,
|
||||||
|
identity: data.identity ?? null,
|
||||||
|
capabilities: data.capabilities ?? {},
|
||||||
|
auxiliary: data.auxiliary ?? {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const factoryItem = integrationStore.getItemById('mail_service_factory', providerId) as any
|
||||||
|
const factory = factoryItem?.factory
|
||||||
|
return factory ? factory(model) : new ServiceObject().fromJson(model)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectedProviderAndService(providerId: string, service: ServiceObject) {
|
||||||
|
selectedProvider.value = providersStore.provider(providerId)
|
||||||
|
selectedService.value = service
|
||||||
|
testAndSaveValid.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleServiceUpdate(service: ServiceObject) {
|
||||||
|
selectedService.value = service
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleServiceTested(success: boolean) {
|
||||||
|
testAndSaveValid.value = success
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedService, () => {
|
||||||
|
testAndSaveValid.value = false
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
// Navigation methods
|
// Navigation methods
|
||||||
function handlePreviousStep() {
|
function handlePreviousStep() {
|
||||||
if (currentStep.value > 1) {
|
if (currentStep.value > 1) {
|
||||||
@@ -259,12 +294,18 @@ function extractLocationMetadata(location: ServiceLocation) {
|
|||||||
|
|
||||||
async function handleProviderSelect(identifier: string) {
|
async function handleProviderSelect(identifier: string) {
|
||||||
// User clicked "Select" on discovered provider - skip config, go to auth
|
// User clicked "Select" on discovered provider - skip config, go to auth
|
||||||
const service = discoveredServices.value.find(s => s.provider === identifier)
|
const discovered = discoveredServices.value.find(s => s.provider === identifier)
|
||||||
if (!service || !service.location) return
|
if (!discovered || !discovered.location) return
|
||||||
|
|
||||||
selectedProviderId.value = identifier
|
const discoveredJson = discovered.toJson()
|
||||||
selectedProviderLabel.value = providersStore.provider(identifier)?.label || identifier
|
const service = createServiceObject(identifier, {
|
||||||
configuredLocation.value = service.location
|
...discoveredJson,
|
||||||
|
label: discoveredJson.label || discoverAddress.value,
|
||||||
|
enabled: discoveredJson.enabled ?? true,
|
||||||
|
primaryAddress: discoveredJson.primaryAddress || discoverAddress.value,
|
||||||
|
location: discoveredJson.location
|
||||||
|
})
|
||||||
|
setSelectedProviderAndService(identifier, service)
|
||||||
|
|
||||||
// Discovery path: Entry → Discovery → Auth → Test
|
// Discovery path: Entry → Discovery → Auth → Test
|
||||||
currentStep.value = DISCOVERY_STEPS.AUTH // Go to auth step
|
currentStep.value = DISCOVERY_STEPS.AUTH // Go to auth step
|
||||||
@@ -272,11 +313,17 @@ async function handleProviderSelect(identifier: string) {
|
|||||||
|
|
||||||
function handleProviderAdvanced(identifier: string) {
|
function handleProviderAdvanced(identifier: string) {
|
||||||
// User clicked "Advanced" - show manual config with pre-filled values
|
// User clicked "Advanced" - show manual config with pre-filled values
|
||||||
selectedProviderId.value = identifier
|
const discovered = discoveredServices.value.find(s => s.provider === identifier)
|
||||||
selectedProviderLabel.value = providersStore.provider(identifier)?.label || identifier
|
const discoveredJson = discovered?.toJson()
|
||||||
const service = discoveredServices.value.find(s => s.provider === identifier)
|
const service = createServiceObject(identifier, {
|
||||||
|
...discoveredJson,
|
||||||
configuredLocation.value = service?.location || null
|
label: discoveredJson?.label || discoverAddress.value,
|
||||||
|
enabled: discoveredJson?.enabled ?? true,
|
||||||
|
primaryAddress: discoveredJson?.primaryAddress || discoverAddress.value,
|
||||||
|
location: discoveredJson?.location ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
setSelectedProviderAndService(identifier, service)
|
||||||
isManualMode.value = true
|
isManualMode.value = true
|
||||||
|
|
||||||
// Manual path: Entry → Discovery → Config → Auth → Test
|
// Manual path: Entry → Discovery → Config → Auth → Test
|
||||||
@@ -293,8 +340,15 @@ function handleManualMode() {
|
|||||||
|
|
||||||
function handleProviderManualSelect(identifier: string) {
|
function handleProviderManualSelect(identifier: string) {
|
||||||
// User selected a provider in manual mode
|
// User selected a provider in manual mode
|
||||||
selectedProviderId.value = identifier
|
const service = createServiceObject(identifier, {
|
||||||
selectedProviderLabel.value = providersStore.provider(identifier)?.label || identifier
|
label: discoverAddress.value,
|
||||||
|
enabled: true,
|
||||||
|
primaryAddress: discoverAddress.value,
|
||||||
|
location: null,
|
||||||
|
identity: null
|
||||||
|
})
|
||||||
|
|
||||||
|
setSelectedProviderAndService(identifier, service)
|
||||||
currentStep.value = MANUAL_STEPS.CONFIG // Go to manual config
|
currentStep.value = MANUAL_STEPS.CONFIG // Go to manual config
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,10 +357,21 @@ function goBackToIdentity() {
|
|||||||
isManualMode.value = false
|
isManualMode.value = false
|
||||||
discoveredServices.value = []
|
discoveredServices.value = []
|
||||||
discoveryStatus.value = {}
|
discoveryStatus.value = {}
|
||||||
|
selectedProvider.value = null
|
||||||
|
selectedService.value = null
|
||||||
|
testAndSaveValid.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testConnection() {
|
async function testConnection() {
|
||||||
if (!selectedProviderId.value || !configuredLocation.value || !configuredIdentity.value) {
|
if (!selectedProvider.value || !selectedService.value) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Missing configuration'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceData = selectedService.value.toJson()
|
||||||
|
if (!serviceData.location || !serviceData.identity) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Missing configuration'
|
message: 'Missing configuration'
|
||||||
@@ -314,31 +379,35 @@ async function testConnection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const testResult = await servicesStore.test(
|
const testResult = await servicesStore.test(
|
||||||
selectedProviderId.value,
|
selectedProvider.value.identifier,
|
||||||
null,
|
null,
|
||||||
configuredLocation.value,
|
serviceData.location,
|
||||||
configuredIdentity.value
|
serviceData.identity
|
||||||
)
|
)
|
||||||
|
|
||||||
return testResult
|
return testResult
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveAccount() {
|
async function saveAccount() {
|
||||||
if (!selectedProviderId.value || !configuredLocation.value || !configuredIdentity.value) return
|
if (!selectedProvider.value || !selectedService.value) return
|
||||||
|
|
||||||
|
const serviceData = selectedService.value.toJson()
|
||||||
|
if (!serviceData.location || !serviceData.identity) return
|
||||||
|
|
||||||
saving.value = true
|
saving.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const accountData = {
|
const accountData = {
|
||||||
label: accountLabel.value || discoverAddress.value,
|
label: serviceData.label || discoverAddress.value,
|
||||||
email: discoverAddress.value,
|
primaryAddress: serviceData.primaryAddress || discoverAddress.value,
|
||||||
enabled: accountEnabled.value,
|
enabled: serviceData.enabled,
|
||||||
location: configuredLocation.value,
|
location: serviceData.location,
|
||||||
identity: configuredIdentity.value
|
identity: serviceData.identity,
|
||||||
|
auxiliary: serviceData.auxiliary
|
||||||
}
|
}
|
||||||
|
|
||||||
await servicesStore.create(
|
await servicesStore.create(
|
||||||
selectedProviderId.value,
|
selectedProvider.value.identifier,
|
||||||
accountData
|
accountData
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -364,13 +433,8 @@ function resetForm() {
|
|||||||
discoverAddress.value = ''
|
discoverAddress.value = ''
|
||||||
discoverSecret.value = null
|
discoverSecret.value = null
|
||||||
discoverHostname.value = null
|
discoverHostname.value = null
|
||||||
selectedProviderId.value = undefined
|
selectedProvider.value = null
|
||||||
selectedProviderLabel.value = ''
|
selectedService.value = null
|
||||||
configuredLocation.value = null
|
|
||||||
configuredIdentity.value = null
|
|
||||||
authValid.value = false
|
|
||||||
accountLabel.value = ''
|
|
||||||
accountEnabled.value = true
|
|
||||||
testAndSaveValid.value = false
|
testAndSaveValid.value = false
|
||||||
discoveredServices.value = []
|
discoveredServices.value = []
|
||||||
discoveryStatus.value = {}
|
discoveryStatus.value = {}
|
||||||
@@ -407,7 +471,7 @@ function resetForm() {
|
|||||||
<!-- Step 1: Discovery Entry -->
|
<!-- Step 1: Discovery Entry -->
|
||||||
<template #item.1>
|
<template #item.1>
|
||||||
<v-card flat class="pa-6">
|
<v-card flat class="pa-6">
|
||||||
<DiscoveryEntryStep
|
<DiscoveryEntryPanel
|
||||||
v-model:address="discoverAddress"
|
v-model:address="discoverAddress"
|
||||||
v-model:secret="discoverSecret"
|
v-model:secret="discoverSecret"
|
||||||
v-model:hostname="discoverHostname"
|
v-model:hostname="discoverHostname"
|
||||||
@@ -421,7 +485,7 @@ function resetForm() {
|
|||||||
<template #item.2>
|
<template #item.2>
|
||||||
<v-card flat class="pa-6">
|
<v-card flat class="pa-6">
|
||||||
<!-- Discovery path -->
|
<!-- Discovery path -->
|
||||||
<DiscoveryStatusStep
|
<DiscoveryStatusPanel
|
||||||
v-if="!isManualMode"
|
v-if="!isManualMode"
|
||||||
:address="discoverAddress"
|
:address="discoverAddress"
|
||||||
:status="discoveryStatus"
|
:status="discoveryStatus"
|
||||||
@@ -432,7 +496,7 @@ function resetForm() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Manual path - provider picker -->
|
<!-- Manual path - provider picker -->
|
||||||
<ProviderSelectionStep
|
<ProviderSelectionPanel
|
||||||
v-else
|
v-else
|
||||||
@select="handleProviderManualSelect"
|
@select="handleProviderManualSelect"
|
||||||
@back="goBackToIdentity"
|
@back="goBackToIdentity"
|
||||||
@@ -444,24 +508,18 @@ function resetForm() {
|
|||||||
<template #item.3>
|
<template #item.3>
|
||||||
<v-card flat class="pa-6">
|
<v-card flat class="pa-6">
|
||||||
<!-- Manual path: Protocol Configuration -->
|
<!-- Manual path: Protocol Configuration -->
|
||||||
<ProviderConfigStep
|
<ProviderProtocolPanel
|
||||||
v-if="isManualMode && selectedProviderId"
|
v-if="isManualMode && selectedProvider && selectedService"
|
||||||
:provider-id="selectedProviderId"
|
:provider="selectedProvider"
|
||||||
:discovered-location="configuredLocation || undefined"
|
:service="selectedService"
|
||||||
v-model="configuredLocation"
|
@update:service="handleServiceUpdate"
|
||||||
@valid="() => { /* Can proceed to next step */ }"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ProviderAuthStep
|
<ProviderAuthPanel
|
||||||
v-else-if="!isManualMode && selectedProviderId"
|
v-else-if="!isManualMode && selectedProvider && selectedService"
|
||||||
:provider-id="selectedProviderId"
|
:provider="selectedProvider"
|
||||||
:provider-label="selectedProviderLabel"
|
:service="selectedService"
|
||||||
:email-address="discoverAddress"
|
@update:service="handleServiceUpdate"
|
||||||
:discovered-location="configuredLocation || undefined"
|
|
||||||
:prefilled-identity="discoverAddress"
|
|
||||||
:prefilled-secret="discoverSecret || undefined"
|
|
||||||
v-model="configuredIdentity"
|
|
||||||
@valid="(valid) => authValid = valid"
|
|
||||||
/>
|
/>
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
@@ -469,31 +527,21 @@ function resetForm() {
|
|||||||
<!-- Step 4: Auth (manual) OR Test (discovery) -->
|
<!-- Step 4: Auth (manual) OR Test (discovery) -->
|
||||||
<template #item.4>
|
<template #item.4>
|
||||||
<v-card flat class="pa-6">
|
<v-card flat class="pa-6">
|
||||||
<ProviderAuthStep
|
<ProviderAuthPanel
|
||||||
v-if="isManualMode && selectedProviderId"
|
v-if="isManualMode && selectedProvider && selectedService"
|
||||||
:provider-id="selectedProviderId"
|
:provider="selectedProvider"
|
||||||
:provider-label="selectedProviderLabel"
|
:service="selectedService"
|
||||||
:email-address="discoverAddress"
|
@update:service="handleServiceUpdate"
|
||||||
:discovered-location="configuredLocation || undefined"
|
|
||||||
:prefilled-identity="discoverAddress"
|
|
||||||
:prefilled-secret="discoverSecret || undefined"
|
|
||||||
v-model="configuredIdentity"
|
|
||||||
@valid="(valid) => authValid = valid"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Discovery path: Test & Save -->
|
<!-- Discovery path: Test & Save -->
|
||||||
<TestAndSaveStep
|
<TestAndSavePanel
|
||||||
v-else-if="!isManualMode && selectedProviderId"
|
v-else-if="!isManualMode && selectedProvider && selectedService"
|
||||||
:provider-id="selectedProviderId"
|
:provider="selectedProvider"
|
||||||
:provider-label="selectedProviderLabel"
|
:service="selectedService"
|
||||||
:email-address="discoverAddress"
|
|
||||||
:location="configuredLocation"
|
|
||||||
:identity="configuredIdentity"
|
|
||||||
:prefilled-label="discoverAddress"
|
|
||||||
:on-test="testConnection"
|
:on-test="testConnection"
|
||||||
@update:label="(val) => accountLabel = val"
|
@update:service="handleServiceUpdate"
|
||||||
@update:enabled="(val) => accountEnabled = val"
|
@tested="handleServiceTested"
|
||||||
@valid="(valid) => testAndSaveValid = valid"
|
|
||||||
/>
|
/>
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
@@ -501,18 +549,13 @@ function resetForm() {
|
|||||||
<!-- Step 5: Test & Save (manual only) -->
|
<!-- Step 5: Test & Save (manual only) -->
|
||||||
<template #item.5>
|
<template #item.5>
|
||||||
<v-card flat class="pa-6">
|
<v-card flat class="pa-6">
|
||||||
<TestAndSaveStep
|
<TestAndSavePanel
|
||||||
v-if="selectedProviderId"
|
v-if="selectedProvider && selectedService"
|
||||||
:provider-id="selectedProviderId"
|
:provider="selectedProvider"
|
||||||
:provider-label="selectedProviderLabel"
|
:service="selectedService"
|
||||||
:email-address="discoverAddress"
|
|
||||||
:location="configuredLocation"
|
|
||||||
:identity="configuredIdentity"
|
|
||||||
:prefilled-label="discoverAddress"
|
|
||||||
:on-test="testConnection"
|
:on-test="testConnection"
|
||||||
@update:label="(val) => accountLabel = val"
|
@update:service="handleServiceUpdate"
|
||||||
@update:enabled="(val) => accountEnabled = val"
|
@tested="handleServiceTested"
|
||||||
@valid="(valid) => testAndSaveValid = valid"
|
|
||||||
/>
|
/>
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,17 +2,12 @@
|
|||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { useServicesStore } from '@MailManager/stores/servicesStore'
|
import { useServicesStore } from '@MailManager/stores/servicesStore'
|
||||||
import { useProvidersStore } from '@MailManager/stores/providersStore'
|
import { useProvidersStore } from '@MailManager/stores/providersStore'
|
||||||
import type { ServiceLocation, ServiceIdentity } from '@MailManager/types'
|
import type { ProviderObject, ServiceObject } from '@MailManager/models'
|
||||||
import type { ServiceObject } from '@MailManager/models/service'
|
import ProviderProtocolPanel from '@MailManager/components/steps/ProviderProtocolPanel.vue'
|
||||||
import ProviderConfigStep from '@MailManager/components/steps/ProviderConfigStep.vue'
|
import ProviderAuthPanel from '@MailManager/components/steps/ProviderAuthPanel.vue'
|
||||||
import ProviderAuthStep from '@MailManager/components/steps/ProviderAuthStep.vue'
|
import TestAndSavePanel from '@MailManager/components/steps/TestAndSavePanel.vue'
|
||||||
import TestAndSaveStep from '@MailManager/components/steps/TestAndSaveStep.vue'
|
|
||||||
|
|
||||||
const EDIT_STEPS = {
|
type EditTab = 'general' | 'protocol' | 'auth'
|
||||||
CONFIG: 1,
|
|
||||||
AUTH: 2,
|
|
||||||
TEST: 3
|
|
||||||
} as const
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@@ -33,146 +28,82 @@ const dialogOpen = computed({
|
|||||||
set: (val) => emit('update:modelValue', val)
|
set: (val) => emit('update:modelValue', val)
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentStep = ref<number>(EDIT_STEPS.CONFIG)
|
const currentTab = ref<EditTab>('general')
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const loadError = ref<string | null>(null)
|
||||||
|
|
||||||
// Service data
|
const localProvider = ref<ProviderObject | null>(null)
|
||||||
const service = ref<ServiceObject | null>(null)
|
const localService = ref<ServiceObject | null>(null)
|
||||||
const providerLabel = ref<string>('')
|
const mutated = ref(false)
|
||||||
|
|
||||||
// Editable fields
|
|
||||||
const accountLabel = ref<string>('')
|
|
||||||
const accountEnabled = ref(true)
|
|
||||||
const configuredLocation = ref<ServiceLocation | null>(null)
|
|
||||||
const configuredIdentity = ref<ServiceIdentity | null>(null)
|
|
||||||
|
|
||||||
// Validation states
|
// Validation states
|
||||||
const configValid = ref(false)
|
|
||||||
const authValid = ref(false)
|
|
||||||
const testAndSaveValid = ref(false)
|
const testAndSaveValid = ref(false)
|
||||||
|
|
||||||
// Load service data when dialog opens
|
const tabItems = [
|
||||||
watch(dialogOpen, async (isOpen) => {
|
{
|
||||||
if (isOpen) {
|
title: 'General',
|
||||||
await loadService()
|
icon: 'mdi-view-dashboard-outline',
|
||||||
|
value: 'general' as const
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Protocol',
|
||||||
|
icon: 'mdi-tune-vertical',
|
||||||
|
value: 'protocol' as const
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Authentication',
|
||||||
|
icon: 'mdi-shield-key-outline',
|
||||||
|
value: 'auth' as const
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
async function loadService() {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
// Load providers if not already loaded
|
|
||||||
if (!providersStore.has) {
|
|
||||||
await providersStore.list()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the service
|
|
||||||
service.value = await servicesStore.fetch(props.serviceProvider, props.serviceIdentifier)
|
|
||||||
|
|
||||||
// Set initial values
|
|
||||||
accountLabel.value = service.value.label || ''
|
|
||||||
accountEnabled.value = service.value.enabled
|
|
||||||
configuredLocation.value = service.value.location
|
|
||||||
configuredIdentity.value = service.value.identity
|
|
||||||
|
|
||||||
// Get provider label
|
|
||||||
const provider = providersStore.provider(props.serviceProvider)
|
|
||||||
providerLabel.value = provider?.label || props.serviceProvider
|
|
||||||
|
|
||||||
// Mark config as valid if location exists
|
|
||||||
configValid.value = !!configuredLocation.value
|
|
||||||
authValid.value = !!configuredIdentity.value
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load service:', error)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stepper configuration
|
|
||||||
const stepperItems = [
|
|
||||||
{ title: 'Protocol', value: EDIT_STEPS.CONFIG },
|
|
||||||
{ title: 'Authentication', value: EDIT_STEPS.AUTH },
|
|
||||||
{ title: 'Test & Save', value: EDIT_STEPS.TEST }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const canSave = computed(() => {
|
const canSave = computed(() => {
|
||||||
return testAndSaveValid.value
|
return testAndSaveValid.value
|
||||||
})
|
})
|
||||||
|
|
||||||
// Navigation button visibility
|
const showSaveButton = computed(() => currentTab.value === 'general')
|
||||||
const showPreviousButton = computed(() => currentStep.value > EDIT_STEPS.CONFIG)
|
const accountReady = computed(() => localProvider.value !== null && localService.value !== null)
|
||||||
const showNextButton = computed(() => currentStep.value < EDIT_STEPS.TEST)
|
|
||||||
const showSaveButton = computed(() => currentStep.value === EDIT_STEPS.TEST)
|
|
||||||
|
|
||||||
const canProceedToNext = computed(() => {
|
// Load service data when the dialog is open and the target account is available.
|
||||||
if (currentStep.value === EDIT_STEPS.CONFIG) {
|
watch(
|
||||||
return configValid.value && !!configuredLocation.value
|
() => [props.modelValue, props.serviceProvider, props.serviceIdentifier] as const,
|
||||||
}
|
async ([isOpen, serviceProvider, serviceIdentifier]) => {
|
||||||
if (currentStep.value === EDIT_STEPS.AUTH) {
|
if (!isOpen || !serviceProvider || !serviceIdentifier) {
|
||||||
return authValid.value
|
return
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
// Navigation methods
|
|
||||||
function handlePreviousStep() {
|
|
||||||
if (currentStep.value > EDIT_STEPS.CONFIG) {
|
|
||||||
currentStep.value--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleNextStep() {
|
|
||||||
if (currentStep.value < EDIT_STEPS.TEST) {
|
|
||||||
currentStep.value++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testConnection() {
|
|
||||||
if (!service.value || !configuredLocation.value || !configuredIdentity.value) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: 'Missing configuration'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await load()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
loadError.value = null
|
||||||
|
localProvider.value = null
|
||||||
|
localService.value = null
|
||||||
|
|
||||||
|
if (!props.serviceProvider || !props.serviceIdentifier) {
|
||||||
|
console.error('[Mail Manager][Edit Account Dialog] - Cannot open dialog missing service or provider identifier')
|
||||||
|
loadError.value = 'missing service or provider identifier'
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const testResult = await servicesStore.test(
|
|
||||||
service.value.provider,
|
|
||||||
service.value.identifier,
|
|
||||||
configuredLocation.value,
|
|
||||||
configuredIdentity.value
|
|
||||||
)
|
|
||||||
|
|
||||||
return testResult
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveAccount() {
|
|
||||||
if (!service.value || !configuredLocation.value || !configuredIdentity.value) return
|
|
||||||
|
|
||||||
saving.value = true
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const accountData = {
|
const [provider, service] = await Promise.all([
|
||||||
label: accountLabel.value || service.value.label,
|
providersStore.provider(props.serviceProvider) ?? providersStore.fetch(props.serviceProvider),
|
||||||
enabled: accountEnabled.value,
|
servicesStore.service(props.serviceProvider, props.serviceIdentifier) ?? servicesStore.fetch(props.serviceProvider, props.serviceIdentifier)
|
||||||
location: configuredLocation.value,
|
])
|
||||||
identity: configuredIdentity.value
|
|
||||||
}
|
|
||||||
|
|
||||||
await servicesStore.update(
|
localProvider.value = provider.clone()
|
||||||
service.value.provider,
|
localService.value = service.clone()
|
||||||
service.value.identifier as string | number,
|
|
||||||
accountData
|
|
||||||
)
|
|
||||||
|
|
||||||
emit('saved')
|
|
||||||
close()
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save account:', error)
|
console.error('[Mail Manager][Edit Account Dialog] - Failed to load service:', error)
|
||||||
// TODO: Show error message to user
|
loadError.value = 'Failed to load service details'
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,21 +114,89 @@ function close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
currentStep.value = EDIT_STEPS.CONFIG
|
currentTab.value = 'general'
|
||||||
service.value = null
|
localService.value = null
|
||||||
accountLabel.value = ''
|
localProvider.value = null
|
||||||
accountEnabled.value = true
|
loadError.value = null
|
||||||
configuredLocation.value = null
|
|
||||||
configuredIdentity.value = null
|
|
||||||
configValid.value = false
|
|
||||||
authValid.value = false
|
|
||||||
testAndSaveValid.value = false
|
testAndSaveValid.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch for location changes
|
function isTabDisabled(tab: EditTab) {
|
||||||
watch(configuredLocation, (newLocation) => {
|
if (tab === 'auth') {
|
||||||
configValid.value = !!newLocation
|
return !localService.value?.location
|
||||||
})
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdate(mutatedService: ServiceObject) {
|
||||||
|
localService.value = mutatedService
|
||||||
|
mutated.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testConnection() {
|
||||||
|
try {
|
||||||
|
let testResult = null
|
||||||
|
|
||||||
|
if (mutated.value) {
|
||||||
|
testResult = await servicesStore.test(
|
||||||
|
localService.value.provider,
|
||||||
|
null,
|
||||||
|
localService.value.location,
|
||||||
|
localService.value.identity
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
testResult = await servicesStore.test(
|
||||||
|
localService.value.provider,
|
||||||
|
localService.value.identifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
testAndSaveValid.value = testResult.success
|
||||||
|
return testResult
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Mail Manager][Edit Account Dialog] - Test connection failed:', error)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Test failed due to an unexpected error'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveAccount() {
|
||||||
|
// No changes made, just close the dialog
|
||||||
|
if (!mutated.value) {
|
||||||
|
close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localService.value?.location || !localService.value?.identity) return
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const accountData = {
|
||||||
|
label: accountLabel.value || localService.value.label,
|
||||||
|
enabled: accountEnabled.value,
|
||||||
|
location: localService.value.location,
|
||||||
|
identity: localService.value.identity
|
||||||
|
}
|
||||||
|
|
||||||
|
await servicesStore.update(
|
||||||
|
localService.value.provider,
|
||||||
|
localService.value.identifier as string | number,
|
||||||
|
accountData
|
||||||
|
)
|
||||||
|
|
||||||
|
emit('saved')
|
||||||
|
close()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Mail Manager][Edit Account Dialog] - Failed to save service:', error)
|
||||||
|
// TODO: Show error message to user
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -219,105 +218,77 @@ watch(configuredLocation, (newLocation) => {
|
|||||||
|
|
||||||
<v-divider />
|
<v-divider />
|
||||||
|
|
||||||
<v-card-text v-if="loading" class="text-center py-8">
|
<v-card-text v-if="loading || (!loadError && !accountReady)" class="text-center py-8">
|
||||||
<v-progress-circular indeterminate color="primary" />
|
<v-progress-circular indeterminate color="primary" />
|
||||||
<p class="text-caption text-medium-emphasis mt-2">Loading account...</p>
|
<p class="text-caption text-medium-emphasis mt-2">Loading account...</p>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-card-text v-else-if="loadError" class="pa-6">
|
||||||
|
<v-alert type="error" variant="tonal">
|
||||||
|
{{ loadError }}
|
||||||
|
</v-alert>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
<v-card-text v-else class="pa-0">
|
<v-card-text v-else class="pa-0">
|
||||||
<!-- Account Info Header -->
|
<v-tabs
|
||||||
<div v-if="service" class="pa-6 bg-surface-variant">
|
v-model="currentTab"
|
||||||
<div class="d-flex align-center gap-3">
|
bg-color="transparent"
|
||||||
<v-avatar color="primary">
|
grow
|
||||||
<v-icon>mdi-email</v-icon>
|
class="px-4 pt-2"
|
||||||
</v-avatar>
|
>
|
||||||
<div>
|
<v-tab
|
||||||
<div class="text-subtitle-1 font-weight-medium">
|
v-for="item in tabItems"
|
||||||
{{ service.label || 'Unnamed Account' }}
|
:key="item.value"
|
||||||
</div>
|
:value="item.value"
|
||||||
<div class="text-caption text-medium-emphasis">
|
:disabled="isTabDisabled(item.value)"
|
||||||
{{ service.primaryAddress || service.identifier }}
|
>
|
||||||
</div>
|
<v-icon start>{{ item.icon }}</v-icon>
|
||||||
<div class="text-caption text-medium-emphasis">
|
{{ item.title }}
|
||||||
Provider: {{ providerLabel }}
|
</v-tab>
|
||||||
</div>
|
</v-tabs>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<v-divider />
|
<v-divider />
|
||||||
|
|
||||||
<v-stepper
|
<v-window v-model="currentTab">
|
||||||
v-model="currentStep"
|
<v-window-item value="general">
|
||||||
:items="stepperItems"
|
|
||||||
alt-labels
|
|
||||||
flat
|
|
||||||
hide-actions
|
|
||||||
>
|
|
||||||
<!-- Step 1: Protocol Configuration -->
|
|
||||||
<template #item.1>
|
|
||||||
<v-card flat class="pa-6">
|
<v-card flat class="pa-6">
|
||||||
<ProviderConfigStep
|
<TestAndSavePanel
|
||||||
v-if="service"
|
v-if="localProvider && localService"
|
||||||
:provider-id="service.provider"
|
:provider="localProvider"
|
||||||
:discovered-location="configuredLocation || undefined"
|
:service="localService"
|
||||||
v-model="configuredLocation"
|
|
||||||
@valid="(valid) => configValid = valid"
|
|
||||||
/>
|
|
||||||
</v-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Step 2: Authentication -->
|
|
||||||
<template #item.2>
|
|
||||||
<v-card flat class="pa-6">
|
|
||||||
<ProviderAuthStep
|
|
||||||
v-if="service"
|
|
||||||
:provider-id="service.provider"
|
|
||||||
:provider-label="providerLabel"
|
|
||||||
:email-address="service.primaryAddress || ''"
|
|
||||||
:discovered-location="configuredLocation || undefined"
|
|
||||||
:prefilled-identity="service.primaryAddress || ''"
|
|
||||||
:prefilled-secret="undefined"
|
|
||||||
v-model="configuredIdentity"
|
|
||||||
@valid="(valid) => authValid = valid"
|
|
||||||
/>
|
|
||||||
</v-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Step 3: Test & Save -->
|
|
||||||
<template #item.3>
|
|
||||||
<v-card flat class="pa-6">
|
|
||||||
<TestAndSaveStep
|
|
||||||
v-if="service"
|
|
||||||
:provider-id="service.provider"
|
|
||||||
:provider-label="providerLabel"
|
|
||||||
:email-address="service.primaryAddress || ''"
|
|
||||||
:location="configuredLocation"
|
|
||||||
:identity="configuredIdentity"
|
|
||||||
:prefilled-label="accountLabel"
|
|
||||||
:on-test="testConnection"
|
:on-test="testConnection"
|
||||||
@update:label="(val) => accountLabel = val"
|
@update:service="handleUpdate"
|
||||||
@update:enabled="(val) => accountEnabled = val"
|
|
||||||
@valid="(valid) => testAndSaveValid = valid"
|
|
||||||
/>
|
/>
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</v-window-item>
|
||||||
</v-stepper>
|
|
||||||
|
<v-window-item value="protocol">
|
||||||
|
<v-card flat class="pa-6">
|
||||||
|
<ProviderProtocolPanel
|
||||||
|
v-if="localProvider && localService"
|
||||||
|
:provider="localProvider"
|
||||||
|
:service="localService"
|
||||||
|
@update:service="handleUpdate"
|
||||||
|
/>
|
||||||
|
</v-card>
|
||||||
|
</v-window-item>
|
||||||
|
|
||||||
|
<v-window-item value="auth">
|
||||||
|
<v-card flat class="pa-6">
|
||||||
|
<ProviderAuthPanel
|
||||||
|
v-if="localProvider && localService"
|
||||||
|
:provider="localProvider"
|
||||||
|
:service="localService"
|
||||||
|
@update:service="handleUpdate"
|
||||||
|
/>
|
||||||
|
</v-card>
|
||||||
|
</v-window-item>
|
||||||
|
</v-window>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-divider />
|
<v-divider />
|
||||||
|
|
||||||
<v-card-actions class="pa-6">
|
<v-card-actions class="pa-6">
|
||||||
<!-- Previous Button -->
|
|
||||||
<v-btn
|
|
||||||
v-if="showPreviousButton"
|
|
||||||
variant="text"
|
|
||||||
prepend-icon="mdi-arrow-left"
|
|
||||||
@click="handlePreviousStep"
|
|
||||||
>
|
|
||||||
Previous
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
@@ -326,18 +297,7 @@ watch(configuredLocation, (newLocation) => {
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<!-- Next Button -->
|
|
||||||
<v-btn
|
|
||||||
v-if="showNextButton"
|
|
||||||
color="primary"
|
|
||||||
append-icon="mdi-arrow-right"
|
|
||||||
:disabled="!canProceedToNext"
|
|
||||||
@click="handleNextStep"
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<!-- Save Button -->
|
<!-- Save Button -->
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="showSaveButton"
|
v-if="showSaveButton"
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ const rules = {
|
|||||||
required: (v: string) => !!v || 'Required',
|
required: (v: string) => !!v || 'Required',
|
||||||
email: (v: string) => /.+@.+\..+/.test(v) || 'Invalid email address'
|
email: (v: string) => /.+@.+\..+/.test(v) || 'Invalid email address'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDiscoverOnEnter() {
|
||||||
|
if (!localAddress.value || rules.email(localAddress.value) !== true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('discover')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -55,6 +63,7 @@ const rules = {
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:rules="[rules.required, rules.email]"
|
:rules="[rules.required, rules.email]"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
|
@keydown.enter.prevent="handleDiscoverOnEnter"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Advanced Options -->
|
<!-- Advanced Options -->
|
||||||
@@ -143,6 +152,8 @@ const rules = {
|
|||||||
<v-btn
|
<v-btn
|
||||||
variant="text"
|
variant="text"
|
||||||
block
|
block
|
||||||
|
class="manual-action-btn"
|
||||||
|
prepend-icon="mdi-tune"
|
||||||
@click="$emit('manual')"
|
@click="$emit('manual')"
|
||||||
>
|
>
|
||||||
Manual Configuration
|
Manual Configuration
|
||||||
@@ -155,4 +166,8 @@ const rules = {
|
|||||||
.gap-3 {
|
.gap-3 {
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.manual-action-btn {
|
||||||
|
background-color: rgba(var(--v-theme-on-surface), 0.06);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import type { ProviderDiscoveryStatus } from '@MailManager/types/service'
|
import type { ProviderDiscoveryStatus } from '@MailManager/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
address: string
|
address: string
|
||||||
@@ -16,7 +16,7 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const sortedStatus = computed(() => {
|
const sortedStatus = computed(() => {
|
||||||
const statusArray = Object.values(props.status)
|
const statusArray = Object.values(props.status)
|
||||||
const order = { success: 0, discovering: 1, pending: 2, failed: 3 }
|
const order: Record<string, number> = { success: 0, discovering: 1, pending: 2, failed: 3 }
|
||||||
return statusArray.sort((a, b) => order[a.status] - order[b.status])
|
return statusArray.sort((a, b) => order[a.status] - order[b.status])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -191,6 +191,22 @@ function getProviderLabel(providerId: string): string {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!isDiscovering && successCount === 0"
|
||||||
|
class="mt-6"
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
size="large"
|
||||||
|
variant="text"
|
||||||
|
block
|
||||||
|
class="manual-action-btn"
|
||||||
|
prepend-icon="mdi-tune"
|
||||||
|
@click="$emit('manual')"
|
||||||
|
>
|
||||||
|
Manual Configuration
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -217,4 +233,8 @@ function getProviderLabel(providerId: string): string {
|
|||||||
.gap-3 {
|
.gap-3 {
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.manual-action-btn {
|
||||||
|
background-color: rgba(var(--v-theme-on-surface), 0.06);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
124
src/components/steps/ProviderAuthPanel.vue
Normal file
124
src/components/steps/ProviderAuthPanel.vue
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, shallowRef, watch } from 'vue'
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
import { useIntegrationStore } from '@KTXC/stores/integrationStore'
|
||||||
|
import type { ServiceObject } from '@MailManager/models/service'
|
||||||
|
import type { ProviderObject } from '@MailManager/models/provider'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
provider: ProviderObject
|
||||||
|
service: ServiceObject
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:service': [value: ServiceObject]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Local state
|
||||||
|
const integrationStore = useIntegrationStore()
|
||||||
|
const panelCache = new Map<string, Component>()
|
||||||
|
const panelLoading = ref(false)
|
||||||
|
const panelActive = shallowRef<Component | null>(null)
|
||||||
|
const localProvider = ref<ProviderObject>(props.provider)
|
||||||
|
const localService = ref<ServiceObject>(props.service)
|
||||||
|
|
||||||
|
// Local watchers
|
||||||
|
watch(
|
||||||
|
() => props.provider,
|
||||||
|
async (provider) => {
|
||||||
|
localProvider.value = provider
|
||||||
|
await loadProviderPanel()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.service,
|
||||||
|
(service) => {
|
||||||
|
localService.value = service
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [localProvider.value?.identifier, localService.value?.provider] as const,
|
||||||
|
async () => {
|
||||||
|
await loadProviderPanel()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Load provider panel
|
||||||
|
async function loadProviderPanel() {
|
||||||
|
const providerIdentifier = localProvider.value?.identifier || localService.value?.provider
|
||||||
|
|
||||||
|
if (!providerIdentifier) {
|
||||||
|
panelActive.value = null
|
||||||
|
panelLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrieve panel from cache if available
|
||||||
|
if (panelCache.has(providerIdentifier)) {
|
||||||
|
panelActive.value = panelCache.get(providerIdentifier) || null
|
||||||
|
panelLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
panelLoading.value = true
|
||||||
|
|
||||||
|
// retrieve panel from integration store
|
||||||
|
const panel = integrationStore.getItems('mail_account_auth_panels').find((panel: any) => {
|
||||||
|
return panel.id === providerIdentifier || panel.id.endsWith(`.${providerIdentifier}`)
|
||||||
|
})
|
||||||
|
if (!panel?.component) {
|
||||||
|
console.warn(`No config panel found for provider ID: ${providerIdentifier}`)
|
||||||
|
panelActive.value = null
|
||||||
|
panelLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const module = await panel.component()
|
||||||
|
const component = module.default || module
|
||||||
|
panelCache.set(providerIdentifier, component)
|
||||||
|
panelActive.value = component
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load panel for ${providerIdentifier}:`, error)
|
||||||
|
panelActive.value = null
|
||||||
|
} finally {
|
||||||
|
panelLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdate(service: ServiceObject) {
|
||||||
|
localService.value = service
|
||||||
|
emit('update:service', localService.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="provider-auth-panel">
|
||||||
|
<h3 class="text-h6 mb-2">Authentication</h3>
|
||||||
|
<p class="text-body-2 text-medium-emphasis mb-6">
|
||||||
|
Configure authentication for {{ localProvider?.label || 'this provider' }}.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-if="panelLoading" 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>
|
||||||
|
|
||||||
|
<v-alert v-else-if="!panelActive" type="error" variant="tonal">
|
||||||
|
<v-icon start>mdi-alert-circle</v-icon>
|
||||||
|
No authentication method available for this provider.
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<component
|
||||||
|
v-else
|
||||||
|
:is="panelActive"
|
||||||
|
:service="localService"
|
||||||
|
@update:service="handleUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
<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>
|
|
||||||
124
src/components/steps/ProviderProtocolPanel.vue
Normal file
124
src/components/steps/ProviderProtocolPanel.vue
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, shallowRef, watch } from 'vue'
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
import { useIntegrationStore } from '@KTXC/stores/integrationStore'
|
||||||
|
import type { ServiceObject } from '@MailManager/models/service'
|
||||||
|
import type { ProviderObject } from '@MailManager/models/provider'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
provider: ProviderObject
|
||||||
|
service: ServiceObject
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:service': [value: ServiceObject]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Local state
|
||||||
|
const integrationStore = useIntegrationStore()
|
||||||
|
const panelCache = new Map<string, Component>()
|
||||||
|
const panelLoading = ref(false)
|
||||||
|
const panelActive = shallowRef<Component | null>(null)
|
||||||
|
const localProvider = ref<ProviderObject>(props.provider)
|
||||||
|
const localService = ref<ServiceObject>(props.service)
|
||||||
|
|
||||||
|
// Local watchers
|
||||||
|
watch(
|
||||||
|
() => props.provider,
|
||||||
|
async (provider) => {
|
||||||
|
localProvider.value = provider
|
||||||
|
await loadProviderPanel()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.service,
|
||||||
|
(service) => {
|
||||||
|
localService.value = service
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [localProvider.value?.identifier, localService.value?.provider] as const,
|
||||||
|
async () => {
|
||||||
|
await loadProviderPanel()
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Load provider panel
|
||||||
|
async function loadProviderPanel() {
|
||||||
|
const providerIdentifier = localProvider.value?.identifier || localService.value?.provider
|
||||||
|
|
||||||
|
if (!providerIdentifier) {
|
||||||
|
panelActive.value = null
|
||||||
|
panelLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrieve panel from cache if available
|
||||||
|
if (panelCache.has(providerIdentifier)) {
|
||||||
|
panelActive.value = panelCache.get(providerIdentifier) || null
|
||||||
|
panelLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
panelLoading.value = true
|
||||||
|
|
||||||
|
// retrieve panel from integration store
|
||||||
|
const panel = integrationStore.getItems('mail_account_protocol_panels').find((panel: any) => {
|
||||||
|
return panel.id === providerIdentifier || panel.id.endsWith(`.${providerIdentifier}`)
|
||||||
|
})
|
||||||
|
if (!panel?.component) {
|
||||||
|
console.warn(`No config panel found for provider ID: ${providerIdentifier}`)
|
||||||
|
panelActive.value = null
|
||||||
|
panelLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const module = await panel.component()
|
||||||
|
const component = module.default || module
|
||||||
|
panelCache.set(providerIdentifier, component)
|
||||||
|
panelActive.value = component
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load panel for ${providerIdentifier}:`, error)
|
||||||
|
panelActive.value = null
|
||||||
|
} finally {
|
||||||
|
panelLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdate(service: ServiceObject) {
|
||||||
|
localService.value = service
|
||||||
|
emit('update:service', localService.value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="provider-protocol-panel">
|
||||||
|
<h3 class="text-h6 mb-2">Protocol Configuration</h3>
|
||||||
|
<p class="text-body-2 text-medium-emphasis mb-6">
|
||||||
|
Configure authentication for {{ localProvider?.label || 'this provider' }}.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-if="panelLoading" 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>
|
||||||
|
|
||||||
|
<v-alert v-else-if="!panelActive" type="info" variant="tonal">
|
||||||
|
<v-icon start>mdi-information</v-icon>
|
||||||
|
No configuration panel available for this provider
|
||||||
|
</v-alert>
|
||||||
|
|
||||||
|
<component
|
||||||
|
v-else
|
||||||
|
:is="panelActive"
|
||||||
|
:service="localService"
|
||||||
|
@update:service="handleUpdate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,3 +1,121 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import type { ProviderObject, ServiceObject } from '@MailManager/models'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
provider: ProviderObject
|
||||||
|
service: ServiceObject
|
||||||
|
onTest?: () => Promise<{ success: boolean; message: string; details?: any }>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:service': [value: ServiceObject]
|
||||||
|
'tested': [success: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Local state
|
||||||
|
const localProvider = ref<ProviderObject>(props.provider)
|
||||||
|
const localService = ref<ServiceObject>(props.service)
|
||||||
|
const testing = ref(false)
|
||||||
|
const testResult = ref<any>(null)
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const testSuccess = computed(() => testResult.value?.success === true)
|
||||||
|
const serviceLocation = computed(() => localService.value.location?.toJson() ?? null)
|
||||||
|
const serviceIdentity = computed(() => localService.value.identity?.toJson() ?? null)
|
||||||
|
|
||||||
|
// 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 (!localService.value.location || !localService.value.identity) {
|
||||||
|
testResult.value = {
|
||||||
|
success: false,
|
||||||
|
message: 'Missing configuration'
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
testing.value = true
|
||||||
|
testResult.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!props.onTest) {
|
||||||
|
throw new Error('No connection test callback provided')
|
||||||
|
}
|
||||||
|
|
||||||
|
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(localService, () => {
|
||||||
|
emit('update:service', localService.value)
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.provider,
|
||||||
|
(provider) => {
|
||||||
|
localProvider.value = provider
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.service,
|
||||||
|
(service) => {
|
||||||
|
localService.value = service
|
||||||
|
testResult.value = null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="test-and-save-step">
|
<div class="test-and-save-step">
|
||||||
<h3 class="text-h6 mb-2">Test & Save</h3>
|
<h3 class="text-h6 mb-2">Test & Save</h3>
|
||||||
@@ -16,7 +134,7 @@
|
|||||||
<v-icon>mdi-label</v-icon>
|
<v-icon>mdi-label</v-icon>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>Account Name</v-list-item-title>
|
<v-list-item-title>Account Name</v-list-item-title>
|
||||||
<v-list-item-subtitle>{{ accountLabel }}</v-list-item-subtitle>
|
<v-list-item-subtitle>{{ localService.label || localService.primaryAddress || 'New Account' }}</v-list-item-subtitle>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|
||||||
<!-- Email Address -->
|
<!-- Email Address -->
|
||||||
@@ -25,7 +143,7 @@
|
|||||||
<v-icon>mdi-email</v-icon>
|
<v-icon>mdi-email</v-icon>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>Email Address</v-list-item-title>
|
<v-list-item-title>Email Address</v-list-item-title>
|
||||||
<v-list-item-subtitle>{{ emailAddress }}</v-list-item-subtitle>
|
<v-list-item-subtitle>{{ localService.primaryAddress }}</v-list-item-subtitle>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|
||||||
<!-- Provider -->
|
<!-- Provider -->
|
||||||
@@ -34,48 +152,48 @@
|
|||||||
<v-icon>mdi-cloud</v-icon>
|
<v-icon>mdi-cloud</v-icon>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>Provider</v-list-item-title>
|
<v-list-item-title>Provider</v-list-item-title>
|
||||||
<v-list-item-subtitle>{{ providerLabel }}</v-list-item-subtitle>
|
<v-list-item-subtitle>{{ localProvider.label }}</v-list-item-subtitle>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|
||||||
<!-- Location Details -->
|
<!-- Location Details -->
|
||||||
<template v-if="location">
|
<template v-if="serviceLocation">
|
||||||
<v-divider class="my-2" />
|
<v-divider class="my-2" />
|
||||||
|
|
||||||
<v-list-item v-if="location.type === 'URI'">
|
<v-list-item v-if="serviceLocation.type === 'URI'">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<v-icon>mdi-web</v-icon>
|
<v-icon>mdi-web</v-icon>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>Service URL</v-list-item-title>
|
<v-list-item-title>Service URL</v-list-item-title>
|
||||||
<v-list-item-subtitle>
|
<v-list-item-subtitle>
|
||||||
{{ location.scheme }}://{{ location.host }}:{{ location.port }}{{ location.path || '' }}
|
{{ serviceLocation.scheme }}://{{ serviceLocation.host }}:{{ serviceLocation.port }}{{ serviceLocation.path || '' }}
|
||||||
</v-list-item-subtitle>
|
</v-list-item-subtitle>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|
||||||
<template v-if="location.type === 'SOCKET_SOLE'">
|
<template v-if="serviceLocation.type === 'SOCKET_SOLE'">
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<v-icon>mdi-server</v-icon>
|
<v-icon>mdi-server</v-icon>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>Server</v-list-item-title>
|
<v-list-item-title>Server</v-list-item-title>
|
||||||
<v-list-item-subtitle>{{ location.host }}:{{ location.port }}</v-list-item-subtitle>
|
<v-list-item-subtitle>{{ serviceLocation.host }}:{{ serviceLocation.port }}</v-list-item-subtitle>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<v-icon>mdi-shield-lock</v-icon>
|
<v-icon>mdi-shield-lock</v-icon>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>Security</v-list-item-title>
|
<v-list-item-title>Security</v-list-item-title>
|
||||||
<v-list-item-subtitle>{{ location.encryption.toUpperCase() }}</v-list-item-subtitle>
|
<v-list-item-subtitle>{{ serviceLocation.encryption.toUpperCase() }}</v-list-item-subtitle>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="location.type === 'SOCKET_SPLIT'">
|
<template v-if="serviceLocation.type === 'SOCKET_SPLIT'">
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<v-icon>mdi-inbox-arrow-down</v-icon>
|
<v-icon>mdi-inbox-arrow-down</v-icon>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>Incoming Mail</v-list-item-title>
|
<v-list-item-title>Incoming Mail</v-list-item-title>
|
||||||
<v-list-item-subtitle>
|
<v-list-item-subtitle>
|
||||||
{{ location.inbound.protocol.toUpperCase() }} - {{ location.inbound.host }}:{{ location.inbound.port }} ({{ location.inbound.encryption.toUpperCase() }})
|
{{ serviceLocation.inboundHost }}:{{ serviceLocation.inboundPort }} ({{ serviceLocation.inboundEncryption.toUpperCase() }})
|
||||||
</v-list-item-subtitle>
|
</v-list-item-subtitle>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
@@ -84,7 +202,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<v-list-item-title>Outgoing Mail</v-list-item-title>
|
<v-list-item-title>Outgoing Mail</v-list-item-title>
|
||||||
<v-list-item-subtitle>
|
<v-list-item-subtitle>
|
||||||
{{ location.outbound.protocol.toUpperCase() }} - {{ location.outbound.host }}:{{ location.outbound.port }} ({{ location.outbound.encryption.toUpperCase() }})
|
{{ serviceLocation.outboundHost }}:{{ serviceLocation.outboundPort }} ({{ serviceLocation.outboundEncryption.toUpperCase() }})
|
||||||
</v-list-item-subtitle>
|
</v-list-item-subtitle>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</template>
|
</template>
|
||||||
@@ -94,10 +212,10 @@
|
|||||||
<v-divider class="my-2" />
|
<v-divider class="my-2" />
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<v-icon>{{ getAuthIcon(identity?.type) }}</v-icon>
|
<v-icon>{{ getAuthIcon(serviceIdentity?.type) }}</v-icon>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>Authentication</v-list-item-title>
|
<v-list-item-title>Authentication</v-list-item-title>
|
||||||
<v-list-item-subtitle>{{ getAuthLabel(identity?.type) }}</v-list-item-subtitle>
|
<v-list-item-subtitle>{{ getAuthLabel(serviceIdentity?.type) }}</v-list-item-subtitle>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
@@ -105,7 +223,7 @@
|
|||||||
|
|
||||||
<!-- Account Label Input -->
|
<!-- Account Label Input -->
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="localAccountLabel"
|
v-model="localService.label"
|
||||||
label="Account Name"
|
label="Account Name"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
hint="A friendly name for this account (e.g., Work Email)"
|
hint="A friendly name for this account (e.g., Work Email)"
|
||||||
@@ -116,7 +234,7 @@
|
|||||||
|
|
||||||
<!-- Enable Account Toggle -->
|
<!-- Enable Account Toggle -->
|
||||||
<v-switch
|
<v-switch
|
||||||
v-model="accountEnabled"
|
v-model="localService.enabled"
|
||||||
label="Enable this account"
|
label="Enable this account"
|
||||||
color="primary"
|
color="primary"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
@@ -176,120 +294,4 @@
|
|||||||
Please test the connection before saving
|
Please test the connection before saving
|
||||||
</v-alert>
|
</v-alert>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
|
||||||
@@ -2,45 +2,48 @@
|
|||||||
* Class model for Collection Interface
|
* Class model for Collection Interface
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CollectionInterface, CollectionPropertiesInterface } from "@/types/collection";
|
import type { CollectionInterface, CollectionModelInterface, CollectionPropertiesInterface, CollectionPropertiesModelInterface } from "@/types/collection";
|
||||||
|
|
||||||
export class CollectionObject implements CollectionInterface {
|
export class CollectionObject implements CollectionModelInterface {
|
||||||
|
|
||||||
_data!: CollectionInterface;
|
_data!: CollectionInterface<CollectionPropertiesInterface>;
|
||||||
|
_properties!: CollectionPropertiesObject;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._data = {
|
this._data = {
|
||||||
|
'@type': 'mail:collection',
|
||||||
|
version: 1,
|
||||||
provider: '',
|
provider: '',
|
||||||
service: '',
|
service: '',
|
||||||
collection: null,
|
collection: null,
|
||||||
identifier: '',
|
identifier: '',
|
||||||
signature: null,
|
properties: {'@type': 'mail:folder', label: ''},
|
||||||
created: null,
|
|
||||||
modified: null,
|
|
||||||
properties: new CollectionPropertiesObject(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fromJson(data: CollectionInterface): CollectionObject {
|
fromJson(data: CollectionInterface): CollectionObject {
|
||||||
this._data = data;
|
this._data = data;
|
||||||
if (data.properties) {
|
|
||||||
this._data.properties = new CollectionPropertiesObject().fromJson(data.properties as CollectionPropertiesInterface);
|
|
||||||
}
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
toJson(): CollectionInterface {
|
toJson(): CollectionInterface {
|
||||||
const json = { ...this._data };
|
const json = {
|
||||||
if (this._data.properties instanceof CollectionPropertiesObject) {
|
...this._data
|
||||||
json.properties = this._data.properties.toJson();
|
};
|
||||||
|
if (this._properties) {
|
||||||
|
json.properties = this._properties.toJson();
|
||||||
}
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
clone(): CollectionObject {
|
clone(): CollectionObject {
|
||||||
const cloned = new CollectionObject();
|
const cloned = new CollectionObject();
|
||||||
cloned._data = { ...this._data };
|
cloned._data = {
|
||||||
cloned._data.properties = this.properties.clone();
|
...this._data,
|
||||||
|
};
|
||||||
|
if (this._properties) {
|
||||||
|
cloned._properties = this._properties.clone();
|
||||||
|
}
|
||||||
return cloned;
|
return cloned;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,36 +78,30 @@ export class CollectionObject implements CollectionInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get properties(): CollectionPropertiesObject {
|
get properties(): CollectionPropertiesObject {
|
||||||
if (this._data.properties instanceof CollectionPropertiesObject) {
|
if (this._properties) {
|
||||||
return this._data.properties;
|
return this._properties;
|
||||||
}
|
}
|
||||||
|
else if (this._data.properties) {
|
||||||
if (this._data.properties) {
|
const properties = new CollectionPropertiesObject().fromJson(this._data.properties as CollectionPropertiesInterface);
|
||||||
const hydrated = new CollectionPropertiesObject().fromJson(this._data.properties as CollectionPropertiesInterface);
|
this._properties = properties;
|
||||||
this._data.properties = hydrated;
|
return properties;
|
||||||
return hydrated;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CollectionPropertiesObject();
|
return new CollectionPropertiesObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
set properties(value: CollectionPropertiesObject) {
|
set properties(value: CollectionPropertiesObject) {
|
||||||
if (value instanceof CollectionPropertiesObject) {
|
this._properties = value;
|
||||||
this._data.properties = value as any;
|
|
||||||
} else {
|
|
||||||
this._data.properties = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CollectionPropertiesObject implements CollectionPropertiesInterface {
|
export class CollectionPropertiesObject implements CollectionPropertiesModelInterface {
|
||||||
|
|
||||||
_data!: CollectionPropertiesInterface;
|
private _data!: CollectionPropertiesInterface;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._data = {
|
this._data = {
|
||||||
'@type': 'mail:collection',
|
'@type': 'mail:folder',
|
||||||
version: 1,
|
|
||||||
total: 0,
|
total: 0,
|
||||||
unread: 0,
|
unread: 0,
|
||||||
role: null,
|
role: null,
|
||||||
@@ -131,14 +128,6 @@ export class CollectionPropertiesObject implements CollectionPropertiesInterface
|
|||||||
|
|
||||||
/** Immutable Properties */
|
/** Immutable Properties */
|
||||||
|
|
||||||
get '@type'(): string {
|
|
||||||
return this._data['@type'];
|
|
||||||
}
|
|
||||||
|
|
||||||
get version(): number {
|
|
||||||
return this._data.version;
|
|
||||||
}
|
|
||||||
|
|
||||||
get role(): string | null | undefined {
|
get role(): string | null | undefined {
|
||||||
return this._data.role;
|
return this._data.role;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,19 @@
|
|||||||
* Class model for Message/Entity Interface
|
* Class model for Message/Entity Interface
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { EntityInterface } from "@/types/entity";
|
import type { EntityInterface, EntityModelInterface } from "@/types/entity";
|
||||||
import type { MessageInterface, MessagePartInterface } from "@/types/message";
|
import type { MessageInterface } from "@/types/message";
|
||||||
import { MessageObject } from "./message";
|
import { MessageObject } from "./message";
|
||||||
|
|
||||||
export class EntityObject {
|
export class EntityObject implements EntityModelInterface {
|
||||||
|
|
||||||
_data!: EntityInterface<MessageInterface>;
|
private _data!: EntityInterface<MessageInterface>;
|
||||||
_message!: MessageObject;
|
private _properties!: MessageObject;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._data = {
|
this._data = {
|
||||||
'@type': 'mail.entity',
|
'@type': 'mail:entity',
|
||||||
|
version: 1,
|
||||||
provider: '',
|
provider: '',
|
||||||
service: '',
|
service: '',
|
||||||
collection: '',
|
collection: '',
|
||||||
@@ -21,24 +22,7 @@ export class EntityObject {
|
|||||||
signature: null,
|
signature: null,
|
||||||
created: null,
|
created: null,
|
||||||
modified: null,
|
modified: null,
|
||||||
properties: {
|
properties: {'@type': 'mail:message'},
|
||||||
'@type': 'mail.message',
|
|
||||||
version: 1,
|
|
||||||
urid: '',
|
|
||||||
size: 0,
|
|
||||||
receivedDate: undefined,
|
|
||||||
date: undefined,
|
|
||||||
subject: '',
|
|
||||||
snippet: '',
|
|
||||||
from: undefined,
|
|
||||||
to: [],
|
|
||||||
cc: [],
|
|
||||||
bcc: [],
|
|
||||||
replyTo: [],
|
|
||||||
flags: {},
|
|
||||||
body: undefined,
|
|
||||||
attachments: [],
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,10 +37,10 @@ export class EntityObject {
|
|||||||
|
|
||||||
clone(): EntityObject {
|
clone(): EntityObject {
|
||||||
const cloned = new EntityObject();
|
const cloned = new EntityObject();
|
||||||
cloned._data = {
|
cloned._data = {
|
||||||
...this._data,
|
...this._data
|
||||||
properties: { ...this._data.properties }
|
|
||||||
};
|
};
|
||||||
|
cloned._properties = this.properties.clone();
|
||||||
return cloned;
|
return cloned;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,15 +77,10 @@ export class EntityObject {
|
|||||||
/** Message Object Properties */
|
/** Message Object Properties */
|
||||||
|
|
||||||
get properties(): MessageObject {
|
get properties(): MessageObject {
|
||||||
if (!this._message) {
|
if (!this._properties) {
|
||||||
this._message = new MessageObject(this._data.properties);
|
this._properties = new MessageObject().fromJson(this._data.properties as MessageInterface);
|
||||||
}
|
}
|
||||||
return this._message;
|
return this._properties;
|
||||||
}
|
|
||||||
|
|
||||||
// Alias for backward compatibility
|
|
||||||
get object(): MessageObject {
|
|
||||||
return this.properties;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ import type {
|
|||||||
*/
|
*/
|
||||||
export abstract class Identity {
|
export abstract class Identity {
|
||||||
abstract toJson(): ServiceIdentity;
|
abstract toJson(): ServiceIdentity;
|
||||||
|
abstract clone(): Identity;
|
||||||
|
|
||||||
|
toJSON(): ServiceIdentity {
|
||||||
|
return this.toJson();
|
||||||
|
}
|
||||||
|
|
||||||
static fromJson(data: ServiceIdentity): Identity {
|
static fromJson(data: ServiceIdentity): Identity {
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case 'NA':
|
case 'NA':
|
||||||
@@ -39,16 +44,30 @@ export abstract class Identity {
|
|||||||
* No authentication
|
* No authentication
|
||||||
*/
|
*/
|
||||||
export class IdentityNone extends Identity {
|
export class IdentityNone extends Identity {
|
||||||
readonly type = 'NA' as const;
|
|
||||||
|
private _data: ServiceIdentityNone;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._data = {
|
||||||
|
type: 'NA'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get type(): 'NA' {
|
||||||
|
return this._data.type;
|
||||||
|
}
|
||||||
|
|
||||||
static fromJson(_data: ServiceIdentityNone): IdentityNone {
|
static fromJson(_data: ServiceIdentityNone): IdentityNone {
|
||||||
return new IdentityNone();
|
return new IdentityNone();
|
||||||
}
|
}
|
||||||
|
|
||||||
toJson(): ServiceIdentityNone {
|
toJson(): ServiceIdentityNone {
|
||||||
return {
|
return { ...this._data };
|
||||||
type: this.type
|
}
|
||||||
};
|
|
||||||
|
clone(): IdentityNone {
|
||||||
|
return IdentityNone.fromJson(this.toJson());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,14 +75,36 @@ export class IdentityNone extends Identity {
|
|||||||
* Basic authentication (username/password)
|
* Basic authentication (username/password)
|
||||||
*/
|
*/
|
||||||
export class IdentityBasic extends Identity {
|
export class IdentityBasic extends Identity {
|
||||||
readonly type = 'BA' as const;
|
|
||||||
identity: string;
|
private _data: ServiceIdentityBasic;
|
||||||
secret: string;
|
|
||||||
|
|
||||||
constructor(identity: string = '', secret: string = '') {
|
constructor(identity: string = '', secret: string = '') {
|
||||||
super();
|
super();
|
||||||
this.identity = identity;
|
this._data = {
|
||||||
this.secret = secret;
|
type: 'BA',
|
||||||
|
identity,
|
||||||
|
secret
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get type(): 'BA' {
|
||||||
|
return this._data.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
get identity(): string {
|
||||||
|
return this._data.identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
set identity(value: string) {
|
||||||
|
this._data.identity = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get secret(): string {
|
||||||
|
return this._data.secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
set secret(value: string) {
|
||||||
|
this._data.secret = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(data: ServiceIdentityBasic): IdentityBasic {
|
static fromJson(data: ServiceIdentityBasic): IdentityBasic {
|
||||||
@@ -71,11 +112,11 @@ export class IdentityBasic extends Identity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toJson(): ServiceIdentityBasic {
|
toJson(): ServiceIdentityBasic {
|
||||||
return {
|
return { ...this._data };
|
||||||
type: this.type,
|
}
|
||||||
identity: this.identity,
|
|
||||||
secret: this.secret
|
clone(): IdentityBasic {
|
||||||
};
|
return IdentityBasic.fromJson(this.toJson());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,12 +124,27 @@ export class IdentityBasic extends Identity {
|
|||||||
* Token authentication (API key, static token)
|
* Token authentication (API key, static token)
|
||||||
*/
|
*/
|
||||||
export class IdentityToken extends Identity {
|
export class IdentityToken extends Identity {
|
||||||
readonly type = 'TA' as const;
|
|
||||||
token: string;
|
private _data: ServiceIdentityToken;
|
||||||
|
|
||||||
constructor(token: string = '') {
|
constructor(token: string = '') {
|
||||||
super();
|
super();
|
||||||
this.token = token;
|
this._data = {
|
||||||
|
type: 'TA',
|
||||||
|
token
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get type(): 'TA' {
|
||||||
|
return this._data.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
get token(): string {
|
||||||
|
return this._data.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
set token(value: string) {
|
||||||
|
this._data.token = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(data: ServiceIdentityToken): IdentityToken {
|
static fromJson(data: ServiceIdentityToken): IdentityToken {
|
||||||
@@ -96,10 +152,11 @@ export class IdentityToken extends Identity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toJson(): ServiceIdentityToken {
|
toJson(): ServiceIdentityToken {
|
||||||
return {
|
return { ...this._data };
|
||||||
type: this.type,
|
}
|
||||||
token: this.token
|
|
||||||
};
|
clone(): IdentityToken {
|
||||||
|
return IdentityToken.fromJson(this.toJson());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,12 +164,8 @@ export class IdentityToken extends Identity {
|
|||||||
* OAuth authentication
|
* OAuth authentication
|
||||||
*/
|
*/
|
||||||
export class IdentityOAuth extends Identity {
|
export class IdentityOAuth extends Identity {
|
||||||
readonly type = 'OA' as const;
|
|
||||||
accessToken: string;
|
private _data: ServiceIdentityOAuth;
|
||||||
accessScope?: string[];
|
|
||||||
accessExpiry?: number;
|
|
||||||
refreshToken?: string;
|
|
||||||
refreshLocation?: string;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
accessToken: string = '',
|
accessToken: string = '',
|
||||||
@@ -122,11 +175,58 @@ export class IdentityOAuth extends Identity {
|
|||||||
refreshLocation?: string
|
refreshLocation?: string
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.accessToken = accessToken;
|
this._data = {
|
||||||
this.accessScope = accessScope;
|
type: 'OA',
|
||||||
this.accessExpiry = accessExpiry;
|
accessToken,
|
||||||
this.refreshToken = refreshToken;
|
accessScope,
|
||||||
this.refreshLocation = refreshLocation;
|
accessExpiry,
|
||||||
|
refreshToken,
|
||||||
|
refreshLocation
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get type(): 'OA' {
|
||||||
|
return this._data.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
get accessToken(): string {
|
||||||
|
return this._data.accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
set accessToken(value: string) {
|
||||||
|
this._data.accessToken = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get accessScope(): string[] | undefined {
|
||||||
|
return this._data.accessScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
set accessScope(value: string[] | undefined) {
|
||||||
|
this._data.accessScope = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get accessExpiry(): number | undefined {
|
||||||
|
return this._data.accessExpiry;
|
||||||
|
}
|
||||||
|
|
||||||
|
set accessExpiry(value: number | undefined) {
|
||||||
|
this._data.accessExpiry = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get refreshToken(): string | undefined {
|
||||||
|
return this._data.refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
set refreshToken(value: string | undefined) {
|
||||||
|
this._data.refreshToken = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get refreshLocation(): string | undefined {
|
||||||
|
return this._data.refreshLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
set refreshLocation(value: string | undefined) {
|
||||||
|
this._data.refreshLocation = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(data: ServiceIdentityOAuth): IdentityOAuth {
|
static fromJson(data: ServiceIdentityOAuth): IdentityOAuth {
|
||||||
@@ -143,13 +243,17 @@ export class IdentityOAuth extends Identity {
|
|||||||
return {
|
return {
|
||||||
type: this.type,
|
type: this.type,
|
||||||
accessToken: this.accessToken,
|
accessToken: this.accessToken,
|
||||||
...(this.accessScope && { accessScope: this.accessScope }),
|
...(this.accessScope !== undefined && { accessScope: [...this.accessScope] }),
|
||||||
...(this.accessExpiry && { accessExpiry: this.accessExpiry }),
|
...(this.accessExpiry !== undefined && { accessExpiry: this.accessExpiry }),
|
||||||
...(this.refreshToken && { refreshToken: this.refreshToken }),
|
...(this.refreshToken !== undefined && { refreshToken: this.refreshToken }),
|
||||||
...(this.refreshLocation && { refreshLocation: this.refreshLocation })
|
...(this.refreshLocation !== undefined && { refreshLocation: this.refreshLocation })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone(): IdentityOAuth {
|
||||||
|
return IdentityOAuth.fromJson(this.toJson());
|
||||||
|
}
|
||||||
|
|
||||||
isExpired(): boolean {
|
isExpired(): boolean {
|
||||||
if (!this.accessExpiry) return false;
|
if (!this.accessExpiry) return false;
|
||||||
return Date.now() / 1000 >= this.accessExpiry;
|
return Date.now() / 1000 >= this.accessExpiry;
|
||||||
@@ -165,16 +269,44 @@ export class IdentityOAuth extends Identity {
|
|||||||
* Client certificate authentication (mTLS)
|
* Client certificate authentication (mTLS)
|
||||||
*/
|
*/
|
||||||
export class IdentityCertificate extends Identity {
|
export class IdentityCertificate extends Identity {
|
||||||
readonly type = 'CC' as const;
|
private _data: ServiceIdentityCertificate;
|
||||||
certificate: string;
|
|
||||||
privateKey: string;
|
|
||||||
passphrase?: string;
|
|
||||||
|
|
||||||
constructor(certificate: string = '', privateKey: string = '', passphrase?: string) {
|
constructor(certificate: string = '', privateKey: string = '', passphrase?: string) {
|
||||||
super();
|
super();
|
||||||
this.certificate = certificate;
|
this._data = {
|
||||||
this.privateKey = privateKey;
|
type: 'CC',
|
||||||
this.passphrase = passphrase;
|
certificate,
|
||||||
|
privateKey,
|
||||||
|
passphrase
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get type(): 'CC' {
|
||||||
|
return this._data.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
get certificate(): string {
|
||||||
|
return this._data.certificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
set certificate(value: string) {
|
||||||
|
this._data.certificate = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get privateKey(): string {
|
||||||
|
return this._data.privateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
set privateKey(value: string) {
|
||||||
|
this._data.privateKey = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get passphrase(): string | undefined {
|
||||||
|
return this._data.passphrase;
|
||||||
|
}
|
||||||
|
|
||||||
|
set passphrase(value: string | undefined) {
|
||||||
|
this._data.passphrase = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(data: ServiceIdentityCertificate): IdentityCertificate {
|
static fromJson(data: ServiceIdentityCertificate): IdentityCertificate {
|
||||||
@@ -190,7 +322,11 @@ export class IdentityCertificate extends Identity {
|
|||||||
type: this.type,
|
type: this.type,
|
||||||
certificate: this.certificate,
|
certificate: this.certificate,
|
||||||
privateKey: this.privateKey,
|
privateKey: this.privateKey,
|
||||||
...(this.passphrase && { passphrase: this.passphrase })
|
...(this.passphrase !== undefined && { passphrase: this.passphrase })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone(): IdentityCertificate {
|
||||||
|
return IdentityCertificate.fromJson(this.toJson());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type {
|
|||||||
*/
|
*/
|
||||||
export abstract class Location {
|
export abstract class Location {
|
||||||
abstract toJson(): ServiceLocation;
|
abstract toJson(): ServiceLocation;
|
||||||
|
abstract clone(): Location;
|
||||||
|
|
||||||
static fromJson(data: ServiceLocation): Location {
|
static fromJson(data: ServiceLocation): Location {
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
@@ -89,6 +90,17 @@ export class LocationUri extends Location {
|
|||||||
const path = this.path || '';
|
const path = this.path || '';
|
||||||
return `${this.scheme}://${this.host}:${this.port}${path}`;
|
return `${this.scheme}://${this.host}:${this.port}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone(): LocationUri {
|
||||||
|
return new LocationUri(
|
||||||
|
this.scheme,
|
||||||
|
this.host,
|
||||||
|
this.port,
|
||||||
|
this.path,
|
||||||
|
this.verifyPeer,
|
||||||
|
this.verifyHost
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,6 +150,16 @@ export class LocationSocketSole extends Location {
|
|||||||
...(this.verifyHost !== undefined && { verifyHost: this.verifyHost })
|
...(this.verifyHost !== undefined && { verifyHost: this.verifyHost })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone(): LocationSocketSole {
|
||||||
|
return new LocationSocketSole(
|
||||||
|
this.host,
|
||||||
|
this.port,
|
||||||
|
this.encryption,
|
||||||
|
this.verifyPeer,
|
||||||
|
this.verifyHost
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -212,6 +234,21 @@ export class LocationSocketSplit extends Location {
|
|||||||
...(this.outboundVerifyHost !== undefined && { outboundVerifyHost: this.outboundVerifyHost })
|
...(this.outboundVerifyHost !== undefined && { outboundVerifyHost: this.outboundVerifyHost })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone(): LocationSocketSplit {
|
||||||
|
return new LocationSocketSplit(
|
||||||
|
this.inboundHost,
|
||||||
|
this.inboundPort,
|
||||||
|
this.inboundEncryption,
|
||||||
|
this.outboundHost,
|
||||||
|
this.outboundPort,
|
||||||
|
this.outboundEncryption,
|
||||||
|
this.inboundVerifyPeer,
|
||||||
|
this.inboundVerifyHost,
|
||||||
|
this.outboundVerifyPeer,
|
||||||
|
this.outboundVerifyHost
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -237,4 +274,8 @@ export class LocationFile extends Location {
|
|||||||
path: this.path
|
path: this.path
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone(): LocationFile {
|
||||||
|
return new LocationFile(this.path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,235 @@
|
|||||||
/**
|
/**
|
||||||
* Message and MessagePart model classes
|
* Message and MessagePart model classes
|
||||||
*/
|
*/
|
||||||
|
import type {
|
||||||
|
MessageAddressInterface,
|
||||||
|
MessageInterface,
|
||||||
|
MessageModelInterface,
|
||||||
|
MessagePartInterface,
|
||||||
|
MessagePartModelInterface
|
||||||
|
} from "@/types/message";
|
||||||
|
|
||||||
import type { MessageInterface, MessagePartInterface } from "@/types/message";
|
/**
|
||||||
|
* Message class for working with message objects
|
||||||
|
*/
|
||||||
|
export class MessageObject implements MessageModelInterface {
|
||||||
|
|
||||||
|
_data: MessageInterface;
|
||||||
|
_body: MessagePartObject | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._data = {
|
||||||
|
'@type': 'mail:message',
|
||||||
|
};
|
||||||
|
this._body = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fromJson(data: MessageInterface): MessageObject {
|
||||||
|
this._data = data;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJson(): MessageInterface {
|
||||||
|
const json = {
|
||||||
|
...this._data
|
||||||
|
};
|
||||||
|
if (this._body) {
|
||||||
|
json.body = this._body.toJson();
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
clone(): MessageObject {
|
||||||
|
const cloned = new MessageObject();
|
||||||
|
cloned._data = {
|
||||||
|
...this._data,
|
||||||
|
};
|
||||||
|
if (this._body) {
|
||||||
|
cloned._body = this._body.clone();
|
||||||
|
}
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Properties */
|
||||||
|
|
||||||
|
get urid(): string | null{
|
||||||
|
return this._data.urid ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get size(): number {
|
||||||
|
return this._data.size ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get receivedDate(): string | null {
|
||||||
|
return this._data.receivedDate ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get sentDate(): string | null {
|
||||||
|
return this._data.sentDate ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get date(): string | null {
|
||||||
|
return this._data.date ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get subject(): string | null {
|
||||||
|
return this._data.subject ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get snippet(): string | null {
|
||||||
|
return this._data.snippet ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get from(): MessageAddressObject | null {
|
||||||
|
return this._data.from ? new MessageAddressObject(this._data.from) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get to(): Array<MessageAddressObject> | null {
|
||||||
|
return this._data.to ? this._data.to.map(addr => new MessageAddressObject(addr)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get cc(): Array<MessageAddressObject> | null {
|
||||||
|
return this._data.cc ? this._data.cc.map(addr => new MessageAddressObject(addr)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get bcc(): Array<MessageAddressObject> | null {
|
||||||
|
return this._data.bcc ? this._data.bcc.map(addr => new MessageAddressObject(addr)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get replyTo(): Array<MessageAddressObject> | null {
|
||||||
|
return this._data.replyTo ? this._data.replyTo.map(addr => new MessageAddressObject(addr)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get flags(): { read?: boolean; flagged?: boolean; answered?: boolean; draft?: boolean } | {} {
|
||||||
|
return this._data.flags ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
|
get body(): MessagePartObject | null {
|
||||||
|
if (this._body) {
|
||||||
|
return this._body;
|
||||||
|
}
|
||||||
|
else if (this._data.body) {
|
||||||
|
this._body = new MessagePartObject(this._data.body);
|
||||||
|
return this._body;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get attachments(): Array<MessagePartObject> {
|
||||||
|
return this._data.attachments ? this._data.attachments.map(att => new MessagePartObject(att)) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper methods */
|
||||||
|
|
||||||
|
get isRead(): boolean {
|
||||||
|
return this._data.flags?.read ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isFlagged(): boolean {
|
||||||
|
return this._data.flags?.flagged ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isAnswered(): boolean {
|
||||||
|
return this._data.flags?.answered ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isDraft(): boolean {
|
||||||
|
return this._data.flags?.draft ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasAttachments(): boolean {
|
||||||
|
return (this._data.attachments?.length ?? 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasRecipients(): boolean {
|
||||||
|
return (this._data.to?.length ?? 0) > 0
|
||||||
|
|| (this._data.cc?.length ?? 0) > 0
|
||||||
|
|| (this._data.bcc?.length ?? 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Body content helpers */
|
||||||
|
|
||||||
|
getBody(): MessagePartObject | null {
|
||||||
|
if (!this._body && this._data.body) {
|
||||||
|
this._body = new MessagePartObject(this._data.body);
|
||||||
|
}
|
||||||
|
return this._body;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasContent(): boolean {
|
||||||
|
return !!this.getTextContent() || !!this.getHtmlContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
hasTextContent(): boolean {
|
||||||
|
return !!this.getTextContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
getTextContent(): string | null {
|
||||||
|
const bodyPart = this.getBody();
|
||||||
|
return bodyPart ? bodyPart.extractTextContent() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasHtmlContent(): boolean {
|
||||||
|
return !!this.getHtmlContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
getHtmlContent(): string | null {
|
||||||
|
const bodyPart = this.getBody();
|
||||||
|
return bodyPart ? bodyPart.extractHtmlContent() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
findPartById(partId: string): MessagePartInterface | null {
|
||||||
|
const bodyPart = this.getBody();
|
||||||
|
return bodyPart ? bodyPart.findPartById(partId) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
findPartsByType(type: string): MessagePartInterface[] {
|
||||||
|
const bodyPart = this.getBody();
|
||||||
|
return bodyPart ? bodyPart.findPartsByType(type) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MessageAddressObject implements MessageAddressInterface {
|
||||||
|
|
||||||
|
_data: MessageAddressInterface;
|
||||||
|
|
||||||
|
constructor(data: MessageAddressInterface) {
|
||||||
|
this._data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
fromJson(data: MessageAddressInterface): MessageAddressObject {
|
||||||
|
this._data = data;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
toJson(): MessageAddressInterface {
|
||||||
|
return this._data;
|
||||||
|
}
|
||||||
|
|
||||||
|
clone(): MessageAddressObject {
|
||||||
|
return new MessageAddressObject({ ...this._data });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Properties */
|
||||||
|
|
||||||
|
get address(): string {
|
||||||
|
return this._data.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
get label(): string | undefined {
|
||||||
|
return this._data.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MessagePart class for working with message body parts
|
* MessagePart class for working with message body parts
|
||||||
*/
|
*/
|
||||||
export class MessagePartObject {
|
export class MessagePartObject implements MessagePartModelInterface {
|
||||||
|
|
||||||
_data: MessagePartInterface;
|
_data: MessagePartInterface;
|
||||||
|
_subParts: MessagePartObject[] = [];
|
||||||
|
|
||||||
constructor(data?: Partial<MessagePartInterface>) {
|
constructor(data?: Partial<MessagePartInterface>) {
|
||||||
this._data = {
|
this._data = {
|
||||||
@@ -17,14 +237,14 @@ export class MessagePartObject {
|
|||||||
blobId: data?.blobId ?? null,
|
blobId: data?.blobId ?? null,
|
||||||
size: data?.size ?? null,
|
size: data?.size ?? null,
|
||||||
name: data?.name ?? null,
|
name: data?.name ?? null,
|
||||||
type: data?.type ?? undefined,
|
type: data?.type ?? null,
|
||||||
charset: data?.charset ?? null,
|
charset: data?.charset ?? null,
|
||||||
disposition: data?.disposition ?? null,
|
disposition: data?.disposition ?? null,
|
||||||
cid: data?.cid ?? null,
|
cid: data?.cid ?? null,
|
||||||
language: data?.language ?? null,
|
language: data?.language ?? null,
|
||||||
location: data?.location ?? null,
|
location: data?.location ?? null,
|
||||||
content: data?.content ?? undefined,
|
content: data?.content ?? null,
|
||||||
subParts: data?.subParts ?? undefined,
|
subParts: data?.subParts ?? [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +254,13 @@ export class MessagePartObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toJson(): MessagePartInterface {
|
toJson(): MessagePartInterface {
|
||||||
return this._data;
|
const json = {
|
||||||
|
...this._data,
|
||||||
|
};
|
||||||
|
if (this._subParts.length > 0) {
|
||||||
|
json.subParts = this._subParts.map(subPart => subPart.toJson());
|
||||||
|
}
|
||||||
|
return json
|
||||||
}
|
}
|
||||||
|
|
||||||
clone(): MessagePartObject {
|
clone(): MessagePartObject {
|
||||||
@@ -43,52 +269,59 @@ export class MessagePartObject {
|
|||||||
|
|
||||||
/** Properties */
|
/** Properties */
|
||||||
|
|
||||||
get partId(): string | null | undefined {
|
get partId(): string | null {
|
||||||
return this._data.partId;
|
return this._data.partId ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get blobId(): string | null | undefined {
|
get blobId(): string | null {
|
||||||
return this._data.blobId;
|
return this._data.blobId ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get size(): number | null | undefined {
|
get size(): number | null {
|
||||||
return this._data.size;
|
return this._data.size ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get name(): string | null | undefined {
|
get name(): string | null {
|
||||||
return this._data.name;
|
return this._data.name ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get type(): string | undefined {
|
get type(): string | null {
|
||||||
return this._data.type;
|
return this._data.type ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get charset(): string | null | undefined {
|
get charset(): string | null {
|
||||||
return this._data.charset;
|
return this._data.charset ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get disposition(): string | null | undefined {
|
get disposition(): string | null {
|
||||||
return this._data.disposition;
|
return this._data.disposition ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get cid(): string | null | undefined {
|
get cid(): string | null {
|
||||||
return this._data.cid;
|
return this._data.cid ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get language(): string | null | undefined {
|
get language(): string | null {
|
||||||
return this._data.language;
|
return this._data.language ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get location(): string | null | undefined {
|
get location(): string | null {
|
||||||
return this._data.location;
|
return this._data.location ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get content(): string | undefined {
|
get content(): string | null {
|
||||||
return this._data.content;
|
return this._data.content ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get subParts(): MessagePartInterface[] | undefined {
|
get subParts(): MessagePartModelInterface[] {
|
||||||
return this._data.subParts;
|
if (this._subParts) {
|
||||||
|
return this._subParts;
|
||||||
|
}
|
||||||
|
else if (this._data.subParts) {
|
||||||
|
this._subParts = this._data.subParts.map((subPart) => new MessagePartObject(subPart));
|
||||||
|
return this._subParts;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Helper methods */
|
/** Helper methods */
|
||||||
@@ -98,7 +331,7 @@ export class MessagePartObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasSubParts(): boolean {
|
hasSubParts(): boolean {
|
||||||
return !!this._data.subParts && this._data.subParts.length > 0;
|
return this.subParts.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
isMultipart(): boolean {
|
isMultipart(): boolean {
|
||||||
@@ -206,171 +439,3 @@ export class MessagePartObject {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Message class for working with message objects
|
|
||||||
*/
|
|
||||||
export class MessageObject {
|
|
||||||
|
|
||||||
_data: MessageInterface;
|
|
||||||
_body: MessagePartObject | null = null;
|
|
||||||
|
|
||||||
constructor(data?: Partial<MessageInterface>) {
|
|
||||||
this._data = {
|
|
||||||
urid: data?.urid ?? undefined,
|
|
||||||
size: data?.size ?? undefined,
|
|
||||||
receivedDate: data?.receivedDate ?? undefined,
|
|
||||||
date: data?.date ?? undefined,
|
|
||||||
subject: data?.subject ?? undefined,
|
|
||||||
snippet: data?.snippet ?? undefined,
|
|
||||||
from: data?.from ?? undefined,
|
|
||||||
to: data?.to ?? [],
|
|
||||||
cc: data?.cc ?? [],
|
|
||||||
bcc: data?.bcc ?? [],
|
|
||||||
replyTo: data?.replyTo ?? [],
|
|
||||||
flags: data?.flags ?? {},
|
|
||||||
body: data?.body ?? undefined,
|
|
||||||
attachments: data?.attachments ?? [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fromJson(data: MessageInterface): MessageObject {
|
|
||||||
this._data = data;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
toJson(): MessageInterface {
|
|
||||||
return this._data;
|
|
||||||
}
|
|
||||||
|
|
||||||
clone(): MessageObject {
|
|
||||||
return new MessageObject(JSON.parse(JSON.stringify(this._data)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Properties */
|
|
||||||
|
|
||||||
get urid(): string | undefined {
|
|
||||||
return this._data.urid;
|
|
||||||
}
|
|
||||||
|
|
||||||
get size(): number | undefined {
|
|
||||||
return this._data.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
get receivedDate(): string | undefined {
|
|
||||||
return this._data.receivedDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
get date(): string | undefined {
|
|
||||||
return this._data.date;
|
|
||||||
}
|
|
||||||
|
|
||||||
get subject(): string | undefined {
|
|
||||||
return this._data.subject;
|
|
||||||
}
|
|
||||||
|
|
||||||
get snippet(): string | undefined {
|
|
||||||
return this._data.snippet;
|
|
||||||
}
|
|
||||||
|
|
||||||
get from(): { address: string; label?: string } | undefined {
|
|
||||||
return this._data.from;
|
|
||||||
}
|
|
||||||
|
|
||||||
get to(): Array<{ address: string; label?: string }> | undefined {
|
|
||||||
return this._data.to;
|
|
||||||
}
|
|
||||||
|
|
||||||
get cc(): Array<{ address: string; label?: string }> | undefined {
|
|
||||||
return this._data.cc;
|
|
||||||
}
|
|
||||||
|
|
||||||
get bcc(): Array<{ address: string; label?: string }> | undefined {
|
|
||||||
return this._data.bcc;
|
|
||||||
}
|
|
||||||
|
|
||||||
get replyTo(): Array<{ address: string; label?: string }> | undefined {
|
|
||||||
return this._data.replyTo;
|
|
||||||
}
|
|
||||||
|
|
||||||
get flags(): { read?: boolean; flagged?: boolean; answered?: boolean; draft?: boolean } | undefined {
|
|
||||||
return this._data.flags;
|
|
||||||
}
|
|
||||||
|
|
||||||
get body(): MessagePartInterface | undefined {
|
|
||||||
return this._data.body;
|
|
||||||
}
|
|
||||||
|
|
||||||
get attachments(): MessageInterface['attachments'] {
|
|
||||||
return this._data.attachments;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Helper methods */
|
|
||||||
|
|
||||||
get isRead(): boolean {
|
|
||||||
return this._data.flags?.read ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isFlagged(): boolean {
|
|
||||||
return this._data.flags?.flagged ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isAnswered(): boolean {
|
|
||||||
return this._data.flags?.answered ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isDraft(): boolean {
|
|
||||||
return this._data.flags?.draft ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasAttachments(): boolean {
|
|
||||||
return (this._data.attachments?.length ?? 0) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasRecipients(): boolean {
|
|
||||||
return (this._data.to?.length ?? 0) > 0
|
|
||||||
|| (this._data.cc?.length ?? 0) > 0
|
|
||||||
|| (this._data.bcc?.length ?? 0) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Body content helpers */
|
|
||||||
|
|
||||||
getBody(): MessagePartObject | null {
|
|
||||||
if (!this._body && this._data.body) {
|
|
||||||
this._body = new MessagePartObject(this._data.body);
|
|
||||||
}
|
|
||||||
return this._body;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasContent(): boolean {
|
|
||||||
return !!this.getTextContent() || !!this.getHtmlContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
hasTextContent(): boolean {
|
|
||||||
return !!this.getTextContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
getTextContent(): string | null {
|
|
||||||
const bodyPart = this.getBody();
|
|
||||||
return bodyPart ? bodyPart.extractTextContent() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasHtmlContent(): boolean {
|
|
||||||
return !!this.getHtmlContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
getHtmlContent(): string | null {
|
|
||||||
const bodyPart = this.getBody();
|
|
||||||
return bodyPart ? bodyPart.extractHtmlContent() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
findPartById(partId: string): MessagePartInterface | null {
|
|
||||||
const bodyPart = this.getBody();
|
|
||||||
return bodyPart ? bodyPart.findPartById(partId) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
findPartsByType(type: string): MessagePartInterface[] {
|
|
||||||
const bodyPart = this.getBody();
|
|
||||||
return bodyPart ? bodyPart.findPartsByType(type) : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,16 +4,18 @@
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
ProviderInterface,
|
ProviderInterface,
|
||||||
ProviderCapabilitiesInterface
|
ProviderCapabilitiesInterface,
|
||||||
|
ProviderModelInterface
|
||||||
} from "@/types/provider";
|
} from "@/types/provider";
|
||||||
|
|
||||||
export class ProviderObject implements ProviderInterface {
|
export class ProviderObject implements ProviderModelInterface {
|
||||||
|
|
||||||
_data!: ProviderInterface;
|
_data!: ProviderInterface;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._data = {
|
this._data = {
|
||||||
'@type': 'mail:provider',
|
'@type': 'mail:provider',
|
||||||
|
version: 1,
|
||||||
identifier: '',
|
identifier: '',
|
||||||
label: '',
|
label: '',
|
||||||
capabilities: {},
|
capabilities: {},
|
||||||
@@ -29,6 +31,12 @@ export class ProviderObject implements ProviderInterface {
|
|||||||
return this._data;
|
return this._data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone(): ProviderObject {
|
||||||
|
const cloned = new ProviderObject();
|
||||||
|
cloned._data = { ...this._data };
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
|
||||||
capable(capability: keyof ProviderCapabilitiesInterface): boolean {
|
capable(capability: keyof ProviderCapabilitiesInterface): boolean {
|
||||||
const value = this._data.capabilities?.[capability];
|
const value = this._data.capabilities?.[capability];
|
||||||
return value !== undefined && value !== false;
|
return value !== undefined && value !== false;
|
||||||
@@ -43,10 +51,6 @@ export class ProviderObject implements ProviderInterface {
|
|||||||
|
|
||||||
/** Immutable Properties */
|
/** Immutable Properties */
|
||||||
|
|
||||||
get '@type'(): string {
|
|
||||||
return this._data['@type'];
|
|
||||||
}
|
|
||||||
|
|
||||||
get identifier(): string {
|
get identifier(): string {
|
||||||
return this._data.identifier;
|
return this._data.identifier;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,19 +5,22 @@
|
|||||||
import type {
|
import type {
|
||||||
ServiceInterface,
|
ServiceInterface,
|
||||||
ServiceCapabilitiesInterface,
|
ServiceCapabilitiesInterface,
|
||||||
ServiceIdentity,
|
ServiceLocation,
|
||||||
ServiceLocation
|
ServiceModelInterface
|
||||||
} from "@/types/service";
|
} from "@/types/service";
|
||||||
import { Identity } from './identity';
|
import { Identity } from './identity';
|
||||||
import { Location } from './location';
|
import { Location } from './location';
|
||||||
|
|
||||||
export class ServiceObject implements ServiceInterface {
|
export class ServiceObject implements ServiceModelInterface {
|
||||||
|
|
||||||
_data!: ServiceInterface;
|
_data!: ServiceInterface;
|
||||||
|
_location: Location | null = null;
|
||||||
|
_identity: Identity | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._data = {
|
this._data = {
|
||||||
'@type': 'mail:service',
|
'@type': 'mail:service',
|
||||||
|
version: 1,
|
||||||
provider: '',
|
provider: '',
|
||||||
identifier: null,
|
identifier: null,
|
||||||
label: null,
|
label: null,
|
||||||
@@ -32,7 +35,35 @@ export class ServiceObject implements ServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toJson(): ServiceInterface {
|
toJson(): ServiceInterface {
|
||||||
return this._data;
|
const json = {
|
||||||
|
...this._data,
|
||||||
|
capabilities: this._data.capabilities ? { ...this._data.capabilities } : this._data.capabilities,
|
||||||
|
secondaryAddresses: this._data.secondaryAddresses ? [...this._data.secondaryAddresses] : this._data.secondaryAddresses,
|
||||||
|
auxiliary: this._data.auxiliary ? { ...this._data.auxiliary } : this._data.auxiliary,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this._location !== null) {
|
||||||
|
json.location = this._location.toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._identity !== null) {
|
||||||
|
json.identity = this._identity.toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
clone(): ServiceObject {
|
||||||
|
const cloned = new ServiceObject();
|
||||||
|
cloned._data = {
|
||||||
|
...this._data,
|
||||||
|
capabilities: this._data.capabilities ? { ...this._data.capabilities } : this._data.capabilities,
|
||||||
|
secondaryAddresses: this._data.secondaryAddresses ? [...this._data.secondaryAddresses] : this._data.secondaryAddresses,
|
||||||
|
auxiliary: this._data.auxiliary ? { ...this._data.auxiliary } : this._data.auxiliary,
|
||||||
|
};
|
||||||
|
cloned._location = this._location ? this._location.clone() : null;
|
||||||
|
cloned._identity = this._identity ? this._identity.clone() : null;
|
||||||
|
return cloned;
|
||||||
}
|
}
|
||||||
|
|
||||||
capable(capability: keyof ServiceCapabilitiesInterface): boolean {
|
capable(capability: keyof ServiceCapabilitiesInterface): boolean {
|
||||||
@@ -49,10 +80,6 @@ export class ServiceObject implements ServiceInterface {
|
|||||||
|
|
||||||
/** Immutable Properties */
|
/** Immutable Properties */
|
||||||
|
|
||||||
get '@type'(): string {
|
|
||||||
return this._data['@type'];
|
|
||||||
}
|
|
||||||
|
|
||||||
get provider(): string {
|
get provider(): string {
|
||||||
return this._data.provider;
|
return this._data.provider;
|
||||||
}
|
}
|
||||||
@@ -61,8 +88,8 @@ export class ServiceObject implements ServiceInterface {
|
|||||||
return this._data.identifier;
|
return this._data.identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
get capabilities(): ServiceCapabilitiesInterface | undefined {
|
get capabilities(): ServiceCapabilitiesInterface {
|
||||||
return this._data.capabilities;
|
return this._data.capabilities ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
get primaryAddress(): string | null {
|
get primaryAddress(): string | null {
|
||||||
@@ -91,20 +118,38 @@ export class ServiceObject implements ServiceInterface {
|
|||||||
this._data.enabled = value;
|
this._data.enabled = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
get location(): ServiceLocation | null {
|
get location(): Location | null {
|
||||||
return this._data.location ?? null;
|
if (this._location) {
|
||||||
|
return this._location;
|
||||||
|
}
|
||||||
|
else if (this._location === null && this._data.location) {
|
||||||
|
const location = Location.fromJson(this._data.location as ServiceLocation);
|
||||||
|
this._location = location;
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
set location(value: ServiceLocation | null) {
|
set location(value: Location | null) {
|
||||||
this._data.location = value;
|
this._location = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
get identity(): ServiceIdentity | null {
|
get identity(): Identity | null {
|
||||||
return this._data.identity ?? null;
|
if (this._identity) {
|
||||||
|
return this._identity;
|
||||||
|
}
|
||||||
|
else if (this._data.identity) {
|
||||||
|
const identity = Identity.fromJson(this._data.identity);
|
||||||
|
this._identity = identity;
|
||||||
|
return identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
set identity(value: ServiceIdentity | null) {
|
set identity(value: Identity | null) {
|
||||||
this._data.identity = value;
|
this._identity = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
get auxiliary(): Record<string, any> {
|
get auxiliary(): Record<string, any> {
|
||||||
@@ -115,22 +160,4 @@ export class ServiceObject implements ServiceInterface {
|
|||||||
this._data.auxiliary = value;
|
this._data.auxiliary = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Helper Methods */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get identity as a class instance for easier manipulation
|
|
||||||
*/
|
|
||||||
getIdentity(): Identity | null {
|
|
||||||
if (!this._data.identity) return null;
|
|
||||||
return Identity.fromJson(this._data.identity);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get location as a class instance for easier manipulation
|
|
||||||
*/
|
|
||||||
getLocation(): Location | null {
|
|
||||||
if (!this._data.location) return null;
|
|
||||||
return Location.fromJson(this._data.location);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import type {
|
|||||||
EntityStreamRequest,
|
EntityStreamRequest,
|
||||||
EntityStreamResponse,
|
EntityStreamResponse,
|
||||||
} from '../types/entity';
|
} from '../types/entity';
|
||||||
|
import type { EntityIdentifier } from '../types/common';
|
||||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
||||||
import { EntityObject } from '../models';
|
import { EntityObject } from '../models';
|
||||||
|
|
||||||
@@ -130,11 +131,11 @@ export const entityService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete an entity
|
* Delete entities by their identifiers
|
||||||
*
|
*
|
||||||
* @param request - delete request parameters
|
* @param request - delete request parameters
|
||||||
*
|
*
|
||||||
* @returns Promise with deletion result
|
* @returns Promise with deletion results keyed by source entity identifier
|
||||||
*/
|
*/
|
||||||
async delete(request: EntityDeleteRequest): Promise<EntityDeleteResponse> {
|
async delete(request: EntityDeleteRequest): Promise<EntityDeleteResponse> {
|
||||||
return await transceivePost<EntityDeleteRequest, EntityDeleteResponse>('entity.delete', request);
|
return await transceivePost<EntityDeleteRequest, EntityDeleteResponse>('entity.delete', request);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { defineStore } from 'pinia'
|
|||||||
import { entityService } from '../services'
|
import { entityService } from '../services'
|
||||||
import { EntityObject } from '../models'
|
import { EntityObject } from '../models'
|
||||||
import type {
|
import type {
|
||||||
|
EntityDeleteResponse,
|
||||||
EntityMoveResponse,
|
EntityMoveResponse,
|
||||||
EntityStreamRequest,
|
EntityStreamRequest,
|
||||||
EntityTransmitRequest,
|
EntityTransmitRequest,
|
||||||
@@ -62,6 +63,35 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
|||||||
return _entities.value[key] || null
|
return _entities.value[key] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve an entity from cache by full entity identifier.
|
||||||
|
*/
|
||||||
|
function entityByIdentifier(identifier: EntityIdentifier, retrieve: boolean = false): EntityObject | null {
|
||||||
|
if (retrieve === true && !_entities.value[identifier]) {
|
||||||
|
console.debug(`[Mail Manager][Store] - Force fetching entity "${identifier}"`)
|
||||||
|
const { provider, service, collection, identifier: id } = parseEntityIdentifier(identifier)
|
||||||
|
fetch(provider, service, collection, [id])
|
||||||
|
}
|
||||||
|
|
||||||
|
return _entities.value[identifier] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve multiple entities from cache by full entity identifiers.
|
||||||
|
*/
|
||||||
|
function entitiesByIdentifiers(identifiers: EntityIdentifier[], retrieve: boolean = false): Record<EntityIdentifier, EntityObject> {
|
||||||
|
const resolved: Record<EntityIdentifier, EntityObject> = {} as Record<EntityIdentifier, EntityObject>
|
||||||
|
|
||||||
|
Array.from(new Set(identifiers)).forEach(identifier => {
|
||||||
|
const entity = entityByIdentifier(identifier, retrieve)
|
||||||
|
if (entity) {
|
||||||
|
resolved[identifier] = entity
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all entities for a specific collection
|
* Get all entities for a specific collection
|
||||||
*
|
*
|
||||||
@@ -205,6 +235,58 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve delta changes for entities
|
||||||
|
*
|
||||||
|
* @param sources - source selector for delta check
|
||||||
|
*
|
||||||
|
* @returns Promise with delta changes (additions, modifications, deletions)
|
||||||
|
*
|
||||||
|
* Note: Delta returns only identifiers, not full entities.
|
||||||
|
* Caller should fetch full entities for additions/modifications separately.
|
||||||
|
*/
|
||||||
|
async function delta(sources: SourceSelector) {
|
||||||
|
transceiving.value = true
|
||||||
|
try {
|
||||||
|
const response = await entityService.delta({ sources })
|
||||||
|
|
||||||
|
// Process delta and update store
|
||||||
|
Object.entries(response).forEach(([provider, providerData]) => {
|
||||||
|
// Skip if no changes for provider
|
||||||
|
if (providerData === false) return
|
||||||
|
|
||||||
|
Object.entries(providerData).forEach(([service, serviceData]) => {
|
||||||
|
// Skip if no changes for service
|
||||||
|
if (serviceData === false) return
|
||||||
|
|
||||||
|
Object.entries(serviceData).forEach(([collection, collectionData]) => {
|
||||||
|
// Skip if no changes for collection
|
||||||
|
if (collectionData === false) return
|
||||||
|
|
||||||
|
// Process deletions (remove from store)
|
||||||
|
if (collectionData.deletions && collectionData.deletions.length > 0) {
|
||||||
|
collectionData.deletions.forEach((identifier) => {
|
||||||
|
const key = identifierKey(provider, service, collection, identifier)
|
||||||
|
delete _entities.value[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: additions and modifications contain only identifiers
|
||||||
|
// The caller should fetch full entities using the fetch() method
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
console.debug('[Mail Manager][Store] - Successfully processed delta changes')
|
||||||
|
return response
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[Mail Manager][Store] - Failed to process delta:', error)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
transceiving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new entity with given provider, service, collection, and data
|
* Create a new entity with given provider, service, collection, and data
|
||||||
*
|
*
|
||||||
@@ -265,80 +347,31 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete an entity by provider, service, collection, and identifier
|
* Delete entities by their identifiers.
|
||||||
*
|
*
|
||||||
* @param provider - provider identifier for the entity to delete
|
* Removes successfully deleted entities from the local store.
|
||||||
* @param service - service identifier for the entity to delete
|
*
|
||||||
* @param collection - collection identifier for the entity to delete
|
* @param sources - entity identifiers to delete
|
||||||
* @param identifier - entity identifier for the entity to delete
|
*
|
||||||
*
|
* @returns Promise with deletion results keyed by source identifier
|
||||||
* @returns Promise with deletion result
|
|
||||||
*/
|
*/
|
||||||
async function remove(provider: string, service: string | number, collection: string | number, identifier: string | number): Promise<any> {
|
async function remove(sources: EntityIdentifier[]): Promise<EntityDeleteResponse> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.delete({ provider, service, collection, identifier })
|
const response = await entityService.delete({ sources })
|
||||||
|
|
||||||
// Remove entity from state
|
|
||||||
const key = identifierKey(provider, service, collection, identifier)
|
|
||||||
delete _entities.value[key]
|
|
||||||
|
|
||||||
console.debug('[Mail Manager][Store] - Successfully deleted entity:', key)
|
Object.entries(response).forEach(([sourceIdentifier, result]) => {
|
||||||
return response
|
if (!result.success) {
|
||||||
} catch (error: any) {
|
return
|
||||||
console.error('[Mail Manager][Store] - Failed to delete entity:', error)
|
}
|
||||||
throw error
|
|
||||||
} finally {
|
|
||||||
transceiving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
delete _entities.value[sourceIdentifier]
|
||||||
* Retrieve delta changes for entities
|
|
||||||
*
|
|
||||||
* @param sources - source selector for delta check
|
|
||||||
*
|
|
||||||
* @returns Promise with delta changes (additions, modifications, deletions)
|
|
||||||
*
|
|
||||||
* Note: Delta returns only identifiers, not full entities.
|
|
||||||
* Caller should fetch full entities for additions/modifications separately.
|
|
||||||
*/
|
|
||||||
async function delta(sources: SourceSelector) {
|
|
||||||
transceiving.value = true
|
|
||||||
try {
|
|
||||||
const response = await entityService.delta({ sources })
|
|
||||||
|
|
||||||
// Process delta and update store
|
|
||||||
Object.entries(response).forEach(([provider, providerData]) => {
|
|
||||||
// Skip if no changes for provider
|
|
||||||
if (providerData === false) return
|
|
||||||
|
|
||||||
Object.entries(providerData).forEach(([service, serviceData]) => {
|
|
||||||
// Skip if no changes for service
|
|
||||||
if (serviceData === false) return
|
|
||||||
|
|
||||||
Object.entries(serviceData).forEach(([collection, collectionData]) => {
|
|
||||||
// Skip if no changes for collection
|
|
||||||
if (collectionData === false) return
|
|
||||||
|
|
||||||
// Process deletions (remove from store)
|
|
||||||
if (collectionData.deletions && collectionData.deletions.length > 0) {
|
|
||||||
collectionData.deletions.forEach((identifier) => {
|
|
||||||
const key = identifierKey(provider, service, collection, identifier)
|
|
||||||
delete _entities.value[key]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: additions and modifications contain only identifiers
|
|
||||||
// The caller should fetch full entities using the fetch() method
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
console.debug('[Mail Manager][Store] - Successfully processed delta changes')
|
console.debug('[Mail Manager][Store] - Successfully deleted', Object.keys(response).length, 'entities')
|
||||||
return response
|
return response
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Mail Manager][Store] - Failed to process delta:', error)
|
console.error('[Mail Manager][Store] - Failed to delete entities:', error)
|
||||||
throw error
|
throw error
|
||||||
} finally {
|
} finally {
|
||||||
transceiving.value = false
|
transceiving.value = false
|
||||||
@@ -461,8 +494,9 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
|||||||
has,
|
has,
|
||||||
entities,
|
entities,
|
||||||
entitiesForCollection,
|
entitiesForCollection,
|
||||||
// Actions
|
entitiesByIdentifiers,
|
||||||
entity,
|
entity,
|
||||||
|
entityByIdentifier,
|
||||||
list,
|
list,
|
||||||
fetch,
|
fetch,
|
||||||
extant,
|
extant,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { defineStore } from 'pinia'
|
|||||||
import { serviceService } from '../services'
|
import { serviceService } from '../services'
|
||||||
import { ServiceObject } from '../models/service'
|
import { ServiceObject } from '../models/service'
|
||||||
import type {
|
import type {
|
||||||
|
ServiceIdentifier,
|
||||||
ServiceLocation,
|
ServiceLocation,
|
||||||
SourceSelector,
|
SourceSelector,
|
||||||
ServiceIdentity,
|
ServiceIdentity,
|
||||||
@@ -59,13 +60,23 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
|||||||
* @returns Service object or null
|
* @returns Service object or null
|
||||||
*/
|
*/
|
||||||
function service(provider: string, identifier: string | number, retrieve: boolean = false): ServiceObject | null {
|
function service(provider: string, identifier: string | number, retrieve: boolean = false): ServiceObject | null {
|
||||||
const key = identifierKey(provider, identifier)
|
return serviceByIdentifier(identifierKey(provider, identifier), retrieve)
|
||||||
if (retrieve === true && !_services.value[key]) {
|
}
|
||||||
console.debug(`[Mail Manager][Store] - Force fetching service "${key}"`)
|
|
||||||
|
/**
|
||||||
|
* Get a service from store by its unique identifier, with optional retrieval
|
||||||
|
*
|
||||||
|
* @param identifier - unique service identifier
|
||||||
|
* @param retrieve - Retrieve behavior: true = fetch if missing or refresh, false = cache only
|
||||||
|
* @returns Service object or null
|
||||||
|
*/
|
||||||
|
function serviceByIdentifier(identifier: ServiceIdentifier, retrieve: boolean = false): ServiceObject | null {
|
||||||
|
if (retrieve === true && !_services.value[identifier]) {
|
||||||
|
console.debug(`[Mail Manager][Store] - Force fetching service "${identifier}"`)
|
||||||
fetch(provider, identifier)
|
fetch(provider, identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
return _services.value[key] || null
|
return _services.value[identifier] ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -341,6 +352,7 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
|||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
service,
|
service,
|
||||||
|
serviceByIdentifier,
|
||||||
serviceForAddress,
|
serviceForAddress,
|
||||||
list,
|
list,
|
||||||
fetch,
|
fetch,
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import type { ListFilter, ListSort, SourceSelector } from './common';
|
|||||||
/**
|
/**
|
||||||
* Collection information
|
* Collection information
|
||||||
*/
|
*/
|
||||||
export interface CollectionInterface {
|
export interface CollectionInterface<T = CollectionPropertiesInterface> {
|
||||||
|
'@type': string;
|
||||||
|
version: number;
|
||||||
provider: string;
|
provider: string;
|
||||||
service: string | number;
|
service: string | number;
|
||||||
collection: string | number | null;
|
collection: string | number | null;
|
||||||
@@ -14,12 +16,15 @@ export interface CollectionInterface {
|
|||||||
signature?: string | null;
|
signature?: string | null;
|
||||||
created?: string | null;
|
created?: string | null;
|
||||||
modified?: string | null;
|
modified?: string | null;
|
||||||
properties: CollectionPropertiesInterface;
|
properties: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CollectionModelInterface extends Omit<CollectionInterface<CollectionPropertiesInterface>, '@type' | 'version' | 'properties'> {
|
||||||
|
properties: CollectionPropertiesModelInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionBaseProperties {
|
export interface CollectionBaseProperties {
|
||||||
'@type': string;
|
'@type': string;
|
||||||
version: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionImmutableProperties extends CollectionBaseProperties {
|
export interface CollectionImmutableProperties extends CollectionBaseProperties {
|
||||||
@@ -36,6 +41,8 @@ export interface CollectionMutableProperties extends CollectionBaseProperties {
|
|||||||
|
|
||||||
export interface CollectionPropertiesInterface extends CollectionMutableProperties, CollectionImmutableProperties {}
|
export interface CollectionPropertiesInterface extends CollectionMutableProperties, CollectionImmutableProperties {}
|
||||||
|
|
||||||
|
export interface CollectionPropertiesModelInterface extends Omit<CollectionPropertiesInterface, '@type'> {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collection list
|
* Collection list
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import type {
|
|||||||
ListRange,
|
ListRange,
|
||||||
ListSort,
|
ListSort,
|
||||||
} from './common';
|
} from './common';
|
||||||
import type { MessageInterface } from './message';
|
import type { MessageInterface, MessageModelInterface } from './message';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity definition
|
* Entity definition
|
||||||
*/
|
*/
|
||||||
export interface EntityInterface<T = MessageInterface> {
|
export interface EntityInterface<T = MessageInterface> {
|
||||||
|
'@type': string;
|
||||||
|
version: number;
|
||||||
provider: string;
|
provider: string;
|
||||||
service: string;
|
service: string;
|
||||||
collection: string | number;
|
collection: string | number;
|
||||||
@@ -25,6 +27,8 @@ export interface EntityInterface<T = MessageInterface> {
|
|||||||
properties: T;
|
properties: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EntityModelInterface extends Omit<EntityInterface<MessageModelInterface>, '@type' | 'version'> {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity list
|
* Entity list
|
||||||
*/
|
*/
|
||||||
@@ -76,6 +80,26 @@ export interface EntityExtantResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity delta
|
||||||
|
*/
|
||||||
|
export interface EntityDeltaRequest {
|
||||||
|
sources: SourceSelector;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntityDeltaResponse {
|
||||||
|
[providerId: string]: false | {
|
||||||
|
[serviceId: string]: false | {
|
||||||
|
[collectionId: string]: false | {
|
||||||
|
signature: string;
|
||||||
|
additions: (string | number)[];
|
||||||
|
modifications: (string | number)[];
|
||||||
|
deletions: (string | number)[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity create
|
* Entity create
|
||||||
*/
|
*/
|
||||||
@@ -105,34 +129,20 @@ export interface EntityUpdateResponse<T = MessageInterface> extends EntityInterf
|
|||||||
* Entity delete
|
* Entity delete
|
||||||
*/
|
*/
|
||||||
export interface EntityDeleteRequest {
|
export interface EntityDeleteRequest {
|
||||||
provider: string;
|
sources: EntityIdentifier[];
|
||||||
service: string | number;
|
|
||||||
collection: string | number;
|
|
||||||
identifier: string | number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntityDeleteResponse {
|
export interface EntityDeleteResultSuccess {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export interface EntityDeleteResultFailure {
|
||||||
* Entity delta
|
success: boolean;
|
||||||
*/
|
error: string;
|
||||||
export interface EntityDeltaRequest {
|
|
||||||
sources: SourceSelector;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntityDeltaResponse {
|
export interface EntityDeleteResponse {
|
||||||
[providerId: string]: false | {
|
[sourceIdentifier: EntityIdentifier]: EntityDeleteResultSuccess | EntityDeleteResultFailure;
|
||||||
[serviceId: string]: false | {
|
|
||||||
[collectionId: string]: false | {
|
|
||||||
signature: string;
|
|
||||||
additions: (string | number)[];
|
|
||||||
modifications: (string | number)[];
|
|
||||||
deletions: (string | number)[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,25 +6,23 @@
|
|||||||
// ==================== Provider Panel Contracts ====================
|
// ==================== Provider Panel Contracts ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props all provider CONFIG panels receive
|
* Props all provider Protocol panels receive
|
||||||
* Config panels handle protocol/location settings only
|
* Protocol panels handle protocol/location settings only
|
||||||
*/
|
*/
|
||||||
export interface ProviderConfigPanelProps {
|
export interface ProviderProtocolPanelProps {
|
||||||
|
/** Current service value for v-model binding */
|
||||||
|
service?: import('../models').ServiceObject;
|
||||||
/** Pre-filled location from discovery (if available) */
|
/** Pre-filled location from discovery (if available) */
|
||||||
discoveredLocation?: import('./service').ServiceLocation;
|
discoveredLocation?: import('./service').ServiceLocation;
|
||||||
/** Current location value for v-model binding */
|
|
||||||
modelValue?: import('./service').ServiceLocation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events all provider CONFIG panels emit
|
* Events all provider Protocol panels emit
|
||||||
* Config panels emit location configuration and validation state
|
* Protocol panels emit service configuration and validation state
|
||||||
*/
|
*/
|
||||||
export interface ProviderConfigPanelEmits {
|
export interface ProviderProtocolPanelEmits {
|
||||||
/** Emit updated location configuration */
|
/** Emit updated service configuration */
|
||||||
'update:modelValue': [value: import('./service').ServiceLocation];
|
'update:service': [value: import('../models').ServiceObject];
|
||||||
/** Emit validation state (true = valid, false = invalid) */
|
|
||||||
'valid': [value: boolean];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,6 +30,8 @@ export interface ProviderConfigPanelEmits {
|
|||||||
* Auth panels handle credentials/authentication only
|
* Auth panels handle credentials/authentication only
|
||||||
*/
|
*/
|
||||||
export interface ProviderAuthPanelProps {
|
export interface ProviderAuthPanelProps {
|
||||||
|
/** Current service value for v-model binding */
|
||||||
|
service?: import('../models').ServiceObject;
|
||||||
/** Email address from discovery entry (for pre-filling username) */
|
/** Email address from discovery entry (for pre-filling username) */
|
||||||
emailAddress?: string;
|
emailAddress?: string;
|
||||||
/** Discovered or configured location (for context/auth decisions) */
|
/** Discovered or configured location (for context/auth decisions) */
|
||||||
@@ -40,8 +40,6 @@ export interface ProviderAuthPanelProps {
|
|||||||
prefilledIdentity?: string;
|
prefilledIdentity?: string;
|
||||||
/** Pre-filled secret/password if user entered during discovery */
|
/** Pre-filled secret/password if user entered during discovery */
|
||||||
prefilledSecret?: string;
|
prefilledSecret?: string;
|
||||||
/** Current identity value for v-model binding */
|
|
||||||
modelValue?: import('./service').ServiceIdentity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,10 +47,6 @@ export interface ProviderAuthPanelProps {
|
|||||||
* Auth panels emit identity configuration, validation state, and errors
|
* Auth panels emit identity configuration, validation state, and errors
|
||||||
*/
|
*/
|
||||||
export interface ProviderAuthPanelEmits {
|
export interface ProviderAuthPanelEmits {
|
||||||
/** Emit updated identity configuration */
|
/** Emit updated service configuration */
|
||||||
'update:modelValue': [value: import('./service').ServiceIdentity];
|
'update:service': [value: import('../models').ServiceObject];
|
||||||
/** Emit validation state (true = valid, false = invalid) */
|
|
||||||
'valid': [value: boolean];
|
|
||||||
/** Emit authentication errors for user feedback */
|
|
||||||
'error': [error: string];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Message object interface
|
||||||
|
*/
|
||||||
|
export interface MessageModelInterface extends Omit<{
|
||||||
|
[K in keyof MessageInterface]-?: Exclude<MessageInterface[K], undefined>;
|
||||||
|
}, '@type' | 'version' | 'body'> {
|
||||||
|
body: MessagePartModelInterface | null;
|
||||||
|
attachments: Array<MessagePartModelInterface>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageInterface {
|
||||||
|
'@type': string;
|
||||||
|
urid?: string | null;
|
||||||
|
size?: number | null;
|
||||||
|
date?: string | null;
|
||||||
|
receivedDate?: string | null;
|
||||||
|
sentDate?: string | null;
|
||||||
|
subject?: string | null;
|
||||||
|
snippet?: string | null;
|
||||||
|
from?: MessageAddressInterface | null;
|
||||||
|
to?: Array<MessageAddressInterface> | null;
|
||||||
|
cc?: Array<MessageAddressInterface> | null;
|
||||||
|
bcc?: Array<MessageAddressInterface> | null;
|
||||||
|
replyTo?: Array<MessageAddressInterface> | null;
|
||||||
|
flags?: MessageFlagsInterface | null;
|
||||||
|
body?: MessagePartInterface | null;
|
||||||
|
attachments?: Array<MessagePartInterface> | [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageAddressInterface {
|
||||||
|
address: string;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageFlagsInterface {
|
||||||
|
read?: boolean;
|
||||||
|
flagged?: boolean;
|
||||||
|
answered?: boolean;
|
||||||
|
draft?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message Part Interface
|
* Message Part Interface
|
||||||
*/
|
*/
|
||||||
|
export interface MessagePartModelInterface extends Omit<{
|
||||||
|
[K in keyof MessagePartInterface]-?: Exclude<MessagePartInterface[K], undefined>;
|
||||||
|
}, 'subParts'> {
|
||||||
|
subParts: MessagePartModelInterface[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface MessagePartInterface {
|
export interface MessagePartInterface {
|
||||||
partId?: string | null;
|
partId?: string | null;
|
||||||
blobId?: string | null;
|
blobId?: string | null;
|
||||||
size?: number | null;
|
size?: number | null;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
type?: string;
|
type?: string | null;
|
||||||
charset?: string | null;
|
charset?: string | null;
|
||||||
disposition?: string | null;
|
disposition?: string | null;
|
||||||
cid?: string | null;
|
cid?: string | null;
|
||||||
language?: string | null;
|
language?: string | null;
|
||||||
location?: string | null;
|
location?: string | null;
|
||||||
content?: string;
|
content?: string | null;
|
||||||
subParts?: MessagePartInterface[];
|
subParts?: MessagePartInterface[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Message object interface
|
|
||||||
*/
|
|
||||||
export interface MessageInterface {
|
|
||||||
urid?: string;
|
|
||||||
size?: number;
|
|
||||||
receivedDate?: string;
|
|
||||||
date?: string;
|
|
||||||
subject?: string;
|
|
||||||
snippet?: string;
|
|
||||||
from?: {
|
|
||||||
address: string;
|
|
||||||
label?: string;
|
|
||||||
};
|
|
||||||
to?: Array<{
|
|
||||||
address: string;
|
|
||||||
label?: string;
|
|
||||||
}>;
|
|
||||||
cc?: Array<{
|
|
||||||
address: string;
|
|
||||||
label?: string;
|
|
||||||
}>;
|
|
||||||
bcc?: Array<{
|
|
||||||
address: string;
|
|
||||||
label?: string;
|
|
||||||
}>;
|
|
||||||
replyTo?: Array<{
|
|
||||||
address: string;
|
|
||||||
label?: string;
|
|
||||||
}>;
|
|
||||||
flags?: {
|
|
||||||
read?: boolean;
|
|
||||||
flagged?: boolean;
|
|
||||||
answered?: boolean;
|
|
||||||
draft?: boolean;
|
|
||||||
};
|
|
||||||
body?: MessagePartInterface;
|
|
||||||
attachments?: Array<{
|
|
||||||
partId?: string;
|
|
||||||
blobId?: string;
|
|
||||||
size?: number;
|
|
||||||
name?: string;
|
|
||||||
type?: string;
|
|
||||||
charset?: string | null;
|
|
||||||
disposition?: string;
|
|
||||||
cid?: string | null;
|
|
||||||
language?: string | null;
|
|
||||||
location?: string | null;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
@@ -23,11 +23,14 @@ export interface ProviderCapabilitiesInterface {
|
|||||||
*/
|
*/
|
||||||
export interface ProviderInterface {
|
export interface ProviderInterface {
|
||||||
'@type': string;
|
'@type': string;
|
||||||
|
version: number;
|
||||||
identifier: string;
|
identifier: string;
|
||||||
label: string;
|
label: string;
|
||||||
capabilities: ProviderCapabilitiesInterface;
|
capabilities: ProviderCapabilitiesInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProviderModelInterface extends Omit<ProviderInterface, '@type' | 'version'> {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider list
|
* Provider list
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Service type definitions
|
* Service type definitions
|
||||||
*/
|
*/
|
||||||
|
import type { Identity } from '@/models/identity';
|
||||||
|
import type { Location } from '@/models/location';
|
||||||
import type {
|
import type {
|
||||||
ListFilterComparisonOperator,
|
ListFilterComparisonOperator,
|
||||||
SourceSelector,
|
SourceSelector,
|
||||||
@@ -43,6 +45,7 @@ export interface ServiceCapabilitiesInterface {
|
|||||||
*/
|
*/
|
||||||
export interface ServiceInterface {
|
export interface ServiceInterface {
|
||||||
'@type': string;
|
'@type': string;
|
||||||
|
version: number;
|
||||||
provider: string;
|
provider: string;
|
||||||
identifier: string | number | null;
|
identifier: string | number | null;
|
||||||
label: string | null;
|
label: string | null;
|
||||||
@@ -55,6 +58,13 @@ export interface ServiceInterface {
|
|||||||
auxiliary?: Record<string, any>; // Provider-specific extension data
|
auxiliary?: Record<string, any>; // Provider-specific extension data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ServiceModelInterface extends Omit<{
|
||||||
|
[K in keyof ServiceInterface]-?: Exclude<ServiceInterface[K], undefined>;
|
||||||
|
}, '@type' | 'version' | 'location' | 'identity'> {
|
||||||
|
location: Location | null;
|
||||||
|
identity: Identity | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service list
|
* Service list
|
||||||
*/
|
*/
|
||||||
@@ -137,6 +147,18 @@ export interface ServiceDiscoverResponse {
|
|||||||
location: ServiceLocation;
|
location: ServiceLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProviderDiscoveryStatus {
|
||||||
|
provider: string;
|
||||||
|
status: 'pending' | 'discovering' | 'success' | 'failed';
|
||||||
|
location?: ServiceLocation;
|
||||||
|
metadata?: {
|
||||||
|
host?: string;
|
||||||
|
port?: number;
|
||||||
|
protocol?: string;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service connection test
|
* Service connection test
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user