refactor: service address
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -50,8 +50,6 @@ class DefaultController extends ControllerAbstract {
|
||||
private const ERR_INVALID_TARGETS = 'Invalid parameter: targets must be an array';
|
||||
private const ERR_INVALID_DATA = 'Invalid parameter: data must be an array';
|
||||
|
||||
private const STREAM_FLUSH_INTERVAL = 1;
|
||||
|
||||
public function __construct(
|
||||
private readonly SessionTenant $tenantIdentity,
|
||||
private readonly SessionIdentity $userIdentity,
|
||||
@@ -162,7 +160,7 @@ class DefaultController extends ControllerAbstract {
|
||||
'entity.patch' => $this->entityPatch($tenantId, $userId, $data),
|
||||
'entity.move' => $this->entityMove($tenantId, $userId, $data),
|
||||
'entity.copy' => throw new InvalidArgumentException('Operation not implemented: ' . $operation),
|
||||
'entity.transmit' => $this->entityTransmit($tenantId, $userId, $data),
|
||||
'entity.submit' => $this->entitySubmit($tenantId, $userId, $data),
|
||||
'entity.download' => $this->entityDownload($tenantId, $userId, $data),
|
||||
|
||||
default => throw new InvalidArgumentException(self::ERR_INVALID_OPERATION . $operation)
|
||||
@@ -839,29 +837,21 @@ class DefaultController extends ControllerAbstract {
|
||||
return $this->mailManager->entityMove($tenantId, $userId, $target, ...$sources->all());
|
||||
}
|
||||
|
||||
private function entityTransmit(string $tenantId, string $userId, array $data): mixed {
|
||||
if (!isset($data['provider'])) {
|
||||
throw new InvalidArgumentException(self::ERR_MISSING_PROVIDER);
|
||||
private function entitySubmit(string $tenantId, string $userId, array $data): mixed {
|
||||
if (!isset($data['sender'])) {
|
||||
throw new InvalidArgumentException(self::ERR_MISSING_SENDER);
|
||||
}
|
||||
if (!is_string($data['provider'])) {
|
||||
throw new InvalidArgumentException(self::ERR_INVALID_PROVIDER);
|
||||
}
|
||||
if (!isset($data['service'])) {
|
||||
throw new InvalidArgumentException(self::ERR_MISSING_SERVICE);
|
||||
}
|
||||
if (!is_string($data['service'])) {
|
||||
throw new InvalidArgumentException(self::ERR_INVALID_SERVICE);
|
||||
if (!is_string($data['sender'])) {
|
||||
throw new InvalidArgumentException(self::ERR_INVALID_SENDER);
|
||||
}
|
||||
|
||||
$jobId = $this->mailManager->entityTransmit(
|
||||
return $this->mailManager->entitySubmit(
|
||||
$tenantId,
|
||||
$userId,
|
||||
$data['provider'],
|
||||
$data['service'],
|
||||
$data['data']
|
||||
$data['sender'],
|
||||
$data['source'] ?? null,
|
||||
$data['message'] ?? null,
|
||||
);
|
||||
|
||||
return ['jobId' => $jobId];
|
||||
}
|
||||
|
||||
private function entityDownload(string $tenantId, string $userId, array $data): mixed {
|
||||
|
||||
+30
-2
@@ -10,6 +10,8 @@ use KTXF\Mail\Collection\CollectionBaseInterface;
|
||||
use KTXF\Mail\Collection\CollectionPropertiesMutableInterface;
|
||||
use KTXF\Mail\Collection\ICollectionBase;
|
||||
use KTXF\Mail\Entity\IMessageBase;
|
||||
use KTXF\Mail\Object\Address;
|
||||
use KTXF\Mail\Object\AddressInterface;
|
||||
use KTXF\Mail\Object\MessagePropertiesMutableInterface;
|
||||
use KTXF\Mail\Provider\ProviderBaseInterface;
|
||||
use KTXF\Mail\Provider\ProviderServiceDiscoverInterface;
|
||||
@@ -19,11 +21,14 @@ use KTXF\Mail\Service\ServiceBaseInterface;
|
||||
use KTXF\Mail\Service\ServiceCollectionMutableInterface;
|
||||
use KTXF\Mail\Service\ServiceConfigurableInterface;
|
||||
use KTXF\Mail\Service\ServiceEntityMutableInterface;
|
||||
use KTXF\Mail\Service\ServiceEntitySubmitInterface;
|
||||
use KTXF\Mail\Service\ServiceMutableInterface;
|
||||
use KTXF\Mail\Submission\EntitySubmitResult;
|
||||
use KTXF\Resource\BinaryResource;
|
||||
use KTXF\Resource\Filter\IFilter;
|
||||
use KTXF\Resource\Identifier\CollectionIdentifier;
|
||||
use KTXF\Resource\Identifier\EntityIdentifier;
|
||||
use KTXF\Resource\Identifier\EntityIdentifierInterface;
|
||||
use KTXF\Resource\Identifier\ResourceIdentifiers;
|
||||
use KTXF\Resource\Provider\ResourceServiceIdentityInterface;
|
||||
use KTXF\Resource\Provider\ResourceServiceLocationInterface;
|
||||
@@ -796,7 +801,7 @@ class Manager {
|
||||
}
|
||||
// retrieve entities for each collection
|
||||
foreach ($collectionSelected as $collectionId) {
|
||||
$entities = $service->entityList($collectionId, $entityFilter, $entitySort, $entityRange, null);
|
||||
$entities = $service->entityListBulk($collectionId, $entityFilter, $entitySort, $entityRange, null);
|
||||
// skip collections with no entities
|
||||
if ($entities === []) {
|
||||
continue;
|
||||
@@ -905,7 +910,7 @@ class Manager {
|
||||
// group identifiers by provider/service
|
||||
$groupedIdentifiers = [];
|
||||
foreach ($identifiers as $identifier) {
|
||||
$groupedIdentifiers[$identifier->provider()][$identifier->service()][] = $identifier->entity();
|
||||
$groupedIdentifiers[$identifier->provider()][$identifier->service()][] = $identifier;
|
||||
}
|
||||
// retrieve each service and fetch entities
|
||||
$list = [];
|
||||
@@ -1302,4 +1307,27 @@ class Manager {
|
||||
return $operationOutcome;
|
||||
}
|
||||
|
||||
public function entitySubmit(string $tenantId, string $userId, AddressInterface|string $sender, EntityIdentifierInterface|null $source = null, MessagePropertiesMutableInterface|array|null $message = null): EntitySubmitResult {
|
||||
$service = $this->serviceFindByAddress($tenantId, $userId, $sender);
|
||||
if ($service === null || $service->getEnabled() === false) {
|
||||
throw new InvalidArgumentException("Service not found for sender '{$sender}' or service is disabled");
|
||||
}
|
||||
if ($service instanceof ServiceEntitySubmitInterface === false) {
|
||||
throw new InvalidArgumentException("Service '{$service->identifier()}' does not support entity submission");
|
||||
}
|
||||
|
||||
if ($source === null && $message === null) {
|
||||
throw new InvalidArgumentException("At least one of source or message must be provided for entity submission");
|
||||
}
|
||||
|
||||
if ($sender instanceof AddressInterface === false) {
|
||||
$sender = new Address($sender);
|
||||
}
|
||||
if ($message !== null && $message instanceof MessagePropertiesMutableInterface === false) {
|
||||
$message = $service->entityFresh()->getProperties()->jsonDeserialize($message);
|
||||
}
|
||||
|
||||
return $service->entitySubmit($sender, $source, $message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user