mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-02 08:15:55 +00:00
fix: use the from override as envelope sender where the server allows it #1009
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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")}>
|
||||
|
||||
@@ -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' } });
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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": "حفظ كقالب",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "ذخیره به عنوان قالب",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "נשכחת קובץ מצורף?",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "テンプレートとして保存",
|
||||
|
||||
@@ -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": "템플릿으로 저장",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Загвар болгон хадгалах",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Сохранить как шаблон",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Зберегти як шаблон",
|
||||
|
||||
@@ -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": "儲存為範本",
|
||||
|
||||
@@ -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": "保存为模板",
|
||||
|
||||
Reference in New Issue
Block a user