From 5aa95fa329b7452b551a06b31ba4b4e06fdbd4ae Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Tue, 28 Jul 2026 21:50:37 -0400 Subject: [PATCH] feat: sync sending identities Signed-off-by: Sebastian Krupinski --- lib/Providers/Mail/Provider.php | 59 ++++++++++++++++++++ src/components/JmapAuxiliaryPanel.vue | 80 +++++---------------------- 2 files changed, 74 insertions(+), 65 deletions(-) diff --git a/lib/Providers/Mail/Provider.php b/lib/Providers/Mail/Provider.php index be16ee3..fed46d4 100644 --- a/lib/Providers/Mail/Provider.php +++ b/lib/Providers/Mail/Provider.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace KTXM\ProviderJmapc\Providers\Mail; +use KTXF\Mail\Object\Address; use KTXF\Mail\Provider\ProviderBaseInterface; use KTXF\Mail\Provider\ProviderServiceDiscoverInterface; use KTXF\Mail\Provider\ProviderServiceMutateInterface; @@ -142,9 +143,66 @@ class Provider implements ProviderBaseInterface, ProviderServiceMutateInterface, } $created = $this->serviceStore->create($tenantId, $userId, $service); + $this->serviceIdentitiesSync($tenantId, $userId, (new Service())->fromStore($created)); return (string) $created['sid']; } + /** + * Fetches sending identities from the JMAP server and overwrites + * primaryAddress/secondaryAddresses on the service with them. + */ + private function serviceIdentitiesSync(string $tenantId, string $userId, Service $service): void + { + try { + $client = RemoteService::freshClient($service); + $identities = RemoteService::mailService($client)->identityFetch(); + + // A JMAP server may expose multiple identities sharing the same + // address (e.g. distinct signatures/display names for one + // mailbox) — collapse those down to one entry per address, + // keeping the first label seen. + $addresses = []; + foreach ($identities as $identity) { + if ($identity->address() === null) { + continue; + } + $key = strtolower($identity->address()); + if (!isset($addresses[$key])) { + $addresses[$key] = Address::fromArray(['address' => $identity->address(), 'label' => $identity->name()]); + } + } + $addresses = array_values($addresses); + + if (empty($addresses)) { + return; + } + + $currentAddress = strtolower($service->getPrimaryAddress()->getAddress()); + $primaryIndex = 0; + foreach ($addresses as $index => $address) { + if (strtolower($address->getAddress()) === $currentAddress) { + $primaryIndex = $index; + break; + } + } + + $secondary = []; + foreach ($addresses as $index => $address) { + if ($index !== $primaryIndex) { + $secondary[] = $address; + } + } + + $service->setPrimaryAddress($addresses[$primaryIndex]); + $service->setSecondaryAddresses($secondary); + + $this->serviceStore->modify($tenantId, $userId, $service); + } catch (\Throwable) { + // Server may not support urn:ietf:params:jmap:submission, or have + // no identities configured — fall back to what the user entered. + } + } + public function serviceModify(string $tenantId, string $userId, ResourceServiceMutateInterface $service): string { if (!($service instanceof Service)) { @@ -152,6 +210,7 @@ class Provider implements ProviderBaseInterface, ProviderServiceMutateInterface, } $this->serviceStore->modify($tenantId, $userId, $service); + $this->serviceIdentitiesSync($tenantId, $userId, $service); return (string) $service->identifier(); } diff --git a/src/components/JmapAuxiliaryPanel.vue b/src/components/JmapAuxiliaryPanel.vue index 1f4862b..c670915 100644 --- a/src/components/JmapAuxiliaryPanel.vue +++ b/src/components/JmapAuxiliaryPanel.vue @@ -25,7 +25,7 @@ const settingGroups = [ title: 'Addresses', value: 'addresses' as const, icon: 'mdi-at', - description: 'Configure the primary mailbox identity and any sender aliases exposed by this service.' + description: 'View the primary mailbox identity and sender aliases synced from the JMAP server.' }, { title: 'Messages', @@ -71,7 +71,7 @@ watch( ) watch( - [deleteMode, deleteDestination, primaryAddress, secondaryAddresses], + [deleteMode, deleteDestination], () => { const nextService = props.service ?? new ServiceObject() const nextAuxiliary = { @@ -83,13 +83,9 @@ watch( } if (sameAuxiliary(nextService.auxiliary ?? {}, nextAuxiliary)) { - if (sameAddresses(nextService)) { - return - } + return } - nextService.primaryAddress = primaryAddress.value.empty ? null : primaryAddress.value - nextService.secondaryAddresses = dedupeAddresses(secondaryAddresses.value) nextService.auxiliary = nextAuxiliary emit('update:service', nextService) }, @@ -111,44 +107,10 @@ function normalizeDeleteDestination(value: string): string { return trimmedValue.length > 0 ? trimmedValue : 'Trash' } -function dedupeAddresses(entries: ServiceAddressObject[]): ServiceAddressObject[] { - const populated = entries.filter(entry => !entry.empty) - return populated.filter((entry, index) => - populated.findIndex(candidate => candidate.matches(entry.address)) === index) -} - -function addSecondaryAddress() { - secondaryAddresses.value.push(new ServiceAddressObject()) -} - -function removeSecondaryAddress(index: number) { - secondaryAddresses.value.splice(index, 1) -} - -function validAddress(value: string): boolean | string { - const trimmedValue = value.trim() - return trimmedValue.length === 0 || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmedValue) || 'Invalid email address' -} - function sameAuxiliary(current: Record, next: Record): boolean { return (current.deleteMode === 'hard' ? 'hard' : 'soft') === next.deleteMode && (current.deleteDestination ?? undefined) === (next.deleteDestination ?? undefined) } - -function sameAddresses(service: ServiceObject): boolean { - const nextPrimary = primaryAddress.value.empty ? null : primaryAddress.value - const nextSecondary = dedupeAddresses(secondaryAddresses.value) - return sameAddress(service.primaryAddress, nextPrimary) - && service.secondaryAddresses.length === nextSecondary.length - && service.secondaryAddresses.every((entry, index) => sameAddress(entry, nextSecondary[index])) -} - -function sameAddress(current: ServiceAddressObject | null, next: ServiceAddressObject | null): boolean { - if (current === null || next === null) { - return current === next - } - return current.equals(next) -}