fix: use the from override as envelope sender where the server allows it #1009

This commit is contained in:
Linus Rath
2026-09-23 19:58:26 +02:00
parent 198c6d41e0
commit 1c00cf75cb
32 changed files with 549 additions and 70 deletions
@@ -0,0 +1,242 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, afterEach } from 'vitest';
import React from 'react';
import { EmailComposer } from '../email-composer';
// ─── Heavy component mocks (mirrors composer-reopened-draft-signature.test.tsx) ─
vi.mock('@/components/email/rich-text-editor', () => ({
RichTextEditor: () => React.createElement('div', { 'data-testid': 'rich-text-editor' }),
}));
vi.mock('@/components/plugins/plugin-slot', () => ({ PluginSlot: () => null }));
vi.mock('@/components/identity/sub-address-helper', () => ({ SubAddressHelper: () => null }));
vi.mock('@/components/templates/template-picker', () => ({ TemplatePicker: () => null }));
vi.mock('@/components/templates/template-form', () => ({ TemplateForm: () => null }));
vi.mock('@/components/files/file-preview-modal', () => ({ FilePreviewModal: () => null }));
vi.mock('@/hooks/use-focus-trap', () => ({
useFocusTrap: () => ({ current: null }),
}));
vi.mock('@/hooks/use-pro-multi-account-identities', () => ({
useProMultiAccountIdentities: () => ({ enabled: false, groups: [], allIdentities: [] }),
stripCrossAccountIdentityPrefix: (id: string) => ({ localAccountId: null, rawId: id }),
}));
// ─── Store mocks ──────────────────────────────────────────────────────────────
vi.mock('@/stores/auth-store', () => {
const state = {
client: null,
identities: [],
primaryIdentity: null,
isAuthenticated: false,
isDemoMode: false,
activeAccountId: null,
connectionLost: false,
getClientForAccount: () => undefined,
getAllConnectedClients: () => new Map(),
syncIdentities: () => {},
refreshIdentities: async () => {},
};
const hook = (sel?: (s: typeof state) => unknown) =>
typeof sel === 'function' ? sel(state) : state;
hook.getState = () => state;
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
return { useAuthStore: hook };
});
vi.mock('@/stores/identity-store', () => {
const state = {
identities: [
{ id: 'id-me', email: 'me@example.com', name: 'Me' },
{ id: 'id-sales', email: 'sales@example.com', name: 'Sales' },
] as Array<Record<string, unknown>>,
defaultIdentityId: 'id-me',
};
const hook = (sel?: (s: typeof state) => unknown) =>
typeof sel === 'function' ? sel(state) : state;
hook.getState = () => state;
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
return { useIdentityStore: hook };
});
vi.mock('@/stores/account-store', () => {
const state = { accounts: [], getAccountById: () => undefined };
const hook = (sel?: (s: typeof state) => unknown) =>
typeof sel === 'function' ? sel(state) : state;
hook.getState = () => state;
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
return { useAccountStore: hook };
});
vi.mock('@/stores/email-store', () => {
const state = {
draftSaveEnabled: false,
sendRawEmail: async () => ({ sent: true }),
};
const hook = (sel?: (s: typeof state) => unknown) =>
typeof sel === 'function' ? sel(state) : state;
hook.getState = () => state;
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
return { useEmailStore: hook };
});
vi.mock('@/stores/settings-store', () => {
const state = {
timeFormat: '24h',
plainTextMode: false,
subAddressDelimiter: '+',
autoSelectReplyIdentity: true,
attachmentReminderEnabled: false,
attachmentReminderKeywords: [],
emptySubjectWarningEnabled: true,
sendDelaySeconds: 0,
// The default position: replies/forwards get the signature appended at
// send time rather than embedded above the quote.
signaturePosition: 'below_quote',
signatureSeparatorEnabled: true,
requestReadReceiptDefault: false,
addTrustedSender: () => {},
trustedSendersAddressBook: null,
updateSetting: () => {},
};
const hook = (sel?: (s: typeof state) => unknown) =>
typeof sel === 'function' ? sel(state) : state;
hook.getState = () => state;
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
return { useSettingsStore: hook };
});
vi.mock('@/stores/contact-store', () => {
const state = {
contacts: [],
getAutocomplete: async () => [],
addToTrustedSendersBook: async () => {},
};
const hook = (sel?: (s: typeof state) => unknown) =>
typeof sel === 'function' ? sel(state) : state;
hook.getState = () => state;
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
return { useContactStore: hook };
});
vi.mock('@/stores/template-store', () => {
const state = { templates: [], addTemplate: async () => {} };
const hook = (sel?: (s: typeof state) => unknown) =>
typeof sel === 'function' ? sel(state) : state;
hook.getState = () => state;
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
return { useTemplateStore: hook };
});
// ─── Misc dependency mocks ────────────────────────────────────────────────────
vi.mock('@/stores/toast-store', () => ({
toast: { info: () => {}, error: () => {}, success: () => {} },
}));
vi.mock('@/lib/plugin-hooks', () => ({
emailHooks: {
onComposerOpen: { call: async () => [] },
onRecipientChange: { call: async () => [] },
getRecipientSuggestions: { call: async () => [] },
onRecipientChipsChange: { transform: async (chips: unknown) => chips },
onDraftChange: { emit: () => {} },
onBeforeDraftAutoSave: { transform: async (draft: unknown) => draft },
onBeforeEmailSend: { intercept: async () => true },
onComposeSend: { intercept: async () => true },
onTransformOutgoingEmail: { transform: async (email: unknown) => email },
},
contactHooks: {
search: { call: async () => [] },
onProvideRecipientSuggestions: { transform: async (initial: unknown) => initial },
},
}));
vi.mock('@/lib/email-sanitization', () => ({
sanitizeSignatureHtml: (v: string) => v,
sanitizeSignatureHtmlForDisplay: (v: string) => v,
sanitizeEmailHtml: (v: string) => v,
sanitizePluginBodyHtml: (v: string) => v,
escapeHtml: (v: string) => v,
parseHtmlSafely: (html: string) => new DOMParser().parseFromString(html, 'text/html'),
}));
vi.mock('@/lib/email-threading', () => ({
computeReplyThreadingHeaders: () => ({ inReplyTo: [], references: [] }),
}));
vi.mock('@/lib/sub-addressing', () => ({ generateSubAddress: () => '' }));
vi.mock('@/lib/debug', () => ({ debug: { log: () => {}, warn: () => {}, error: () => {} } }));
vi.mock('@/components/email/quoted-html', () => ({
buildQuotedHtmlBlock: () => '',
serializeEditorContent: () => '',
}));
vi.mock('@/lib/template-utils', () => ({ substitutePlaceholders: (s: string) => s }));
// ─── Tests ────────────────────────────────────────────────────────────────────
// #1009: a From override only changed the header From. The envelope MAIL FROM
// (the Return-Path recipients see) stayed the identity's address.
const DRAFT = {
to: 'bob@example.com',
cc: '',
bcc: '',
subject: 'Hello',
body: '<p>Hi</p>',
showCc: false,
showBcc: false,
selectedIdentityId: 'id-me',
subAddressTag: '',
mode: 'compose' as const,
draftId: null,
fromOverrideEnabled: true,
fromOverrideName: 'Shop',
};
const NOTICE = 'from_override.envelope_notice';
async function send(onSend: ReturnType<typeof vi.fn>) {
fireEvent.click(screen.getAllByTestId('composer-send')[0]);
await waitFor(() => expect(onSend).toHaveBeenCalledTimes(1));
return onSend.mock.calls[0][0] as { fromEmail?: string; fromName?: string; identityId?: string; envelopeMailFrom?: string };
}
describe('composer From override envelope (#1009)', () => {
afterEach(() => {
vi.clearAllMocks();
});
it('asks for the override address as the envelope MAIL FROM', async () => {
const onSend = vi.fn();
render(<EmailComposer initialData={{ ...DRAFT, fromOverrideEmail: 'shop@example.com' }} onSend={onSend} />);
const sent = await send(onSend);
expect(sent).toMatchObject({
fromEmail: 'shop@example.com',
fromName: 'Shop',
identityId: 'id-me',
envelopeMailFrom: 'shop@example.com',
});
});
it("warns that the server may use the identity's address instead", () => {
render(<EmailComposer initialData={{ ...DRAFT, fromOverrideEmail: 'shop@example.com' }} />);
expect(screen.getByText(NOTICE)).toBeInTheDocument();
});
it('does not warn when an identity owns the override address', () => {
render(<EmailComposer initialData={{ ...DRAFT, fromOverrideEmail: 'Sales@Example.com' }} />);
expect(screen.queryByText(NOTICE)).not.toBeInTheDocument();
});
it('sends no envelope request without an override', async () => {
const onSend = vi.fn();
render(<EmailComposer initialData={{ ...DRAFT, fromOverrideEnabled: false }} onSend={onSend} />);
expect(screen.queryByText(NOTICE)).not.toBeInTheDocument();
const sent = await send(onSend);
expect(sent.fromEmail).toBe('me@example.com');
expect(sent.envelopeMailFrom).toBeUndefined();
});
});
+21 -5
View File
@@ -131,7 +131,7 @@ export interface ComposerDraftData {
* next save/send rebuilds the draft without them - silent data loss (#849).
*/
attachments?: Array<{ blobId: string; name?: string; type?: string; size: number; cid?: string; disposition?: string }>;
/** When set, overrides the header From: - sent through the selected identity's envelope. */
/** When set, overrides the header From: and the envelope MAIL FROM, where the server allows it. */
fromOverrideEmail?: string;
fromOverrideName?: string;
fromOverrideEnabled?: boolean;
@@ -697,6 +697,16 @@ export function EmailComposer({
const composerClientRef = useRef(composerClient);
composerClientRef.current = composerClient;
const currentIdentityRawId = currentIdentityParts.rawId ?? currentIdentity?.id;
// A From override is asked for as the envelope MAIL FROM too, but a server
// may only accept the identity's own address there (Stalwart does), which
// then shows in the Return-Path. Unless one of the account's identities owns
// the override address - the send goes through that one - say so (#1009).
const overrideAddress = fromOverrideEnabled ? fromOverrideEmail.trim().toLowerCase() : '';
const overrideEnvelopeFallback = overrideAddress && currentIdentity?.email
&& !identities.some((identity) => identity.email.toLowerCase() === overrideAddress
&& stripCrossAccountIdentityPrefix(identity.id).localAccountId === currentIdentityParts.localAccountId)
? currentIdentity.email
: null;
// Alias identities often lack a configured signature - fall back to the primary
// identity's signature so replies (which auto-select a matching alias) still
// populate the user's signature.
@@ -2205,15 +2215,16 @@ export function EmailComposer({
: currentIdentity.email
: undefined;
// When the user has typed a From override, that becomes the header From
// (and MIME-builder From in the S/MIME path). The identity still drives
// the SMTP envelope MAIL FROM - set explicitly so it doesn't mistakenly
// default to the override address.
// (and MIME-builder From in the S/MIME path) and is asked for as the SMTP
// envelope MAIL FROM too, so the Return-Path doesn't reveal the identity's
// address (#1009). A server that only accepts the identity's own address
// there gets that instead; the From row says so before sending.
const overrideActive = fromOverrideEnabled && fromOverrideEmail.trim().length > 0;
const fromEmail = overrideActive ? fromOverrideEmail.trim() : identityFromEmail;
const fromName = overrideActive
? (fromOverrideName.trim() || undefined)
: (currentIdentity?.name || undefined);
const envelopeMailFrom = overrideActive ? identityFromEmail : undefined;
const envelopeMailFrom = overrideActive ? fromEmail : undefined;
// Body is already HTML from the rich text editor (or plain text in plain
// text mode). Where the signature is already part of it (compose mode,
@@ -2807,6 +2818,11 @@ export function EmailComposer({
</Button>
</div>
</div>
{overrideEnvelopeFallback && (
<p className="ps-[4.5rem] md:ps-[5.5rem] pe-4 py-1.5 text-xs text-muted-foreground border-b border-border/50">
{t('from_override.envelope_notice', { identity: overrideEnvelopeFallback })}
</p>
)}
{/* To field */}
<div data-testid="composer-to" className={cn("flex items-center gap-2 px-4 py-2.5 border-b border-border/50 relative", shakeField === 'to' && "animate-shake")}>
+148
View File
@@ -0,0 +1,148 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { JMAPClient } from '../jmap/client';
// #1009: a From override is asked for as the envelope MAIL FROM, so the
// Return-Path doesn't reveal the identity's address where the server allows it.
type MethodCall = [string, Record<string, unknown>, string];
function createClient(): JMAPClient {
const client = new JMAPClient('https://jmap.example.com', 'user@example.com', 'pass');
Object.assign(client, {
apiUrl: 'https://jmap.example.com/api',
accountId: 'account-1',
username: 'user@example.com',
});
return client;
}
function enableDelayedSend(client: JMAPClient) {
Object.assign(client, {
capabilities: {
'urn:ietf:params:jmap:core': {},
'urn:ietf:params:jmap:mail': {},
'urn:ietf:params:jmap:submission': {},
},
session: {
primaryAccounts: {
'urn:ietf:params:jmap:mail': 'account-1',
'urn:ietf:params:jmap:submission': 'account-1',
},
accounts: {
'account-1': {
accountCapabilities: {
'urn:ietf:params:jmap:mail': {},
'urn:ietf:params:jmap:submission': { maxDelayedSend: 3600, submissionExtensions: { FUTURERELEASE: true } },
},
},
},
},
});
}
const IDENTITIES = [
{ id: 'identity-1', email: 'user@example.com', mayDelete: false },
{ id: 'identity-2', email: 'Sales@example.com', mayDelete: true, replyTo: [{ email: 'team@example.com' }] },
];
/**
* Answers the requests sendEmail makes; `refuseMailFrom` makes every
* EmailSubmission/set whose envelope asks for another MAIL FROM than the
* identity's fail the way Stalwart does.
*/
function mockServer(options: { refuseMailFrom?: boolean } = {}) {
const submissions: Array<Record<string, unknown>> = [];
vi.spyOn(globalThis, 'fetch').mockImplementation(async (_url, init) => {
const { methodCalls } = JSON.parse((init as { body: string }).body) as { methodCalls: MethodCall[] };
const methodResponses = methodCalls.map(([name, args, callId]) => {
if (name === 'Mailbox/get') {
return [name, { list: [{ id: 'mb-drafts', name: 'Drafts', role: 'drafts' }, { id: 'mb-sent', name: 'Sent', role: 'sent' }] }, callId];
}
if (name === 'Identity/get') return [name, { list: IDENTITIES }, callId];
if (name === 'Email/set') {
const created = Object.keys((args.create ?? {}) as object);
return [name, { created: Object.fromEntries(created.map((id) => [id, { id: 'email-9' }])) }, callId];
}
if (name === 'EmailSubmission/set') {
const create = (args.create as Record<string, Record<string, unknown>>)['1'];
submissions.push(create);
const identity = IDENTITIES.find((i) => i.id === create.identityId);
const mailFrom = (create.envelope as { mailFrom?: { email: string } } | undefined)?.mailFrom?.email;
if (options.refuseMailFrom && mailFrom && mailFrom.toLowerCase() !== identity?.email.toLowerCase()) {
return [name, { notCreated: { '1': { type: 'forbiddenFrom', description: 'Envelope mailFrom does not match identity email address.' } } }, callId];
}
return [name, { created: { '1': { id: `sub-${submissions.length}` } } }, callId];
}
return ['error', { type: 'unknownMethod' }, callId];
});
const payload = { methodResponses };
return {
ok: true,
status: 200,
text: () => Promise.resolve(JSON.stringify(payload)),
json: () => Promise.resolve(payload),
} as Response;
});
return submissions;
}
function send(client: JMAPClient, fromEmail: string, envelopeMailFrom?: string, delayedUntil?: string) {
return client.sendEmail(
['Recipient <rcpt@example.net>'],
'Subject',
'body',
undefined, undefined, 'identity-1', fromEmail,
undefined, undefined, undefined, undefined,
undefined, undefined,
delayedUntil,
envelopeMailFrom,
);
}
describe('JMAPClient.sendEmail envelope MAIL FROM (#1009)', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it('asks for the From override as MAIL FROM', async () => {
const submissions = mockServer();
const result = await send(createClient(), 'alias@example.com', 'alias@example.com');
expect(submissions).toEqual([{
emailId: expect.stringMatching(/^#send-/),
identityId: 'identity-1',
envelope: { mailFrom: { email: 'alias@example.com' }, rcptTo: [{ email: 'rcpt@example.net' }] },
}]);
expect(result).toMatchObject({ emailId: 'email-9', emailSubmissionId: 'sub-1' });
});
it("submits again with the identity's address when the server refuses the override", async () => {
const submissions = mockServer({ refuseMailFrom: true });
const result = await send(createClient(), 'alias@example.com', 'alias@example.com');
expect(submissions).toHaveLength(2);
expect(submissions[1]).toEqual({ emailId: 'email-9', identityId: 'identity-1' });
expect(result).toMatchObject({ emailId: 'email-9', emailSubmissionId: 'sub-2' });
});
it('sends through the identity that owns the override address', async () => {
const submissions = mockServer({ refuseMailFrom: true });
await send(createClient(), 'sales@example.com', 'sales@example.com');
expect(submissions).toEqual([{ emailId: expect.stringMatching(/^#send-/), identityId: 'identity-2' }]);
});
it("keeps the identity's address as MAIL FROM of a sub-addressed scheduled send", async () => {
const client = createClient();
enableDelayedSend(client);
const submissions = mockServer({ refuseMailFrom: true });
await send(client, 'user+shop@example.com', undefined, new Date(Date.now() + 60_000).toISOString());
expect(submissions).toHaveLength(1);
expect(submissions[0].envelope).toMatchObject({ mailFrom: { email: 'user@example.com' } });
});
});
+3
View File
@@ -304,6 +304,9 @@ export interface IJMAPClient {
inReplyTo?: string[],
references?: string[],
delayedUntil?: string,
// The SMTP MAIL FROM wanted instead of the identity's address (a From
// override). Sent through the identity that owns it when there is one;
// a server that refuses it gets the identity's address instead.
envelopeMailFrom?: string,
// requestDsn / requireTls map to RFC 3461 / RFC 8689 envelope parameters
// and need the matching `submissionExtensions` entry (see
+56 -13
View File
@@ -3640,8 +3640,10 @@ export class JMAPClient implements IJMAPClient {
throw new Error('No drafts mailbox found');
}
const requestedMailFrom = envelopeMailFrom ? parseRecipientString(envelopeMailFrom).email : undefined;
let finalIdentityId = identityId;
let identityReplyTo: EmailAddress[] | undefined;
let identityEmail: string | undefined;
{
const identityResponse = await this.request([
["Identity/get", { accountId: targetAccountId }, "0"]
@@ -3653,9 +3655,15 @@ export class JMAPClient implements IJMAPClient {
if (identityResponse.methodResponses?.[0]?.[0] === "Identity/get") {
const identities = (identityResponse.methodResponses[0][1].list || []) as Identity[];
if (identities.length > 0) {
let matchingIdentity = identityId
? identities.find((id) => id.id === identityId)
// A requested MAIL FROM that is itself one of the account's
// identities is sent through that identity, so the server accepts
// it as the envelope sender (#1009).
let matchingIdentity = requestedMailFrom
? identities.find((id) => id.email.toLowerCase() === requestedMailFrom.toLowerCase())
: undefined;
if (!matchingIdentity && identityId) {
matchingIdentity = identities.find((id) => id.id === identityId);
}
if (!matchingIdentity) {
const target = fromEmail || this.username;
matchingIdentity = identities.find((id) => id.email === target)
@@ -3663,9 +3671,16 @@ export class JMAPClient implements IJMAPClient {
}
finalIdentityId = matchingIdentity?.id || identities[0].id;
identityReplyTo = matchingIdentity?.replyTo || identities[0].replyTo;
identityEmail = (matchingIdentity || identities[0]).email;
}
}
}
// The MAIL FROM the server derives from the identity when the envelope is
// omitted. A wildcard identity (RFC 8621 §6, "*@example.com") names no
// concrete address, so the header From stands in for it.
const identityMailFrom = identityEmail && !identityEmail.includes('*')
? identityEmail
: parseRecipientString(fromEmail || this.username).email;
// Per RFC 8621 §4.1.2.3 inReplyTo/references are arrays of bare msg-ids
// (no angle brackets). Stalwart may return them either way, so normalize.
@@ -3733,20 +3748,20 @@ export class JMAPClient implements IJMAPClient {
},
};
// When an explicit envelope MAIL FROM is provided (header From ≠ envelope,
// e.g. sending from a domain-catch-all alias without a dedicated Identity),
// set the EmailSubmission envelope explicitly. JMAP §7.3: when `envelope`
// is omitted the server derives mailFrom from the Identity.
const buildSubmissionCreate = (submissionId: string): Record<string, unknown> => {
const create: Record<string, unknown> = { emailId: `#${emailId}`, identityId: finalIdentityId };
// An explicit envelope is only sent when MAIL FROM should differ from the
// identity's address (a From override, e.g. replying from a catch-all
// alias without its own Identity) or carries SMTP parameters. JMAP §7.5:
// when `envelope` is omitted the server derives mailFrom itself.
const buildSubmissionCreate = (emailRef: string, mailFrom: string): Record<string, unknown> => {
const create: Record<string, unknown> = { emailId: emailRef, identityId: finalIdentityId };
const parameters = submissionEnvelopeParameters(options, holdForSeconds);
const hasMailFromParams = Object.keys(parameters.mailFrom).length > 0;
const hasRcptToParams = Object.keys(parameters.rcptTo).length > 0;
if (hasMailFromParams || hasRcptToParams || envelopeMailFrom) {
if (hasMailFromParams || hasRcptToParams || mailFrom.toLowerCase() !== identityMailFrom.toLowerCase()) {
const envelopeRecipients = normalizeEnvelopeRecipients([...to, ...(cc || []), ...(bcc || [])]);
create.envelope = {
mailFrom: {
email: parseRecipientString(envelopeMailFrom || fromEmail || this.username).email,
email: mailFrom,
...(hasMailFromParams ? { parameters: parameters.mailFrom } : {}),
},
rcptTo: hasRcptToParams
@@ -3754,8 +3769,9 @@ export class JMAPClient implements IJMAPClient {
: envelopeRecipients,
};
}
return { [submissionId]: create };
return { "1": create };
};
const firstMailFrom = requestedMailFrom || identityMailFrom;
// The old draft is destroyed in a separate request only after the
// submission succeeded (see below, #849). Destroying it up front in the
@@ -3768,11 +3784,38 @@ export class JMAPClient implements IJMAPClient {
}, "0"]);
methodCalls.push(["EmailSubmission/set", {
accountId: this.getSubmissionAccountId(targetAccountId),
create: buildSubmissionCreate("1"),
create: buildSubmissionCreate(`#${emailId}`, firstMailFrom),
onSuccessUpdateEmail,
}, "1"]);
const response = await this.request(methodCalls);
let response = await this.request(methodCalls);
// The server may refuse a MAIL FROM other than the identity's own address
// (RFC 8621 §7.5.1 forbiddenMailFrom). Stalwart always does, and reports
// it as forbiddenFrom. The message exists in Drafts by then, so submit it
// once more with the identity's address instead of failing the send; the
// composer warns about this before sending (#1009).
if (firstMailFrom.toLowerCase() !== identityMailFrom.toLowerCase()) {
const refusal = response.methodResponses
?.find(([name]) => name === 'EmailSubmission/set')?.[1]?.notCreated?.['1'] as { type?: string } | undefined;
const draftCopyId = response.methodResponses
?.find(([name]) => name === 'Email/set')?.[1]?.created?.[emailId]?.id as string | undefined;
if (draftCopyId && (refusal?.type === 'forbiddenMailFrom' || refusal?.type === 'forbiddenFrom')) {
debug.warn('email', `[sendEmail] server refused ${firstMailFrom} as MAIL FROM (${refusal.type}), sending with ${identityMailFrom}`);
const retry = await this.request([["EmailSubmission/set", {
accountId: this.getSubmissionAccountId(targetAccountId),
create: buildSubmissionCreate(draftCopyId, identityMailFrom),
onSuccessUpdateEmail,
}, "1"]]);
response = {
...retry,
methodResponses: [
...response.methodResponses.filter(([name]) => name === 'Email/set'),
...(retry.methodResponses ?? []),
],
};
}
}
let createdEmailId: string | undefined;
let emailSubmissionId: string | undefined;
+3 -2
View File
@@ -643,11 +643,12 @@
"from_override": {
"toggle_off": "تجاوز",
"toggle_on": "إلغاء التجاوز",
"toggle_tooltip": "عدّل اسم وعنوان \"من\" بحرية. سيُرسل البريد عبر هويتك، لكن ترويسة \"من\" الظاهرة فقط هي التي تتغير.",
"toggle_tooltip": "عدّل اسم وعنوان \"من\" بحرية. سيُرسل البريد عبر هويتك.",
"name_label": "اسم المرسل",
"name_placeholder": "الاسم",
"email_label": "عنوان بريد المرسل",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "إذا لم يقبل خادمك هذا العنوان كمرسل للمغلف، فسيُستخدم {identity} بدلاً منه، ويمكن للمستلمين رؤيته في ترويسة Return-Path."
},
"use_template": "قالب",
"save_as_template": "حفظ كقالب",
+3 -2
View File
@@ -643,11 +643,12 @@
"from_override": {
"toggle_off": "Sobreescriu",
"toggle_on": "Cancel·la la sobreescriptura",
"toggle_tooltip": "Editeu lliurement el nom i l'adreça del remitent. El correu es continua enviant a través de la vostra identitat: només canvia la capçalera «De» visible.",
"toggle_tooltip": "Editeu lliurement el nom i l'adreça del remitent. El correu es continua enviant a través de la vostra identitat.",
"name_label": "Nom del remitent",
"name_placeholder": "Nom",
"email_label": "Adreça electrònica del remitent",
"email_placeholder": "alias@exemple.com"
"email_placeholder": "alias@exemple.com",
"envelope_notice": "Si el vostre servidor no accepta aquesta adreça com a remitent del sobre, s'hi fa servir {identity}, i els destinataris la poden veure a la capçalera Return-Path."
},
"use_template": "Plantilla",
"save_as_template": "Desa com a plantilla",
+3 -2
View File
@@ -640,11 +640,12 @@
"from_override": {
"toggle_off": "Přepsat",
"toggle_on": "Zrušit přepsání",
"toggle_tooltip": "Volně upravujte jméno a adresu odesílatele. Pošta se stále odesílá přes vaši identitu - mění se pouze viditelné záhlaví Od.",
"toggle_tooltip": "Volně upravujte jméno a adresu odesílatele. Pošta se stále odesílá přes vaši identitu.",
"name_label": "Jméno odesílatele",
"name_placeholder": "Jméno",
"email_label": "E-mailová adresa odesílatele",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Pokud váš server tuto adresu jako odesílatele obálky nepřijme, použije se místo ní {identity} a příjemci ji uvidí v záhlaví Return-Path."
},
"use_template": "Šablona",
"save_as_template": "Uložit jako šablonu",
+3 -2
View File
@@ -642,11 +642,12 @@
"from_override": {
"toggle_off": "Tilsidesæt",
"toggle_on": "Annuller tilsidesættelse",
"toggle_tooltip": "Redigér Fra-navn og -adresse frit. Mail sendes stadig gennem din identitet - kun den synlige Fra-header ændres.",
"toggle_tooltip": "Redigér Fra-navn og -adresse frit. Mail sendes stadig gennem din identitet.",
"name_label": "Fra-navn",
"name_placeholder": "Navn",
"email_label": "Fra-e-mailadresse",
"email_placeholder": "alias@eksempel.dk"
"email_placeholder": "alias@eksempel.dk",
"envelope_notice": "Hvis din server ikke accepterer denne adresse som konvolutafsender, bruges {identity} i stedet, og modtagerne kan se den i Return-Path-headeren."
},
"use_template": "Skabelon",
"save_as_template": "Gem som skabelon",
+3 -2
View File
@@ -640,11 +640,12 @@
"from_override": {
"toggle_off": "Überschreiben",
"toggle_on": "Überschreibung aufheben",
"toggle_tooltip": "Bearbeiten Sie Absendername und -adresse frei. Die E-Mail wird weiterhin über Ihre Identität gesendet - nur die sichtbare Absenderkopfzeile ändert sich.",
"toggle_tooltip": "Bearbeiten Sie Absendername und -adresse frei. Die E-Mail wird weiterhin über Ihre Identität gesendet.",
"name_label": "Absendername",
"name_placeholder": "Name",
"email_label": "Absender-E-Mail-Adresse",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Akzeptiert Ihr Server diese Adresse nicht als Umschlag-Absender, wird dort {identity} verwendet, und Empfänger können sie in der Kopfzeile Return-Path sehen."
},
"use_template": "Vorlage",
"save_as_template": "Als Vorlage speichern",
+3 -2
View File
@@ -643,11 +643,12 @@
"from_override": {
"toggle_off": "Override",
"toggle_on": "Cancel override",
"toggle_tooltip": "Edit the From name and address freely. Mail is still sent through your identity - only the visible From header changes.",
"toggle_tooltip": "Edit the From name and address freely. Mail is still sent through your identity.",
"name_label": "From name",
"name_placeholder": "Name",
"email_label": "From email address",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "If your server doesn't accept this address as the envelope sender, {identity} is used there instead, and recipients can see it in the Return-Path header."
},
"use_template": "Template",
"save_as_template": "Save as Template",
+3 -2
View File
@@ -640,11 +640,12 @@
"from_override": {
"toggle_off": "Anular",
"toggle_on": "Cancelar anulación",
"toggle_tooltip": "Edita libremente el nombre y la dirección del remitente. El correo aún se envía a través de tu identidad - solo cambia el encabezado De visible.",
"toggle_tooltip": "Edita libremente el nombre y la dirección del remitente. El correo aún se envía a través de tu identidad.",
"name_label": "Nombre del remitente",
"name_placeholder": "Nombre",
"email_label": "Dirección de correo del remitente",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Si tu servidor no acepta esta dirección como remitente del sobre, se usa {identity} en su lugar, y los destinatarios pueden verla en el encabezado Return-Path."
},
"use_template": "Plantilla",
"save_as_template": "Guardar como plantilla",
+2 -1
View File
@@ -647,7 +647,8 @@
"name_label": "نام فرستنده",
"name_placeholder": "نام",
"email_label": "آدرس ایمیل فرستنده",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "اگر سرور شما این آدرس را به‌عنوان فرستندهٔ پاکت نپذیرد، به‌جای آن {identity} استفاده می‌شود و گیرندگان می‌توانند آن را در سرآیند Return-Path ببینند."
},
"use_template": "قالب",
"save_as_template": "ذخیره به عنوان قالب",
+3 -2
View File
@@ -640,11 +640,12 @@
"from_override": {
"toggle_off": "Remplacer",
"toggle_on": "Annuler le remplacement",
"toggle_tooltip": "Modifiez librement le nom et l'adresse d'expéditeur. Le courrier est toujours envoyé via votre identité - seul l'en-tête De visible change.",
"toggle_tooltip": "Modifiez librement le nom et l'adresse d'expéditeur. Le courrier est toujours envoyé via votre identité.",
"name_label": "Nom de l'expéditeur",
"name_placeholder": "Nom",
"email_label": "Adresse e-mail de l'expéditeur",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Si votre serveur n'accepte pas cette adresse comme expéditeur d'enveloppe, {identity} est utilisée à la place, et les destinataires peuvent la voir dans l'en-tête Return-Path."
},
"use_template": "Modèle",
"save_as_template": "Enregistrer comme modèle",
+3 -2
View File
@@ -637,11 +637,12 @@
"from_override": {
"toggle_off": "עקוף",
"toggle_on": "בטל עקיפה",
"toggle_tooltip": "ערוך את שם ההַשגחה וכתובת בחופשיות. הדוא״ל עדיין נשלח דרך הזהות שלך - רק כותרת ההַשגחה הנראית משתנה.",
"toggle_tooltip": "ערוך בחופשיות את שם וכתובת השולח. הדוא״ל עדיין נשלח דרך הזהות שלך.",
"name_label": "שם ממנו",
"name_placeholder": "שם",
"email_label": "כתובת דוא״ל ממנו",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "אם השרת שלך לא מקבל את הכתובת הזו כשולח המעטפה, ייעשה שימוש ב-{identity} במקומה, והנמענים יכולים לראות אותה בכותרת Return-Path."
},
"forgot_attachment": {
"title": "נשכחת קובץ מצורף?",
+3 -2
View File
@@ -643,11 +643,12 @@
"from_override": {
"toggle_off": "Felülbírálás",
"toggle_on": "Felülbírálás megszüntetése",
"toggle_tooltip": "A Feladó név és cím szabad szerkesztése. A levél továbbra is az identitásodon keresztül kerül elküldésre - csak a látható Feladó fejléc változik.",
"toggle_tooltip": "A Feladó név és cím szabad szerkesztése. A levél továbbra is az identitásodon keresztül kerül elküldésre.",
"name_label": "Feladó neve",
"name_placeholder": "Név",
"email_label": "Feladó e-mail címe",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Ha a szervered nem fogadja el ezt a címet borítékfeladóként, helyette a(z) {identity} cím kerül oda, és a címzettek láthatják a Return-Path fejlécben."
},
"use_template": "Sablon",
"save_as_template": "Mentés sablonként",
+3 -2
View File
@@ -640,11 +640,12 @@
"from_override": {
"toggle_off": "Sovrascrivi",
"toggle_on": "Annulla sovrascrittura",
"toggle_tooltip": "Modifica liberamente nome e indirizzo del mittente. La posta viene comunque inviata tramite la tua identità - cambia solo l'intestazione Da visibile.",
"toggle_tooltip": "Modifica liberamente nome e indirizzo del mittente. La posta viene comunque inviata tramite la tua identità.",
"name_label": "Nome mittente",
"name_placeholder": "Nome",
"email_label": "Indirizzo email del mittente",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Se il tuo server non accetta questo indirizzo come mittente della busta, viene usato {identity} al suo posto, e i destinatari possono vederlo nell'intestazione Return-Path."
},
"use_template": "Modello",
"save_as_template": "Salva come modello",
+3 -2
View File
@@ -640,11 +640,12 @@
"from_override": {
"toggle_off": "上書き",
"toggle_on": "上書きを取り消す",
"toggle_tooltip": "差出人名とアドレスを自由に編集できます。メールは引き続きあなたのアイデンティティ経由で送信されます - 表示される差出人ヘッダーのみが変更されます。",
"toggle_tooltip": "差出人名とアドレスを自由に編集できます。メールは引き続きあなたのアイデンティティ経由で送信されます。",
"name_label": "差出人名",
"name_placeholder": "名前",
"email_label": "差出人メールアドレス",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "サーバーがこのアドレスをエンベロープ送信者として受け付けない場合は、代わりに {identity} が使われ、受信者は Return-Path ヘッダーでそのアドレスを確認できます。"
},
"use_template": "テンプレート",
"save_as_template": "テンプレートとして保存",
+3 -2
View File
@@ -640,11 +640,12 @@
"from_override": {
"toggle_off": "재정의",
"toggle_on": "재정의 취소",
"toggle_tooltip": "보낸 사람 이름과 주소를 자유롭게 편집하세요. 메일은 여전히 사용자의 ID를 통해 전송되며 - 표시되는 보낸 사람 헤더만 변경됩니다.",
"toggle_tooltip": "보낸 사람 이름과 주소를 자유롭게 편집하세요. 메일은 여전히 사용자의 ID를 통해 전송됩니다.",
"name_label": "보낸 사람 이름",
"name_placeholder": "이름",
"email_label": "보낸 사람 이메일 주소",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "서버가 이 주소를 봉투 발신자로 허용하지 않으면 대신 {identity}이(가) 사용되며, 수신자는 Return-Path 헤더에서 이 주소를 볼 수 있습니다."
},
"use_template": "템플릿",
"save_as_template": "템플릿으로 저장",
+3 -2
View File
@@ -640,11 +640,12 @@
"from_override": {
"toggle_off": "Pārrakstīt",
"toggle_on": "Atcelt pārrakstīšanu",
"toggle_tooltip": "Brīvi rediģējiet sūtītāja vārdu un adresi. Pasts joprojām tiek sūtīts caur jūsu identitāti - mainās tikai redzamais No galvenes ieraksts.",
"toggle_tooltip": "Brīvi rediģējiet sūtītāja vārdu un adresi. Pasts joprojām tiek sūtīts caur jūsu identitāti.",
"name_label": "Sūtītāja vārds",
"name_placeholder": "Vārds",
"email_label": "Sūtītāja e-pasta adrese",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Ja jūsu serveris nepieņem šo adresi kā aploksnes sūtītāju, tās vietā tiek izmantota {identity}, un adresāti to var redzēt galvenē Return-Path."
},
"use_template": "Veidne",
"save_as_template": "Saglabāt kā veidni",
+3 -2
View File
@@ -643,11 +643,12 @@
"from_override": {
"toggle_off": "Дараах",
"toggle_on": "Дараахыг цуцлах",
"toggle_tooltip": "From нэр хаягийг чөлөөтэй засварлана. Таны хувийн мэдээллээр шуудан илгээгдсэн хэвээр байна - зөвхөн харагдах \"From\" гарчиг өөрчлөгдөнө.",
"toggle_tooltip": "From нэр хаягийг чөлөөтэй засварлана. Таны хувийн мэдээллээр шуудан илгээгдсэн хэвээр байна.",
"name_label": "Нэрнээс",
"name_placeholder": "Нэр",
"email_label": "Имэйл хаягаас",
"email_placeholder": "user@example.com"
"email_placeholder": "user@example.com",
"envelope_notice": "Хэрэв таны сервер энэ хаягийг дугтуйн илгээгчээр хүлээн авахгүй бол оронд нь {identity} ашиглагдах бөгөөд хүлээн авагчид үүнийг Return-Path гарчигт харж болно."
},
"use_template": "Загвар",
"save_as_template": "Загвар болгон хадгалах",
+3 -2
View File
@@ -643,11 +643,12 @@
"from_override": {
"toggle_off": "Endre avsender",
"toggle_on": "Avbryt endringen",
"toggle_tooltip": "Rediger Fra-navnet og -adressen fritt. E-posten sendes fortsatt gjennom identiteten din – bare den synlige Fra-overskriften endres.",
"toggle_tooltip": "Rediger Fra-navnet og -adressen fritt. E-posten sendes fortsatt gjennom identiteten din.",
"name_label": "Navn i Fra-feltet",
"name_placeholder": "Navn",
"email_label": "E-postadresse i Fra-feltet",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Hvis serveren din ikke godtar denne adressen som konvoluttavsender, brukes {identity} i stedet, og mottakerne kan se den i Return-Path-overskriften."
},
"use_template": "Mal",
"save_as_template": "Lagre som mal",
+3 -2
View File
@@ -640,11 +640,12 @@
"from_override": {
"toggle_off": "Overschrijven",
"toggle_on": "Overschrijven annuleren",
"toggle_tooltip": "Bewerk de naam en het adres van de afzender vrij. E-mail wordt nog steeds via je identiteit verzonden - alleen de zichtbare Van-koptekst verandert.",
"toggle_tooltip": "Bewerk de naam en het adres van de afzender vrij. E-mail wordt nog steeds via je identiteit verzonden.",
"name_label": "Afzendernaam",
"name_placeholder": "Naam",
"email_label": "E-mailadres afzender",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Als je server dit adres niet accepteert als envelopafzender, wordt daar {identity} gebruikt, en ontvangers kunnen dat zien in de Return-Path-header."
},
"use_template": "Sjabloon",
"save_as_template": "Opslaan als sjabloon",
+3 -2
View File
@@ -640,11 +640,12 @@
"from_override": {
"toggle_off": "Zastąp",
"toggle_on": "Anuluj zastąpienie",
"toggle_tooltip": "Swobodnie edytuj nazwę i adres nadawcy. Poczta jest nadal wysyłana przez twoją tożsamość - zmienia się tylko widoczny nagłówek Od.",
"toggle_tooltip": "Swobodnie edytuj nazwę i adres nadawcy. Poczta jest nadal wysyłana przez twoją tożsamość.",
"name_label": "Nazwa nadawcy",
"name_placeholder": "Nazwa",
"email_label": "Adres e-mail nadawcy",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Jeśli twój serwer nie akceptuje tego adresu jako nadawcy koperty, zostanie użyty {identity}, a odbiorcy zobaczą go w nagłówku Return-Path."
},
"use_template": "Szablon",
"save_as_template": "Zapisz jako szablon",
+3 -2
View File
@@ -640,11 +640,12 @@
"from_override": {
"toggle_off": "Substituir",
"toggle_on": "Cancelar substituição",
"toggle_tooltip": "Edite livremente o nome e o endereço do remetente. O email ainda é enviado através da sua identidade - apenas o cabeçalho De visível muda.",
"toggle_tooltip": "Edite livremente o nome e o endereço do remetente. O email ainda é enviado através da sua identidade.",
"name_label": "Nome do remetente",
"name_placeholder": "Nome",
"email_label": "Endereço de email do remetente",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Se o seu servidor não aceitar este endereço como remetente do envelope, {identity} é usado em seu lugar, e os destinatários podem vê-lo no cabeçalho Return-Path."
},
"use_template": "Modelo",
"save_as_template": "Salvar como modelo",
+3 -2
View File
@@ -643,11 +643,12 @@
"from_override": {
"toggle_off": "Suprascrieți",
"toggle_on": "Anulare suprascriere",
"toggle_tooltip": "Puteți modifica liber numele și adresa din câmpul „De la”. E-mailul este trimis în continuare sub identitatea dvs. – se modifică doar antetul vizibil „De la”.",
"toggle_tooltip": "Puteți modifica liber numele și adresa din câmpul „De la”. E-mailul este trimis în continuare sub identitatea dvs.",
"name_label": "Numele expeditorului",
"name_placeholder": "Nume",
"email_label": "De la adresa de e-mail",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Dacă serverul dvs. nu acceptă această adresă ca expeditor al plicului, se folosește în schimb {identity}, iar destinatarii o pot vedea în antetul Return-Path."
},
"use_template": "Șablon",
"save_as_template": "Salvează ca șablon",
+3 -2
View File
@@ -640,11 +640,12 @@
"from_override": {
"toggle_off": "Переопределить",
"toggle_on": "Отменить переопределение",
"toggle_tooltip": "Свободно редактируйте имя и адрес отправителя. Письмо по-прежнему отправляется через вашу учётную запись - меняется только видимый заголовок От.",
"toggle_tooltip": "Свободно редактируйте имя и адрес отправителя. Письмо по-прежнему отправляется через вашу учётную запись.",
"name_label": "Имя отправителя",
"name_placeholder": "Имя",
"email_label": "Email отправителя",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Если ваш сервер не принимает этот адрес как отправителя конверта, вместо него используется {identity}, и получатели увидят его в заголовке Return-Path."
},
"use_template": "Шаблон",
"save_as_template": "Сохранить как шаблон",
+2 -1
View File
@@ -647,7 +647,8 @@
"name_label": "Meno odosielateľa",
"name_placeholder": "Meno",
"email_label": "E-mailová adresa odosielateľa",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Ak váš server túto adresu neprijme ako odosielateľa obálky, použije sa namiesto nej {identity} a príjemcovia ju uvidia v hlavičke Return-Path."
},
"use_template": "Šablóna",
"save_as_template": "Uložiť ako šablónu",
+3 -2
View File
@@ -643,11 +643,12 @@
"from_override": {
"toggle_off": "Geçersiz kıl",
"toggle_on": "Geçersiz kılmayı iptal et",
"toggle_tooltip": "Gönderen adını ve adresini serbestçe düzenleyin. Posta hâlâ kimliğiniz üzerinden gönderilir - yalnızca görünür Kimden başlığı değişir.",
"toggle_tooltip": "Gönderen adını ve adresini serbestçe düzenleyin. Posta hâlâ kimliğiniz üzerinden gönderilir.",
"name_label": "Gönderen adı",
"name_placeholder": "Ad",
"email_label": "Gönderen e-posta adresi",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Sunucunuz bu adresi zarf göndereni olarak kabul etmezse yerine {identity} kullanılır ve alıcılar bunu Return-Path başlığında görebilir."
},
"use_template": "Şablon",
"save_as_template": "Şablon Olarak Kaydet",
+3 -2
View File
@@ -640,11 +640,12 @@
"from_override": {
"toggle_off": "Замінити",
"toggle_on": "Скасувати заміну",
"toggle_tooltip": "Вільно редагуйте ім'я та адресу відправника. Пошта все ще надсилається через вашу ідентичність - змінюється лише видимий заголовок Від.",
"toggle_tooltip": "Вільно редагуйте ім'я та адресу відправника. Пошта все ще надсилається через вашу ідентичність.",
"name_label": "Ім'я відправника",
"name_placeholder": "Ім'я",
"email_label": "Електронна адреса відправника",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "Якщо ваш сервер не приймає цю адресу як відправника конверта, замість неї використовується {identity}, і одержувачі бачитимуть її в заголовку Return-Path."
},
"use_template": "Шаблон",
"save_as_template": "Зберегти як шаблон",
+3 -2
View File
@@ -643,11 +643,12 @@
"from_override": {
"toggle_off": "覆寫",
"toggle_on": "取消覆寫",
"toggle_tooltip": "自由編輯寄件者名稱與電子郵件地址。郵件仍會透過您的寄件身分傳送,只有顯示的 From 標頭會變更。",
"toggle_tooltip": "自由編輯寄件者名稱與電子郵件地址。郵件仍會透過您的寄件身分傳送。",
"name_label": "寄件者姓名",
"name_placeholder": "姓名",
"email_label": "寄件者電子郵件地址",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "如果您的伺服器不接受此地址作為信封寄件者,將改用 {identity},收件者可以在 Return-Path 標頭中看到它。"
},
"use_template": "範本",
"save_as_template": "儲存為範本",
+3 -2
View File
@@ -640,11 +640,12 @@
"from_override": {
"toggle_off": "覆盖",
"toggle_on": "取消覆盖",
"toggle_tooltip": "自由编辑发件人姓名和地址。邮件仍通过您的身份发送 - 仅可见的发件人标题发生变化。",
"toggle_tooltip": "自由编辑发件人姓名和地址。邮件仍通过您的身份发送。",
"name_label": "发件人姓名",
"name_placeholder": "姓名",
"email_label": "发件人电子邮件地址",
"email_placeholder": "alias@example.com"
"email_placeholder": "alias@example.com",
"envelope_notice": "如果您的服务器不接受此地址作为信封发件人,将改用 {identity},收件人可以在 Return-Path 标头中看到它。"
},
"use_template": "模板",
"save_as_template": "保存为模板",