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:
dealerweb
2026-07-08 15:41:46 +02:00
committed by Linus Rath
parent 22418c17cf
commit 94af4725b6
26 changed files with 185 additions and 31 deletions
+24
View File
@@ -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);
+20 -9
View File
@@ -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')}
+31
View File
@@ -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');
});
});
+44
View File
@@ -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;
}
+3 -1
View File
@@ -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"
}, },
+3 -1
View File
@@ -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"
}, },
+3 -1
View File
@@ -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"
}, },
+3 -1
View File
@@ -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"
}, },
+3 -1
View File
@@ -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"
}, },
+3 -1
View File
@@ -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": "رد کردن"
}, },
+3 -1
View File
@@ -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"
}, },
+3 -1
View File
@@ -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": "לְפַטֵר"
}, },
+3 -1
View File
@@ -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"
}, },
+3 -1
View File
@@ -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"
}, },
+3 -1
View File
@@ -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": "閉じる"
}, },
+3 -1
View File
@@ -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": "닫기"
}, },
+3 -1
View File
@@ -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"
}, },
+3 -1
View File
@@ -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"
}, },
+3 -1
View File
@@ -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"
}, },
+3 -1
View File
@@ -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"
}, },
+3 -1
View File
@@ -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ă"
}, },
+3 -1
View File
@@ -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": "Закрыть"
}, },
+3 -1
View File
@@ -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ť"
}, },
+3 -1
View File
@@ -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"
}, },
+3 -1
View File
@@ -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": "Відхилити"
}, },
+3 -1
View File
@@ -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": "关闭"
}, },