feat: sync sending identities
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -9,6 +9,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace KTXM\ProviderJmapc\Providers\Mail;
|
namespace KTXM\ProviderJmapc\Providers\Mail;
|
||||||
|
|
||||||
|
use KTXF\Mail\Object\Address;
|
||||||
use KTXF\Mail\Provider\ProviderBaseInterface;
|
use KTXF\Mail\Provider\ProviderBaseInterface;
|
||||||
use KTXF\Mail\Provider\ProviderServiceDiscoverInterface;
|
use KTXF\Mail\Provider\ProviderServiceDiscoverInterface;
|
||||||
use KTXF\Mail\Provider\ProviderServiceMutateInterface;
|
use KTXF\Mail\Provider\ProviderServiceMutateInterface;
|
||||||
@@ -142,9 +143,66 @@ class Provider implements ProviderBaseInterface, ProviderServiceMutateInterface,
|
|||||||
}
|
}
|
||||||
|
|
||||||
$created = $this->serviceStore->create($tenantId, $userId, $service);
|
$created = $this->serviceStore->create($tenantId, $userId, $service);
|
||||||
|
$this->serviceIdentitiesSync($tenantId, $userId, (new Service())->fromStore($created));
|
||||||
return (string) $created['sid'];
|
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
|
public function serviceModify(string $tenantId, string $userId, ResourceServiceMutateInterface $service): string
|
||||||
{
|
{
|
||||||
if (!($service instanceof Service)) {
|
if (!($service instanceof Service)) {
|
||||||
@@ -152,6 +210,7 @@ class Provider implements ProviderBaseInterface, ProviderServiceMutateInterface,
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->serviceStore->modify($tenantId, $userId, $service);
|
$this->serviceStore->modify($tenantId, $userId, $service);
|
||||||
|
$this->serviceIdentitiesSync($tenantId, $userId, $service);
|
||||||
return (string) $service->identifier();
|
return (string) $service->identifier();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const settingGroups = [
|
|||||||
title: 'Addresses',
|
title: 'Addresses',
|
||||||
value: 'addresses' as const,
|
value: 'addresses' as const,
|
||||||
icon: 'mdi-at',
|
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',
|
title: 'Messages',
|
||||||
@@ -71,7 +71,7 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[deleteMode, deleteDestination, primaryAddress, secondaryAddresses],
|
[deleteMode, deleteDestination],
|
||||||
() => {
|
() => {
|
||||||
const nextService = props.service ?? new ServiceObject()
|
const nextService = props.service ?? new ServiceObject()
|
||||||
const nextAuxiliary = {
|
const nextAuxiliary = {
|
||||||
@@ -83,13 +83,9 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sameAuxiliary(nextService.auxiliary ?? {}, nextAuxiliary)) {
|
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
|
nextService.auxiliary = nextAuxiliary
|
||||||
emit('update:service', nextService)
|
emit('update:service', nextService)
|
||||||
},
|
},
|
||||||
@@ -111,44 +107,10 @@ function normalizeDeleteDestination(value: string): string {
|
|||||||
return trimmedValue.length > 0 ? trimmedValue : 'Trash'
|
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<string, any>, next: Record<string, any>): boolean {
|
function sameAuxiliary(current: Record<string, any>, next: Record<string, any>): boolean {
|
||||||
return (current.deleteMode === 'hard' ? 'hard' : 'soft') === next.deleteMode
|
return (current.deleteMode === 'hard' ? 'hard' : 'soft') === next.deleteMode
|
||||||
&& (current.deleteDestination ?? undefined) === (next.deleteDestination ?? undefined)
|
&& (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)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -176,69 +138,57 @@ function sameAddress(current: ServiceAddressObject | null, next: ServiceAddressO
|
|||||||
<div class="jmap-settings-card">
|
<div class="jmap-settings-card">
|
||||||
<h3 class="text-h6 mb-2">Addresses</h3>
|
<h3 class="text-h6 mb-2">Addresses</h3>
|
||||||
<p class="text-body-2 text-medium-emphasis mb-6">
|
<p class="text-body-2 text-medium-emphasis mb-6">
|
||||||
Configure the primary mailbox identity and any additional sender aliases exposed by this service.
|
The primary mailbox identity and sender aliases are managed by the JMAP server and synced automatically.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="text-subtitle-2 mb-2">Primary Address</div>
|
<div class="text-subtitle-2 mb-2">Primary Address</div>
|
||||||
<div class="d-flex ga-2 mb-6">
|
<div class="d-flex ga-2 mb-6">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="primaryAddress.label"
|
:model-value="primaryAddress.label"
|
||||||
label="Display Name"
|
label="Display Name"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
density="compact"
|
density="compact"
|
||||||
hide-details="auto"
|
hide-details="auto"
|
||||||
|
readonly
|
||||||
/>
|
/>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="primaryAddress.address"
|
:model-value="primaryAddress.address"
|
||||||
label="Email Address"
|
label="Email Address"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
density="compact"
|
density="compact"
|
||||||
prepend-inner-icon="mdi-email-outline"
|
prepend-inner-icon="mdi-email-outline"
|
||||||
:rules="[validAddress]"
|
|
||||||
hide-details="auto"
|
hide-details="auto"
|
||||||
|
readonly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-subtitle-2 mb-2">Secondary Addresses</div>
|
<div class="text-subtitle-2 mb-2">Secondary Addresses</div>
|
||||||
|
<p v-if="secondaryAddresses.length === 0" class="text-body-2 text-medium-emphasis">
|
||||||
|
No aliases reported by the server.
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
v-for="(entry, index) in secondaryAddresses"
|
v-for="(entry, index) in secondaryAddresses"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="d-flex ga-2 mb-2"
|
class="d-flex ga-2 mb-2"
|
||||||
>
|
>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="entry.label"
|
:model-value="entry.label"
|
||||||
label="Display Name"
|
label="Display Name"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
density="compact"
|
density="compact"
|
||||||
hide-details="auto"
|
hide-details="auto"
|
||||||
|
readonly
|
||||||
/>
|
/>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="entry.address"
|
:model-value="entry.address"
|
||||||
label="Email Address"
|
label="Email Address"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
density="compact"
|
density="compact"
|
||||||
prepend-inner-icon="mdi-email-multiple-outline"
|
prepend-inner-icon="mdi-email-multiple-outline"
|
||||||
:rules="[validAddress]"
|
|
||||||
hide-details="auto"
|
hide-details="auto"
|
||||||
|
readonly
|
||||||
/>
|
/>
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
@click="removeSecondaryAddress(index)"
|
|
||||||
>
|
|
||||||
<v-icon>mdi-delete-outline</v-icon>
|
|
||||||
<v-tooltip activator="parent" location="bottom">Remove Alias</v-tooltip>
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-btn
|
|
||||||
variant="tonal"
|
|
||||||
prepend-icon="mdi-plus"
|
|
||||||
@click="addSecondaryAddress"
|
|
||||||
>
|
|
||||||
Add Alias
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
</div>
|
||||||
</v-window-item>
|
</v-window-item>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user