refactor: message properties

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-06-16 13:36:11 -04:00
parent 60cfefcfee
commit 7df98223f8
7 changed files with 181 additions and 184 deletions
+1 -1
View File
@@ -168,7 +168,7 @@ final class MessagePart
$data = [ $data = [
'partId' => $this->partId, 'partId' => $this->partId,
'blobId' => $this->partId, 'blobId' => $this->partId,
'cId' => $this->contentId, 'cid' => $this->contentId,
'type' => $this->mimeType, 'type' => $this->mimeType,
'charset' => $this->parameters['charset'] ?? null, 'charset' => $this->parameters['charset'] ?? null,
'name' => $this->parameters['name'] ?? $this->dispositionParameters['filename'] ?? null, 'name' => $this->parameters['name'] ?? $this->dispositionParameters['filename'] ?? null,
+11 -1
View File
@@ -121,10 +121,20 @@ class MessageAttachment implements MessagePartInterface {
public function getBlobId(): ?string { return $this->_meta->getBlobId(); } public function getBlobId(): ?string { return $this->_meta->getBlobId(); }
public function getId(): ?string { return $this->_meta->getId(); } public function getId(): ?string { return $this->_meta->getId(); }
public function getSize(): ?int { return $this->_meta->getSize(); }
public function getDisposition(): ?string { return $this->_meta->getDisposition(); } public function getDisposition(): ?string { return $this->_meta->getDisposition(); }
public function getContentId(): ?string { return $this->_meta->getContentId(); }
public function getCharset(): ?string { return $this->_meta->getCharset(); } public function getCharset(): ?string { return $this->_meta->getCharset(); }
public function getLanguage(): ?string { return $this->_meta->getLanguage(); } public function getLanguage(): ?string { return $this->_meta->getLanguage(); }
public function getLocation(): ?string { return $this->_meta->getLocation(); } public function getLocation(): ?string { return $this->_meta->getLocation(); }
public function getContent(): ?string { return $this->_contents; }
public function getParts(): array { return $this->_meta->getParts(); } public function getParts(): array { return $this->_meta->getParts(); }
public function jsonSerialize(): array { return $this->_meta->jsonSerialize(); } public function jsonSerialize(): array {
$data = $this->_meta->jsonSerialize();
if ($this->_contents !== null) {
$data['content'] = $this->_contents;
}
return $data;
}
} }
+22 -75
View File
@@ -17,6 +17,24 @@ use KTXM\ProviderImap\Client\MessagePart as ImapMessagePart;
*/ */
class MessagePart extends MessagePartMutableAbstract { class MessagePart extends MessagePartMutableAbstract {
/**
* @param array<string,mixed> $data
*/
private function hydrateArray(array $data): static {
$this->data = $data;
if (isset($this->data['subParts']) && is_array($this->data['subParts'])) {
foreach ($this->data['subParts'] as $entry) {
if (is_array($entry)) {
$this->parts[] = (new self())->hydrateArray($entry);
}
}
unset($this->data['subParts']);
}
return $this;
}
/** /**
* Convert gricob BodyStructure part to message part object * Convert gricob BodyStructure part to message part object
* *
@@ -24,82 +42,11 @@ class MessagePart extends MessagePartMutableAbstract {
* @param string $partId numeric part identifier (e.g. "1", "1.1", "2") * @param string $partId numeric part identifier (e.g. "1", "1.1", "2")
*/ */
public function fromImap(ImapMessagePart $part, string $partId = '1'): static { public function fromImap(ImapMessagePart $part, string $partId = '1'): static {
$data = $part->toArray();
$data['partId'] = $partId;
$data['blobId'] = $data['blobId'] ?? $partId;
$this->data['partId'] = $partId; return $this->hydrateArray($data);
if ($part instanceof SinglePart) {
$mimeType = strtolower($part->type) . '/' . strtolower($part->subtype);
$this->data['type'] = $mimeType;
if ($part->id !== null) {
$this->data['blobId'] = trim($part->id, '<>');
}
// Content-Type parameters (name, charset, etc.)
if (!empty($part->attributes)) {
foreach ($part->attributes as $key => $value) {
$keyLower = strtolower($key);
if ($keyLower === 'name') {
$this->data['name'] = $value;
} elseif ($keyLower === 'charset') {
$this->data['charset'] = $value;
}
}
}
if ($part->encoding !== null) {
$this->data['encoding'] = strtolower($part->encoding);
}
if ($part->size !== null) {
$this->data['size'] = $part->size;
}
if ($part->disposition !== null) {
$this->data['disposition'] = strtolower($part->disposition->type);
// disposition filename attribute
if (!empty($part->disposition->attributes)) {
foreach ($part->disposition->attributes as $key => $value) {
if (strtolower($key) === 'filename') {
$this->data['name'] = $this->data['name'] ?? $value;
}
}
}
}
if (!empty($part->language)) {
$this->data['language'] = implode(',', $part->language);
}
if ($part->location !== null) {
$this->data['location'] = $part->location;
}
} elseif ($part instanceof MultiPart) {
$this->data['type'] = 'multipart/' . strtolower($part->subtype);
if ($part->disposition !== null) {
$this->data['disposition'] = strtolower($part->disposition->type);
}
if (!empty($part->language)) {
$this->data['language'] = implode(',', $part->language);
}
if ($part->location !== null) {
$this->data['location'] = $part->location;
}
// Recursively process sub-parts
// When this part has no section ID (root multipart) children are
// numbered "1", "2", … to match IMAP section numbering.
foreach ($part->parts as $index => $subPart) {
$subPartId = ($partId === '') ? (string)($index + 1) : $partId . '.' . ($index + 1);
$this->parts[] = (new MessagePart())->fromImap($subPart, $subPartId);
}
}
return $this;
} }
} }
+9 -29
View File
@@ -82,36 +82,16 @@ class MessageProperties extends MessagePropertiesMutableAbstract {
} }
if ($message->bodyStructure() !== null) { if ($message->bodyStructure() !== null) {
$this->data[static::PROPERTY_BODY] = $message->bodyStructure()->toArray(); $body = $message->bodyStructure()->withInjectedSections($message->bodySections() ?? [])->toArray();
$this->data[static::PROPERTY_BODY] = $body;
$attachments = []; $attachments = [];
$this->collectAttachments($message->bodyStructure(), $attachments); $this->collectAttachments($body, $attachments);
if ($attachments !== []) { if ($attachments !== []) {
$this->data[static::PROPERTY_ATTACHMENTS] = $attachments; $this->data[static::PROPERTY_ATTACHMENTS] = $attachments;
} }
} }
if ($message->bodyStructure() !== null) {
$this->data[static::PROPERTY_BODY] = $message->bodyStructure()->toArray();
// Recursively add content from bodyValues to matching parts
if (is_array($message->bodySections())) {
$addContentToParts = function(&$structure, $bodyValues) use (&$addContentToParts) {
// If this part has a partId and matching bodyValue, add content
if (isset($structure['partId']) && isset($bodyValues[$structure['partId']])) {
$structure['content'] = $bodyValues[$structure['partId']] ?? null;
}
// Recursively process subParts
if (isset($structure['subParts']) && is_array($structure['subParts'])) {
foreach ($structure['subParts'] as &$subPart) {
$addContentToParts($subPart, $bodyValues);
}
}
};
$addContentToParts($this->data[static::PROPERTY_BODY], $message->bodySections());
}
}
$this->data[static::PROPERTY_FLAGS] = []; $this->data[static::PROPERTY_FLAGS] = [];
foreach ($message->flags() as $flag) { foreach ($message->flags() as $flag) {
$flag = ltrim($flag, '\\'); $flag = ltrim($flag, '\\');
@@ -132,9 +112,9 @@ class MessageProperties extends MessagePropertiesMutableAbstract {
/** /**
* Recursively collect attachment parts from body structure * Recursively collect attachment parts from body structure
*/ */
private function collectAttachments(ClientMessagePart $part, array &$attachments): void private function collectAttachments(array $part, array &$attachments): void
{ {
$children = $part->parts(); $children = $part['subParts'] ?? [];
if ($children !== []) { if ($children !== []) {
foreach ($children as $childPart) { foreach ($children as $childPart) {
$this->collectAttachments($childPart, $attachments); $this->collectAttachments($childPart, $attachments);
@@ -142,9 +122,9 @@ class MessageProperties extends MessagePropertiesMutableAbstract {
return; return;
} }
$mimeType = strtolower($part->mimeType()); $mimeType = strtolower((string)($part['type'] ?? ''));
$disposition = strtolower($part->disposition() ?? ''); $disposition = strtolower((string)($part['disposition'] ?? ''));
$name = $part->parameters()['name'] ?? $part->dispositionParameters()['filename'] ?? null; $name = $part['name'] ?? null;
$isInlineText = str_starts_with($mimeType, 'text/') $isInlineText = str_starts_with($mimeType, 'text/')
&& in_array($mimeType, ['text/plain', 'text/html'], true) && in_array($mimeType, ['text/plain', 'text/html'], true)
&& $disposition !== 'attachment'; && $disposition !== 'attachment';
@@ -153,7 +133,7 @@ class MessageProperties extends MessagePropertiesMutableAbstract {
return; return;
} }
$attachments[] = $part->toArray(); $attachments[] = $part;
} }
} }
+4 -4
View File
@@ -49,10 +49,10 @@ class Provider implements ProviderBaseInterface, ProviderServiceMutateInterface,
public function jsonSerialize(): array public function jsonSerialize(): array
{ {
return [ return [
self::JSON_PROPERTY_TYPE => self::JSON_TYPE, self::PROPERTY_TYPE => self::JSON_TYPE,
self::JSON_PROPERTY_IDENTIFIER => self::PROVIDER_IDENTIFIER, self::PROPERTY_IDENTIFIER => self::PROVIDER_IDENTIFIER,
self::JSON_PROPERTY_LABEL => self::PROVIDER_LABEL, self::PROPERTY_LABEL => self::PROVIDER_LABEL,
self::JSON_PROPERTY_CAPABILITIES => $this->providerAbilities, self::PROPERTY_CAPABILITIES => $this->providerAbilities,
]; ];
} }
+38 -35
View File
@@ -44,9 +44,6 @@ use KTXF\Resource\Identifier\EntityIdentifierInterface;
use KTXM\ProviderImap\Providers\EntityResource; use KTXM\ProviderImap\Providers\EntityResource;
use KTXM\ProviderImap\Client\Mailbox; use KTXM\ProviderImap\Client\Mailbox;
/**
* IMAP Mail Service
*/
class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceConfigurableInterface, ServiceCollectionMutableInterface, ServiceEntityMutableInterface class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceConfigurableInterface, ServiceCollectionMutableInterface, ServiceEntityMutableInterface
{ {
private const PROVIDER_IDENTIFIER = 'imap'; private const PROVIDER_IDENTIFIER = 'imap';
@@ -56,7 +53,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
private ?string $serviceIdentifier = null; private ?string $serviceIdentifier = null;
private ?string $serviceLabel = null; private ?string $serviceLabel = null;
private bool $serviceEnabled = false; private bool $serviceEnabled = false;
private string $primaryAddress = ''; private array $primaryAddress = [];
private array $secondaryAddresses = []; private array $secondaryAddresses = [];
private ?ServiceLocation $location = null; private ?ServiceLocation $location = null;
private ?ServiceIdentityBasic $identity = null; private ?ServiceIdentityBasic $identity = null;
@@ -109,6 +106,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
self::CAPABILITY_ENTITY_DELETE => true, self::CAPABILITY_ENTITY_DELETE => true,
self::CAPABILITY_ENTITY_MOVE => true, self::CAPABILITY_ENTITY_MOVE => true,
self::CAPABILITY_ENTITY_COPY => false, self::CAPABILITY_ENTITY_COPY => false,
'EntityTransmit' => true,
]; ];
private RemoteMailService $remoteService; private RemoteMailService $remoteService;
@@ -169,17 +167,17 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
public function jsonSerialize(): array public function jsonSerialize(): array
{ {
return array_filter([ return array_filter([
self::JSON_PROPERTY_TYPE => self::JSON_TYPE, self::PROPERTY_TYPE => self::JSON_TYPE,
self::JSON_PROPERTY_PROVIDER => self::PROVIDER_IDENTIFIER, self::PROPERTY_PROVIDER => self::PROVIDER_IDENTIFIER,
self::JSON_PROPERTY_IDENTIFIER => $this->serviceIdentifier, self::PROPERTY_IDENTIFIER => $this->serviceIdentifier,
self::JSON_PROPERTY_LABEL => $this->serviceLabel, self::PROPERTY_LABEL => $this->serviceLabel,
self::JSON_PROPERTY_ENABLED => $this->serviceEnabled, self::PROPERTY_ENABLED => $this->serviceEnabled,
self::JSON_PROPERTY_CAPABILITIES => $this->serviceAbilities, self::PROPERTY_CAPABILITIES => $this->serviceAbilities,
self::JSON_PROPERTY_PRIMARY_ADDRESS => $this->primaryAddress, self::PROPERTY_PRIMARY_ADDRESS => $this->primaryAddress,
self::JSON_PROPERTY_SECONDARY_ADDRESSES => $this->secondaryAddresses, self::PROPERTY_SECONDARY_ADDRESSES => $this->secondaryAddresses,
self::JSON_PROPERTY_LOCATION => $this->location?->jsonSerialize(), self::PROPERTY_LOCATION => $this->location?->jsonSerialize(),
self::JSON_PROPERTY_IDENTITY => $this->identity?->jsonSerialize(), self::PROPERTY_IDENTITY => $this->identity?->jsonSerialize(),
self::JSON_PROPERTY_AUXILIARY => $this->auxiliary, self::PROPERTY_AUXILIARY => $this->auxiliary,
], fn($v) => $v !== null); ], fn($v) => $v !== null);
} }
@@ -189,29 +187,29 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR); $data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
} }
if (isset($data[self::JSON_PROPERTY_ENABLED])) { if (isset($data[self::PROPERTY_ENABLED])) {
$this->setEnabled($data[self::JSON_PROPERTY_ENABLED]); $this->setEnabled($data[self::PROPERTY_ENABLED]);
} }
if (isset($data[self::JSON_PROPERTY_LABEL])) { if (isset($data[self::PROPERTY_LABEL])) {
$this->setLabel($data[self::JSON_PROPERTY_LABEL]); $this->setLabel($data[self::PROPERTY_LABEL]);
} }
if (isset($data[self::JSON_PROPERTY_LOCATION])) { if (isset($data[self::PROPERTY_LOCATION])) {
$this->setLocation($this->freshLocation(null, $data[self::JSON_PROPERTY_LOCATION])); $this->setLocation($this->freshLocation(null, $data[self::PROPERTY_LOCATION]));
} }
if (isset($data[self::JSON_PROPERTY_IDENTITY])) { if (isset($data[self::PROPERTY_IDENTITY])) {
$this->setIdentity($this->freshIdentity(null, $data[self::JSON_PROPERTY_IDENTITY])); $this->setIdentity($this->freshIdentity(null, $data[self::PROPERTY_IDENTITY]));
} }
if (isset($data[self::JSON_PROPERTY_PRIMARY_ADDRESS]) && is_string($data[self::JSON_PROPERTY_PRIMARY_ADDRESS])) { if (isset($data[self::PROPERTY_PRIMARY_ADDRESS]) && is_string($data[self::PROPERTY_PRIMARY_ADDRESS])) {
$this->setPrimaryAddress(new Address($data[self::JSON_PROPERTY_PRIMARY_ADDRESS])); $this->setPrimaryAddress(new Address($data[self::PROPERTY_PRIMARY_ADDRESS]));
} }
if (isset($data[self::JSON_PROPERTY_SECONDARY_ADDRESSES]) && is_array($data[self::JSON_PROPERTY_SECONDARY_ADDRESSES])) { if (isset($data[self::PROPERTY_SECONDARY_ADDRESSES]) && is_array($data[self::PROPERTY_SECONDARY_ADDRESSES])) {
$this->setSecondaryAddresses(array_map( $this->setSecondaryAddresses(array_map(
fn($addr) => new Address(is_array($addr) ? ($addr['address'] ?? $addr) : $addr), fn($addr) => new Address(is_array($addr) ? ($addr['address'] ?? $addr) : $addr),
$data[self::JSON_PROPERTY_SECONDARY_ADDRESSES] $data[self::PROPERTY_SECONDARY_ADDRESSES]
)); ));
} }
if (isset($data[self::JSON_PROPERTY_AUXILIARY]) && is_array($data[self::JSON_PROPERTY_AUXILIARY])) { if (isset($data[self::PROPERTY_AUXILIARY]) && is_array($data[self::PROPERTY_AUXILIARY])) {
$this->setAuxiliary($data[self::JSON_PROPERTY_AUXILIARY]); $this->setAuxiliary($data[self::PROPERTY_AUXILIARY]);
} }
return $this; return $this;
@@ -261,23 +259,28 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
public function getPrimaryAddress(): AddressInterface public function getPrimaryAddress(): AddressInterface
{ {
return new Address($this->primaryAddress); return Address::fromArray($this->primaryAddress);
} }
public function setPrimaryAddress(AddressInterface $value): static public function setPrimaryAddress(AddressInterface $value): static
{ {
$this->primaryAddress = $value->getAddress(); $this->primaryAddress = $value->toArray();
return $this; return $this;
} }
public function getSecondaryAddresses(): array public function getSecondaryAddresses(): array
{ {
return $this->secondaryAddresses; return array_map(
fn($addr) => $addr instanceof AddressInterface ? $addr : Address::fromArray(is_array($addr) ? $addr : ['address' => (string) $addr])
, $this->secondaryAddresses);
} }
public function setSecondaryAddresses(array $addresses): static public function setSecondaryAddresses(array $addresses): static
{ {
$this->secondaryAddresses = $addresses; $this->secondaryAddresses = array_map(
fn($addr) => $addr instanceof AddressInterface ? $addr : Address::fromArray(is_array($addr) ? $addr : ['address' => (string) $addr]),
$addresses
);
return $this; return $this;
} }
@@ -285,7 +288,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
{ {
$address = strtolower(trim($address)); $address = strtolower(trim($address));
if ($this->primaryAddress && strtolower($this->primaryAddress) === $address) { if ($this->primaryAddress && strtolower($this->primaryAddress['address'] ?? '') === $address) {
return true; return true;
} }
foreach ($this->secondaryAddresses as $secondary) { foreach ($this->secondaryAddresses as $secondary) {
@@ -580,7 +583,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
foreach ($identifiers as $collection => $entities) { foreach ($identifiers as $collection => $entities) {
$uids = array_keys($entities); $uids = array_keys($entities);
foreach ($this->remoteService->entityFetch((string) $collection, ...$uids) as $uid => $message) { foreach ($this->remoteService->entityFetch((string) $collection, null, ...$uids) as $uid => $message) {
$resource = $this->entityFresh(); $resource = $this->entityFresh();
$resource->fromImap($message, $collection); $resource->fromImap($message, $collection);
yield $resource->urn() => $resource; yield $resource->urn() => $resource;
+90 -33
View File
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { ServiceObject } from '@KTXM/MailManager/models/service' import { ServiceObject } from '@KTXM/MailManager/models/service'
import { ServiceAddressObject } from '@KTXM/MailManager/models/address'
type AuxiliaryTab = 'addresses' | 'messages' | 'sync' type AuxiliaryTab = 'addresses' | 'messages' | 'sync'
type DeleteMode = 'soft' | 'hard' type DeleteMode = 'soft' | 'hard'
@@ -16,8 +17,8 @@ const emit = defineEmits<{
const activeTab = ref<AuxiliaryTab>('addresses') const activeTab = ref<AuxiliaryTab>('addresses')
const deleteMode = ref<DeleteMode>('soft') const deleteMode = ref<DeleteMode>('soft')
const deleteDestination = ref('Trash') const deleteDestination = ref('Trash')
const primaryAddress = ref('') const primaryAddress = ref<ServiceAddressObject>(new ServiceAddressObject())
const secondaryAddresses = ref('') const secondaryAddresses = ref<ServiceAddressObject[]>([])
const settingGroups = [ const settingGroups = [
{ {
@@ -61,10 +62,6 @@ const destinationHint = computed(() => {
return 'Mailbox identifier or well-known role target, for example Trash.' return 'Mailbox identifier or well-known role target, for example Trash.'
}) })
const secondaryAddressesHint = computed(() => {
return 'Use one address per line. Commas are also accepted.'
})
watch( watch(
() => props.service, () => props.service,
service => { service => {
@@ -86,17 +83,17 @@ watch(
} }
if (sameAuxiliary(nextService.auxiliary ?? {}, nextAuxiliary)) { if (sameAuxiliary(nextService.auxiliary ?? {}, nextAuxiliary)) {
if (sameAddresses(nextService, primaryAddress.value, secondaryAddresses.value)) { if (sameAddresses(nextService)) {
return return
} }
} }
nextService.primaryAddress = normalizePrimaryAddress(primaryAddress.value) nextService.primaryAddress = primaryAddress.value.empty ? null : primaryAddress.value
nextService.secondaryAddresses = normalizeSecondaryAddresses(secondaryAddresses.value) nextService.secondaryAddresses = dedupeAddresses(secondaryAddresses.value)
nextService.auxiliary = nextAuxiliary nextService.auxiliary = nextAuxiliary
emit('update:service', nextService) emit('update:service', nextService)
}, },
{ immediate: true } { deep: true, immediate: true }
) )
function syncFromService(service?: ServiceObject) { function syncFromService(service?: ServiceObject) {
@@ -105,8 +102,8 @@ function syncFromService(service?: ServiceObject) {
deleteDestination.value = typeof auxiliary.deleteDestination === 'string' && auxiliary.deleteDestination.length > 0 deleteDestination.value = typeof auxiliary.deleteDestination === 'string' && auxiliary.deleteDestination.length > 0
? auxiliary.deleteDestination ? auxiliary.deleteDestination
: 'Trash' : 'Trash'
primaryAddress.value = service?.primaryAddress ?? '' primaryAddress.value = service?.primaryAddress ?? new ServiceAddressObject()
secondaryAddresses.value = (service?.secondaryAddresses ?? []).join('\n') secondaryAddresses.value = service?.secondaryAddresses ?? []
} }
function normalizeDeleteDestination(value: string): string { function normalizeDeleteDestination(value: string): string {
@@ -114,16 +111,23 @@ function normalizeDeleteDestination(value: string): string {
return trimmedValue.length > 0 ? trimmedValue : 'Trash' return trimmedValue.length > 0 ? trimmedValue : 'Trash'
} }
function normalizePrimaryAddress(value: string): string | null { function dedupeAddresses(entries: ServiceAddressObject[]): ServiceAddressObject[] {
const trimmedValue = value.trim() const populated = entries.filter(entry => !entry.empty)
return trimmedValue.length > 0 ? trimmedValue : null return populated.filter((entry, index) =>
populated.findIndex(candidate => candidate.matches(entry.address)) === index)
} }
function normalizeSecondaryAddresses(value: string): string[] { function addSecondaryAddress() {
return value secondaryAddresses.value.push(new ServiceAddressObject())
.split(/\r?\n|,/) }
.map(entry => entry.trim())
.filter((entry, index, entries) => entry.length > 0 && entries.indexOf(entry) === index) 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 {
@@ -131,9 +135,19 @@ function sameAuxiliary(current: Record<string, any>, next: Record<string, any>):
&& (current.deleteDestination ?? undefined) === (next.deleteDestination ?? undefined) && (current.deleteDestination ?? undefined) === (next.deleteDestination ?? undefined)
} }
function sameAddresses(service: ServiceObject, nextPrimaryAddress: string, nextSecondaryAddresses: string): boolean { function sameAddresses(service: ServiceObject): boolean {
return (service.primaryAddress ?? null) === normalizePrimaryAddress(nextPrimaryAddress) const nextPrimary = primaryAddress.value.empty ? null : primaryAddress.value
&& JSON.stringify(service.secondaryAddresses) === JSON.stringify(normalizeSecondaryAddresses(nextSecondaryAddresses)) 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>
@@ -165,23 +179,66 @@ function sameAddresses(service: ServiceObject, nextPrimaryAddress: string, nextS
Configure the primary mailbox identity and any additional sender aliases exposed by this service. Configure the primary mailbox identity and any additional sender aliases exposed by this service.
</p> </p>
<div class="text-subtitle-2 mb-2">Primary Address</div>
<div class="d-flex ga-2 mb-6">
<v-text-field <v-text-field
v-model="primaryAddress" v-model="primaryAddress.label"
label="Primary Address" label="Display Name"
variant="outlined" variant="outlined"
density="compact"
hide-details="auto"
/>
<v-text-field
v-model="primaryAddress.address"
label="Email Address"
variant="outlined"
density="compact"
prepend-inner-icon="mdi-email-outline" prepend-inner-icon="mdi-email-outline"
class="mb-4" :rules="[validAddress]"
hide-details="auto"
/> />
</div>
<v-textarea <div class="text-subtitle-2 mb-2">Secondary Addresses</div>
v-model="secondaryAddresses" <div
label="Secondary Addresses" v-for="(entry, index) in secondaryAddresses"
:key="index"
class="d-flex ga-2 mb-2"
>
<v-text-field
v-model="entry.label"
label="Display Name"
variant="outlined" variant="outlined"
prepend-inner-icon="mdi-email-multiple-outline" density="compact"
rows="4" hide-details="auto"
:hint="secondaryAddressesHint"
persistent-hint
/> />
<v-text-field
v-model="entry.address"
label="Email Address"
variant="outlined"
density="compact"
prepend-inner-icon="mdi-email-multiple-outline"
:rules="[validAddress]"
hide-details="auto"
/>
<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>
<v-btn
variant="tonal"
prepend-icon="mdi-plus"
@click="addSecondaryAddress"
>
Add Alias
</v-btn>
</div> </div>
</v-window-item> </v-window-item>