From 94af4725b6ad18a16d4bcdb9fd7709810c80337e Mon Sep 17 00:00:00 2001 From: dealerweb Date: Wed, 8 Jul 2026 15:01:54 +0200 Subject: [PATCH] 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. --- components/email/email-viewer.tsx | 24 ++++++++++++++ components/email/unsubscribe-banner.tsx | 29 +++++++++++----- lib/__tests__/validation.test.ts | 31 +++++++++++++++++ lib/validation.ts | 44 +++++++++++++++++++++++++ locales/cs/common.json | 4 ++- locales/da/common.json | 4 ++- locales/de/common.json | 4 ++- locales/en/common.json | 4 ++- locales/es/common.json | 4 ++- locales/fa/common.json | 4 ++- locales/fr/common.json | 4 ++- locales/he/common.json | 4 ++- locales/hu/common.json | 4 ++- locales/it/common.json | 4 ++- locales/ja/common.json | 4 ++- locales/ko/common.json | 4 ++- locales/lv/common.json | 4 ++- locales/nl/common.json | 4 ++- locales/pl/common.json | 4 ++- locales/pt/common.json | 4 ++- locales/ro/common.json | 4 ++- locales/ru/common.json | 4 ++- locales/sk/common.json | 4 ++- locales/tr/common.json | 4 ++- locales/uk/common.json | 4 ++- locales/zh/common.json | 4 ++- 26 files changed, 185 insertions(+), 31 deletions(-) diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index 7055e928..ca98cffd 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -714,6 +714,28 @@ export function EmailViewer({ const { tabletListVisible } = useUIStore(); const { identities, client, isDemoMode, activeAccountId } = useAuthStore(); 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 value = window.prompt(t('reschedule_prompt')); if (!value) return null; @@ -3579,6 +3601,7 @@ export function EmailViewer({ { const messageId = email?.messageId || ''; const newSet = new Set(dismissedUnsubBanners).add(messageId); @@ -3824,6 +3847,7 @@ export function EmailViewer({ { const messageId = email?.messageId || ''; const newSet = new Set(dismissedUnsubBanners).add(messageId); diff --git a/components/email/unsubscribe-banner.tsx b/components/email/unsubscribe-banner.tsx index 00736e69..01a53dad 100644 --- a/components/email/unsubscribe-banner.tsx +++ b/components/email/unsubscribe-banner.tsx @@ -3,7 +3,7 @@ import { useState, useRef, useEffect } from 'react'; import { Loader2, CheckCircle, AlertCircle } from 'lucide-react'; import { useTranslations } from 'next-intl'; -import { isValidUnsubscribeUrl } from '@/lib/validation'; +import { isValidUnsubscribeUrl, parseMailtoUrl } from '@/lib/validation'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { useIsDesktop } from '@/hooks/use-media-query'; @@ -14,12 +14,17 @@ interface UnsubscribeBannerProps { preferred?: 'http' | 'mailto'; }; 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; onDismiss: () => void; } export function UnsubscribeBanner({ listUnsubscribe, senderEmail: _senderEmail, + onSendMailtoUnsubscribe, onDismiss }: UnsubscribeBannerProps) { const t = useTranslations(); @@ -74,12 +79,18 @@ export function UnsubscribeBanner({ setShowConfirm(false); setTimeout(onDismiss, 3000); } else { - const link = document.createElement('a'); - link.href = unsubUrl; - link.style.display = 'none'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + // Send the unsubscribe message ourselves and only report success + // once the server accepted it. The previous hidden-link click handed + // the mailto: to the OS mail handler and claimed success even though + // nothing was ever sent. + const fields = parseMailtoUrl(unsubUrl); + if (!fields) { + setError(true); + setProcessing(false); + setShowConfirm(false); + return; + } + await onSendMailtoUnsubscribe(fields); setSuccess(true); setProcessing(false); @@ -171,8 +182,8 @@ export function UnsubscribeBanner({ }} title={t('email_viewer.unsubscribe_banner.confirm_title')} message={t(unsubMethod === 'http' - ? 'email_viewer.unsubscribe_banner.success_http' - : 'email_viewer.unsubscribe_banner.success_mailto' + ? 'email_viewer.unsubscribe_banner.confirm_message_http' + : 'email_viewer.unsubscribe_banner.confirm_message_mailto' )} confirmText={t('email_viewer.unsubscribe_banner.confirm_button')} cancelText={t('email_viewer.unsubscribe_banner.cancel')} diff --git a/lib/__tests__/validation.test.ts b/lib/__tests__/validation.test.ts index a1238968..e096a37c 100644 --- a/lib/__tests__/validation.test.ts +++ b/lib/__tests__/validation.test.ts @@ -5,6 +5,7 @@ import { getEmailValidationError, isValidUnsubscribeUrl, parseUnsubscribeUrls, + parseMailtoUrl, } from '../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'); + }); +}); diff --git a/lib/validation.ts b/lib/validation.ts index cb6b3f0c..0c2a5c77 100644 --- a/lib/validation.ts +++ b/lib/validation.ts @@ -121,3 +121,47 @@ export function parseUnsubscribeUrls(header: string): { 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; +} diff --git a/locales/cs/common.json b/locales/cs/common.json index 2441349d..d2f0686b 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -436,7 +436,9 @@ "confirm_button": "Potvrdit", "cancel": "Zrušit", "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", "dismiss": "Zavřít" }, diff --git a/locales/da/common.json b/locales/da/common.json index 08717b4a..9bfefea7 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -461,7 +461,9 @@ "confirm_button": "Bekræft", "cancel": "Annuller", "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", "dismiss": "Afvis" }, diff --git a/locales/de/common.json b/locales/de/common.json index 113b6c3e..7c912e69 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -434,7 +434,9 @@ "confirm_button": "Bestätigen", "cancel": "Abbrechen", "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", "dismiss": "Schließen" }, diff --git a/locales/en/common.json b/locales/en/common.json index e8a451e7..9f6b1109 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -461,7 +461,9 @@ "confirm_button": "Confirm", "cancel": "Cancel", "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", "dismiss": "Dismiss" }, diff --git a/locales/es/common.json b/locales/es/common.json index 362149a3..5bb9b924 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -434,7 +434,9 @@ "confirm_button": "Confirmar", "cancel": "Cancelar", "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", "dismiss": "Descartar" }, diff --git a/locales/fa/common.json b/locales/fa/common.json index 1d9696f6..2e890be6 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -461,7 +461,9 @@ "confirm_button": "تأیید", "cancel": "انصراف", "success_http": "صفحه لغو اشتراک در تب جدید باز شد", - "success_mailto": "درخواست لغو اشتراک ارسال شد", + "success_mailto": "ایمیل لغو اشتراک ارسال شد", + "confirm_message_http": "صفحه لغو اشتراک در برگه جدیدی باز خواهد شد.", + "confirm_message_mailto": "ایمیل لغو اشتراک برای فرستنده ارسال خواهد شد.", "error": "لغو اشتراک ممکن نیست", "dismiss": "رد کردن" }, diff --git a/locales/fr/common.json b/locales/fr/common.json index a4d93373..187b54ae 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -434,7 +434,9 @@ "confirm_button": "Confirmer", "cancel": "Annuler", "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", "dismiss": "Ignorer" }, diff --git a/locales/he/common.json b/locales/he/common.json index 64b26d91..b8186b7d 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -385,7 +385,9 @@ "confirm_button": "אשר", "cancel": "לְבַטֵל", "success_http": "דף ביטול הרשמה נפתח בכרטיסייה חדשה", - "success_mailto": "בקשת ביטול הרשמה נשלחה ללקוח הדוא\"ל שלך", + "success_mailto": "אימייל ביטול ההרשמה נשלח", + "confirm_message_http": "דף ביטול ההרשמה ייפתח בכרטיסייה חדשה.", + "confirm_message_mailto": "אימייל ביטול הרשמה יישלח לשולח.", "error": "לא ניתן לבטל את המנוי", "dismiss": "לְפַטֵר" }, diff --git a/locales/hu/common.json b/locales/hu/common.json index 6568903f..744402a1 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -461,7 +461,9 @@ "confirm_button": "Megerősítés", "cancel": "Mégse", "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", "dismiss": "Elutasítás" }, diff --git a/locales/it/common.json b/locales/it/common.json index a071bfdf..e5768cde 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -434,7 +434,9 @@ "confirm_button": "Conferma", "cancel": "Annulla", "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", "dismiss": "Ignora" }, diff --git a/locales/ja/common.json b/locales/ja/common.json index ddb4a859..46f07087 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -434,7 +434,9 @@ "confirm_button": "確認", "cancel": "キャンセル", "success_http": "購読解除ページを新しいタブで開きました", - "success_mailto": "購読解除リクエストをメールクライアントに送信しました", + "success_mailto": "配信停止メールを送信しました", + "confirm_message_http": "配信停止ページを新しいタブで開きます。", + "confirm_message_mailto": "送信者に配信停止メールを送信します。", "error": "購読解除できませんでした", "dismiss": "閉じる" }, diff --git a/locales/ko/common.json b/locales/ko/common.json index 748ffe9a..583d99a5 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -436,7 +436,9 @@ "confirm_button": "확인", "cancel": "취소", "success_http": "새 탭에서 구독 취소 페이지가 열렸어요", - "success_mailto": "이메일 클라이언트를 통해 구독 취소 요청이 전송되었어요", + "success_mailto": "수신 거부 이메일을 보냈습니다", + "confirm_message_http": "수신 거부 페이지가 새 탭에서 열립니다.", + "confirm_message_mailto": "발신자에게 수신 거부 이메일을 보냅니다.", "error": "구독을 취소할 수 없어요", "dismiss": "닫기" }, diff --git a/locales/lv/common.json b/locales/lv/common.json index c05be639..42912bbe 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -436,7 +436,9 @@ "confirm_button": "Apstiprināt", "cancel": "Atcelt", "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", "dismiss": "Aizvērt" }, diff --git a/locales/nl/common.json b/locales/nl/common.json index 68609d29..2532201e 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -434,7 +434,9 @@ "confirm_button": "Bevestigen", "cancel": "Annuleren", "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", "dismiss": "Sluiten" }, diff --git a/locales/pl/common.json b/locales/pl/common.json index 0cfa6562..cc7308db 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -436,7 +436,9 @@ "confirm_button": "Potwierdź", "cancel": "Anuluj", "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ć", "dismiss": "Zamknij" }, diff --git a/locales/pt/common.json b/locales/pt/common.json index 34b8b818..548a42ab 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -434,7 +434,9 @@ "confirm_button": "Confirmar", "cancel": "Cancelar", "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", "dismiss": "Dispensar" }, diff --git a/locales/ro/common.json b/locales/ro/common.json index 3266fa51..82dfa1fe 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -461,7 +461,9 @@ "confirm_button": "Confirmare", "cancel": "Anulează", "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", "dismiss": "Ignoră" }, diff --git a/locales/ru/common.json b/locales/ru/common.json index dbdf5ff6..8908d08e 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -436,7 +436,9 @@ "confirm_button": "Подтвердить", "cancel": "Отмена", "success_http": "Страница отписки открыта в новой вкладке", - "success_mailto": "Запрос на отписку отправлен в ваш почтовый клиент", + "success_mailto": "Письмо для отписки отправлено", + "confirm_message_http": "Страница отписки откроется в новой вкладке.", + "confirm_message_mailto": "Отправителю будет отправлено письмо для отписки.", "error": "Не удалось отписаться", "dismiss": "Закрыть" }, diff --git a/locales/sk/common.json b/locales/sk/common.json index 53b05303..699a2418 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -461,7 +461,9 @@ "confirm_button": "Potvrdiť", "cancel": "Zrušiť", "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ť", "dismiss": "Zavrieť" }, diff --git a/locales/tr/common.json b/locales/tr/common.json index c9d34c9e..3df650c3 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -436,7 +436,9 @@ "confirm_button": "Onayla", "cancel": "İptal", "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", "dismiss": "Kapat" }, diff --git a/locales/uk/common.json b/locales/uk/common.json index af31d3b5..c9971ce8 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -436,7 +436,9 @@ "confirm_button": "Підтвердити", "cancel": "Скасувати", "success_http": "Сторінка скасування підписки відкрилася в новій вкладці", - "success_mailto": "Запит на скасування підписки надіслано на ваш поштовий клієнт", + "success_mailto": "Лист для відписки надіслано", + "confirm_message_http": "Сторінка відписки відкриється в новій вкладці.", + "confirm_message_mailto": "Відправнику буде надіслано лист для відписки.", "error": "Неможливо скасувати підписку", "dismiss": "Відхилити" }, diff --git a/locales/zh/common.json b/locales/zh/common.json index 0f1cf3a4..5821fc96 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -436,7 +436,9 @@ "confirm_button": "确认", "cancel": "取消", "success_http": "取消订阅页面已在新标签页中打开", - "success_mailto": "取消订阅请求已发送", + "success_mailto": "退订邮件已发送", + "confirm_message_http": "退订页面将在新标签页中打开。", + "confirm_message_mailto": "将向发件人发送退订邮件。", "error": "无法取消订阅", "dismiss": "关闭" },