refactor: service address
Build Test / test (pull_request) Successful in 37s
JS Unit Tests / test (pull_request) Failing after 37s
PHP Unit Tests / test (pull_request) Successful in 59s

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-06-16 13:31:23 -04:00
parent 0e4bd905a2
commit bea7c1d0fb
10 changed files with 168 additions and 44 deletions
+11 -6
View File
@@ -4,7 +4,7 @@ import { useIntegrationStore } from '@KTXC/stores/integrationStore'
import { useServicesStore } from '@MailManager/stores/servicesStore'
import { useProvidersStore } from '@MailManager/stores/providersStore'
import { ServiceObject, type ProviderObject } from '@MailManager/models'
import type { ProviderDiscoveryStatus, ServiceInterface, ServiceLocation } from '@MailManager/types'
import type { ProviderDiscoveryStatus, ServiceAddressInterface, ServiceInterface, ServiceLocation } from '@MailManager/types'
import DiscoveryEntryPanel from '@MailManager/components/steps/DiscoveryEntryPanel.vue'
import DiscoveryStatusPanel from '@MailManager/components/steps/DiscoveryStatusPanel.vue'
import ProviderSelectionPanel from '@MailManager/components/steps/ProviderSelectionPanel.vue'
@@ -57,6 +57,11 @@ const discoverAddress = ref<string>('')
const discoverSecret = ref<string | null>(null)
const discoverHostname = ref<string | null>(null)
// Address entered during discovery, as a service address object
const discoverServiceAddress = computed<ServiceAddressInterface | null>(() =>
discoverAddress.value ? { address: discoverAddress.value } : null
)
// Step 2: Discovery Status / Provider Selection
const selectedProvider = shallowRef<ProviderObject | null>(null)
const selectedService = shallowRef<ServiceObject | null>(null)
@@ -154,7 +159,7 @@ function createServiceObject(
identifier: null,
label: data.label ?? null,
enabled: data.enabled ?? true,
primaryAddress: data.primaryAddress ?? (discoverAddress.value || null),
primaryAddress: data.primaryAddress ?? discoverServiceAddress.value,
secondaryAddresses: data.secondaryAddresses ?? null,
location: data.location ?? null,
identity: data.identity ?? null,
@@ -300,7 +305,7 @@ async function handleProviderSelect(identifier: string) {
...discoveredJson,
label: discoveredJson.label || discoverAddress.value,
enabled: discoveredJson.enabled ?? true,
primaryAddress: discoveredJson.primaryAddress || discoverAddress.value,
primaryAddress: discoveredJson.primaryAddress ?? discoverServiceAddress.value,
location: discoveredJson.location
})
setSelectedProviderAndService(identifier, service)
@@ -317,7 +322,7 @@ function handleProviderAdvanced(identifier: string) {
...discoveredJson,
label: discoveredJson?.label || discoverAddress.value,
enabled: discoveredJson?.enabled ?? true,
primaryAddress: discoveredJson?.primaryAddress || discoverAddress.value,
primaryAddress: discoveredJson?.primaryAddress ?? discoverServiceAddress.value,
location: discoveredJson?.location ?? null
})
@@ -341,7 +346,7 @@ function handleProviderManualSelect(identifier: string) {
const service = createServiceObject(identifier, {
label: discoverAddress.value,
enabled: true,
primaryAddress: discoverAddress.value,
primaryAddress: discoverServiceAddress.value,
location: null,
identity: null
})
@@ -396,7 +401,7 @@ async function saveAccount() {
try {
const accountData = {
label: serviceData.label || discoverAddress.value,
primaryAddress: serviceData.primaryAddress || discoverAddress.value,
primaryAddress: serviceData.primaryAddress ?? discoverServiceAddress.value,
enabled: serviceData.enabled,
location: serviceData.location,
identity: serviceData.identity,
+2 -2
View File
@@ -134,7 +134,7 @@ watch(
<v-icon>mdi-label</v-icon>
</template>
<v-list-item-title>Account Name</v-list-item-title>
<v-list-item-subtitle>{{ localService.label || localService.primaryAddress || 'New Account' }}</v-list-item-subtitle>
<v-list-item-subtitle>{{ localService.label || localService.primaryAddress?.address || 'New Account' }}</v-list-item-subtitle>
</v-list-item>
<!-- Email Address -->
@@ -143,7 +143,7 @@ watch(
<v-icon>mdi-email</v-icon>
</template>
<v-list-item-title>Email Address</v-list-item-title>
<v-list-item-subtitle>{{ localService.primaryAddress }}</v-list-item-subtitle>
<v-list-item-subtitle>{{ localService.primaryAddress?.address }}</v-list-item-subtitle>
</v-list-item>
<!-- Provider -->
+83
View File
@@ -0,0 +1,83 @@
/**
* Address implementation class for Mail Manager services
*
* Plain value object (no mutation tracking). ServiceObject getters return
* fresh instances built from raw data; mutating a getter result does not
* write back — assign through the setter to persist changes.
*/
import type { ServiceAddressInterface } from '@/types/service';
export class ServiceAddressObject implements ServiceAddressInterface {
_address: string;
_label: string | null;
constructor(address: string = '', label: string | null = null) {
this._address = address;
this._label = label;
}
static fromJson(data: ServiceAddressInterface): ServiceAddressObject {
return new ServiceAddressObject(data.address, data.label ?? null);
}
toJson(): ServiceAddressInterface {
const label = this._label?.trim() ?? '';
return {
address: this._address.trim(),
label: label.length > 0 ? label : null,
};
}
toJSON(): ServiceAddressInterface {
return this.toJson();
}
clone(): ServiceAddressObject {
return new ServiceAddressObject(this._address, this._label);
}
/** Case-insensitive address comparison, ignoring labels */
matches(address: string): boolean {
return this._address.trim().toLowerCase() === address.trim().toLowerCase();
}
equals(other: ServiceAddressObject | null | undefined): boolean {
if (!other) {
return false;
}
const current = this.toJson();
const next = other.toJson();
return current.address === next.address && (current.label ?? null) === (next.label ?? null);
}
/** Display form: "Label <address>" or bare address */
format(): string {
const { address, label } = this.toJson();
return label ? `${label} <${address}>` : address;
}
get empty(): boolean {
return this._address.trim().length === 0;
}
/** Properties (raw values for editing; normalization happens in toJson) */
get address(): string {
return this._address;
}
set address(value: string) {
this._address = value;
}
get label(): string | null {
return this._label;
}
set label(value: string | null) {
this._label = value;
}
}
+2
View File
@@ -1,5 +1,6 @@
export { ProviderObject } from './provider';
export { ServiceObject } from './service';
export { ServiceAddressObject } from './address';
export {
CollectionObject,
CollectionPropertiesObject
@@ -7,6 +8,7 @@ export {
export { EntityObject } from './entity';
export {
MessageObject,
MessageAddressObject,
MessagePartObject
} from './message';
export {
+9 -8
View File
@@ -8,6 +8,7 @@ import type {
ServiceLocation,
ServiceModelInterface
} from "@/types/service";
import { ServiceAddressObject } from './address';
import { Identity } from './identity';
import { Location } from './location';
import { MutationProxy } from './mutation-proxy';
@@ -118,20 +119,20 @@ export class ServiceObject implements ServiceModelInterface {
return this._data.capabilities ?? {};
}
get primaryAddress(): string | null {
return this._data.primaryAddress ?? null;
get primaryAddress(): ServiceAddressObject | null {
return this._data.primaryAddress ? ServiceAddressObject.fromJson(this._data.primaryAddress) : null;
}
set primaryAddress(value: string | null) {
this._data.primaryAddress = value;
set primaryAddress(value: ServiceAddressObject | null) {
this._data.primaryAddress = value ? value.toJson() : null;
}
get secondaryAddresses(): string[] {
return this._data.secondaryAddresses ?? [];
get secondaryAddresses(): ServiceAddressObject[] {
return (this._data.secondaryAddresses ?? []).map(entry => ServiceAddressObject.fromJson(entry));
}
set secondaryAddresses(value: string[] | null) {
this._data.secondaryAddresses = value;
set secondaryAddresses(value: ServiceAddressObject[] | null) {
this._data.secondaryAddresses = value ? value.map(entry => entry.toJson()) : null;
}
/** Mutable Properties */
+1 -1
View File
@@ -212,7 +212,7 @@ async function handleAccountSaved() {
<div>
<h3 class="text-h6">{{ service.label }}</h3>
<p class="text-caption text-medium-emphasis">
{{ service.primaryAddress || (service.identity?.type === 'BA' ? service.identity.identity : 'No email configured') }}
{{ service.primaryAddress?.address || (service.identity?.type === 'BA' ? service.identity.identity : 'No email configured') }}
</p>
</div>
</div>
+4 -1
View File
@@ -101,7 +101,10 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
* @returns Service object or null
*/
function serviceForAddress(address: string, retrieve: boolean = false): ServiceObject | null {
const service = Object.values(_services.value).find(s => s.primaryAddress === address || s.secondaryAddresses?.includes(address))
const service = Object.values(_services.value).find(s =>
s.primaryAddress?.matches(address) ||
s.secondaryAddresses.some(candidate => candidate.matches(address)),
)
if (retrieve === true && !service) {
console.debug(`[Mail Manager][Store] - No service found for address "${address}", discovery may be needed`)
+15 -3
View File
@@ -1,6 +1,7 @@
/**
* Service type definitions
*/
import type { ServiceAddressObject } from '@/models/address';
import type { Identity } from '@/models/identity';
import type { Location } from '@/models/location';
import type {
@@ -40,6 +41,15 @@ export interface ServiceCapabilitiesInterface {
[key: string]: boolean | object | string | string[] | undefined;
}
/**
* Service sender address (primary or secondary/alias)
* Mirrors the shared Mail AddressInterface JSON shape
*/
export interface ServiceAddressInterface {
address: string;
label?: string | null;
}
/**
* Service information
*/
@@ -53,16 +63,18 @@ export interface ServiceInterface {
capabilities?: ServiceCapabilitiesInterface;
location?: ServiceLocation | null;
identity?: ServiceIdentity | null;
primaryAddress?: string | null;
secondaryAddresses?: string[] | null;
primaryAddress?: ServiceAddressInterface | null;
secondaryAddresses?: ServiceAddressInterface[] | null;
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'> {
}, '@type' | 'version' | 'location' | 'identity' | 'primaryAddress' | 'secondaryAddresses'> {
location: Location | null;
identity: Identity | null;
primaryAddress: ServiceAddressObject | null;
secondaryAddresses: ServiceAddressObject[];
}
/**