From 1c00cf75cbd7a7ccba28d213aaf0c6bb7bd6a328 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:58:26 +0200 Subject: [PATCH] fix: use the from override as envelope sender where the server allows it #1009 --- .../composer-from-override-envelope.test.tsx | 242 ++++++++++++++++++ components/email/email-composer.tsx | 26 +- lib/__tests__/jmap-send-envelope.test.ts | 148 +++++++++++ lib/jmap/client-interface.ts | 3 + lib/jmap/client.ts | 69 ++++- locales/ar/common.json | 5 +- locales/ca/common.json | 5 +- locales/cs/common.json | 5 +- locales/da/common.json | 5 +- locales/de/common.json | 5 +- locales/en/common.json | 5 +- locales/es/common.json | 5 +- locales/fa/common.json | 3 +- locales/fr/common.json | 5 +- locales/he/common.json | 5 +- locales/hu/common.json | 5 +- locales/it/common.json | 5 +- locales/ja/common.json | 5 +- locales/ko/common.json | 5 +- locales/lv/common.json | 5 +- locales/mn/common.json | 5 +- locales/nb/common.json | 5 +- locales/nl/common.json | 5 +- locales/pl/common.json | 5 +- locales/pt/common.json | 5 +- locales/ro/common.json | 5 +- locales/ru/common.json | 5 +- locales/sk/common.json | 3 +- locales/tr/common.json | 5 +- locales/uk/common.json | 5 +- locales/zh-TW/common.json | 5 +- locales/zh/common.json | 5 +- 32 files changed, 549 insertions(+), 70 deletions(-) create mode 100644 components/email/__tests__/composer-from-override-envelope.test.tsx create mode 100644 lib/__tests__/jmap-send-envelope.test.ts diff --git a/components/email/__tests__/composer-from-override-envelope.test.tsx b/components/email/__tests__/composer-from-override-envelope.test.tsx new file mode 100644 index 000000000..9add3f877 --- /dev/null +++ b/components/email/__tests__/composer-from-override-envelope.test.tsx @@ -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) => 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>, + defaultIdentityId: 'id-me', + }; + const hook = (sel?: (s: typeof state) => unknown) => + typeof sel === 'function' ? sel(state) : state; + hook.getState = () => state; + hook.setState = (p: Partial) => 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) => 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) => 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) => 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) => 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) => 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: '

Hi

', + 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) { + 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(); + + 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(); + expect(screen.getByText(NOTICE)).toBeInTheDocument(); + }); + + it('does not warn when an identity owns the override address', () => { + render(); + expect(screen.queryByText(NOTICE)).not.toBeInTheDocument(); + }); + + it('sends no envelope request without an override', async () => { + const onSend = vi.fn(); + render(); + expect(screen.queryByText(NOTICE)).not.toBeInTheDocument(); + + const sent = await send(onSend); + expect(sent.fromEmail).toBe('me@example.com'); + expect(sent.envelopeMailFrom).toBeUndefined(); + }); +}); diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index 736da48d2..4a234a895 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -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({ + {overrideEnvelopeFallback && ( +

+ {t('from_override.envelope_notice', { identity: overrideEnvelopeFallback })} +

+ )} {/* To field */}
diff --git a/lib/__tests__/jmap-send-envelope.test.ts b/lib/__tests__/jmap-send-envelope.test.ts new file mode 100644 index 000000000..1d18f1f00 --- /dev/null +++ b/lib/__tests__/jmap-send-envelope.test.ts @@ -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]; + +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> = []; + 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>)['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 '], + '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' } }); + }); +}); diff --git a/lib/jmap/client-interface.ts b/lib/jmap/client-interface.ts index 74eac7dbd..f8cd19a87 100644 --- a/lib/jmap/client-interface.ts +++ b/lib/jmap/client-interface.ts @@ -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 diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index 525df1f72..7cfa8b495 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -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 => { - const create: Record = { 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 => { + const create: Record = { 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; diff --git a/locales/ar/common.json b/locales/ar/common.json index 33b8e269c..beb8ce1d2 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -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": "حفظ كقالب", diff --git a/locales/ca/common.json b/locales/ca/common.json index 357d9e555..473d564ce 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -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", diff --git a/locales/cs/common.json b/locales/cs/common.json index c5c2b267a..d56568d41 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -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", diff --git a/locales/da/common.json b/locales/da/common.json index 0cd7c2cbf..eb1c70afd 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -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", diff --git a/locales/de/common.json b/locales/de/common.json index 0c191ee54..f60f704d9 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -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", diff --git a/locales/en/common.json b/locales/en/common.json index c1738ecbb..fd6b0748f 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -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", diff --git a/locales/es/common.json b/locales/es/common.json index 41c93bf48..68ab42dc8 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -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", diff --git a/locales/fa/common.json b/locales/fa/common.json index eed2075a7..234cf81dd 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -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": "ذخیره به عنوان قالب", diff --git a/locales/fr/common.json b/locales/fr/common.json index 3b7f702d0..95310d185 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -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", diff --git a/locales/he/common.json b/locales/he/common.json index acc064321..0897f879b 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -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": "נשכחת קובץ מצורף?", diff --git a/locales/hu/common.json b/locales/hu/common.json index c0ea47154..7e71f32d5 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -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", diff --git a/locales/it/common.json b/locales/it/common.json index a82942555..1e337883c 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -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", diff --git a/locales/ja/common.json b/locales/ja/common.json index 1559e49e7..3675f2deb 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -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": "テンプレートとして保存", diff --git a/locales/ko/common.json b/locales/ko/common.json index 58b3a4e25..6e1295922 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -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": "템플릿으로 저장", diff --git a/locales/lv/common.json b/locales/lv/common.json index 797a9f72c..9166d4894 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -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", diff --git a/locales/mn/common.json b/locales/mn/common.json index c7318e151..7bd45981f 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -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": "Загвар болгон хадгалах", diff --git a/locales/nb/common.json b/locales/nb/common.json index b81337894..cb379e3e7 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -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", diff --git a/locales/nl/common.json b/locales/nl/common.json index d27b9183d..710d595f9 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -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", diff --git a/locales/pl/common.json b/locales/pl/common.json index 3440eafc4..824f2508a 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -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", diff --git a/locales/pt/common.json b/locales/pt/common.json index 506083540..7643b23df 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -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", diff --git a/locales/ro/common.json b/locales/ro/common.json index 832fca2d7..edcc46676 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -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", diff --git a/locales/ru/common.json b/locales/ru/common.json index 05b44ac1c..8cb74803a 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -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": "Сохранить как шаблон", diff --git a/locales/sk/common.json b/locales/sk/common.json index 1b8eec6cb..31ca9f633 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -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", diff --git a/locales/tr/common.json b/locales/tr/common.json index 8b8992823..d69b6f487 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -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", diff --git a/locales/uk/common.json b/locales/uk/common.json index f210dbb33..335d9b084 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -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": "Зберегти як шаблон", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index 26e8e8990..4b4d98001 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -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": "儲存為範本", diff --git a/locales/zh/common.json b/locales/zh/common.json index 4751d63a6..bd387bb01 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -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": "保存为模板",