feat: add auto-select reply identity feature with settings and localization
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { findReplyIdentityId } from '../reply-identity';
|
||||
import type { Identity } from '../jmap/types';
|
||||
|
||||
const identities: Identity[] = [
|
||||
{
|
||||
id: 'primary',
|
||||
name: 'Harry Primary',
|
||||
email: 'harry@primary.com',
|
||||
mayDelete: false,
|
||||
},
|
||||
{
|
||||
id: 'secondary',
|
||||
name: 'Harry Secondary',
|
||||
email: 'harry@secondary.com',
|
||||
mayDelete: false,
|
||||
},
|
||||
];
|
||||
|
||||
describe('findReplyIdentityId', () => {
|
||||
it('matches the identity that received the original message', () => {
|
||||
const selected = findReplyIdentityId(identities, {
|
||||
to: [{ email: 'harry@secondary.com' }],
|
||||
});
|
||||
|
||||
expect(selected).toBe('secondary');
|
||||
});
|
||||
|
||||
it('matches case-insensitively across recipients', () => {
|
||||
const selected = findReplyIdentityId(identities, {
|
||||
cc: [{ email: 'HARRY@PRIMARY.COM' }],
|
||||
});
|
||||
|
||||
expect(selected).toBe('primary');
|
||||
});
|
||||
|
||||
it('falls back to sub-address matching when needed', () => {
|
||||
const selected = findReplyIdentityId(identities, {
|
||||
to: [{ email: 'harry+news@secondary.com' }],
|
||||
});
|
||||
|
||||
expect(selected).toBe('secondary');
|
||||
});
|
||||
|
||||
it('returns null when no reply recipient matches an identity', () => {
|
||||
const selected = findReplyIdentityId(identities, {
|
||||
to: [{ email: 'other@example.com' }],
|
||||
});
|
||||
|
||||
expect(selected).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { Identity } from '@/lib/jmap/types';
|
||||
|
||||
interface ReplyRecipient {
|
||||
email?: string | null;
|
||||
}
|
||||
|
||||
interface ReplyRecipients {
|
||||
to?: ReplyRecipient[];
|
||||
cc?: ReplyRecipient[];
|
||||
bcc?: ReplyRecipient[];
|
||||
}
|
||||
|
||||
function normalizeEmailAddress(email: string): string {
|
||||
return email.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeBaseEmailAddress(email: string): string {
|
||||
const normalized = normalizeEmailAddress(email);
|
||||
const atIndex = normalized.indexOf('@');
|
||||
|
||||
if (atIndex <= 0) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const localPart = normalized.slice(0, atIndex);
|
||||
const domain = normalized.slice(atIndex + 1);
|
||||
const plusIndex = localPart.indexOf('+');
|
||||
|
||||
return `${plusIndex >= 0 ? localPart.slice(0, plusIndex) : localPart}@${domain}`;
|
||||
}
|
||||
|
||||
export function findReplyIdentityId(
|
||||
identities: Identity[],
|
||||
recipients?: ReplyRecipients,
|
||||
): string | null {
|
||||
if (identities.length === 0 || !recipients) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const receivedAddresses = [
|
||||
...(recipients.to || []),
|
||||
...(recipients.cc || []),
|
||||
...(recipients.bcc || []),
|
||||
]
|
||||
.map((recipient) => recipient.email?.trim())
|
||||
.filter((email): email is string => Boolean(email));
|
||||
|
||||
if (receivedAddresses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const exactMatches = new Set(receivedAddresses.map(normalizeEmailAddress));
|
||||
const exactIdentity = identities.find((identity) => exactMatches.has(normalizeEmailAddress(identity.email)));
|
||||
if (exactIdentity) {
|
||||
return exactIdentity.id;
|
||||
}
|
||||
|
||||
const baseMatches = new Set(receivedAddresses.map(normalizeBaseEmailAddress));
|
||||
const baseIdentity = identities.find((identity) => baseMatches.has(normalizeBaseEmailAddress(identity.email)));
|
||||
|
||||
return baseIdentity?.id ?? null;
|
||||
}
|
||||
Reference in New Issue
Block a user