feat: configurable sub-addressing delimiter #239
This commit is contained in:
@@ -124,6 +124,7 @@ export function EmailComposer({
|
||||
const tCommon = useTranslations('common');
|
||||
const timeFormat = useSettingsStore((state) => state.timeFormat);
|
||||
const plainTextMode = useSettingsStore((state) => state.plainTextMode);
|
||||
const subAddressDelimiter = useSettingsStore((state) => state.subAddressDelimiter);
|
||||
const autoSelectReplyIdentity = useSettingsStore((state) => state.autoSelectReplyIdentity);
|
||||
const attachmentReminderEnabled = useSettingsStore((state) => state.attachmentReminderEnabled);
|
||||
const attachmentReminderKeywords = useSettingsStore((state) => state.attachmentReminderKeywords);
|
||||
@@ -729,7 +730,7 @@ export function EmailComposer({
|
||||
// Generate sub-addressed email if tag is set
|
||||
const fromEmail = currentIdentity?.email
|
||||
? subAddressTag
|
||||
? generateSubAddress(currentIdentity.email, subAddressTag)
|
||||
? generateSubAddress(currentIdentity.email, subAddressTag, subAddressDelimiter)
|
||||
: currentIdentity.email
|
||||
: undefined;
|
||||
|
||||
@@ -906,7 +907,7 @@ export function EmailComposer({
|
||||
|
||||
const fromEmail = currentIdentity?.email
|
||||
? subAddressTag
|
||||
? generateSubAddress(currentIdentity.email, subAddressTag)
|
||||
? generateSubAddress(currentIdentity.email, subAddressTag, subAddressDelimiter)
|
||||
: currentIdentity.email
|
||||
: undefined;
|
||||
|
||||
@@ -1215,7 +1216,7 @@ export function EmailComposer({
|
||||
>
|
||||
{identities.map((identity) => {
|
||||
const displayEmail = subAddressTag
|
||||
? generateSubAddress(identity.email, subAddressTag)
|
||||
? generateSubAddress(identity.email, subAddressTag, subAddressDelimiter)
|
||||
: identity.email;
|
||||
return (
|
||||
<option key={identity.id} value={identity.id}>
|
||||
@@ -1228,7 +1229,7 @@ export function EmailComposer({
|
||||
<span className="text-sm text-foreground flex-1 truncate">
|
||||
{subAddressTag ? (
|
||||
<span className="font-mono">
|
||||
{generateSubAddress(primaryIdentity?.email || '', subAddressTag)}
|
||||
{generateSubAddress(primaryIdentity?.email || '', subAddressTag, subAddressDelimiter)}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Mail, Tag } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { Email, Identity } from '@/lib/jmap/types';
|
||||
import { parseSubAddress } from '@/lib/sub-addressing';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
|
||||
interface EmailIdentityBadgeProps {
|
||||
email: Email;
|
||||
@@ -20,12 +21,13 @@ export function EmailIdentityBadge({
|
||||
className,
|
||||
}: EmailIdentityBadgeProps) {
|
||||
const t = useTranslations('identities.badge');
|
||||
const subAddressDelimiter = useSettingsStore((state) => state.subAddressDelimiter);
|
||||
|
||||
const fromAddress = email.from?.[0]?.email;
|
||||
if (!fromAddress) return null;
|
||||
|
||||
// Parse the from address to check for sub-addressing
|
||||
const parsedFrom = parseSubAddress(fromAddress);
|
||||
const parsedFrom = parseSubAddress(fromAddress, subAddressDelimiter);
|
||||
|
||||
// Find matching identity (email sent BY the user)
|
||||
const matchingIdentity = identities.find(
|
||||
@@ -37,7 +39,7 @@ export function EmailIdentityBadge({
|
||||
if (!matchingIdentity) {
|
||||
// Check all TO addresses for sub-address tags matching user's identities
|
||||
for (const recipient of email.to || []) {
|
||||
const parsedTo = parseSubAddress(recipient.email);
|
||||
const parsedTo = parseSubAddress(recipient.email, subAddressDelimiter);
|
||||
if (parsedTo.tag) {
|
||||
// Check if this base email matches any of the user's identities
|
||||
const matchingToIdentity = identities.find(
|
||||
@@ -70,7 +72,7 @@ export function EmailIdentityBadge({
|
||||
title={t('sub_address_tag', { tag: displayTag })}
|
||||
>
|
||||
<Tag className="w-3 h-3" />
|
||||
<span className="font-mono">+{displayTag}</span>
|
||||
<span className="font-mono">{subAddressDelimiter}{displayTag}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -114,7 +116,7 @@ export function EmailIdentityBadge({
|
||||
aria-label={t('sub_address_tag', { tag: displayTag })}
|
||||
>
|
||||
<Tag className="w-3 h-3" />
|
||||
<span className="font-mono">{t('subaddress_tag', { tag: displayTag })}</span>
|
||||
<span className="font-mono">{subAddressDelimiter}{displayTag}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useIdentityStore } from '@/stores/identity-store';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import {
|
||||
generateSubAddress,
|
||||
extractDomain,
|
||||
@@ -35,6 +36,7 @@ export function SubAddressHelper({
|
||||
const popoverRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { subAddress, addRecentTag, addTagSuggestion } = useIdentityStore();
|
||||
const subAddressDelimiter = useSettingsStore((state) => state.subAddressDelimiter);
|
||||
|
||||
// Get suggestions based on recipient (memoized for performance)
|
||||
const suggestions = useMemo(() => {
|
||||
@@ -47,7 +49,7 @@ export function SubAddressHelper({
|
||||
}, [recipientEmails]);
|
||||
|
||||
// Generate preview
|
||||
const preview = tag ? generateSubAddress(baseEmail, tag) : baseEmail;
|
||||
const preview = tag ? generateSubAddress(baseEmail, tag, subAddressDelimiter) : baseEmail;
|
||||
|
||||
// Close popover when clicking outside
|
||||
useEffect(() => {
|
||||
@@ -226,7 +228,7 @@ export function SubAddressHelper({
|
||||
|
||||
{/* Help Text */}
|
||||
<div className="mb-3 text-xs text-muted-foreground">
|
||||
{t('help_text')}
|
||||
{t('help_text', { delimiter: subAddressDelimiter })}
|
||||
</div>
|
||||
|
||||
{/* Use Address Button */}
|
||||
|
||||
@@ -4,8 +4,12 @@ import { useState, useCallback } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useConfig } from '@/hooks/use-config';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import { SettingsSection, SettingItem, ToggleSwitch } from './settings-section';
|
||||
import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-section';
|
||||
import { Mail, X } from 'lucide-react';
|
||||
import {
|
||||
SUPPORTED_SUB_ADDRESS_DELIMITERS,
|
||||
type SubAddressDelimiter,
|
||||
} from '@/lib/sub-addressing';
|
||||
|
||||
export function ComposingSettings() {
|
||||
const t = useTranslations('settings.email_behavior');
|
||||
@@ -17,6 +21,7 @@ export function ComposingSettings() {
|
||||
autoSelectReplyIdentity,
|
||||
attachmentReminderEnabled,
|
||||
attachmentReminderKeywords,
|
||||
subAddressDelimiter,
|
||||
updateSetting,
|
||||
} = useSettingsStore();
|
||||
|
||||
@@ -40,6 +45,20 @@ export function ComposingSettings() {
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t('sub_address_delimiter.label')}
|
||||
description={t('sub_address_delimiter.description', { delimiter: subAddressDelimiter })}
|
||||
>
|
||||
<Select
|
||||
value={subAddressDelimiter}
|
||||
onChange={(value) => updateSetting('subAddressDelimiter', value as SubAddressDelimiter)}
|
||||
options={SUPPORTED_SUB_ADDRESS_DELIMITERS.map((delim) => ({
|
||||
value: delim,
|
||||
label: t('sub_address_delimiter.option', { delimiter: delim }),
|
||||
}))}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('attachment_reminder.label')} description={t('attachment_reminder.description')}>
|
||||
<ToggleSwitch
|
||||
checked={attachmentReminderEnabled}
|
||||
|
||||
@@ -6,6 +6,9 @@ import {
|
||||
suggestTagsForDomain,
|
||||
isValidTag,
|
||||
getTagValidationError,
|
||||
isSupportedSubAddressDelimiter,
|
||||
SUPPORTED_SUB_ADDRESS_DELIMITERS,
|
||||
DEFAULT_SUB_ADDRESS_DELIMITER,
|
||||
MAX_TAG_LENGTH,
|
||||
} from '../sub-addressing';
|
||||
|
||||
@@ -354,3 +357,74 @@ describe('getTagValidationError', () => {
|
||||
expect(getTagValidationError('日本語')).toBe('INVALID_CHARS');
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom delimiter', () => {
|
||||
describe('parseSubAddress with non-default delimiter', () => {
|
||||
it('should parse with "-" delimiter', () => {
|
||||
const result = parseSubAddress('user-shopping@example.com', '-');
|
||||
expect(result.baseUser).toBe('user');
|
||||
expect(result.tag).toBe('shopping');
|
||||
});
|
||||
|
||||
it('should parse with "." delimiter', () => {
|
||||
const result = parseSubAddress('user.shopping@example.com', '.');
|
||||
expect(result.baseUser).toBe('user');
|
||||
expect(result.tag).toBe('shopping');
|
||||
});
|
||||
|
||||
it('should parse with "=" delimiter', () => {
|
||||
const result = parseSubAddress('user=shopping@example.com', '=');
|
||||
expect(result.baseUser).toBe('user');
|
||||
expect(result.tag).toBe('shopping');
|
||||
});
|
||||
|
||||
it('should ignore "+" when "-" is configured as the delimiter', () => {
|
||||
const result = parseSubAddress('user+shopping@example.com', '-');
|
||||
expect(result.baseUser).toBe('user+shopping');
|
||||
expect(result.tag).toBeNull();
|
||||
});
|
||||
|
||||
it('should split on first occurrence when delimiter appears multiple times', () => {
|
||||
const result = parseSubAddress('alice-shop-orders@example.com', '-');
|
||||
expect(result.baseUser).toBe('alice');
|
||||
expect(result.tag).toBe('shop-orders');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateSubAddress with non-default delimiter', () => {
|
||||
it('should generate using "-" delimiter', () => {
|
||||
expect(generateSubAddress('user@example.com', 'shopping', '-')).toBe('user-shopping@example.com');
|
||||
});
|
||||
|
||||
it('should generate using "." delimiter', () => {
|
||||
expect(generateSubAddress('user@example.com', 'shopping', '.')).toBe('user.shopping@example.com');
|
||||
});
|
||||
|
||||
it('should replace existing tag using the configured delimiter', () => {
|
||||
expect(generateSubAddress('user-old@example.com', 'new', '-')).toBe('user-new@example.com');
|
||||
});
|
||||
|
||||
it('should not strip a "+" sign in the local part when delimiter is "-"', () => {
|
||||
// "+" is not the delimiter so it should remain part of the base user
|
||||
expect(generateSubAddress('user+plus@example.com', 'tag', '-')).toBe('user+plus-tag@example.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSupportedSubAddressDelimiter', () => {
|
||||
it('accepts every supported delimiter', () => {
|
||||
for (const delim of SUPPORTED_SUB_ADDRESS_DELIMITERS) {
|
||||
expect(isSupportedSubAddressDelimiter(delim)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects unsupported characters', () => {
|
||||
expect(isSupportedSubAddressDelimiter('_')).toBe(false);
|
||||
expect(isSupportedSubAddressDelimiter('++')).toBe(false);
|
||||
expect(isSupportedSubAddressDelimiter('')).toBe(false);
|
||||
});
|
||||
|
||||
it('default delimiter is supported', () => {
|
||||
expect(isSupportedSubAddressDelimiter(DEFAULT_SUB_ADDRESS_DELIMITER)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+36
-14
@@ -1,12 +1,23 @@
|
||||
/**
|
||||
* Sub-addressing utilities for user+tag@domain.com format
|
||||
* Sub-addressing utilities for user{delimiter}tag@domain.com format
|
||||
* Works server-side automatically - no JMAP API calls needed
|
||||
*
|
||||
* The delimiter character is configurable per server (RFC 5233). Common
|
||||
* choices: "+" (Postfix, Stalwart default), "-" (qmail), ".", "=".
|
||||
*/
|
||||
|
||||
// Constants for tag validation
|
||||
const MAX_TAG_LENGTH = 30;
|
||||
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 function isSupportedSubAddressDelimiter(value: string): value is SubAddressDelimiter {
|
||||
return (SUPPORTED_SUB_ADDRESS_DELIMITERS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
export type TagValidationErrorCode =
|
||||
| 'EMPTY'
|
||||
| 'TOO_LONG'
|
||||
@@ -22,10 +33,14 @@ export interface ParsedAddress {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an email address to extract sub-address tag
|
||||
* Example: "user+shopping@example.com" -> { baseUser: "user", tag: "shopping" }
|
||||
* Parse an email address to extract sub-address tag.
|
||||
* The first occurrence of the delimiter in the local part separates the
|
||||
* base user from the tag, matching the behavior of Postfix/qmail/Sieve.
|
||||
*/
|
||||
export function parseSubAddress(email: string): ParsedAddress {
|
||||
export function parseSubAddress(
|
||||
email: string,
|
||||
delimiter: string = DEFAULT_SUB_ADDRESS_DELIMITER,
|
||||
): ParsedAddress {
|
||||
const [localPart, domain] = email.split('@');
|
||||
|
||||
if (!localPart || !domain) {
|
||||
@@ -38,9 +53,9 @@ export function parseSubAddress(email: string): ParsedAddress {
|
||||
};
|
||||
}
|
||||
|
||||
const plusIndex = localPart.indexOf('+');
|
||||
const delimiterIndex = localPart.indexOf(delimiter);
|
||||
|
||||
if (plusIndex === -1) {
|
||||
if (delimiterIndex === -1) {
|
||||
return {
|
||||
localPart,
|
||||
baseUser: localPart,
|
||||
@@ -50,8 +65,8 @@ export function parseSubAddress(email: string): ParsedAddress {
|
||||
};
|
||||
}
|
||||
|
||||
const baseUser = localPart.substring(0, plusIndex);
|
||||
const tag = localPart.substring(plusIndex + 1);
|
||||
const baseUser = localPart.substring(0, delimiterIndex);
|
||||
const tag = localPart.substring(delimiterIndex + delimiter.length);
|
||||
|
||||
return {
|
||||
localPart,
|
||||
@@ -63,18 +78,25 @@ export function parseSubAddress(email: string): ParsedAddress {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a sub-addressed email
|
||||
* Example: generateSubAddress("user@example.com", "shopping") -> "user+shopping@example.com"
|
||||
* Generate a sub-addressed email.
|
||||
* Example: generateSubAddress("user@example.com", "shopping", "+") -> "user+shopping@example.com"
|
||||
*/
|
||||
export function generateSubAddress(baseEmail: string, tag: string): string {
|
||||
export function generateSubAddress(
|
||||
baseEmail: string,
|
||||
tag: string,
|
||||
delimiter: string = DEFAULT_SUB_ADDRESS_DELIMITER,
|
||||
): string {
|
||||
const [localPart, domain] = baseEmail.split('@');
|
||||
|
||||
if (!localPart || !domain || !tag) {
|
||||
return baseEmail;
|
||||
}
|
||||
|
||||
// Remove existing tag if present
|
||||
const cleanLocal = localPart.split('+')[0];
|
||||
// Strip an existing tag if one is already present
|
||||
const existingDelimiterIndex = localPart.indexOf(delimiter);
|
||||
const cleanLocal = existingDelimiterIndex === -1
|
||||
? localPart
|
||||
: localPart.substring(0, existingDelimiterIndex);
|
||||
|
||||
// Sanitize tag (alphanumeric and dash only)
|
||||
const cleanTag = tag.replace(/[^a-zA-Z0-9-]/g, '').toLowerCase();
|
||||
@@ -83,7 +105,7 @@ export function generateSubAddress(baseEmail: string, tag: string): string {
|
||||
return baseEmail;
|
||||
}
|
||||
|
||||
return `${cleanLocal}+${cleanTag}@${domain}`;
|
||||
return `${cleanLocal}${delimiter}${cleanTag}@${domain}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -911,6 +911,11 @@
|
||||
"label": "Automaticky vybírat adresu pro odpověď",
|
||||
"description": "Při odpovídání automaticky přepnout adresu odesílatele na identitu, která původně obdržela zprávu"
|
||||
},
|
||||
"sub_address_delimiter": {
|
||||
"label": "Oddělovač sub-adresy",
|
||||
"description": "Znak oddělující uživatelské jméno od sub-adresy. Zvolte oddělovač používaný vaším poštovním serverem (např. uzivatel{delimiter}stitek@domena.cz).",
|
||||
"option": "{delimiter} (uzivatel{delimiter}stitek@domena.cz)"
|
||||
},
|
||||
"attachment_click_action": {
|
||||
"label": "Akce po kliknutí na přílohu",
|
||||
"description": "Vyberte, zda má kliknutí na přílohu zobrazit náhled, nebo ji ihned stáhnout",
|
||||
@@ -1756,7 +1761,7 @@
|
||||
"use_address": "Použít tuto adresu",
|
||||
"invalid_tag": "Štítek může obsahovat pouze písmena, číslice a pomlčky",
|
||||
"tag_too_long": "Štítek může mít maximálně 30 znaků",
|
||||
"help_text": "Zprávy odeslané na adresu uzivatel+stitek@domena.cz budou doručeny do vaší doručené pošty",
|
||||
"help_text": "Zprávy odeslané na adresu uzivatel{delimiter}stitek@domena.cz budou doručeny do vaší doručené pošty",
|
||||
"validation": {
|
||||
"empty": "Štítek nesmí být prázdný",
|
||||
"too_long": "Štítek může mít maximálně {max} znaků",
|
||||
|
||||
@@ -911,6 +911,11 @@
|
||||
"label": "Antwortadresse automatisch wählen",
|
||||
"description": "Beim Antworten die Absenderadresse automatisch auf die Identität umstellen, die die ursprüngliche Nachricht erhalten hat"
|
||||
},
|
||||
"sub_address_delimiter": {
|
||||
"label": "Sub-Adress-Trennzeichen",
|
||||
"description": "Zeichen, das Ihren Benutzernamen vom Sub-Adress-Tag trennt. Verwenden Sie das von Ihrem Mailserver verwendete Trennzeichen (z. B. benutzer{delimiter}tag@domain.de).",
|
||||
"option": "{delimiter} (benutzer{delimiter}tag@domain.de)"
|
||||
},
|
||||
"attachment_click_action": {
|
||||
"label": "Aktion beim Klick auf Anhänge",
|
||||
"description": "Festlegen, ob ein Dateianhang beim Anklicken in der Vorschau geöffnet oder sofort heruntergeladen wird",
|
||||
@@ -1756,7 +1761,7 @@
|
||||
"use_address": "Diese Adresse verwenden",
|
||||
"invalid_tag": "Tag darf nur alphanumerisch und Bindestriche enthalten",
|
||||
"tag_too_long": "Tag darf maximal 30 Zeichen lang sein",
|
||||
"help_text": "E-Mails an benutzer+tag@domain.de werden in Ihrem Posteingang ankommen",
|
||||
"help_text": "E-Mails an benutzer{delimiter}tag@domain.de werden in Ihrem Posteingang ankommen",
|
||||
"validation": {
|
||||
"empty": "Tag darf nicht leer sein",
|
||||
"too_long": "Tag darf maximal {max} Zeichen lang sein",
|
||||
|
||||
@@ -911,6 +911,11 @@
|
||||
"label": "Auto-select Reply Address",
|
||||
"description": "When replying, automatically switch the From address to the identity that originally received the message"
|
||||
},
|
||||
"sub_address_delimiter": {
|
||||
"label": "Sub-Address Delimiter",
|
||||
"description": "Character separating your username from a sub-address tag. Match the delimiter your mail server uses (e.g. user{delimiter}tag@domain.com).",
|
||||
"option": "{delimiter} (user{delimiter}tag@domain.com)"
|
||||
},
|
||||
"attachment_click_action": {
|
||||
"label": "Attachment Click Action",
|
||||
"description": "Choose whether clicking a file attachment previews it or downloads it immediately",
|
||||
@@ -1756,7 +1761,7 @@
|
||||
"use_address": "Use This Address",
|
||||
"invalid_tag": "Tag must be alphanumeric and dashes only",
|
||||
"tag_too_long": "Tag must be 30 characters or less",
|
||||
"help_text": "Emails sent to user+tag@domain.com will arrive in your inbox",
|
||||
"help_text": "Emails sent to user{delimiter}tag@domain.com will arrive in your inbox",
|
||||
"validation": {
|
||||
"empty": "Tag cannot be empty",
|
||||
"too_long": "Tag must be {max} characters or less",
|
||||
|
||||
@@ -906,6 +906,11 @@
|
||||
"label": "Seleccionar dirección de respuesta automáticamente",
|
||||
"description": "Al responder, cambia automáticamente la dirección del remitente a la identidad que recibió el mensaje original"
|
||||
},
|
||||
"sub_address_delimiter": {
|
||||
"label": "Delimitador de sub-dirección",
|
||||
"description": "Carácter que separa tu nombre de usuario de la etiqueta de sub-dirección. Usa el delimitador que utilice tu servidor de correo (por ejemplo, usuario{delimiter}etiqueta@dominio.com).",
|
||||
"option": "{delimiter} (usuario{delimiter}etiqueta@dominio.com)"
|
||||
},
|
||||
"show_preview": {
|
||||
"label": "Mostrar Vista Previa",
|
||||
"description": "Mostrar vista previa del correo en la lista",
|
||||
@@ -1756,7 +1761,7 @@
|
||||
"use_address": "Usar Esta Dirección",
|
||||
"invalid_tag": "La etiqueta debe ser solo alfanumérica y guiones",
|
||||
"tag_too_long": "La etiqueta debe tener 30 caracteres o menos",
|
||||
"help_text": "Los correos enviados a usuario+etiqueta@dominio.com llegarán a su bandeja de entrada",
|
||||
"help_text": "Los correos enviados a usuario{delimiter}etiqueta@dominio.com llegarán a su bandeja de entrada",
|
||||
"validation": {
|
||||
"empty": "La etiqueta no puede estar vacía",
|
||||
"too_long": "La etiqueta debe tener {max} caracteres o menos",
|
||||
|
||||
@@ -906,6 +906,11 @@
|
||||
"label": "Sélection automatique de l'adresse de réponse",
|
||||
"description": "Lors d'une réponse, bascule automatiquement l'adresse d'expédition vers l'identité qui a reçu le message d'origine"
|
||||
},
|
||||
"sub_address_delimiter": {
|
||||
"label": "Délimiteur de sous-adresse",
|
||||
"description": "Caractère séparant votre nom d'utilisateur de l'étiquette de sous-adresse. Utilisez le délimiteur configuré sur votre serveur de messagerie (par ex. utilisateur{delimiter}tag@domaine.com).",
|
||||
"option": "{delimiter} (utilisateur{delimiter}tag@domaine.com)"
|
||||
},
|
||||
"show_preview": {
|
||||
"label": "Afficher l'aperçu",
|
||||
"description": "Afficher l'aperçu de l'email dans la liste",
|
||||
@@ -1756,7 +1761,7 @@
|
||||
"use_address": "Utiliser cette adresse",
|
||||
"invalid_tag": "Le tag doit contenir uniquement des lettres, chiffres et tirets",
|
||||
"tag_too_long": "Le tag doit faire 30 caractères ou moins",
|
||||
"help_text": "Les emails envoyés à utilisateur+tag@domaine.com arriveront dans votre boîte de réception",
|
||||
"help_text": "Les emails envoyés à utilisateur{delimiter}tag@domaine.com arriveront dans votre boîte de réception",
|
||||
"validation": {
|
||||
"empty": "Le tag ne peut pas être vide",
|
||||
"too_long": "Le tag doit faire {max} caractères ou moins",
|
||||
|
||||
@@ -906,6 +906,11 @@
|
||||
"label": "Seleziona automaticamente l'indirizzo di risposta",
|
||||
"description": "Quando rispondi, passa automaticamente l'indirizzo mittente all'identità che ha ricevuto il messaggio originale"
|
||||
},
|
||||
"sub_address_delimiter": {
|
||||
"label": "Delimitatore sub-indirizzo",
|
||||
"description": "Carattere che separa il tuo nome utente dall'etichetta del sub-indirizzo. Usa il delimitatore configurato sul tuo server di posta (es. utente{delimiter}tag@dominio.com).",
|
||||
"option": "{delimiter} (utente{delimiter}tag@dominio.com)"
|
||||
},
|
||||
"show_preview": {
|
||||
"label": "Mostra anteprima testo",
|
||||
"description": "Visualizza l'anteprima del messaggio nell'elenco",
|
||||
@@ -1756,7 +1761,7 @@
|
||||
"use_address": "Usa questo indirizzo",
|
||||
"invalid_tag": "Il tag deve contenere solo caratteri alfanumerici e trattini",
|
||||
"tag_too_long": "Il tag deve essere di massimo 30 caratteri",
|
||||
"help_text": "I messaggi inviati a utente+tag@dominio.com arriveranno nella tua casella di posta",
|
||||
"help_text": "I messaggi inviati a utente{delimiter}tag@dominio.com arriveranno nella tua casella di posta",
|
||||
"validation": {
|
||||
"empty": "Il tag non può essere vuoto",
|
||||
"too_long": "Il tag deve essere di massimo {max} caratteri",
|
||||
|
||||
@@ -906,6 +906,11 @@
|
||||
"label": "返信元アドレスを自動選択",
|
||||
"description": "返信時に、元のメッセージを受信したIDへ差出人アドレスを自動的に切り替えます"
|
||||
},
|
||||
"sub_address_delimiter": {
|
||||
"label": "サブアドレス区切り文字",
|
||||
"description": "ユーザー名とサブアドレスタグを区切る文字です。お使いのメールサーバーが使用する区切り文字に合わせてください(例: user{delimiter}tag@domain.com)。",
|
||||
"option": "{delimiter} (user{delimiter}tag@domain.com)"
|
||||
},
|
||||
"show_preview": {
|
||||
"label": "プレビューテキストを表示",
|
||||
"description": "リストにメールのプレビューを表示",
|
||||
@@ -1756,7 +1761,7 @@
|
||||
"use_address": "このアドレスを使用",
|
||||
"invalid_tag": "タグは英数字とハイフンのみ使用できます",
|
||||
"tag_too_long": "タグは30文字以内にしてください",
|
||||
"help_text": "user+tag@domain.comに送信されたメールは受信トレイに届きます",
|
||||
"help_text": "user{delimiter}tag@domain.comに送信されたメールは受信トレイに届きます",
|
||||
"validation": {
|
||||
"empty": "タグは空にできません",
|
||||
"too_long": "タグは{max}文字以内にしてください",
|
||||
|
||||
@@ -911,6 +911,11 @@
|
||||
"label": "답장 시 보내는 사람 자동 선택",
|
||||
"description": "답장할 때 메일을 받았던 주소로 보내는 사람을 자동으로 변경해요"
|
||||
},
|
||||
"sub_address_delimiter": {
|
||||
"label": "서브 주소 구분자",
|
||||
"description": "사용자 이름과 서브 주소 태그를 나누는 문자예요. 메일 서버가 사용하는 구분자에 맞춰 주세요 (예: user{delimiter}tag@domain.com).",
|
||||
"option": "{delimiter} (user{delimiter}tag@domain.com)"
|
||||
},
|
||||
"attachment_click_action": {
|
||||
"label": "첨부파일 클릭 동작",
|
||||
"description": "파일을 클릭했을 때 미리보기를 할지, 바로 다운로드할지 선택해 주세요",
|
||||
@@ -1756,7 +1761,7 @@
|
||||
"use_address": "이 주소 사용하기",
|
||||
"invalid_tag": "태그는 알파벳, 숫자, 대시(-)만 쓸 수 있어요",
|
||||
"tag_too_long": "태그는 30자 이하여야 해요",
|
||||
"help_text": "user+tag@domain.com 으로 보낸 메일은 내 받은편지함으로 들어와요",
|
||||
"help_text": "user{delimiter}tag@domain.com 으로 보낸 메일은 내 받은편지함으로 들어와요",
|
||||
"validation": {
|
||||
"empty": "태그를 비워둘 수 없어요",
|
||||
"too_long": "태그는 {max}자 이하여야 해요",
|
||||
|
||||
@@ -906,6 +906,11 @@
|
||||
"label": "Automātiski izvēlēties atbildes adresi",
|
||||
"description": "Atbildot automātiski izmantot to kontu, uz kuru vēstule tika saņemta"
|
||||
},
|
||||
"sub_address_delimiter": {
|
||||
"label": "Apakšadreses atdalītājs",
|
||||
"description": "Zīme, kas atdala lietotājvārdu no apakšadreses tagu. Izvēlieties atdalītāju, ko lieto jūsu pasta serveris (piem. lietotajs{delimiter}tags@domens.lv).",
|
||||
"option": "{delimiter} (lietotajs{delimiter}tags@domens.lv)"
|
||||
},
|
||||
"show_preview": {
|
||||
"label": "Rādīt priekšskatījuma tekstu",
|
||||
"description": "Rādīt vēstules fragmentu sarakstā",
|
||||
@@ -1756,7 +1761,7 @@
|
||||
"use_address": "Izmantot šo adresi",
|
||||
"invalid_tag": "Tags var saturēt tikai burtus, ciparus un domuzīmes",
|
||||
"tag_too_long": "Tags nedrīkst pārsniegt 30 rakstzīmes",
|
||||
"help_text": "Vēstules uz lietotajs+tags@domens.lv nonāks jūsu pastkastē",
|
||||
"help_text": "Vēstules uz lietotajs{delimiter}tags@domens.lv nonāks jūsu pastkastē",
|
||||
"validation": {
|
||||
"empty": "Tags nevar būt tukšs",
|
||||
"too_long": "Tags nedrīkst pārsniegt {max} rakstzīmes",
|
||||
|
||||
@@ -906,6 +906,11 @@
|
||||
"label": "Antwoordadres automatisch selecteren",
|
||||
"description": "Schakel bij het beantwoorden automatisch het Van-adres om naar de identiteit die het oorspronkelijke bericht ontving"
|
||||
},
|
||||
"sub_address_delimiter": {
|
||||
"label": "Sub-adres scheidingsteken",
|
||||
"description": "Teken dat je gebruikersnaam scheidt van het sub-adres-label. Gebruik het scheidingsteken dat je mailserver gebruikt (bv. gebruiker{delimiter}tag@domein.nl).",
|
||||
"option": "{delimiter} (gebruiker{delimiter}tag@domein.nl)"
|
||||
},
|
||||
"show_preview": {
|
||||
"label": "Voorbeeldtekst tonen",
|
||||
"description": "E-mailvoorbeeld weergeven in de lijst",
|
||||
@@ -1756,7 +1761,7 @@
|
||||
"use_address": "Dit adres gebruiken",
|
||||
"invalid_tag": "Tag mag alleen letters, cijfers en streepjes bevatten",
|
||||
"tag_too_long": "Tag mag maximaal 30 tekens bevatten",
|
||||
"help_text": "E-mails verzonden naar gebruiker+tag@domein.nl komen in je postvak IN aan",
|
||||
"help_text": "E-mails verzonden naar gebruiker{delimiter}tag@domein.nl komen in je postvak IN aan",
|
||||
"validation": {
|
||||
"empty": "Tag mag niet leeg zijn",
|
||||
"too_long": "Tag mag maximaal {max} tekens bevatten",
|
||||
|
||||
@@ -911,6 +911,11 @@
|
||||
"label": "Automatycznie wybieraj adres odpowiedzi",
|
||||
"description": "Podczas odpowiadania automatycznie przełączaj adres nadawcy na tożsamość, która pierwotnie otrzymała wiadomość"
|
||||
},
|
||||
"sub_address_delimiter": {
|
||||
"label": "Separator sub-adresu",
|
||||
"description": "Znak oddzielający Twoją nazwę użytkownika od tagu sub-adresu. Użyj separatora zgodnego z Twoim serwerem pocztowym (np. user{delimiter}tag@domain.com).",
|
||||
"option": "{delimiter} (user{delimiter}tag@domain.com)"
|
||||
},
|
||||
"attachment_click_action": {
|
||||
"label": "Akcja po kliknięciu załącznika",
|
||||
"description": "Wybierz, czy kliknięcie załącznika pliku ma pokazać podgląd, czy od razu go pobrać",
|
||||
@@ -1756,7 +1761,7 @@
|
||||
"use_address": "Użyj tego adresu",
|
||||
"invalid_tag": "Tag może zawierać tylko litery, cyfry i myślniki",
|
||||
"tag_too_long": "Tag może mieć maksymalnie 30 znaków",
|
||||
"help_text": "Wiadomości wysłane na adres user+tag@domain.com trafią do Twojej skrzynki odbiorczej",
|
||||
"help_text": "Wiadomości wysłane na adres user{delimiter}tag@domain.com trafią do Twojej skrzynki odbiorczej",
|
||||
"validation": {
|
||||
"empty": "Tag nie może być pusty",
|
||||
"too_long": "Tag może mieć maksymalnie {max} znaków",
|
||||
|
||||
@@ -906,6 +906,11 @@
|
||||
"label": "Selecionar automaticamente o endereço de resposta",
|
||||
"description": "Ao responder, muda automaticamente o endereço do remetente para a identidade que recebeu a mensagem original"
|
||||
},
|
||||
"sub_address_delimiter": {
|
||||
"label": "Delimitador de sub-endereço",
|
||||
"description": "Caractere que separa seu nome de usuário da tag de sub-endereço. Use o delimitador configurado no seu servidor de e-mail (ex.: usuario{delimiter}tag@dominio.com).",
|
||||
"option": "{delimiter} (usuario{delimiter}tag@dominio.com)"
|
||||
},
|
||||
"show_preview": {
|
||||
"label": "Mostrar Texto de Visualização",
|
||||
"description": "Exibir visualização do e-mail na lista",
|
||||
@@ -1756,7 +1761,7 @@
|
||||
"use_address": "Usar Este Endereço",
|
||||
"invalid_tag": "A tag deve conter apenas caracteres alfanuméricos e hífens",
|
||||
"tag_too_long": "A tag deve ter no máximo 30 caracteres",
|
||||
"help_text": "E-mails enviados para usuario+tag@dominio.com chegarão na sua caixa de entrada",
|
||||
"help_text": "E-mails enviados para usuario{delimiter}tag@dominio.com chegarão na sua caixa de entrada",
|
||||
"validation": {
|
||||
"empty": "A tag não pode estar vazia",
|
||||
"too_long": "A tag deve ter no máximo {max} caracteres",
|
||||
|
||||
@@ -906,6 +906,11 @@
|
||||
"label": "Автоматически выбирать адрес для ответа",
|
||||
"description": "При ответе автоматически переключать адрес отправителя на ту учетную запись, которая получила исходное сообщение"
|
||||
},
|
||||
"sub_address_delimiter": {
|
||||
"label": "Разделитель суб-адресов",
|
||||
"description": "Символ, отделяющий имя пользователя от тега суб-адреса. Используйте разделитель, настроенный на вашем почтовом сервере (например, user{delimiter}tag@domain.com).",
|
||||
"option": "{delimiter} (user{delimiter}tag@domain.com)"
|
||||
},
|
||||
"show_preview": {
|
||||
"label": "Показывать текст предпросмотра",
|
||||
"description": "Отображать предпросмотр письма в списке",
|
||||
@@ -1756,7 +1761,7 @@
|
||||
"use_address": "Использовать этот адрес",
|
||||
"invalid_tag": "Тег должен содержать только буквы, цифры и дефисы",
|
||||
"tag_too_long": "Тег не должен превышать 30 символов",
|
||||
"help_text": "Письма на адрес user+tag@domain.com будут приходить в ваш ящик",
|
||||
"help_text": "Письма на адрес user{delimiter}tag@domain.com будут приходить в ваш ящик",
|
||||
"validation": {
|
||||
"empty": "Тег не может быть пустым",
|
||||
"too_long": "Тег не должен превышать {max} символов",
|
||||
|
||||
@@ -911,6 +911,11 @@
|
||||
"label": "Yanıt Adresini Otomatik Seç",
|
||||
"description": "Yanıtlarken, Kimden adresini iletiyi başlangıçta alan kimliğe otomatik olarak değiştir"
|
||||
},
|
||||
"sub_address_delimiter": {
|
||||
"label": "Alt Adres Ayırıcı",
|
||||
"description": "Kullanıcı adını alt adres etiketinden ayıran karakter. Posta sunucunuzun kullandığı ayırıcıyı seçin (ör. kullanici{delimiter}etiket@domain.com).",
|
||||
"option": "{delimiter} (kullanici{delimiter}etiket@domain.com)"
|
||||
},
|
||||
"attachment_click_action": {
|
||||
"label": "Ek Tıklama İşlemi",
|
||||
"description": "Bir dosya ekine tıklandığında önizleme mi yoksa anında indirme mi yapılacağını seçin",
|
||||
@@ -1756,7 +1761,7 @@
|
||||
"use_address": "Bu Adresi Kullan",
|
||||
"invalid_tag": "Etiket yalnızca harf, rakam ve tire içermelidir",
|
||||
"tag_too_long": "Etiket en fazla 30 karakter olmalıdır",
|
||||
"help_text": "kullanici+etiket@domain.com adresine gönderilen e-postalar gelen kutunuza ulaşır",
|
||||
"help_text": "kullanici{delimiter}etiket@domain.com adresine gönderilen e-postalar gelen kutunuza ulaşır",
|
||||
"validation": {
|
||||
"empty": "Etiket boş olamaz",
|
||||
"too_long": "Etiket en fazla {max} karakter olmalıdır",
|
||||
|
||||
@@ -911,6 +911,11 @@
|
||||
"label": "Автоматичний вибір адреси для відповіді",
|
||||
"description": "Під час відповіді автоматично змінюйте адресу відправника на особу, яка спочатку отримала повідомлення"
|
||||
},
|
||||
"sub_address_delimiter": {
|
||||
"label": "Розділювач під-адреси",
|
||||
"description": "Символ, який відокремлює ім'я користувача від мітки під-адреси. Використовуйте розділювач, налаштований на вашому поштовому сервері (напр. user{delimiter}tag@domain.com).",
|
||||
"option": "{delimiter} (user{delimiter}tag@domain.com)"
|
||||
},
|
||||
"attachment_click_action": {
|
||||
"label": "Вкладення Натисніть Дія",
|
||||
"description": "Виберіть, чи клацання вкладеного файлу попередньо переглядає його чи негайно завантажує",
|
||||
@@ -1756,7 +1761,7 @@
|
||||
"use_address": "Використовуйте цю адресу",
|
||||
"invalid_tag": "Тег має бути лише буквено-цифровим і тире",
|
||||
"tag_too_long": "Тег має містити 30 символів або менше",
|
||||
"help_text": "Електронні листи, надіслані на user+tag@domain.com, надходитимуть до вашої скриньки",
|
||||
"help_text": "Електронні листи, надіслані на user{delimiter}tag@domain.com, надходитимуть до вашої скриньки",
|
||||
"validation": {
|
||||
"empty": "Тег не може бути порожнім",
|
||||
"too_long": "Тег має містити не більше {max} символів",
|
||||
|
||||
@@ -911,6 +911,11 @@
|
||||
"label": "自动选择回复地址",
|
||||
"description": "回复时自动将发件人地址切换为最初收到该邮件的身份"
|
||||
},
|
||||
"sub_address_delimiter": {
|
||||
"label": "子地址分隔符",
|
||||
"description": "用于分隔用户名和子地址标签的字符。请选择与您的邮件服务器一致的分隔符(例如 user{delimiter}tag@domain.com)。",
|
||||
"option": "{delimiter} (user{delimiter}tag@domain.com)"
|
||||
},
|
||||
"attachment_click_action": {
|
||||
"label": "附件单击操作",
|
||||
"description": "选择点击附件时是预览还是直接下载",
|
||||
@@ -1756,7 +1761,7 @@
|
||||
"use_address": "使用此地址",
|
||||
"invalid_tag": "标签只能是字母数字和破折号",
|
||||
"tag_too_long": "标签不得超过 30 个字符",
|
||||
"help_text": "发送到 user+tag@domain.com 的邮件将送达您的收件箱",
|
||||
"help_text": "发送到 user{delimiter}tag@domain.com 的邮件将送达您的收件箱",
|
||||
"validation": {
|
||||
"empty": "标签不能为空",
|
||||
"too_long": "标签不得超过 {max} 个字符",
|
||||
|
||||
@@ -4,6 +4,11 @@ import { useThemeStore } from './theme-store';
|
||||
import { useLocaleStore } from './locale-store';
|
||||
import type { NotificationSoundChoice } from '@/lib/notification-sound';
|
||||
import { apiFetch } from '@/lib/browser-navigation';
|
||||
import {
|
||||
DEFAULT_SUB_ADDRESS_DELIMITER,
|
||||
isSupportedSubAddressDelimiter,
|
||||
type SubAddressDelimiter,
|
||||
} from '@/lib/sub-addressing';
|
||||
|
||||
// Use console directly to avoid circular dependency with lib/debug.ts
|
||||
// (debug.ts imports useSettingsStore for debugMode check)
|
||||
@@ -138,6 +143,7 @@ interface SettingsState {
|
||||
defaultReplyMode: ReplyMode;
|
||||
autoSelectReplyIdentity: boolean;
|
||||
plainTextMode: boolean; // Send plain text only (no rich text editor)
|
||||
subAddressDelimiter: SubAddressDelimiter; // Character separating user from tag (e.g. "user+tag@")
|
||||
|
||||
// Privacy & Security
|
||||
sessionTimeout: number; // minutes (0 = never)
|
||||
@@ -286,6 +292,7 @@ const DEFAULT_SETTINGS = {
|
||||
defaultReplyMode: 'reply' as ReplyMode,
|
||||
autoSelectReplyIdentity: false,
|
||||
plainTextMode: false,
|
||||
subAddressDelimiter: DEFAULT_SUB_ADDRESS_DELIMITER as SubAddressDelimiter,
|
||||
|
||||
// Privacy & Security
|
||||
sessionTimeout: 0, // Never
|
||||
@@ -456,6 +463,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
defaultReplyMode: state.defaultReplyMode,
|
||||
autoSelectReplyIdentity: state.autoSelectReplyIdentity,
|
||||
plainTextMode: state.plainTextMode,
|
||||
subAddressDelimiter: state.subAddressDelimiter,
|
||||
sessionTimeout: state.sessionTimeout,
|
||||
emailNotificationsEnabled: state.emailNotificationsEnabled,
|
||||
emailNotificationSound: state.emailNotificationSound,
|
||||
@@ -508,6 +516,9 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
// Apply settings
|
||||
Object.keys(settings).forEach((key) => {
|
||||
if (key in DEFAULT_SETTINGS) {
|
||||
if (key === 'subAddressDelimiter' && !isSupportedSubAddressDelimiter(settings[key])) {
|
||||
return;
|
||||
}
|
||||
set({ [key]: settings[key] });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user