Fix: send mailto unsubscribe ourselves instead of via the OS handler
The List-Unsubscribe action for mailto: links created a hidden anchor, clicked it and reported success. That hands the mailto: URL to the OS default mail handler - for a webmail user that opens the wrong program or nothing at all, and the unsubscribe message is never sent, while the banner still claims it was. The confirm flow now parses the mailto: URL (address, subject, body - percent-decoded manually since RFC 6068 does not use plus-encoding) and sends the message through the account's own JMAP client, preferring the identity that received the newsletter so the list can match the subscriber. In unified views the send is routed to the email's owning account. Success is only reported once the server accepted the message. The mobile confirm dialog reused the success strings as its question text; it gets proper confirm_message strings in all 22 locales, and success_mailto now says what actually happened.
This commit is contained in:
@@ -714,6 +714,28 @@ export function EmailViewer({
|
|||||||
const { tabletListVisible } = useUIStore();
|
const { tabletListVisible } = useUIStore();
|
||||||
const { identities, client, isDemoMode, activeAccountId } = useAuthStore();
|
const { identities, client, isDemoMode, activeAccountId } = useAuthStore();
|
||||||
const activeAccount = useAccountStore((s) => s.accounts.find((a) => a.id === activeAccountId));
|
const activeAccount = useAccountStore((s) => s.accounts.find((a) => a.id === activeAccountId));
|
||||||
|
|
||||||
|
// List-Unsubscribe mailto: send the message ourselves - this is a webmail
|
||||||
|
// client, handing a mailto: URL to the OS mail handler goes nowhere for
|
||||||
|
// most users. Route to the email's own account in unified views and prefer
|
||||||
|
// the identity that received the newsletter, so the list can match the
|
||||||
|
// subscriber; sendEmail resolves the identity (with its own fallback to
|
||||||
|
// the account default) from the address we pass.
|
||||||
|
const handleSendMailtoUnsubscribe = async (fields: { to: string[]; subject?: string; body?: string }) => {
|
||||||
|
const sendClient = (email?.sourceClientAccountId
|
||||||
|
? useAuthStore.getState().getClientForAccount(email.sourceClientAccountId)
|
||||||
|
: undefined) ?? client;
|
||||||
|
if (!sendClient) throw new Error('Not connected');
|
||||||
|
|
||||||
|
const recipientAddresses = [...(email?.to ?? []), ...(email?.cc ?? [])].map(r => r.email?.toLowerCase());
|
||||||
|
// In unified views the owning account's identities are not loaded here -
|
||||||
|
// pass nothing and let its client fall back to its default identity.
|
||||||
|
const fromIdentity = email?.sourceClientAccountId
|
||||||
|
? undefined
|
||||||
|
: identities.find(i => i.email && recipientAddresses.includes(i.email.toLowerCase()));
|
||||||
|
|
||||||
|
await sendClient.sendEmail(fields.to, fields.subject ?? '', fields.body ?? '', undefined, undefined, fromIdentity?.id, fromIdentity?.email, undefined, fromIdentity?.name);
|
||||||
|
};
|
||||||
const promptForRescheduleDelayedUntil = useCallback((): string | null => {
|
const promptForRescheduleDelayedUntil = useCallback((): string | null => {
|
||||||
const value = window.prompt(t('reschedule_prompt'));
|
const value = window.prompt(t('reschedule_prompt'));
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
@@ -3579,6 +3601,7 @@ export function EmailViewer({
|
|||||||
<UnsubscribeBanner
|
<UnsubscribeBanner
|
||||||
listUnsubscribe={listHeaders.listUnsubscribe}
|
listUnsubscribe={listHeaders.listUnsubscribe}
|
||||||
senderEmail={email?.from?.[0]?.email || ''}
|
senderEmail={email?.from?.[0]?.email || ''}
|
||||||
|
onSendMailtoUnsubscribe={handleSendMailtoUnsubscribe}
|
||||||
onDismiss={() => {
|
onDismiss={() => {
|
||||||
const messageId = email?.messageId || '';
|
const messageId = email?.messageId || '';
|
||||||
const newSet = new Set(dismissedUnsubBanners).add(messageId);
|
const newSet = new Set(dismissedUnsubBanners).add(messageId);
|
||||||
@@ -3824,6 +3847,7 @@ export function EmailViewer({
|
|||||||
<UnsubscribeBanner
|
<UnsubscribeBanner
|
||||||
listUnsubscribe={listHeaders.listUnsubscribe}
|
listUnsubscribe={listHeaders.listUnsubscribe}
|
||||||
senderEmail={email?.from?.[0]?.email || ''}
|
senderEmail={email?.from?.[0]?.email || ''}
|
||||||
|
onSendMailtoUnsubscribe={handleSendMailtoUnsubscribe}
|
||||||
onDismiss={() => {
|
onDismiss={() => {
|
||||||
const messageId = email?.messageId || '';
|
const messageId = email?.messageId || '';
|
||||||
const newSet = new Set(dismissedUnsubBanners).add(messageId);
|
const newSet = new Set(dismissedUnsubBanners).add(messageId);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { Loader2, CheckCircle, AlertCircle } from 'lucide-react';
|
import { Loader2, CheckCircle, AlertCircle } from 'lucide-react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { isValidUnsubscribeUrl } from '@/lib/validation';
|
import { isValidUnsubscribeUrl, parseMailtoUrl } from '@/lib/validation';
|
||||||
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||||
import { useIsDesktop } from '@/hooks/use-media-query';
|
import { useIsDesktop } from '@/hooks/use-media-query';
|
||||||
|
|
||||||
@@ -14,12 +14,17 @@ interface UnsubscribeBannerProps {
|
|||||||
preferred?: 'http' | 'mailto';
|
preferred?: 'http' | 'mailto';
|
||||||
};
|
};
|
||||||
senderEmail: string;
|
senderEmail: string;
|
||||||
|
// Sends the unsubscribe message through the app's own account. This is a
|
||||||
|
// webmail client - handing a mailto: URL to the OS mail handler goes
|
||||||
|
// nowhere for most users.
|
||||||
|
onSendMailtoUnsubscribe: (fields: { to: string[]; subject?: string; body?: string }) => Promise<void>;
|
||||||
onDismiss: () => void;
|
onDismiss: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UnsubscribeBanner({
|
export function UnsubscribeBanner({
|
||||||
listUnsubscribe,
|
listUnsubscribe,
|
||||||
senderEmail: _senderEmail,
|
senderEmail: _senderEmail,
|
||||||
|
onSendMailtoUnsubscribe,
|
||||||
onDismiss
|
onDismiss
|
||||||
}: UnsubscribeBannerProps) {
|
}: UnsubscribeBannerProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -74,12 +79,18 @@ export function UnsubscribeBanner({
|
|||||||
setShowConfirm(false);
|
setShowConfirm(false);
|
||||||
setTimeout(onDismiss, 3000);
|
setTimeout(onDismiss, 3000);
|
||||||
} else {
|
} else {
|
||||||
const link = document.createElement('a');
|
// Send the unsubscribe message ourselves and only report success
|
||||||
link.href = unsubUrl;
|
// once the server accepted it. The previous hidden-link click handed
|
||||||
link.style.display = 'none';
|
// the mailto: to the OS mail handler and claimed success even though
|
||||||
document.body.appendChild(link);
|
// nothing was ever sent.
|
||||||
link.click();
|
const fields = parseMailtoUrl(unsubUrl);
|
||||||
document.body.removeChild(link);
|
if (!fields) {
|
||||||
|
setError(true);
|
||||||
|
setProcessing(false);
|
||||||
|
setShowConfirm(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await onSendMailtoUnsubscribe(fields);
|
||||||
|
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
@@ -171,8 +182,8 @@ export function UnsubscribeBanner({
|
|||||||
}}
|
}}
|
||||||
title={t('email_viewer.unsubscribe_banner.confirm_title')}
|
title={t('email_viewer.unsubscribe_banner.confirm_title')}
|
||||||
message={t(unsubMethod === 'http'
|
message={t(unsubMethod === 'http'
|
||||||
? 'email_viewer.unsubscribe_banner.success_http'
|
? 'email_viewer.unsubscribe_banner.confirm_message_http'
|
||||||
: 'email_viewer.unsubscribe_banner.success_mailto'
|
: 'email_viewer.unsubscribe_banner.confirm_message_mailto'
|
||||||
)}
|
)}
|
||||||
confirmText={t('email_viewer.unsubscribe_banner.confirm_button')}
|
confirmText={t('email_viewer.unsubscribe_banner.confirm_button')}
|
||||||
cancelText={t('email_viewer.unsubscribe_banner.cancel')}
|
cancelText={t('email_viewer.unsubscribe_banner.cancel')}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
getEmailValidationError,
|
getEmailValidationError,
|
||||||
isValidUnsubscribeUrl,
|
isValidUnsubscribeUrl,
|
||||||
parseUnsubscribeUrls,
|
parseUnsubscribeUrls,
|
||||||
|
parseMailtoUrl,
|
||||||
} from '../validation';
|
} from '../validation';
|
||||||
|
|
||||||
describe('validation', () => {
|
describe('validation', () => {
|
||||||
@@ -359,3 +360,33 @@ describe('validation', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('parseMailtoUrl', () => {
|
||||||
|
it('parses address, subject and body', () => {
|
||||||
|
const r = parseMailtoUrl('mailto:list@example.com?subject=Unsubscribe%20123&body=Please%20remove');
|
||||||
|
expect(r).toEqual({ to: ['list@example.com'], subject: 'Unsubscribe 123', body: 'Please remove' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps a literal plus (RFC 6068 uses percent-encoding only)', () => {
|
||||||
|
const r = parseMailtoUrl('mailto:owner+unsub@example.com?subject=a+b');
|
||||||
|
expect(r?.to).toEqual(['owner+unsub@example.com']);
|
||||||
|
expect(r?.subject).toBe('a+b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports multiple recipients and the to param', () => {
|
||||||
|
const r = parseMailtoUrl('mailto:a@example.com,b@example.com?to=c@example.com');
|
||||||
|
expect(r?.to).toEqual(['a@example.com', 'b@example.com', 'c@example.com']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null without a valid recipient', () => {
|
||||||
|
expect(parseMailtoUrl('mailto:?subject=x')).toBeNull();
|
||||||
|
expect(parseMailtoUrl('mailto:not-an-address')).toBeNull();
|
||||||
|
expect(parseMailtoUrl('https://example.com/unsub')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('survives malformed percent-encoding', () => {
|
||||||
|
const r = parseMailtoUrl('mailto:list@example.com?subject=%E0%A4%A');
|
||||||
|
expect(r?.to).toEqual(['list@example.com']);
|
||||||
|
expect(r?.subject).toBe('%E0%A4%A');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -121,3 +121,47 @@ export function parseUnsubscribeUrls(header: string): {
|
|||||||
|
|
||||||
return { http, mailto, preferred };
|
return { http, mailto, preferred };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a mailto: URL into its parts so the client can send the message
|
||||||
|
* itself. Query values are percent-decoded manually rather than via
|
||||||
|
* URLSearchParams because RFC 6068 uses %-encoding only - a literal "+"
|
||||||
|
* in a subject or address must stay a plus, not become a space.
|
||||||
|
* @param url - mailto: URL, e.g. "mailto:a@b.c?subject=Unsubscribe%20123"
|
||||||
|
* @returns Recipients plus optional subject/body, or null without a valid recipient
|
||||||
|
*/
|
||||||
|
export function parseMailtoUrl(url: string): { to: string[]; subject?: string; body?: string } | null {
|
||||||
|
if (!url?.startsWith('mailto:')) return null;
|
||||||
|
|
||||||
|
const rest = url.slice(7);
|
||||||
|
const queryIndex = rest.indexOf('?');
|
||||||
|
const addressPart = queryIndex === -1 ? rest : rest.slice(0, queryIndex);
|
||||||
|
const query = queryIndex === -1 ? '' : rest.slice(queryIndex + 1);
|
||||||
|
|
||||||
|
const decode = (value: string): string => {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(value);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const to = addressPart
|
||||||
|
.split(',')
|
||||||
|
.map(a => decode(a).trim())
|
||||||
|
.filter(a => isValidEmail(a));
|
||||||
|
|
||||||
|
let subject: string | undefined;
|
||||||
|
let body: string | undefined;
|
||||||
|
for (const pair of query.split('&')) {
|
||||||
|
const eq = pair.indexOf('=');
|
||||||
|
if (eq === -1) continue;
|
||||||
|
const key = pair.slice(0, eq).toLowerCase();
|
||||||
|
const value = decode(pair.slice(eq + 1));
|
||||||
|
if (key === 'subject') subject = value;
|
||||||
|
else if (key === 'body') body = value;
|
||||||
|
else if (key === 'to' && isValidEmail(value.trim())) to.push(value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
return to.length > 0 ? { to, subject, body } : null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -436,7 +436,9 @@
|
|||||||
"confirm_button": "Potvrdit",
|
"confirm_button": "Potvrdit",
|
||||||
"cancel": "Zrušit",
|
"cancel": "Zrušit",
|
||||||
"success_http": "Stránka pro odhlášení byla otevřena na nové kartě",
|
"success_http": "Stránka pro odhlášení byla otevřena na nové kartě",
|
||||||
"success_mailto": "Požadavek na odhlášení byl odeslán do e-mailového klienta",
|
"success_mailto": "Odhlašovací e-mail byl odeslán",
|
||||||
|
"confirm_message_http": "Odhlašovací stránka se otevře na nové kartě.",
|
||||||
|
"confirm_message_mailto": "Odesílateli bude zaslán odhlašovací e-mail.",
|
||||||
"error": "Odběr nelze odhlásit",
|
"error": "Odběr nelze odhlásit",
|
||||||
"dismiss": "Zavřít"
|
"dismiss": "Zavřít"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -461,7 +461,9 @@
|
|||||||
"confirm_button": "Bekræft",
|
"confirm_button": "Bekræft",
|
||||||
"cancel": "Annuller",
|
"cancel": "Annuller",
|
||||||
"success_http": "Afmeldingsside åbnet i ny fane",
|
"success_http": "Afmeldingsside åbnet i ny fane",
|
||||||
"success_mailto": "Afmeldingsanmodning sendt til din e-mailklient",
|
"success_mailto": "Afmeldings-e-mail sendt",
|
||||||
|
"confirm_message_http": "Afmeldingssiden åbnes i en ny fane.",
|
||||||
|
"confirm_message_mailto": "Der sendes en afmeldings-e-mail til afsenderen.",
|
||||||
"error": "Kan ikke afmelde",
|
"error": "Kan ikke afmelde",
|
||||||
"dismiss": "Afvis"
|
"dismiss": "Afvis"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -434,7 +434,9 @@
|
|||||||
"confirm_button": "Bestätigen",
|
"confirm_button": "Bestätigen",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"success_http": "Abmeldeseite in neuem Tab geöffnet",
|
"success_http": "Abmeldeseite in neuem Tab geöffnet",
|
||||||
"success_mailto": "Abmeldeanfrage an Ihr E-Mail-Programm gesendet",
|
"success_mailto": "Abmelde-E-Mail gesendet",
|
||||||
|
"confirm_message_http": "Die Abmeldeseite wird in einem neuen Tab geöffnet.",
|
||||||
|
"confirm_message_mailto": "Es wird eine Abmelde-E-Mail an den Absender gesendet.",
|
||||||
"error": "Abmeldung nicht möglich",
|
"error": "Abmeldung nicht möglich",
|
||||||
"dismiss": "Schließen"
|
"dismiss": "Schließen"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -461,7 +461,9 @@
|
|||||||
"confirm_button": "Confirm",
|
"confirm_button": "Confirm",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"success_http": "Unsubscribe page opened in new tab",
|
"success_http": "Unsubscribe page opened in new tab",
|
||||||
"success_mailto": "Unsubscribe request sent to your email client",
|
"success_mailto": "Unsubscribe email sent",
|
||||||
|
"confirm_message_http": "The unsubscribe page will open in a new tab.",
|
||||||
|
"confirm_message_mailto": "An unsubscribe email will be sent to the sender.",
|
||||||
"error": "Unable to unsubscribe",
|
"error": "Unable to unsubscribe",
|
||||||
"dismiss": "Dismiss"
|
"dismiss": "Dismiss"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -434,7 +434,9 @@
|
|||||||
"confirm_button": "Confirmar",
|
"confirm_button": "Confirmar",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"success_http": "Página de cancelación de suscripción abierta en nueva pestaña",
|
"success_http": "Página de cancelación de suscripción abierta en nueva pestaña",
|
||||||
"success_mailto": "Solicitud de cancelación enviada a su cliente de correo",
|
"success_mailto": "Correo de cancelación enviado",
|
||||||
|
"confirm_message_http": "La página de cancelación se abrirá en una pestaña nueva.",
|
||||||
|
"confirm_message_mailto": "Se enviará un correo de cancelación al remitente.",
|
||||||
"error": "No se pudo cancelar la suscripción",
|
"error": "No se pudo cancelar la suscripción",
|
||||||
"dismiss": "Descartar"
|
"dismiss": "Descartar"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -461,7 +461,9 @@
|
|||||||
"confirm_button": "تأیید",
|
"confirm_button": "تأیید",
|
||||||
"cancel": "انصراف",
|
"cancel": "انصراف",
|
||||||
"success_http": "صفحه لغو اشتراک در تب جدید باز شد",
|
"success_http": "صفحه لغو اشتراک در تب جدید باز شد",
|
||||||
"success_mailto": "درخواست لغو اشتراک ارسال شد",
|
"success_mailto": "ایمیل لغو اشتراک ارسال شد",
|
||||||
|
"confirm_message_http": "صفحه لغو اشتراک در برگه جدیدی باز خواهد شد.",
|
||||||
|
"confirm_message_mailto": "ایمیل لغو اشتراک برای فرستنده ارسال خواهد شد.",
|
||||||
"error": "لغو اشتراک ممکن نیست",
|
"error": "لغو اشتراک ممکن نیست",
|
||||||
"dismiss": "رد کردن"
|
"dismiss": "رد کردن"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -434,7 +434,9 @@
|
|||||||
"confirm_button": "Confirmer",
|
"confirm_button": "Confirmer",
|
||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"success_http": "Page de désabonnement ouverte dans un nouvel onglet",
|
"success_http": "Page de désabonnement ouverte dans un nouvel onglet",
|
||||||
"success_mailto": "Demande de désabonnement envoyée à votre client mail",
|
"success_mailto": "E-mail de désabonnement envoyé",
|
||||||
|
"confirm_message_http": "La page de désabonnement s'ouvrira dans un nouvel onglet.",
|
||||||
|
"confirm_message_mailto": "Un e-mail de désabonnement sera envoyé à l'expéditeur.",
|
||||||
"error": "Impossible de se désabonner",
|
"error": "Impossible de se désabonner",
|
||||||
"dismiss": "Ignorer"
|
"dismiss": "Ignorer"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -385,7 +385,9 @@
|
|||||||
"confirm_button": "אשר",
|
"confirm_button": "אשר",
|
||||||
"cancel": "לְבַטֵל",
|
"cancel": "לְבַטֵל",
|
||||||
"success_http": "דף ביטול הרשמה נפתח בכרטיסייה חדשה",
|
"success_http": "דף ביטול הרשמה נפתח בכרטיסייה חדשה",
|
||||||
"success_mailto": "בקשת ביטול הרשמה נשלחה ללקוח הדוא\"ל שלך",
|
"success_mailto": "אימייל ביטול ההרשמה נשלח",
|
||||||
|
"confirm_message_http": "דף ביטול ההרשמה ייפתח בכרטיסייה חדשה.",
|
||||||
|
"confirm_message_mailto": "אימייל ביטול הרשמה יישלח לשולח.",
|
||||||
"error": "לא ניתן לבטל את המנוי",
|
"error": "לא ניתן לבטל את המנוי",
|
||||||
"dismiss": "לְפַטֵר"
|
"dismiss": "לְפַטֵר"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -461,7 +461,9 @@
|
|||||||
"confirm_button": "Megerősítés",
|
"confirm_button": "Megerősítés",
|
||||||
"cancel": "Mégse",
|
"cancel": "Mégse",
|
||||||
"success_http": "Leiratkozási oldal megnyílt egy új lapon",
|
"success_http": "Leiratkozási oldal megnyílt egy új lapon",
|
||||||
"success_mailto": "Leiratkozási kérelem elküldve az e-mail kliensnek",
|
"success_mailto": "Leiratkozó e-mail elküldve",
|
||||||
|
"confirm_message_http": "A leiratkozási oldal új lapon nyílik meg.",
|
||||||
|
"confirm_message_mailto": "Leiratkozó e-mailt küldünk a feladónak.",
|
||||||
"error": "Nem sikerült leiratkozni",
|
"error": "Nem sikerült leiratkozni",
|
||||||
"dismiss": "Elutasítás"
|
"dismiss": "Elutasítás"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -434,7 +434,9 @@
|
|||||||
"confirm_button": "Conferma",
|
"confirm_button": "Conferma",
|
||||||
"cancel": "Annulla",
|
"cancel": "Annulla",
|
||||||
"success_http": "Pagina di annullamento iscrizione aperta in una nuova scheda",
|
"success_http": "Pagina di annullamento iscrizione aperta in una nuova scheda",
|
||||||
"success_mailto": "Richiesta di annullamento iscrizione inviata al tuo client email",
|
"success_mailto": "Email di disiscrizione inviata",
|
||||||
|
"confirm_message_http": "La pagina di disiscrizione si aprirà in una nuova scheda.",
|
||||||
|
"confirm_message_mailto": "Verrà inviata un'email di disiscrizione al mittente.",
|
||||||
"error": "Impossibile annullare l'iscrizione",
|
"error": "Impossibile annullare l'iscrizione",
|
||||||
"dismiss": "Ignora"
|
"dismiss": "Ignora"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -434,7 +434,9 @@
|
|||||||
"confirm_button": "確認",
|
"confirm_button": "確認",
|
||||||
"cancel": "キャンセル",
|
"cancel": "キャンセル",
|
||||||
"success_http": "購読解除ページを新しいタブで開きました",
|
"success_http": "購読解除ページを新しいタブで開きました",
|
||||||
"success_mailto": "購読解除リクエストをメールクライアントに送信しました",
|
"success_mailto": "配信停止メールを送信しました",
|
||||||
|
"confirm_message_http": "配信停止ページを新しいタブで開きます。",
|
||||||
|
"confirm_message_mailto": "送信者に配信停止メールを送信します。",
|
||||||
"error": "購読解除できませんでした",
|
"error": "購読解除できませんでした",
|
||||||
"dismiss": "閉じる"
|
"dismiss": "閉じる"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -436,7 +436,9 @@
|
|||||||
"confirm_button": "확인",
|
"confirm_button": "확인",
|
||||||
"cancel": "취소",
|
"cancel": "취소",
|
||||||
"success_http": "새 탭에서 구독 취소 페이지가 열렸어요",
|
"success_http": "새 탭에서 구독 취소 페이지가 열렸어요",
|
||||||
"success_mailto": "이메일 클라이언트를 통해 구독 취소 요청이 전송되었어요",
|
"success_mailto": "수신 거부 이메일을 보냈습니다",
|
||||||
|
"confirm_message_http": "수신 거부 페이지가 새 탭에서 열립니다.",
|
||||||
|
"confirm_message_mailto": "발신자에게 수신 거부 이메일을 보냅니다.",
|
||||||
"error": "구독을 취소할 수 없어요",
|
"error": "구독을 취소할 수 없어요",
|
||||||
"dismiss": "닫기"
|
"dismiss": "닫기"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -436,7 +436,9 @@
|
|||||||
"confirm_button": "Apstiprināt",
|
"confirm_button": "Apstiprināt",
|
||||||
"cancel": "Atcelt",
|
"cancel": "Atcelt",
|
||||||
"success_http": "Atteikšanās lapa atvērta jaunā cilnē",
|
"success_http": "Atteikšanās lapa atvērta jaunā cilnē",
|
||||||
"success_mailto": "Atteikšanās pieprasījums nosūtīts",
|
"success_mailto": "Atrakstīšanās e-pasts nosūtīts",
|
||||||
|
"confirm_message_http": "Atrakstīšanās lapa tiks atvērta jaunā cilnē.",
|
||||||
|
"confirm_message_mailto": "Sūtītājam tiks nosūtīts atrakstīšanās e-pasts.",
|
||||||
"error": "Neizdevās atteikties",
|
"error": "Neizdevās atteikties",
|
||||||
"dismiss": "Aizvērt"
|
"dismiss": "Aizvērt"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -434,7 +434,9 @@
|
|||||||
"confirm_button": "Bevestigen",
|
"confirm_button": "Bevestigen",
|
||||||
"cancel": "Annuleren",
|
"cancel": "Annuleren",
|
||||||
"success_http": "Uitschrijfpagina geopend in nieuw tabblad",
|
"success_http": "Uitschrijfpagina geopend in nieuw tabblad",
|
||||||
"success_mailto": "Uitschrijfverzoek verzonden naar je e-mailclient",
|
"success_mailto": "Afmeldingsmail verzonden",
|
||||||
|
"confirm_message_http": "De afmeldpagina wordt in een nieuw tabblad geopend.",
|
||||||
|
"confirm_message_mailto": "Er wordt een afmeldingsmail naar de afzender gestuurd.",
|
||||||
"error": "Kan niet uitschrijven",
|
"error": "Kan niet uitschrijven",
|
||||||
"dismiss": "Sluiten"
|
"dismiss": "Sluiten"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -436,7 +436,9 @@
|
|||||||
"confirm_button": "Potwierdź",
|
"confirm_button": "Potwierdź",
|
||||||
"cancel": "Anuluj",
|
"cancel": "Anuluj",
|
||||||
"success_http": "Strona wypisania otwarta w nowej karcie",
|
"success_http": "Strona wypisania otwarta w nowej karcie",
|
||||||
"success_mailto": "Żądanie wypisania wysłane do klienta poczty",
|
"success_mailto": "Wysłano e-mail rezygnacji z subskrypcji",
|
||||||
|
"confirm_message_http": "Strona rezygnacji z subskrypcji otworzy się w nowej karcie.",
|
||||||
|
"confirm_message_mailto": "Do nadawcy zostanie wysłany e-mail rezygnacji z subskrypcji.",
|
||||||
"error": "Nie można się wypisać",
|
"error": "Nie można się wypisać",
|
||||||
"dismiss": "Zamknij"
|
"dismiss": "Zamknij"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -434,7 +434,9 @@
|
|||||||
"confirm_button": "Confirmar",
|
"confirm_button": "Confirmar",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"success_http": "Página de cancelamento aberta em nova aba",
|
"success_http": "Página de cancelamento aberta em nova aba",
|
||||||
"success_mailto": "Solicitação de cancelamento enviada para seu cliente de e-mail",
|
"success_mailto": "E-mail de cancelamento enviado",
|
||||||
|
"confirm_message_http": "A página de cancelamento será aberta em uma nova aba.",
|
||||||
|
"confirm_message_mailto": "Um e-mail de cancelamento será enviado ao remetente.",
|
||||||
"error": "Não foi possível cancelar a inscrição",
|
"error": "Não foi possível cancelar a inscrição",
|
||||||
"dismiss": "Dispensar"
|
"dismiss": "Dispensar"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -461,7 +461,9 @@
|
|||||||
"confirm_button": "Confirmare",
|
"confirm_button": "Confirmare",
|
||||||
"cancel": "Anulează",
|
"cancel": "Anulează",
|
||||||
"success_http": "Pagina de dezabonare se deschide într-o filă nouă",
|
"success_http": "Pagina de dezabonare se deschide într-o filă nouă",
|
||||||
"success_mailto": "Cerere de dezabonare trimisă către clientul dvs. de e-mail",
|
"success_mailto": "E-mailul de dezabonare a fost trimis",
|
||||||
|
"confirm_message_http": "Pagina de dezabonare se va deschide într-o filă nouă.",
|
||||||
|
"confirm_message_mailto": "Un e-mail de dezabonare va fi trimis expeditorului.",
|
||||||
"error": "Nu se poate dezabona",
|
"error": "Nu se poate dezabona",
|
||||||
"dismiss": "Ignoră"
|
"dismiss": "Ignoră"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -436,7 +436,9 @@
|
|||||||
"confirm_button": "Подтвердить",
|
"confirm_button": "Подтвердить",
|
||||||
"cancel": "Отмена",
|
"cancel": "Отмена",
|
||||||
"success_http": "Страница отписки открыта в новой вкладке",
|
"success_http": "Страница отписки открыта в новой вкладке",
|
||||||
"success_mailto": "Запрос на отписку отправлен в ваш почтовый клиент",
|
"success_mailto": "Письмо для отписки отправлено",
|
||||||
|
"confirm_message_http": "Страница отписки откроется в новой вкладке.",
|
||||||
|
"confirm_message_mailto": "Отправителю будет отправлено письмо для отписки.",
|
||||||
"error": "Не удалось отписаться",
|
"error": "Не удалось отписаться",
|
||||||
"dismiss": "Закрыть"
|
"dismiss": "Закрыть"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -461,7 +461,9 @@
|
|||||||
"confirm_button": "Potvrdiť",
|
"confirm_button": "Potvrdiť",
|
||||||
"cancel": "Zrušiť",
|
"cancel": "Zrušiť",
|
||||||
"success_http": "Stránka pre odhlásenie bola otvorená v novej karte",
|
"success_http": "Stránka pre odhlásenie bola otvorená v novej karte",
|
||||||
"success_mailto": "Požiadavka na odhlásenie bola odoslaná do e-mailového klienta",
|
"success_mailto": "E-mail na odhlásenie bol odoslaný",
|
||||||
|
"confirm_message_http": "Stránka odhlásenia sa otvorí na novej karte.",
|
||||||
|
"confirm_message_mailto": "Odosielateľovi bude odoslaný e-mail na odhlásenie.",
|
||||||
"error": "Odber sa nedá odhlásiť",
|
"error": "Odber sa nedá odhlásiť",
|
||||||
"dismiss": "Zavrieť"
|
"dismiss": "Zavrieť"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -436,7 +436,9 @@
|
|||||||
"confirm_button": "Onayla",
|
"confirm_button": "Onayla",
|
||||||
"cancel": "İptal",
|
"cancel": "İptal",
|
||||||
"success_http": "Abonelik iptali sayfası yeni sekmede açıldı",
|
"success_http": "Abonelik iptali sayfası yeni sekmede açıldı",
|
||||||
"success_mailto": "Abonelik iptali isteği e-posta istemcinize gönderildi",
|
"success_mailto": "Abonelikten çıkma e-postası gönderildi",
|
||||||
|
"confirm_message_http": "Abonelikten çıkma sayfası yeni sekmede açılacak.",
|
||||||
|
"confirm_message_mailto": "Gönderene abonelikten çıkma e-postası gönderilecek.",
|
||||||
"error": "Abonelik iptal edilemedi",
|
"error": "Abonelik iptal edilemedi",
|
||||||
"dismiss": "Kapat"
|
"dismiss": "Kapat"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -436,7 +436,9 @@
|
|||||||
"confirm_button": "Підтвердити",
|
"confirm_button": "Підтвердити",
|
||||||
"cancel": "Скасувати",
|
"cancel": "Скасувати",
|
||||||
"success_http": "Сторінка скасування підписки відкрилася в новій вкладці",
|
"success_http": "Сторінка скасування підписки відкрилася в новій вкладці",
|
||||||
"success_mailto": "Запит на скасування підписки надіслано на ваш поштовий клієнт",
|
"success_mailto": "Лист для відписки надіслано",
|
||||||
|
"confirm_message_http": "Сторінка відписки відкриється в новій вкладці.",
|
||||||
|
"confirm_message_mailto": "Відправнику буде надіслано лист для відписки.",
|
||||||
"error": "Неможливо скасувати підписку",
|
"error": "Неможливо скасувати підписку",
|
||||||
"dismiss": "Відхилити"
|
"dismiss": "Відхилити"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -436,7 +436,9 @@
|
|||||||
"confirm_button": "确认",
|
"confirm_button": "确认",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"success_http": "取消订阅页面已在新标签页中打开",
|
"success_http": "取消订阅页面已在新标签页中打开",
|
||||||
"success_mailto": "取消订阅请求已发送",
|
"success_mailto": "退订邮件已发送",
|
||||||
|
"confirm_message_http": "退订页面将在新标签页中打开。",
|
||||||
|
"confirm_message_mailto": "将向发件人发送退订邮件。",
|
||||||
"error": "无法取消订阅",
|
"error": "无法取消订阅",
|
||||||
"dismiss": "关闭"
|
"dismiss": "关闭"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user