feat: allow custom sub-addressing delimiter character #239
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
isValidTag,
|
||||
getTagValidationError,
|
||||
isSupportedSubAddressDelimiter,
|
||||
isValidSubAddressDelimiter,
|
||||
SUPPORTED_SUB_ADDRESS_DELIMITERS,
|
||||
DEFAULT_SUB_ADDRESS_DELIMITER,
|
||||
MAX_TAG_LENGTH,
|
||||
@@ -427,4 +428,52 @@ describe('custom delimiter', () => {
|
||||
expect(isSupportedSubAddressDelimiter(DEFAULT_SUB_ADDRESS_DELIMITER)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidSubAddressDelimiter', () => {
|
||||
it('accepts every preset delimiter', () => {
|
||||
for (const delim of SUPPORTED_SUB_ADDRESS_DELIMITERS) {
|
||||
expect(isValidSubAddressDelimiter(delim)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts atext special characters as custom delimiters', () => {
|
||||
const customs = ['~', '!', '#', '$', '%', '&', "'", '*', '/', '?', '^', '_', '`', '{', '|', '}'];
|
||||
for (const c of customs) {
|
||||
expect(isValidSubAddressDelimiter(c)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects alphanumeric characters', () => {
|
||||
expect(isValidSubAddressDelimiter('a')).toBe(false);
|
||||
expect(isValidSubAddressDelimiter('Z')).toBe(false);
|
||||
expect(isValidSubAddressDelimiter('0')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects "@", whitespace, and quotes', () => {
|
||||
expect(isValidSubAddressDelimiter('@')).toBe(false);
|
||||
expect(isValidSubAddressDelimiter(' ')).toBe(false);
|
||||
expect(isValidSubAddressDelimiter('\t')).toBe(false);
|
||||
expect(isValidSubAddressDelimiter('"')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects multi-character strings', () => {
|
||||
expect(isValidSubAddressDelimiter('++')).toBe(false);
|
||||
expect(isValidSubAddressDelimiter('abc')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects empty / non-string inputs', () => {
|
||||
expect(isValidSubAddressDelimiter('')).toBe(false);
|
||||
expect(isValidSubAddressDelimiter(null)).toBe(false);
|
||||
expect(isValidSubAddressDelimiter(undefined)).toBe(false);
|
||||
expect(isValidSubAddressDelimiter(1)).toBe(false);
|
||||
});
|
||||
|
||||
it('round-trips through parse/generate with a custom "~" delimiter', () => {
|
||||
const generated = generateSubAddress('user@example.com', 'shopping', '~');
|
||||
expect(generated).toBe('user~shopping@example.com');
|
||||
const parsed = parseSubAddress(generated, '~');
|
||||
expect(parsed.baseUser).toBe('user');
|
||||
expect(parsed.tag).toBe('shopping');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+11
-2
@@ -12,12 +12,21 @@ const TAG_REGEX = /^[a-zA-Z0-9-]{1,30}$/;
|
||||
|
||||
export const DEFAULT_SUB_ADDRESS_DELIMITER = '+';
|
||||
export const SUPPORTED_SUB_ADDRESS_DELIMITERS = ['+', '-', '.', '='] as const;
|
||||
export type SubAddressDelimiter = (typeof SUPPORTED_SUB_ADDRESS_DELIMITERS)[number];
|
||||
export type SubAddressDelimiterPreset = (typeof SUPPORTED_SUB_ADDRESS_DELIMITERS)[number];
|
||||
|
||||
export function isSupportedSubAddressDelimiter(value: string): value is SubAddressDelimiter {
|
||||
export function isSupportedSubAddressDelimiter(value: string): value is SubAddressDelimiterPreset {
|
||||
return (SUPPORTED_SUB_ADDRESS_DELIMITERS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
// RFC 5321 atext "special" characters, minus alphanumerics and "@". A custom
|
||||
// delimiter must be exactly one of these — they're safe to embed in a local
|
||||
// part and unambiguously separate the user from the tag.
|
||||
const VALID_DELIMITER_REGEX = /^[!#$%&'*+\-./=?^_`{|}~]$/;
|
||||
|
||||
export function isValidSubAddressDelimiter(value: unknown): value is string {
|
||||
return typeof value === 'string' && VALID_DELIMITER_REGEX.test(value);
|
||||
}
|
||||
|
||||
export type TagValidationErrorCode =
|
||||
| 'EMPTY'
|
||||
| 'TOO_LONG'
|
||||
|
||||
Reference in New Issue
Block a user