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 7055e9288..ca98cffdb 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 00736e697..01a53dad4 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 a1238968e..e096a37c9 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 cb6b3f0c0..0c2a5c770 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 2441349db..d2f0686bb 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 08717b4a7..9bfefea79 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 113b6c3e9..7c912e69e 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 e8a451e73..9f6b1109d 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 362149a3e..5bb9b924a 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 1d9696f6e..2e890be6f 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 a4d933732..187b54aeb 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 64b26d91d..b8186b7d6 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 6568903fb..744402a14 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 a071bfdf5..e5768cde3 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 ddb4a859a..46f070879 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 748ffe9a0..583d99a51 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 c05be639e..42912bbee 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 68609d297..2532201e3 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 0cfa65627..cc7308db2 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 34b8b8188..548a42abf 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 3266fa519..82dfa1fef 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 dbdf5ff6c..8908d08ee 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 53b053038..699a2418f 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 c9d34c9e8..3df650c3e 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 af31d3b50..c9971ce85 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 0f1cf3a4b..5821fc961 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": "关闭" },