Feature: read receipts (MDN, RFC 8098)
Bulwark had no read-receipt support (JMAP/Stalwart have no native MDN). End-to-end, client-side, in three parts: - Request (compose): a toolbar toggle (MailCheck, green when on) sets Disposition-Notification-To on the outgoing message via the JMAP "header:<name>:asText" create property. Threaded composer -> page -> email-store -> client.sendEmail. Default from requestReadReceiptDefault. - Detect (viewer): reads Disposition-Notification-To case-insensitively from the parsed headers and shows a banner (green Send / red Ignore) in the unified notification bar. Hidden in Sent/Drafts/Trash/Junk and once handled. message/disposition-notification + message/delivery-status report parts are filtered out of the attachment list. - Respond (MDN): lib/mdn.ts builds an RFC 8098 multipart/report (text/plain + message/disposition-notification, UTF-8/base64, localized subject + body). client.sendReadReceipt uploads the blob, imports it into Sent via Email/import, then submits with an explicit envelope. Both Send and Ignore set the $MDNSent keyword (RFC 3503) so no client re-prompts. Behaviour configurable: ask / always / never. New: lib/mdn.ts, read-receipt-banner.tsx. Settings (requestReadReceiptDefault, readReceiptResponse) + UI. All 17 locales.
This commit is contained in:
@@ -1130,6 +1130,7 @@ export default function Home() {
|
|||||||
inReplyTo?: string[];
|
inReplyTo?: string[];
|
||||||
references?: string[];
|
references?: string[];
|
||||||
delayedUntil?: string;
|
delayedUntil?: string;
|
||||||
|
requestReadReceipt?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
if (!client) return;
|
if (!client) return;
|
||||||
|
|
||||||
@@ -1137,7 +1138,7 @@ export default function Home() {
|
|||||||
const effectiveMode = pendingDraft?.mode ?? composerMode;
|
const effectiveMode = pendingDraft?.mode ?? composerMode;
|
||||||
const originalEmailId = selectedEmail?.id;
|
const originalEmailId = selectedEmail?.id;
|
||||||
|
|
||||||
const result = await sendEmail(client, data.to, data.subject, data.body, data.cc, data.bcc, data.identityId, data.fromEmail, data.draftId, data.fromName, data.htmlBody, data.attachments, data.inReplyTo, data.references, data.delayedUntil, data.envelopeMailFrom);
|
const result = await sendEmail(client, data.to, data.subject, data.body, data.cc, data.bcc, data.identityId, data.fromEmail, data.draftId, data.fromName, data.htmlBody, data.attachments, data.inReplyTo, data.references, data.delayedUntil, data.envelopeMailFrom, { requestReadReceipt: data.requestReadReceipt });
|
||||||
setShowComposer(false);
|
setShowComposer(false);
|
||||||
if (result.scheduled) {
|
if (result.scheduled) {
|
||||||
await refreshScheduledMetadata(client);
|
await refreshScheduledMetadata(client);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useFocusTrap } from "@/hooks/use-focus-trap";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { X, Paperclip, Send, Save, Check, Loader2, AlertCircle, FileText, BookmarkPlus, ShieldCheck, Lock, CalendarClock, ChevronDown } from "lucide-react";
|
import { X, Paperclip, Send, Save, Check, Loader2, AlertCircle, FileText, BookmarkPlus, ShieldCheck, Lock, CalendarClock, ChevronDown, MailCheck } from "lucide-react";
|
||||||
import { cn, formatFileSize, formatDateTime, generateUUID } from "@/lib/utils";
|
import { cn, formatFileSize, formatDateTime, generateUUID } from "@/lib/utils";
|
||||||
import { debug } from "@/lib/debug";
|
import { debug } from "@/lib/debug";
|
||||||
import { toast } from "@/stores/toast-store";
|
import { toast } from "@/stores/toast-store";
|
||||||
@@ -90,6 +90,7 @@ interface EmailComposerProps {
|
|||||||
inReplyTo?: string[];
|
inReplyTo?: string[];
|
||||||
references?: string[];
|
references?: string[];
|
||||||
delayedUntil?: string;
|
delayedUntil?: string;
|
||||||
|
requestReadReceipt?: boolean;
|
||||||
}) => void | Promise<void>;
|
}) => void | Promise<void>;
|
||||||
onScheduledSendCreated?: () => void | Promise<void>;
|
onScheduledSendCreated?: () => void | Promise<void>;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
@@ -206,6 +207,7 @@ export function EmailComposer({
|
|||||||
const sendDelaySeconds = useSettingsStore((state) => state.sendDelaySeconds);
|
const sendDelaySeconds = useSettingsStore((state) => state.sendDelaySeconds);
|
||||||
const signaturePosition = useSettingsStore((state) => state.signaturePosition);
|
const signaturePosition = useSettingsStore((state) => state.signaturePosition);
|
||||||
const signatureSeparatorEnabled = useSettingsStore((state) => state.signatureSeparatorEnabled);
|
const signatureSeparatorEnabled = useSettingsStore((state) => state.signatureSeparatorEnabled);
|
||||||
|
const requestReadReceiptDefault = useSettingsStore((state) => state.requestReadReceiptDefault);
|
||||||
const activeIdentities = useIdentityStore((s) => s.identities);
|
const activeIdentities = useIdentityStore((s) => s.identities);
|
||||||
// Pro shell: surface identities from every connected account, grouped
|
// Pro shell: surface identities from every connected account, grouped
|
||||||
// for the From dropdown's <optgroup>s. Outside Pro this collapses to
|
// for the From dropdown's <optgroup>s. Outside Pro this collapses to
|
||||||
@@ -384,6 +386,7 @@ export function EmailComposer({
|
|||||||
const [body, setBody] = useState(initialData?.body ?? getInitialBody());
|
const [body, setBody] = useState(initialData?.body ?? getInitialBody());
|
||||||
const [showCc, setShowCc] = useState(initialData?.showCc ?? !!getInitialCc());
|
const [showCc, setShowCc] = useState(initialData?.showCc ?? !!getInitialCc());
|
||||||
const [showBcc, setShowBcc] = useState(initialData?.showBcc ?? false);
|
const [showBcc, setShowBcc] = useState(initialData?.showBcc ?? false);
|
||||||
|
const [requestReadReceipt, setRequestReadReceipt] = useState(requestReadReceiptDefault);
|
||||||
const [draftId, setDraftId] = useState<string | null>(initialData?.draftId ?? null);
|
const [draftId, setDraftId] = useState<string | null>(initialData?.draftId ?? null);
|
||||||
// Mirror of draftId for synchronous reads inside chained saves; React's
|
// Mirror of draftId for synchronous reads inside chained saves; React's
|
||||||
// setDraftId is async, so a queued saveDraft would otherwise see the old
|
// setDraftId is async, so a queued saveDraft would otherwise see the old
|
||||||
@@ -1656,6 +1659,7 @@ export function EmailComposer({
|
|||||||
attachments: uploadedAttachments.length > 0 ? uploadedAttachments : undefined,
|
attachments: uploadedAttachments.length > 0 ? uploadedAttachments : undefined,
|
||||||
inReplyTo: threadingHeaders?.inReplyTo,
|
inReplyTo: threadingHeaders?.inReplyTo,
|
||||||
references: threadingHeaders?.references,
|
references: threadingHeaders?.references,
|
||||||
|
requestReadReceipt,
|
||||||
delayedUntil: effectiveDelayedUntil,
|
delayedUntil: effectiveDelayedUntil,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2217,7 +2221,7 @@ export function EmailComposer({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Bottom toolbar */}
|
{/* Bottom toolbar */}
|
||||||
<div className="flex items-center justify-between px-4 py-2.5 border-t bg-background shrink-0 pb-[calc(env(safe-area-inset-bottom)/2)]">
|
<div className="flex items-center justify-between px-4 py-2.5 border-t bg-background shrink-0 pb-[calc(0.625rem+env(safe-area-inset-bottom)/2)]">
|
||||||
{/* Left side actions */}
|
{/* Left side actions */}
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<input
|
<input
|
||||||
@@ -2280,6 +2284,21 @@ export function EmailComposer({
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Read-receipt request toggle */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setRequestReadReceipt(v => !v)}
|
||||||
|
className={cn(
|
||||||
|
"h-9 w-9",
|
||||||
|
requestReadReceipt && "bg-green-600 text-white hover:bg-green-600 hover:text-white dark:bg-green-600 dark:hover:bg-green-600"
|
||||||
|
)}
|
||||||
|
title={requestReadReceipt ? t('read_receipt_on') : t('read_receipt_off')}
|
||||||
|
aria-pressed={requestReadReceipt}
|
||||||
|
>
|
||||||
|
<MailCheck className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
<PluginSlot name="composer-toolbar" />
|
<PluginSlot name="composer-toolbar" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ import { useThemeStore } from "@/stores/theme-store";
|
|||||||
import { EmailIdentityBadge } from "./email-identity-badge";
|
import { EmailIdentityBadge } from "./email-identity-badge";
|
||||||
import { UnsubscribeBanner } from "./unsubscribe-banner";
|
import { UnsubscribeBanner } from "./unsubscribe-banner";
|
||||||
import { CalendarInvitationBanner } from "./calendar-invitation-banner";
|
import { CalendarInvitationBanner } from "./calendar-invitation-banner";
|
||||||
|
import { ReadReceiptBanner } from "./read-receipt-banner";
|
||||||
|
import { stripCrossAccountIdentityPrefix } from "@/hooks/use-pro-multi-account-identities";
|
||||||
import { useTour } from "@/components/tour/tour-provider";
|
import { useTour } from "@/components/tour/tour-provider";
|
||||||
import { useIsEmbedded } from "@/hooks/use-is-embedded";
|
import { useIsEmbedded } from "@/hooks/use-is-embedded";
|
||||||
import { SmimePassphraseDialog } from "@/components/settings/smime-passphrase-dialog";
|
import { SmimePassphraseDialog } from "@/components/settings/smime-passphrase-dialog";
|
||||||
@@ -911,6 +913,7 @@ export function EmailViewer({
|
|||||||
const showToolbarLabels = useSettingsStore((state) => state.showToolbarLabels);
|
const showToolbarLabels = useSettingsStore((state) => state.showToolbarLabels);
|
||||||
const mailLayout = useSettingsStore((state) => state.mailLayout);
|
const mailLayout = useSettingsStore((state) => state.mailLayout);
|
||||||
const calendarInvitationParsingEnabled = useSettingsStore((state) => state.calendarInvitationParsingEnabled);
|
const calendarInvitationParsingEnabled = useSettingsStore((state) => state.calendarInvitationParsingEnabled);
|
||||||
|
const readReceiptResponse = useSettingsStore((state) => state.readReceiptResponse);
|
||||||
const hideInlineImageAttachments = useSettingsStore((state) => state.hideInlineImageAttachments);
|
const hideInlineImageAttachments = useSettingsStore((state) => state.hideInlineImageAttachments);
|
||||||
const attachmentImagePreviewsEnabled = useSettingsStore((state) => state.attachmentImagePreviewsEnabled);
|
const attachmentImagePreviewsEnabled = useSettingsStore((state) => state.attachmentImagePreviewsEnabled);
|
||||||
const dragOutActive = useMemo(() => isDragOutSupported(), []);
|
const dragOutActive = useMemo(() => isDragOutSupported(), []);
|
||||||
@@ -2193,6 +2196,9 @@ export function EmailViewer({
|
|||||||
// Hide inline cid-referenced images when the user has opted to keep them
|
// Hide inline cid-referenced images when the user has opted to keep them
|
||||||
// out of the attachment list (default on): these are embedded in the body.
|
// out of the attachment list (default on): these are embedded in the body.
|
||||||
.filter(att => !(hideInlineImageAttachments && att.cid && att.disposition === 'inline' && (att.type || '').startsWith('image/')))
|
.filter(att => !(hideInlineImageAttachments && att.cid && att.disposition === 'inline' && (att.type || '').startsWith('image/')))
|
||||||
|
// Hide machine-readable report parts (MDN read-receipts, DSN bounce
|
||||||
|
// reports). These are required MIME parts, not real user attachments.
|
||||||
|
.filter(att => att.type !== 'message/disposition-notification' && att.type !== 'message/delivery-status')
|
||||||
.map((attachment, index) => ({
|
.map((attachment, index) => ({
|
||||||
id: attachment.blobId || `${attachment.name || 'attachment'}-${index}`,
|
id: attachment.blobId || `${attachment.name || 'attachment'}-${index}`,
|
||||||
name: attachment.name || null,
|
name: attachment.name || null,
|
||||||
@@ -3252,6 +3258,103 @@ export function EmailViewer({
|
|||||||
? calendarInvitationParsingEnabled && !!findCalendarAttachment(email)
|
? calendarInvitationParsingEnabled && !!findCalendarAttachment(email)
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
|
// ── Read receipt (MDN, RFC 8098) ──────────────────────────────
|
||||||
|
// Detect a Disposition-Notification-To request on the open message. The
|
||||||
|
// header is parsed into email.headers by the client; look it up
|
||||||
|
// case-insensitively and extract the bare address.
|
||||||
|
const readReceiptRequestedBy = useMemo(() => {
|
||||||
|
const headers = email?.headers as Record<string, string | string[]> | undefined;
|
||||||
|
if (!headers) return null;
|
||||||
|
let raw: string | undefined;
|
||||||
|
for (const key of Object.keys(headers)) {
|
||||||
|
if (key.toLowerCase() === 'disposition-notification-to') {
|
||||||
|
const v = headers[key];
|
||||||
|
raw = Array.isArray(v) ? v[0] : v;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!raw) return null;
|
||||||
|
const m = raw.match(/<([^>]+)>/);
|
||||||
|
const addr = (m ? m[1] : raw).trim();
|
||||||
|
return addr || null;
|
||||||
|
}, [email?.headers]);
|
||||||
|
|
||||||
|
// The identity whose address received the original message — the MDN is sent
|
||||||
|
// "from" that address. Falls back to the primary identity.
|
||||||
|
const receiptIdentity = useMemo(() => {
|
||||||
|
if (!identities?.length) return null;
|
||||||
|
const recipients = [...(email?.to || []), ...(email?.cc || [])]
|
||||||
|
.map(r => r.email?.toLowerCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
return identities.find(i => recipients.includes(i.email?.toLowerCase())) || identities[0];
|
||||||
|
}, [identities, email?.to, email?.cc]);
|
||||||
|
|
||||||
|
const mdnAlreadyHandled = email?.keywords?.['$mdnsent'] === true;
|
||||||
|
const [mdnHandledLocally, setMdnHandledLocally] = useState(false);
|
||||||
|
useEffect(() => { setMdnHandledLocally(false); }, [email?.id]);
|
||||||
|
|
||||||
|
// Only offer the receipt for mail you're actually reading in a "received"
|
||||||
|
// location. Suppress your own copies (sent/drafts), discarded mail (trash),
|
||||||
|
// and spam (junk) - never confirm your address to spammers. Inbox, Archive
|
||||||
|
// and user folders all qualify.
|
||||||
|
const inReceiptEligibleFolder = !['sent', 'drafts', 'trash', 'junk'].includes(currentMailboxRole || '');
|
||||||
|
|
||||||
|
const shouldOfferReadReceipt =
|
||||||
|
!!readReceiptRequestedBy &&
|
||||||
|
!mdnAlreadyHandled &&
|
||||||
|
!mdnHandledLocally &&
|
||||||
|
readReceiptResponse !== 'never' &&
|
||||||
|
inReceiptEligibleFolder &&
|
||||||
|
!isDraft &&
|
||||||
|
!!receiptIdentity;
|
||||||
|
|
||||||
|
const sendReadReceiptNow = useCallback(async (automatic: boolean) => {
|
||||||
|
if (!client || !email || !readReceiptRequestedBy || !receiptIdentity) return;
|
||||||
|
const { rawId } = stripCrossAccountIdentityPrefix(receiptIdentity.id);
|
||||||
|
try {
|
||||||
|
await client.sendReadReceipt({
|
||||||
|
to: readReceiptRequestedBy,
|
||||||
|
fromEmail: receiptIdentity.email,
|
||||||
|
fromName: receiptIdentity.name,
|
||||||
|
identityId: rawId ?? receiptIdentity.id,
|
||||||
|
originalMessageId: email.messageId,
|
||||||
|
originalSubject: email.subject,
|
||||||
|
originalRecipient: receiptIdentity.email,
|
||||||
|
automatic,
|
||||||
|
subject: t('read_receipt.mdn_subject', { subject: email.subject || '' }),
|
||||||
|
humanText: t('read_receipt.mdn_body', { recipient: receiptIdentity.email }),
|
||||||
|
});
|
||||||
|
await client.setKeyword(email.id, '$mdnsent');
|
||||||
|
} catch (err) {
|
||||||
|
// Surface the failure instead of silently resetting the banner so we can
|
||||||
|
// see which step (upload / import / submission) failed.
|
||||||
|
console.error('Read-receipt (MDN) send failed:', err);
|
||||||
|
toast.error(t('read_receipt.send_failed'), {
|
||||||
|
message: err instanceof Error ? err.message : String(err),
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}, [client, email, readReceiptRequestedBy, receiptIdentity, t]);
|
||||||
|
|
||||||
|
const ignoreReadReceipt = useCallback(async () => {
|
||||||
|
setMdnHandledLocally(true);
|
||||||
|
if (client && email) {
|
||||||
|
// $MDNSent is the RFC 3503 flag every IMAP/JMAP client honours, so the
|
||||||
|
// request is suppressed everywhere - not just locally.
|
||||||
|
try { await client.setKeyword(email.id, '$mdnsent'); } catch { /* best effort */ }
|
||||||
|
}
|
||||||
|
}, [client, email]);
|
||||||
|
|
||||||
|
// "always" mode: auto-send the MDN once when the message is opened.
|
||||||
|
const autoMdnRef = useRef<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (readReceiptResponse !== 'always') return;
|
||||||
|
if (!shouldOfferReadReceipt || !email) return;
|
||||||
|
if (autoMdnRef.current === email.id) return;
|
||||||
|
autoMdnRef.current = email.id;
|
||||||
|
sendReadReceiptNow(true).catch(() => { autoMdnRef.current = null; });
|
||||||
|
}, [readReceiptResponse, shouldOfferReadReceipt, email?.id, sendReadReceiptNow]);
|
||||||
|
|
||||||
// Show loading skeleton while email is being fetched
|
// Show loading skeleton while email is being fetched
|
||||||
if (isLoading && !email) {
|
if (isLoading && !email) {
|
||||||
return (
|
return (
|
||||||
@@ -4946,9 +5049,10 @@ export function EmailViewer({
|
|||||||
error={smimeUnlockError}
|
error={smimeUnlockError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Unified Notification Banner - External Content + Calendar Invitation */}
|
{/* Unified Notification Banner - External Content + Calendar Invitation + Read Receipt */}
|
||||||
{((hasBlockedContent && !allowExternalContent && externalContentPolicy !== 'allow') ||
|
{((hasBlockedContent && !allowExternalContent && externalContentPolicy !== 'allow') ||
|
||||||
hasCalendarInvitation) && (
|
hasCalendarInvitation ||
|
||||||
|
(readReceiptResponse === 'ask' && shouldOfferReadReceipt)) && (
|
||||||
<div className="border-b border-border bg-muted/30 isolate">
|
<div className="border-b border-border bg-muted/30 isolate">
|
||||||
<div className="px-6 py-1.5">
|
<div className="px-6 py-1.5">
|
||||||
<div className="flex flex-col gap-3 isolate">
|
<div className="flex flex-col gap-3 isolate">
|
||||||
@@ -5003,6 +5107,17 @@ export function EmailViewer({
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{/* Read-receipt (MDN) request banner — only in "ask" mode */}
|
||||||
|
{readReceiptResponse === 'ask' && shouldOfferReadReceipt && readReceiptRequestedBy && (
|
||||||
|
<div className="py-1">
|
||||||
|
<ReadReceiptBanner
|
||||||
|
requestedBy={readReceiptRequestedBy}
|
||||||
|
onSend={() => sendReadReceiptNow(false)}
|
||||||
|
onIgnore={ignoreReadReceipt}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Calendar Invitation Banner */}
|
{/* Calendar Invitation Banner */}
|
||||||
{hasCalendarInvitation && (
|
{hasCalendarInvitation && (
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { MailCheck, Loader2, CheckCircle } from 'lucide-react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
interface ReadReceiptBannerProps {
|
||||||
|
/** Address that requested the receipt (Disposition-Notification-To). */
|
||||||
|
requestedBy: string;
|
||||||
|
/** Sends the MDN. Should resolve when the receipt has been submitted. */
|
||||||
|
onSend: () => Promise<void>;
|
||||||
|
/** Suppresses the request without sending (sets $MDNSent server-side). */
|
||||||
|
onIgnore: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ReadReceiptBanner({ requestedBy, onSend, onIgnore }: ReadReceiptBannerProps) {
|
||||||
|
const t = useTranslations('email_viewer.read_receipt');
|
||||||
|
const [state, setState] = useState<'idle' | 'sending' | 'sent'>('idle');
|
||||||
|
|
||||||
|
if (state === 'sent') {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 rounded-md border border-border bg-muted/40 px-3 py-2 text-sm text-muted-foreground">
|
||||||
|
<CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400 shrink-0" />
|
||||||
|
<span>{t('sent')}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap items-center gap-2 rounded-md border border-amber-300/60 bg-amber-50 px-3 py-2 text-sm dark:border-amber-700/50 dark:bg-amber-950/30">
|
||||||
|
<MailCheck className="w-4 h-4 shrink-0 text-amber-600 dark:text-amber-400" />
|
||||||
|
<span className="text-foreground">{t('prompt')}</span>
|
||||||
|
<span className="break-all text-muted-foreground">{requestedBy}</span>
|
||||||
|
<div className="ml-auto flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
setState('sending');
|
||||||
|
try {
|
||||||
|
await onSend();
|
||||||
|
setState('sent');
|
||||||
|
} catch {
|
||||||
|
setState('idle');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={state === 'sending'}
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-md bg-green-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{state === 'sending' && <Loader2 className="w-3 h-3 animate-spin" />}
|
||||||
|
{t('send')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onIgnore}
|
||||||
|
className="rounded-md bg-red-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-red-700"
|
||||||
|
>
|
||||||
|
{t('ignore')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -28,6 +28,8 @@ export function ComposingSettings() {
|
|||||||
subAddressDelimiter,
|
subAddressDelimiter,
|
||||||
signaturePosition,
|
signaturePosition,
|
||||||
signatureSeparatorEnabled,
|
signatureSeparatorEnabled,
|
||||||
|
requestReadReceiptDefault,
|
||||||
|
readReceiptResponse,
|
||||||
updateSetting,
|
updateSetting,
|
||||||
} = useSettingsStore();
|
} = useSettingsStore();
|
||||||
const { client } = useAuthStore();
|
const { client } = useAuthStore();
|
||||||
@@ -78,6 +80,25 @@ export function ComposingSettings() {
|
|||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
|
<SettingItem label={t('request_read_receipt.label')} description={t('request_read_receipt.description')}>
|
||||||
|
<ToggleSwitch
|
||||||
|
checked={requestReadReceiptDefault}
|
||||||
|
onChange={(checked) => updateSetting('requestReadReceiptDefault', checked)}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
|
<SettingItem label={t('read_receipt_response.label')} description={t('read_receipt_response.description')}>
|
||||||
|
<Select
|
||||||
|
value={readReceiptResponse}
|
||||||
|
onChange={(value) => updateSetting('readReceiptResponse', value as 'ask' | 'always' | 'never')}
|
||||||
|
options={[
|
||||||
|
{ value: 'ask', label: t('read_receipt_response.ask') },
|
||||||
|
{ value: 'always', label: t('read_receipt_response.always') },
|
||||||
|
{ value: 'never', label: t('read_receipt_response.never') },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
<SettingItem
|
<SettingItem
|
||||||
label={t('sub_address_delimiter.label')}
|
label={t('sub_address_delimiter.label')}
|
||||||
description={t('sub_address_delimiter.description', { delimiter: subAddressDelimiter })}
|
description={t('sub_address_delimiter.description', { delimiter: subAddressDelimiter })}
|
||||||
|
|||||||
@@ -530,6 +530,14 @@ export class DemoJMAPClient implements IJMAPClient {
|
|||||||
return { blobId, size: file.size, type: file.type };
|
return { blobId, size: file.size, type: file.type };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async importEmail(): Promise<string | null> {
|
||||||
|
return generateDemoId('email');
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendReadReceipt(): Promise<void> {
|
||||||
|
// Demo mode: no real network send.
|
||||||
|
}
|
||||||
|
|
||||||
getBlobDownloadUrl(blobId: string): string {
|
getBlobDownloadUrl(blobId: string): string {
|
||||||
return `data:application/octet-stream;demo-blob=${blobId}`;
|
return `data:application/octet-stream;demo-blob=${blobId}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,8 +150,30 @@ export interface IJMAPClient {
|
|||||||
references?: string[],
|
references?: string[],
|
||||||
delayedUntil?: string,
|
delayedUntil?: string,
|
||||||
envelopeMailFrom?: string,
|
envelopeMailFrom?: string,
|
||||||
|
options?: { requestReadReceipt?: boolean },
|
||||||
): Promise<SendEmailResult>;
|
): Promise<SendEmailResult>;
|
||||||
|
|
||||||
|
importEmail(
|
||||||
|
blobId: string,
|
||||||
|
mailboxIds: Record<string, boolean>,
|
||||||
|
keywords?: Record<string, boolean>,
|
||||||
|
accountId?: string,
|
||||||
|
): Promise<string | null>;
|
||||||
|
|
||||||
|
sendReadReceipt(params: {
|
||||||
|
to: string;
|
||||||
|
fromEmail: string;
|
||||||
|
fromName?: string;
|
||||||
|
identityId: string;
|
||||||
|
originalMessageId?: string | string[];
|
||||||
|
originalSubject?: string;
|
||||||
|
originalRecipient?: string;
|
||||||
|
automatic?: boolean;
|
||||||
|
accountId?: string;
|
||||||
|
subject?: string;
|
||||||
|
humanText?: string;
|
||||||
|
}): Promise<void>;
|
||||||
|
|
||||||
sendRawEmail(blob: Blob, identityId: string, sentMailboxId: string, draftMailboxId?: string, delayedUntil?: string, envelopeRecipients?: string[]): Promise<SendEmailResult>;
|
sendRawEmail(blob: Blob, identityId: string, sentMailboxId: string, draftMailboxId?: string, delayedUntil?: string, envelopeRecipients?: string[]): Promise<SendEmailResult>;
|
||||||
getScheduledEmails(limit?: number, position?: number): Promise<{ emails: ScheduledEmail[]; hasMore: boolean; total: number; nextPosition: number }>;
|
getScheduledEmails(limit?: number, position?: number): Promise<{ emails: ScheduledEmail[]; hasMore: boolean; total: number; nextPosition: number }>;
|
||||||
cancelEmailSubmission(submissionId: string): Promise<void>;
|
cancelEmailSubmission(submissionId: string): Promise<void>;
|
||||||
|
|||||||
+111
-1
@@ -2149,7 +2149,8 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
inReplyTo?: string[],
|
inReplyTo?: string[],
|
||||||
references?: string[],
|
references?: string[],
|
||||||
delayedUntil?: string,
|
delayedUntil?: string,
|
||||||
envelopeMailFrom?: string
|
envelopeMailFrom?: string,
|
||||||
|
options?: { requestReadReceipt?: boolean }
|
||||||
): Promise<SendEmailResult> {
|
): Promise<SendEmailResult> {
|
||||||
const holdForSeconds = delayedUntil ? this.validateDelayedUntil(delayedUntil) : undefined;
|
const holdForSeconds = delayedUntil ? this.validateDelayedUntil(delayedUntil) : undefined;
|
||||||
const emailId = `send-${Date.now()}`;
|
const emailId = `send-${Date.now()}`;
|
||||||
@@ -2219,6 +2220,13 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
mailboxIds: { [draftsMailbox.id]: true },
|
mailboxIds: { [draftsMailbox.id]: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (options?.requestReadReceipt) {
|
||||||
|
// RFC 8098: ask the recipient's client to return a Message Disposition
|
||||||
|
// Notification to our address. JMAP lets us set the raw header on create
|
||||||
|
// via the "header:<Name>:asText" property form.
|
||||||
|
emailCreate["header:Disposition-Notification-To:asText"] = fromEmail || this.username;
|
||||||
|
}
|
||||||
|
|
||||||
if (htmlBody) {
|
if (htmlBody) {
|
||||||
// Send as multipart/alternative with both text and HTML
|
// Send as multipart/alternative with both text and HTML
|
||||||
emailCreate.bodyValues = {
|
emailCreate.bodyValues = {
|
||||||
@@ -3007,6 +3015,108 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
throw new Error('Invalid upload response: blobId not found');
|
throw new Error('Invalid upload response: blobId not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import a raw RFC822 message (referenced by a previously-uploaded blob) into
|
||||||
|
* one or more mailboxes. Returns the new email id. Used for sending MDNs,
|
||||||
|
* where the exact MIME bytes must be preserved (Email/set can't express a
|
||||||
|
* multipart/report report-type parameter reliably).
|
||||||
|
*/
|
||||||
|
async importEmail(
|
||||||
|
blobId: string,
|
||||||
|
mailboxIds: Record<string, boolean>,
|
||||||
|
keywords?: Record<string, boolean>,
|
||||||
|
accountId?: string
|
||||||
|
): Promise<string | null> {
|
||||||
|
const targetAccountId = accountId || this.accountId;
|
||||||
|
const creationId = `imp-${Date.now()}`;
|
||||||
|
const response = await this.request([
|
||||||
|
["Email/import", {
|
||||||
|
accountId: targetAccountId,
|
||||||
|
emails: {
|
||||||
|
[creationId]: { blobId, mailboxIds, keywords: keywords || { "$seen": true } },
|
||||||
|
},
|
||||||
|
}, "0"],
|
||||||
|
]);
|
||||||
|
const res = response.methodResponses?.[0];
|
||||||
|
if (res?.[0] !== "Email/import") {
|
||||||
|
console.error('Email/import: unexpected response', res);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const payload = res[1] as {
|
||||||
|
created?: Record<string, { id: string }>;
|
||||||
|
notCreated?: Record<string, { type?: string; description?: string }>;
|
||||||
|
};
|
||||||
|
const created = payload?.created?.[creationId];
|
||||||
|
if (!created) {
|
||||||
|
const reason = payload?.notCreated?.[creationId];
|
||||||
|
console.error('Email/import failed:', reason || payload);
|
||||||
|
throw new Error(`Email/import: ${reason?.description || reason?.type || 'unknown error'}`);
|
||||||
|
}
|
||||||
|
return created.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an RFC 8098 Message Disposition Notification (read receipt) in reply
|
||||||
|
* to a message that carried a Disposition-Notification-To header. Builds the
|
||||||
|
* multipart/report, uploads it as a blob, imports it into Sent, then submits
|
||||||
|
* it with an explicit envelope (MAIL FROM = our identity, RCPT TO = the
|
||||||
|
* requesting address).
|
||||||
|
*/
|
||||||
|
async sendReadReceipt(params: {
|
||||||
|
to: string;
|
||||||
|
fromEmail: string;
|
||||||
|
fromName?: string;
|
||||||
|
identityId: string;
|
||||||
|
originalMessageId?: string | string[];
|
||||||
|
originalSubject?: string;
|
||||||
|
originalRecipient?: string;
|
||||||
|
automatic?: boolean;
|
||||||
|
accountId?: string;
|
||||||
|
subject?: string;
|
||||||
|
humanText?: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const targetAccountId = params.accountId || this.accountId;
|
||||||
|
const { buildMdnMessage } = await import("@/lib/mdn");
|
||||||
|
const raw = buildMdnMessage(params);
|
||||||
|
|
||||||
|
const file = new File([raw], "receipt.eml", { type: "message/rfc822" });
|
||||||
|
const { blobId } = await this.uploadBlob(file);
|
||||||
|
|
||||||
|
const mailboxes = await this.getMailboxes();
|
||||||
|
const targetMailbox = mailboxes.find(mb => mb.role === 'sent') || mailboxes[0];
|
||||||
|
if (!targetMailbox) throw new Error('No mailbox available for MDN import');
|
||||||
|
|
||||||
|
const emailId = await this.importEmail(
|
||||||
|
blobId,
|
||||||
|
{ [targetMailbox.id]: true },
|
||||||
|
{ "$seen": true },
|
||||||
|
targetAccountId
|
||||||
|
);
|
||||||
|
if (!emailId) throw new Error('MDN import failed');
|
||||||
|
|
||||||
|
const subId = `mdnsub-${Date.now()}`;
|
||||||
|
const response = await this.request([
|
||||||
|
["EmailSubmission/set", {
|
||||||
|
accountId: targetAccountId,
|
||||||
|
create: {
|
||||||
|
[subId]: {
|
||||||
|
emailId,
|
||||||
|
identityId: params.identityId,
|
||||||
|
envelope: {
|
||||||
|
mailFrom: { email: params.fromEmail },
|
||||||
|
rcptTo: [{ email: params.to }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, "0"],
|
||||||
|
]);
|
||||||
|
const subRes = response.methodResponses?.[0];
|
||||||
|
const notCreated = (subRes?.[1] as { notCreated?: Record<string, { type?: string; description?: string }> })?.notCreated?.[subId];
|
||||||
|
if (notCreated) {
|
||||||
|
throw new Error(`MDN submission failed: ${notCreated.description || notCreated.type || 'unknown'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getBlobDownloadUrl(blobId: string, name?: string, type?: string): string {
|
getBlobDownloadUrl(blobId: string, name?: string, type?: string): string {
|
||||||
if (!this.downloadUrl) {
|
if (!this.downloadUrl) {
|
||||||
throw new Error('Download URL not available. Please reconnect.');
|
throw new Error('Download URL not available. Please reconnect.');
|
||||||
|
|||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
// Builds an RFC 8098 Message Disposition Notification (MDN) as a raw RFC 5322
|
||||||
|
// message string. JMAP/Stalwart has no native MDN support, so the client
|
||||||
|
// constructs the multipart/report itself and sends it via
|
||||||
|
// blob-upload -> Email/import -> EmailSubmission/set (see client.sendReadReceipt).
|
||||||
|
//
|
||||||
|
// The message has two parts:
|
||||||
|
// 1. text/plain — human-readable explanation (English, ASCII; rarely shown)
|
||||||
|
// 2. message/disposition-notification — the machine-readable fields
|
||||||
|
// The optional third part (original message/headers) is omitted; RFC 8098 §3.1
|
||||||
|
// permits a two-part report.
|
||||||
|
|
||||||
|
export interface MdnOptions {
|
||||||
|
/** Address that requested the receipt (Disposition-Notification-To) — the MDN recipient. */
|
||||||
|
to: string;
|
||||||
|
/** Our identity address (sender of the MDN). */
|
||||||
|
fromEmail: string;
|
||||||
|
/** Optional display name for the From header. */
|
||||||
|
fromName?: string;
|
||||||
|
/** Original Message-ID. JMAP may hand this back as a string[]
|
||||||
|
* (header:Message-ID:asMessageIds), so accept both. */
|
||||||
|
originalMessageId?: string | string[];
|
||||||
|
/** Original Subject (used to build the MDN subject). */
|
||||||
|
originalSubject?: string;
|
||||||
|
/**
|
||||||
|
* The address the original message was delivered to (our address/alias).
|
||||||
|
* Used for Final-Recipient/Original-Recipient. Falls back to fromEmail.
|
||||||
|
*/
|
||||||
|
originalRecipient?: string;
|
||||||
|
/** true => automatic-action (setting "always"); false => manual-action (user clicked send). */
|
||||||
|
automatic?: boolean;
|
||||||
|
/** Reporting-UA value, e.g. "mail.dornig.de; Bulwark Webmail". */
|
||||||
|
reportingUa?: string;
|
||||||
|
/** Localized full Subject line. Defaults to "Read: <originalSubject>". */
|
||||||
|
subject?: string;
|
||||||
|
/** Localized human-readable explanation (first report part). Defaults to English. */
|
||||||
|
humanText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||||
|
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||||
|
|
||||||
|
/** RFC 5322 date in UTC, e.g. "Thu, 28 May 2026 14:23:00 +0000". */
|
||||||
|
function rfc5322Date(d: Date = new Date()): string {
|
||||||
|
const pad = (n: number) => String(n).padStart(2, "0");
|
||||||
|
return `${DAYS[d.getUTCDay()]}, ${pad(d.getUTCDate())} ${MONTHS[d.getUTCMonth()]} ${d.getUTCFullYear()} ` +
|
||||||
|
`${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())} +0000`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** UTF-8 string -> base64, without the deprecated unescape(). */
|
||||||
|
function utf8ToBase64(value: string): string {
|
||||||
|
const bytes = new TextEncoder().encode(value);
|
||||||
|
let binary = "";
|
||||||
|
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** UTF-8 base64 body, wrapped at 76 chars per RFC 2045. */
|
||||||
|
function base64Body(text: string): string {
|
||||||
|
return (utf8ToBase64(text).match(/.{1,76}/g) || []).join("\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RFC 2047 encoded-word for header values that contain non-ASCII characters. */
|
||||||
|
function encodeHeaderWord(value: string): string {
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
if (!/[^\x00-\x7F]/.test(value)) return value;
|
||||||
|
return `=?UTF-8?B?${utf8ToBase64(value)}?=`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureAngles(messageId: string | string[] | undefined): string {
|
||||||
|
// JMAP often returns Message-ID as a string[] (header:...:asMessageIds), so
|
||||||
|
// normalize string | string[] | undefined down to a single bracketed id.
|
||||||
|
const raw = Array.isArray(messageId) ? messageId[0] : messageId;
|
||||||
|
if (typeof raw !== "string") return "";
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return "";
|
||||||
|
return trimmed.startsWith("<") ? trimmed : `<${trimmed}>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomToken(): string {
|
||||||
|
const rnd = Math.random().toString(36).slice(2);
|
||||||
|
return `${Date.now().toString(36)}.${rnd}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the raw RFC 5322 MDN message. Lines are CRLF-terminated as required
|
||||||
|
* by the MIME standard so the bytes import/transmit verbatim.
|
||||||
|
*/
|
||||||
|
export function buildMdnMessage(opts: MdnOptions): string {
|
||||||
|
const finalRecipient = opts.originalRecipient || opts.fromEmail;
|
||||||
|
const domain = (opts.fromEmail.split("@")[1] || "localhost").trim();
|
||||||
|
const messageId = `<mdn.${randomToken()}@${domain}>`;
|
||||||
|
const boundary = `----=_MDN_${randomToken()}`;
|
||||||
|
const origMsgId = ensureAngles(opts.originalMessageId); // normalized "<...>" or ""
|
||||||
|
|
||||||
|
const fromHeader = opts.fromName
|
||||||
|
? `${encodeHeaderWord(opts.fromName)} <${opts.fromEmail}>`
|
||||||
|
: opts.fromEmail;
|
||||||
|
|
||||||
|
const subject = encodeHeaderWord(
|
||||||
|
opts.subject ?? `Read: ${opts.originalSubject || ""}`.trim()
|
||||||
|
);
|
||||||
|
|
||||||
|
const disposition = opts.automatic
|
||||||
|
? "automatic-action/MDN-sent-automatically; displayed"
|
||||||
|
: "manual-action/MDN-sent-manually; displayed";
|
||||||
|
|
||||||
|
const reportingUa = opts.reportingUa || `${domain}; Bulwark Webmail`;
|
||||||
|
|
||||||
|
// Human-readable part. Caller passes a localized humanText; fall back to
|
||||||
|
// English. Encoded as UTF-8/base64 below so any language survives.
|
||||||
|
const humanText = opts.humanText ?? [
|
||||||
|
`This is a return receipt for the message you sent to ${finalRecipient}.`,
|
||||||
|
``,
|
||||||
|
`Note: This receipt only acknowledges that the message was displayed on the`,
|
||||||
|
`recipient's computer. There is no guarantee that the recipient has read or`,
|
||||||
|
`understood the message contents.`,
|
||||||
|
].join("\r\n");
|
||||||
|
|
||||||
|
// Machine-readable disposition-notification part (pure ASCII tokens).
|
||||||
|
const mdnFields = [
|
||||||
|
`Reporting-UA: ${reportingUa}`,
|
||||||
|
`Final-Recipient: rfc822;${finalRecipient}`,
|
||||||
|
...(opts.originalRecipient ? [`Original-Recipient: rfc822;${opts.originalRecipient}`] : []),
|
||||||
|
...(origMsgId ? [`Original-Message-ID: ${origMsgId}`] : []),
|
||||||
|
`Disposition: ${disposition}`,
|
||||||
|
].join("\r\n");
|
||||||
|
|
||||||
|
return [
|
||||||
|
`Date: ${rfc5322Date()}`,
|
||||||
|
`From: ${fromHeader}`,
|
||||||
|
`To: ${opts.to}`,
|
||||||
|
`Subject: ${subject}`,
|
||||||
|
`Message-ID: ${messageId}`,
|
||||||
|
...(origMsgId ? [`In-Reply-To: ${origMsgId}`] : []),
|
||||||
|
`MIME-Version: 1.0`,
|
||||||
|
`Content-Type: multipart/report; report-type=disposition-notification;`,
|
||||||
|
`\tboundary="${boundary}"`,
|
||||||
|
``,
|
||||||
|
`--${boundary}`,
|
||||||
|
`Content-Type: text/plain; charset=utf-8`,
|
||||||
|
`Content-Transfer-Encoding: base64`,
|
||||||
|
``,
|
||||||
|
base64Body(humanText),
|
||||||
|
``,
|
||||||
|
`--${boundary}`,
|
||||||
|
`Content-Type: message/disposition-notification`,
|
||||||
|
`Content-Transfer-Encoding: 7bit`,
|
||||||
|
``,
|
||||||
|
mdnFields,
|
||||||
|
``,
|
||||||
|
`--${boundary}--`,
|
||||||
|
``,
|
||||||
|
].join("\r\n");
|
||||||
|
}
|
||||||
@@ -260,6 +260,15 @@
|
|||||||
"reschedule_prompt": "Zadejte nové datum/čas, např. 2026-05-04T15:30"
|
"reschedule_prompt": "Zadejte nové datum/čas, např. 2026-05-04T15:30"
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
|
"read_receipt": {
|
||||||
|
"prompt": "Odesílatel žádá o potvrzení o přečtení:",
|
||||||
|
"send": "Odeslat potvrzení",
|
||||||
|
"ignore": "Ignorovat",
|
||||||
|
"sent": "Potvrzení o přečtení odesláno.",
|
||||||
|
"send_failed": "Potvrzení o přečtení se nepodařilo odeslat",
|
||||||
|
"mdn_subject": "Přečteno: {subject}",
|
||||||
|
"mdn_body": "Toto je potvrzení o přečtení zprávy, kterou jste odeslali na adresu {recipient}.\n\nPoznámka: Toto potvrzení pouze potvrzuje, že zpráva byla zobrazena na počítači příjemce. Nezaručuje, že příjemce obsah přečetl nebo mu porozuměl."
|
||||||
|
},
|
||||||
"no_email_selected": "Není vybrána žádná zpráva",
|
"no_email_selected": "Není vybrána žádná zpráva",
|
||||||
"no_email_description": "Vyberte zprávu ze seznamu pro její zobrazení",
|
"no_email_description": "Vyberte zprávu ze seznamu pro její zobrazení",
|
||||||
"no_conversation_selected": "Není vybrána žádná konverzace",
|
"no_conversation_selected": "Není vybrána žádná konverzace",
|
||||||
@@ -545,6 +554,8 @@
|
|||||||
"undo_send": "Vrátit odeslání"
|
"undo_send": "Vrátit odeslání"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
|
"read_receipt_on": "Vyžádáno potvrzení o přečtení (kliknutím vypnete)",
|
||||||
|
"read_receipt_off": "Vyžádat potvrzení o přečtení",
|
||||||
"new_message": "Nová zpráva",
|
"new_message": "Nová zpráva",
|
||||||
"reply": "Odpovědět",
|
"reply": "Odpovědět",
|
||||||
"reply_all": "Odpovědět všem",
|
"reply_all": "Odpovědět všem",
|
||||||
@@ -1006,6 +1017,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"email_behavior": {
|
"email_behavior": {
|
||||||
|
"request_read_receipt": {
|
||||||
|
"label": "Standardně vyžadovat potvrzení o přečtení",
|
||||||
|
"description": "Při psaní nové zprávy předem zapnout žádost o potvrzení o přečtení."
|
||||||
|
},
|
||||||
|
"read_receipt_response": {
|
||||||
|
"label": "Reagovat na žádosti o potvrzení o přečtení",
|
||||||
|
"description": "Co dělat, když příchozí zpráva žádá o potvrzení o přečtení.",
|
||||||
|
"ask": "Vždy se zeptat",
|
||||||
|
"always": "Vždy odeslat",
|
||||||
|
"never": "Nikdy neodesílat"
|
||||||
|
},
|
||||||
"title": "Chování e-mailu",
|
"title": "Chování e-mailu",
|
||||||
"description": "Nakonfigurujte způsob zpracování e-mailů",
|
"description": "Nakonfigurujte způsob zpracování e-mailů",
|
||||||
"mark_read": {
|
"mark_read": {
|
||||||
|
|||||||
@@ -260,6 +260,15 @@
|
|||||||
"reschedule_prompt": "Indtast en ny dato/tid, f.eks. 2026-05-04T15:30"
|
"reschedule_prompt": "Indtast en ny dato/tid, f.eks. 2026-05-04T15:30"
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
|
"read_receipt": {
|
||||||
|
"prompt": "Afsenderen anmoder om en læsekvittering:",
|
||||||
|
"send": "Send kvittering",
|
||||||
|
"ignore": "Ignorér",
|
||||||
|
"sent": "Læsekvittering sendt.",
|
||||||
|
"send_failed": "Læsekvittering kunne ikke sendes",
|
||||||
|
"mdn_subject": "Læst: {subject}",
|
||||||
|
"mdn_body": "Dette er en læsekvittering for den besked, du sendte til {recipient}.\n\nBemærk: Denne kvittering bekræfter kun, at beskeden blev vist på modtagerens computer. Der er ingen garanti for, at modtageren har læst eller forstået indholdet."
|
||||||
|
},
|
||||||
"no_email_selected": "Ingen e-mail valgt",
|
"no_email_selected": "Ingen e-mail valgt",
|
||||||
"no_email_description": "Vælg en e-mail fra listen for at se den her",
|
"no_email_description": "Vælg en e-mail fra listen for at se den her",
|
||||||
"no_conversation_selected": "Ingen samtale valgt",
|
"no_conversation_selected": "Ingen samtale valgt",
|
||||||
@@ -545,6 +554,8 @@
|
|||||||
"undo_send": "Fortryd afsendelse"
|
"undo_send": "Fortryd afsendelse"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
|
"read_receipt_on": "Læsekvittering anmodet (klik for at deaktivere)",
|
||||||
|
"read_receipt_off": "Anmod om læsekvittering",
|
||||||
"new_message": "Ny besked",
|
"new_message": "Ny besked",
|
||||||
"reply": "Svar",
|
"reply": "Svar",
|
||||||
"reply_all": "Svar alle",
|
"reply_all": "Svar alle",
|
||||||
@@ -1007,6 +1018,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"email_behavior": {
|
"email_behavior": {
|
||||||
|
"request_read_receipt": {
|
||||||
|
"label": "Anmod om læsekvittering som standard",
|
||||||
|
"description": "Aktivér anmodningen om læsekvittering på forhånd, når du skriver en ny besked."
|
||||||
|
},
|
||||||
|
"read_receipt_response": {
|
||||||
|
"label": "Svar på anmodninger om læsekvittering",
|
||||||
|
"description": "Hvad der skal ske, når en indgående besked beder om en læsekvittering.",
|
||||||
|
"ask": "Spørg hver gang",
|
||||||
|
"always": "Send altid",
|
||||||
|
"never": "Send aldrig"
|
||||||
|
},
|
||||||
"title": "E-mail-adfærd",
|
"title": "E-mail-adfærd",
|
||||||
"description": "Konfigurér hvordan e-mails håndteres",
|
"description": "Konfigurér hvordan e-mails håndteres",
|
||||||
"mark_read": {
|
"mark_read": {
|
||||||
|
|||||||
@@ -260,6 +260,15 @@
|
|||||||
"reschedule_prompt": "Neues Datum/Uhrzeit eingeben, z. B. 2026-05-04T15:30"
|
"reschedule_prompt": "Neues Datum/Uhrzeit eingeben, z. B. 2026-05-04T15:30"
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
|
"read_receipt": {
|
||||||
|
"prompt": "Der Absender bittet um eine Lesebestätigung:",
|
||||||
|
"send": "Bestätigung senden",
|
||||||
|
"ignore": "Ignorieren",
|
||||||
|
"sent": "Lesebestätigung gesendet.",
|
||||||
|
"send_failed": "Lesebestätigung konnte nicht gesendet werden",
|
||||||
|
"mdn_subject": "Gelesen: {subject}",
|
||||||
|
"mdn_body": "Dies ist eine Lesebestätigung für die Nachricht, die Sie an {recipient} gesendet haben.\n\nHinweis: Diese Bestätigung bestätigt lediglich, dass die Nachricht auf dem Computer des Empfängers angezeigt wurde. Es gibt keine Garantie, dass der Empfänger den Inhalt gelesen oder verstanden hat."
|
||||||
|
},
|
||||||
"no_email_selected": "Keine E-Mail ausgewählt",
|
"no_email_selected": "Keine E-Mail ausgewählt",
|
||||||
"no_email_description": "Wählen Sie eine E-Mail aus der Liste aus, um sie hier anzuzeigen",
|
"no_email_description": "Wählen Sie eine E-Mail aus der Liste aus, um sie hier anzuzeigen",
|
||||||
"no_conversation_selected": "Keine Unterhaltung ausgewählt",
|
"no_conversation_selected": "Keine Unterhaltung ausgewählt",
|
||||||
@@ -545,6 +554,8 @@
|
|||||||
"undo_send": "Senden rückgängig"
|
"undo_send": "Senden rückgängig"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
|
"read_receipt_on": "Lesebestätigung angefordert (klicken zum Deaktivieren)",
|
||||||
|
"read_receipt_off": "Lesebestätigung anfordern",
|
||||||
"new_message": "Neue Nachricht",
|
"new_message": "Neue Nachricht",
|
||||||
"reply": "Antworten",
|
"reply": "Antworten",
|
||||||
"reply_all": "Allen antworten",
|
"reply_all": "Allen antworten",
|
||||||
@@ -1006,6 +1017,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"email_behavior": {
|
"email_behavior": {
|
||||||
|
"request_read_receipt": {
|
||||||
|
"label": "Lesebestätigung standardmäßig anfordern",
|
||||||
|
"description": "Beim Verfassen einer neuen Nachricht die Anforderung einer Lesebestätigung vorab aktivieren."
|
||||||
|
},
|
||||||
|
"read_receipt_response": {
|
||||||
|
"label": "Auf Lesebestätigungs-Anfragen reagieren",
|
||||||
|
"description": "Verhalten, wenn eine eingehende Nachricht um eine Lesebestätigung bittet.",
|
||||||
|
"ask": "Jedes Mal fragen",
|
||||||
|
"always": "Immer senden",
|
||||||
|
"never": "Nie senden"
|
||||||
|
},
|
||||||
"title": "E-Mail-Verhalten",
|
"title": "E-Mail-Verhalten",
|
||||||
"description": "Konfigurieren Sie, wie E-Mails verarbeitet werden",
|
"description": "Konfigurieren Sie, wie E-Mails verarbeitet werden",
|
||||||
"mark_read": {
|
"mark_read": {
|
||||||
|
|||||||
@@ -260,6 +260,15 @@
|
|||||||
"reschedule_prompt": "Enter a new date/time, e.g. 2026-05-04T15:30"
|
"reschedule_prompt": "Enter a new date/time, e.g. 2026-05-04T15:30"
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
|
"read_receipt": {
|
||||||
|
"prompt": "The sender requested a read receipt:",
|
||||||
|
"send": "Send receipt",
|
||||||
|
"ignore": "Ignore",
|
||||||
|
"sent": "Read receipt sent.",
|
||||||
|
"send_failed": "Read receipt could not be sent",
|
||||||
|
"mdn_subject": "Read: {subject}",
|
||||||
|
"mdn_body": "This is a return receipt for the message you sent to {recipient}.\n\nNote: This receipt only acknowledges that the message was displayed on the recipient''s computer. There is no guarantee that the recipient has read or understood the message contents."
|
||||||
|
},
|
||||||
"no_email_selected": "No email selected",
|
"no_email_selected": "No email selected",
|
||||||
"no_email_description": "Select an email from the list to view it here",
|
"no_email_description": "Select an email from the list to view it here",
|
||||||
"no_conversation_selected": "No conversation selected",
|
"no_conversation_selected": "No conversation selected",
|
||||||
@@ -545,6 +554,8 @@
|
|||||||
"undo_send": "Undo send"
|
"undo_send": "Undo send"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
|
"read_receipt_on": "Read receipt requested (click to disable)",
|
||||||
|
"read_receipt_off": "Request a read receipt",
|
||||||
"new_message": "New Message",
|
"new_message": "New Message",
|
||||||
"reply": "Reply",
|
"reply": "Reply",
|
||||||
"reply_all": "Reply All",
|
"reply_all": "Reply All",
|
||||||
@@ -1007,6 +1018,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"email_behavior": {
|
"email_behavior": {
|
||||||
|
"request_read_receipt": {
|
||||||
|
"label": "Request read receipts by default",
|
||||||
|
"description": "Pre-enable the read-receipt request when composing a new message."
|
||||||
|
},
|
||||||
|
"read_receipt_response": {
|
||||||
|
"label": "Respond to read-receipt requests",
|
||||||
|
"description": "What to do when an incoming message asks for a read receipt.",
|
||||||
|
"ask": "Ask each time",
|
||||||
|
"always": "Always send",
|
||||||
|
"never": "Never send"
|
||||||
|
},
|
||||||
"title": "Email Behavior",
|
"title": "Email Behavior",
|
||||||
"description": "Configure how emails are handled",
|
"description": "Configure how emails are handled",
|
||||||
"mark_read": {
|
"mark_read": {
|
||||||
|
|||||||
@@ -260,6 +260,15 @@
|
|||||||
"reschedule_prompt": "Introduce una nueva fecha/hora, p. ej. 2026-05-04T15:30"
|
"reschedule_prompt": "Introduce una nueva fecha/hora, p. ej. 2026-05-04T15:30"
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
|
"read_receipt": {
|
||||||
|
"prompt": "El remitente solicita una confirmación de lectura:",
|
||||||
|
"send": "Enviar confirmación",
|
||||||
|
"ignore": "Ignorar",
|
||||||
|
"sent": "Confirmación de lectura enviada.",
|
||||||
|
"send_failed": "No se pudo enviar la confirmación de lectura",
|
||||||
|
"mdn_subject": "Leído: {subject}",
|
||||||
|
"mdn_body": "Este es un acuse de recibo del mensaje que enviaste a {recipient}.\n\nNota: Este acuse solo confirma que el mensaje se mostró en el ordenador del destinatario. No garantiza que el destinatario haya leído o entendido el contenido."
|
||||||
|
},
|
||||||
"no_email_selected": "Ningún correo seleccionado",
|
"no_email_selected": "Ningún correo seleccionado",
|
||||||
"no_email_description": "Seleccione un correo de la lista para verlo aquí",
|
"no_email_description": "Seleccione un correo de la lista para verlo aquí",
|
||||||
"no_conversation_selected": "Ninguna conversación seleccionada",
|
"no_conversation_selected": "Ninguna conversación seleccionada",
|
||||||
@@ -545,6 +554,8 @@
|
|||||||
"undo_send": "Deshacer envío"
|
"undo_send": "Deshacer envío"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
|
"read_receipt_on": "Confirmación de lectura solicitada (haz clic para desactivar)",
|
||||||
|
"read_receipt_off": "Solicitar confirmación de lectura",
|
||||||
"new_message": "Nuevo Mensaje",
|
"new_message": "Nuevo Mensaje",
|
||||||
"reply": "Responder",
|
"reply": "Responder",
|
||||||
"reply_all": "Responder a Todos",
|
"reply_all": "Responder a Todos",
|
||||||
@@ -1006,6 +1017,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"email_behavior": {
|
"email_behavior": {
|
||||||
|
"request_read_receipt": {
|
||||||
|
"label": "Solicitar confirmaciones de lectura de forma predeterminada",
|
||||||
|
"description": "Activar previamente la solicitud de confirmación de lectura al redactar un mensaje nuevo."
|
||||||
|
},
|
||||||
|
"read_receipt_response": {
|
||||||
|
"label": "Responder a las solicitudes de confirmación de lectura",
|
||||||
|
"description": "Qué hacer cuando un mensaje entrante solicita una confirmación de lectura.",
|
||||||
|
"ask": "Preguntar cada vez",
|
||||||
|
"always": "Enviar siempre",
|
||||||
|
"never": "No enviar nunca"
|
||||||
|
},
|
||||||
"title": "Comportamiento del Correo",
|
"title": "Comportamiento del Correo",
|
||||||
"description": "Configure cómo se manejan los correos",
|
"description": "Configure cómo se manejan los correos",
|
||||||
"mark_read": {
|
"mark_read": {
|
||||||
|
|||||||
@@ -260,6 +260,15 @@
|
|||||||
"reschedule_prompt": "Saisissez une nouvelle date/heure, p. ex. 2026-05-04T15:30"
|
"reschedule_prompt": "Saisissez une nouvelle date/heure, p. ex. 2026-05-04T15:30"
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
|
"read_receipt": {
|
||||||
|
"prompt": "L''expéditeur demande un accusé de lecture :",
|
||||||
|
"send": "Envoyer l''accusé",
|
||||||
|
"ignore": "Ignorer",
|
||||||
|
"sent": "Accusé de lecture envoyé.",
|
||||||
|
"send_failed": "Impossible d''envoyer l''accusé de lecture",
|
||||||
|
"mdn_subject": "Lu : {subject}",
|
||||||
|
"mdn_body": "Ceci est un accusé de réception du message que vous avez envoyé à {recipient}.\n\nRemarque : cet accusé confirme uniquement que le message a été affiché sur l''ordinateur du destinataire. Il ne garantit pas que le destinataire a lu ou compris le contenu."
|
||||||
|
},
|
||||||
"no_email_selected": "Aucun email sélectionné",
|
"no_email_selected": "Aucun email sélectionné",
|
||||||
"no_email_description": "Sélectionnez un email dans la liste pour le voir ici",
|
"no_email_description": "Sélectionnez un email dans la liste pour le voir ici",
|
||||||
"no_conversation_selected": "Aucune conversation sélectionnée",
|
"no_conversation_selected": "Aucune conversation sélectionnée",
|
||||||
@@ -545,6 +554,8 @@
|
|||||||
"undo_send": "Annuler l’envoi"
|
"undo_send": "Annuler l’envoi"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
|
"read_receipt_on": "Accusé de lecture demandé (cliquez pour désactiver)",
|
||||||
|
"read_receipt_off": "Demander un accusé de lecture",
|
||||||
"new_message": "Nouveau message",
|
"new_message": "Nouveau message",
|
||||||
"reply": "Répondre",
|
"reply": "Répondre",
|
||||||
"reply_all": "Répondre à tous",
|
"reply_all": "Répondre à tous",
|
||||||
@@ -1006,6 +1017,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"email_behavior": {
|
"email_behavior": {
|
||||||
|
"request_read_receipt": {
|
||||||
|
"label": "Demander un accusé de lecture par défaut",
|
||||||
|
"description": "Activer au préalable la demande d''accusé de lecture lors de la rédaction d''un nouveau message."
|
||||||
|
},
|
||||||
|
"read_receipt_response": {
|
||||||
|
"label": "Répondre aux demandes d''accusé de lecture",
|
||||||
|
"description": "Que faire lorsqu''un message entrant demande un accusé de lecture.",
|
||||||
|
"ask": "Demander à chaque fois",
|
||||||
|
"always": "Toujours envoyer",
|
||||||
|
"never": "Ne jamais envoyer"
|
||||||
|
},
|
||||||
"title": "Comportement email",
|
"title": "Comportement email",
|
||||||
"description": "Configurez la gestion des emails",
|
"description": "Configurez la gestion des emails",
|
||||||
"mark_read": {
|
"mark_read": {
|
||||||
|
|||||||
@@ -260,6 +260,15 @@
|
|||||||
"reschedule_prompt": "Inserisci una nuova data/ora, ad es. 2026-05-04T15:30"
|
"reschedule_prompt": "Inserisci una nuova data/ora, ad es. 2026-05-04T15:30"
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
|
"read_receipt": {
|
||||||
|
"prompt": "Il mittente richiede una conferma di lettura:",
|
||||||
|
"send": "Invia conferma",
|
||||||
|
"ignore": "Ignora",
|
||||||
|
"sent": "Conferma di lettura inviata.",
|
||||||
|
"send_failed": "Impossibile inviare la conferma di lettura",
|
||||||
|
"mdn_subject": "Letto: {subject}",
|
||||||
|
"mdn_body": "Questa è una conferma di lettura del messaggio che hai inviato a {recipient}.\n\nNota: questa conferma attesta solo che il messaggio è stato visualizzato sul computer del destinatario. Non garantisce che il destinatario abbia letto o compreso il contenuto."
|
||||||
|
},
|
||||||
"no_email_selected": "Nessun messaggio selezionato",
|
"no_email_selected": "Nessun messaggio selezionato",
|
||||||
"no_email_description": "Seleziona un messaggio dall'elenco per visualizzarlo qui",
|
"no_email_description": "Seleziona un messaggio dall'elenco per visualizzarlo qui",
|
||||||
"no_conversation_selected": "Nessuna conversazione selezionata",
|
"no_conversation_selected": "Nessuna conversazione selezionata",
|
||||||
@@ -545,6 +554,8 @@
|
|||||||
"undo_send": "Annulla invio"
|
"undo_send": "Annulla invio"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
|
"read_receipt_on": "Conferma di lettura richiesta (clicca per disattivare)",
|
||||||
|
"read_receipt_off": "Richiedi una conferma di lettura",
|
||||||
"new_message": "Nuovo messaggio",
|
"new_message": "Nuovo messaggio",
|
||||||
"reply": "Rispondi",
|
"reply": "Rispondi",
|
||||||
"reply_all": "Rispondi a tutti",
|
"reply_all": "Rispondi a tutti",
|
||||||
@@ -1006,6 +1017,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"email_behavior": {
|
"email_behavior": {
|
||||||
|
"request_read_receipt": {
|
||||||
|
"label": "Richiedi conferme di lettura per impostazione predefinita",
|
||||||
|
"description": "Attiva in anticipo la richiesta di conferma di lettura durante la composizione di un nuovo messaggio."
|
||||||
|
},
|
||||||
|
"read_receipt_response": {
|
||||||
|
"label": "Rispondi alle richieste di conferma di lettura",
|
||||||
|
"description": "Cosa fare quando un messaggio in arrivo richiede una conferma di lettura.",
|
||||||
|
"ask": "Chiedi ogni volta",
|
||||||
|
"always": "Invia sempre",
|
||||||
|
"never": "Non inviare mai"
|
||||||
|
},
|
||||||
"title": "Comportamento email",
|
"title": "Comportamento email",
|
||||||
"description": "Configura come vengono gestiti i messaggi",
|
"description": "Configura come vengono gestiti i messaggi",
|
||||||
"mark_read": {
|
"mark_read": {
|
||||||
|
|||||||
@@ -260,6 +260,15 @@
|
|||||||
"reschedule_prompt": "新しい日時を入力してください。例: 2026-05-04T15:30"
|
"reschedule_prompt": "新しい日時を入力してください。例: 2026-05-04T15:30"
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
|
"read_receipt": {
|
||||||
|
"prompt": "送信者が開封確認を求めています:",
|
||||||
|
"send": "確認を送信",
|
||||||
|
"ignore": "無視",
|
||||||
|
"sent": "開封確認を送信しました。",
|
||||||
|
"send_failed": "開封確認を送信できませんでした",
|
||||||
|
"mdn_subject": "開封済み: {subject}",
|
||||||
|
"mdn_body": "これは、あなたが {recipient} に送信したメッセージの開封確認です。\n\n注意: この確認は、メッセージが受信者のコンピューターに表示されたことを示すだけです。受信者が内容を読んだ、または理解したことを保証するものではありません。"
|
||||||
|
},
|
||||||
"no_email_selected": "メールが選択されていません",
|
"no_email_selected": "メールが選択されていません",
|
||||||
"no_email_description": "リストからメールを選択して表示してください",
|
"no_email_description": "リストからメールを選択して表示してください",
|
||||||
"no_conversation_selected": "会話が選択されていません",
|
"no_conversation_selected": "会話が選択されていません",
|
||||||
@@ -545,6 +554,8 @@
|
|||||||
"undo_send": "送信を取り消す"
|
"undo_send": "送信を取り消す"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
|
"read_receipt_on": "開封確認を要求中(クリックで無効化)",
|
||||||
|
"read_receipt_off": "開封確認を要求する",
|
||||||
"new_message": "新規メッセージ",
|
"new_message": "新規メッセージ",
|
||||||
"reply": "返信",
|
"reply": "返信",
|
||||||
"reply_all": "全員に返信",
|
"reply_all": "全員に返信",
|
||||||
@@ -1006,6 +1017,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"email_behavior": {
|
"email_behavior": {
|
||||||
|
"request_read_receipt": {
|
||||||
|
"label": "既定で開封確認を要求する",
|
||||||
|
"description": "新しいメッセージの作成時に、開封確認の要求をあらかじめ有効にします。"
|
||||||
|
},
|
||||||
|
"read_receipt_response": {
|
||||||
|
"label": "開封確認の要求に応答する",
|
||||||
|
"description": "受信メッセージが開封確認を求めたときの動作。",
|
||||||
|
"ask": "毎回確認する",
|
||||||
|
"always": "常に送信",
|
||||||
|
"never": "送信しない"
|
||||||
|
},
|
||||||
"title": "メール動作",
|
"title": "メール動作",
|
||||||
"description": "メールの処理方法を設定",
|
"description": "メールの処理方法を設定",
|
||||||
"mark_read": {
|
"mark_read": {
|
||||||
|
|||||||
@@ -260,6 +260,15 @@
|
|||||||
"reschedule_prompt": "새 날짜/시간을 입력하세요. 예: 2026-05-04T15:30"
|
"reschedule_prompt": "새 날짜/시간을 입력하세요. 예: 2026-05-04T15:30"
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
|
"read_receipt": {
|
||||||
|
"prompt": "보낸 사람이 읽음 확인을 요청합니다:",
|
||||||
|
"send": "확인 보내기",
|
||||||
|
"ignore": "무시",
|
||||||
|
"sent": "읽음 확인을 보냈습니다.",
|
||||||
|
"send_failed": "읽음 확인을 보낼 수 없습니다",
|
||||||
|
"mdn_subject": "읽음: {subject}",
|
||||||
|
"mdn_body": "이것은 {recipient}(으)로 보낸 메시지에 대한 읽음 확인입니다.\n\n참고: 이 확인은 메시지가 수신자의 컴퓨터에 표시되었음을 알릴 뿐입니다. 수신자가 내용을 읽거나 이해했다는 보장은 없습니다."
|
||||||
|
},
|
||||||
"no_email_selected": "메일이 선택되지 않았어요",
|
"no_email_selected": "메일이 선택되지 않았어요",
|
||||||
"no_email_description": "목록에서 메일을 선택하면 여기에 내용이 표시돼요",
|
"no_email_description": "목록에서 메일을 선택하면 여기에 내용이 표시돼요",
|
||||||
"no_conversation_selected": "대화가 선택되지 않았어요",
|
"no_conversation_selected": "대화가 선택되지 않았어요",
|
||||||
@@ -545,6 +554,8 @@
|
|||||||
"undo_send": "보내기 실행 취소"
|
"undo_send": "보내기 실행 취소"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
|
"read_receipt_on": "읽음 확인 요청됨 (클릭하여 해제)",
|
||||||
|
"read_receipt_off": "읽음 확인 요청",
|
||||||
"new_message": "새 메시지",
|
"new_message": "새 메시지",
|
||||||
"reply": "답장",
|
"reply": "답장",
|
||||||
"reply_all": "전체 답장",
|
"reply_all": "전체 답장",
|
||||||
@@ -1006,6 +1017,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"email_behavior": {
|
"email_behavior": {
|
||||||
|
"request_read_receipt": {
|
||||||
|
"label": "기본적으로 읽음 확인 요청",
|
||||||
|
"description": "새 메시지를 작성할 때 읽음 확인 요청을 미리 켭니다."
|
||||||
|
},
|
||||||
|
"read_receipt_response": {
|
||||||
|
"label": "읽음 확인 요청에 응답",
|
||||||
|
"description": "수신 메시지가 읽음 확인을 요청할 때의 동작.",
|
||||||
|
"ask": "매번 묻기",
|
||||||
|
"always": "항상 보내기",
|
||||||
|
"never": "보내지 않음"
|
||||||
|
},
|
||||||
"title": "메일 동작",
|
"title": "메일 동작",
|
||||||
"description": "이메일 관련 동작 방식을 설정해 주세요",
|
"description": "이메일 관련 동작 방식을 설정해 주세요",
|
||||||
"mark_read": {
|
"mark_read": {
|
||||||
|
|||||||
@@ -260,6 +260,15 @@
|
|||||||
"reschedule_prompt": "Ievadiet jaunu datumu/laiku, piem. 2026-05-04T15:30"
|
"reschedule_prompt": "Ievadiet jaunu datumu/laiku, piem. 2026-05-04T15:30"
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
|
"read_receipt": {
|
||||||
|
"prompt": "Sūtītājs pieprasa lasīšanas apstiprinājumu:",
|
||||||
|
"send": "Sūtīt apstiprinājumu",
|
||||||
|
"ignore": "Ignorēt",
|
||||||
|
"sent": "Lasīšanas apstiprinājums nosūtīts.",
|
||||||
|
"send_failed": "Neizdevās nosūtīt lasīšanas apstiprinājumu",
|
||||||
|
"mdn_subject": "Izlasīts: {subject}",
|
||||||
|
"mdn_body": "Šis ir lasīšanas apstiprinājums ziņojumam, ko nosūtījāt uz {recipient}.\n\nPiezīme: šis apstiprinājums tikai apliecina, ka ziņojums tika parādīts saņēmēja datorā. Tas negarantē, ka saņēmējs ir izlasījis vai sapratis saturu."
|
||||||
|
},
|
||||||
"no_email_selected": "Nav atlasīta neviena vēstule",
|
"no_email_selected": "Nav atlasīta neviena vēstule",
|
||||||
"no_email_description": "Atlasiet vēstuli no saraksta, lai to skatītu šeit",
|
"no_email_description": "Atlasiet vēstuli no saraksta, lai to skatītu šeit",
|
||||||
"no_conversation_selected": "Nav atlasīta saruna",
|
"no_conversation_selected": "Nav atlasīta saruna",
|
||||||
@@ -545,6 +554,8 @@
|
|||||||
"undo_send": "Atsaukt sūtīšanu"
|
"undo_send": "Atsaukt sūtīšanu"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
|
"read_receipt_on": "Pieprasīts lasīšanas apstiprinājums (noklikšķiniet, lai atspējotu)",
|
||||||
|
"read_receipt_off": "Pieprasīt lasīšanas apstiprinājumu",
|
||||||
"new_message": "Jauns ziņojums",
|
"new_message": "Jauns ziņojums",
|
||||||
"reply": "Atbildēt",
|
"reply": "Atbildēt",
|
||||||
"reply_all": "Atbildēt visiem",
|
"reply_all": "Atbildēt visiem",
|
||||||
@@ -1006,6 +1017,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"email_behavior": {
|
"email_behavior": {
|
||||||
|
"request_read_receipt": {
|
||||||
|
"label": "Pēc noklusējuma pieprasīt lasīšanas apstiprinājumus",
|
||||||
|
"description": "Sastādot jaunu ziņojumu, iepriekš ieslēgt lasīšanas apstiprinājuma pieprasījumu."
|
||||||
|
},
|
||||||
|
"read_receipt_response": {
|
||||||
|
"label": "Atbildēt uz lasīšanas apstiprinājuma pieprasījumiem",
|
||||||
|
"description": "Ko darīt, kad ienākošs ziņojums pieprasa lasīšanas apstiprinājumu.",
|
||||||
|
"ask": "Vaicāt katru reizi",
|
||||||
|
"always": "Vienmēr sūtīt",
|
||||||
|
"never": "Nekad nesūtīt"
|
||||||
|
},
|
||||||
"title": "Pasta darbība",
|
"title": "Pasta darbība",
|
||||||
"description": "Pielāgojiet vēstuļu apstrādi",
|
"description": "Pielāgojiet vēstuļu apstrādi",
|
||||||
"mark_read": {
|
"mark_read": {
|
||||||
|
|||||||
@@ -260,6 +260,15 @@
|
|||||||
"reschedule_prompt": "Voer een nieuwe datum/tijd in, bijv. 2026-05-04T15:30"
|
"reschedule_prompt": "Voer een nieuwe datum/tijd in, bijv. 2026-05-04T15:30"
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
|
"read_receipt": {
|
||||||
|
"prompt": "De afzender vraagt om een leesbevestiging:",
|
||||||
|
"send": "Bevestiging verzenden",
|
||||||
|
"ignore": "Negeren",
|
||||||
|
"sent": "Leesbevestiging verzonden.",
|
||||||
|
"send_failed": "Leesbevestiging kon niet worden verzonden",
|
||||||
|
"mdn_subject": "Gelezen: {subject}",
|
||||||
|
"mdn_body": "Dit is een leesbevestiging voor het bericht dat u hebt verzonden naar {recipient}.\n\nLet op: deze bevestiging geeft alleen aan dat het bericht is weergegeven op de computer van de ontvanger. Er is geen garantie dat de ontvanger de inhoud heeft gelezen of begrepen."
|
||||||
|
},
|
||||||
"no_email_selected": "Geen e-mail geselecteerd",
|
"no_email_selected": "Geen e-mail geselecteerd",
|
||||||
"no_email_description": "Selecteer een e-mail uit de lijst om deze hier te bekijken",
|
"no_email_description": "Selecteer een e-mail uit de lijst om deze hier te bekijken",
|
||||||
"no_conversation_selected": "Geen gesprek geselecteerd",
|
"no_conversation_selected": "Geen gesprek geselecteerd",
|
||||||
@@ -545,6 +554,8 @@
|
|||||||
"undo_send": "Verzenden ongedaan maken"
|
"undo_send": "Verzenden ongedaan maken"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
|
"read_receipt_on": "Leesbevestiging aangevraagd (klik om uit te schakelen)",
|
||||||
|
"read_receipt_off": "Leesbevestiging aanvragen",
|
||||||
"new_message": "Nieuw bericht",
|
"new_message": "Nieuw bericht",
|
||||||
"reply": "Beantwoorden",
|
"reply": "Beantwoorden",
|
||||||
"reply_all": "Allen beantwoorden",
|
"reply_all": "Allen beantwoorden",
|
||||||
@@ -1006,6 +1017,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"email_behavior": {
|
"email_behavior": {
|
||||||
|
"request_read_receipt": {
|
||||||
|
"label": "Standaard om leesbevestiging vragen",
|
||||||
|
"description": "De aanvraag voor een leesbevestiging vooraf inschakelen bij het opstellen van een nieuw bericht."
|
||||||
|
},
|
||||||
|
"read_receipt_response": {
|
||||||
|
"label": "Reageren op verzoeken om leesbevestiging",
|
||||||
|
"description": "Wat te doen wanneer een inkomend bericht om een leesbevestiging vraagt.",
|
||||||
|
"ask": "Elke keer vragen",
|
||||||
|
"always": "Altijd verzenden",
|
||||||
|
"never": "Nooit verzenden"
|
||||||
|
},
|
||||||
"title": "E-mailgedrag",
|
"title": "E-mailgedrag",
|
||||||
"description": "Configureer hoe e-mails worden verwerkt",
|
"description": "Configureer hoe e-mails worden verwerkt",
|
||||||
"mark_read": {
|
"mark_read": {
|
||||||
|
|||||||
@@ -260,6 +260,15 @@
|
|||||||
"reschedule_prompt": "Wprowadź nową datę/godzinę, np. 2026-05-04T15:30"
|
"reschedule_prompt": "Wprowadź nową datę/godzinę, np. 2026-05-04T15:30"
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
|
"read_receipt": {
|
||||||
|
"prompt": "Nadawca prosi o potwierdzenie przeczytania:",
|
||||||
|
"send": "Wyślij potwierdzenie",
|
||||||
|
"ignore": "Ignoruj",
|
||||||
|
"sent": "Wysłano potwierdzenie przeczytania.",
|
||||||
|
"send_failed": "Nie udało się wysłać potwierdzenia przeczytania",
|
||||||
|
"mdn_subject": "Przeczytano: {subject}",
|
||||||
|
"mdn_body": "To jest potwierdzenie przeczytania wiadomości wysłanej do {recipient}.\n\nUwaga: to potwierdzenie oznacza jedynie, że wiadomość została wyświetlona na komputerze odbiorcy. Nie gwarantuje, że odbiorca przeczytał lub zrozumiał treść."
|
||||||
|
},
|
||||||
"no_email_selected": "Nie wybrano wiadomości",
|
"no_email_selected": "Nie wybrano wiadomości",
|
||||||
"no_email_description": "Wybierz wiadomość z listy, aby ją wyświetlić",
|
"no_email_description": "Wybierz wiadomość z listy, aby ją wyświetlić",
|
||||||
"no_conversation_selected": "Nie wybrano konwersacji",
|
"no_conversation_selected": "Nie wybrano konwersacji",
|
||||||
@@ -545,6 +554,8 @@
|
|||||||
"undo_send": "Cofnij wysyłkę"
|
"undo_send": "Cofnij wysyłkę"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
|
"read_receipt_on": "Zażądano potwierdzenia przeczytania (kliknij, aby wyłączyć)",
|
||||||
|
"read_receipt_off": "Zażądaj potwierdzenia przeczytania",
|
||||||
"new_message": "Nowa wiadomość",
|
"new_message": "Nowa wiadomość",
|
||||||
"reply": "Odpowiedz",
|
"reply": "Odpowiedz",
|
||||||
"reply_all": "Odpowiedz wszystkim",
|
"reply_all": "Odpowiedz wszystkim",
|
||||||
@@ -1006,6 +1017,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"email_behavior": {
|
"email_behavior": {
|
||||||
|
"request_read_receipt": {
|
||||||
|
"label": "Domyślnie żądaj potwierdzeń przeczytania",
|
||||||
|
"description": "Włącz wcześniej żądanie potwierdzenia przeczytania podczas tworzenia nowej wiadomości."
|
||||||
|
},
|
||||||
|
"read_receipt_response": {
|
||||||
|
"label": "Odpowiadaj na żądania potwierdzenia przeczytania",
|
||||||
|
"description": "Co zrobić, gdy przychodząca wiadomość prosi o potwierdzenie przeczytania.",
|
||||||
|
"ask": "Pytaj za każdym razem",
|
||||||
|
"always": "Zawsze wysyłaj",
|
||||||
|
"never": "Nigdy nie wysyłaj"
|
||||||
|
},
|
||||||
"title": "Zachowanie poczty e-mail",
|
"title": "Zachowanie poczty e-mail",
|
||||||
"description": "Skonfiguruj sposób obsługi wiadomości e-mail",
|
"description": "Skonfiguruj sposób obsługi wiadomości e-mail",
|
||||||
"mark_read": {
|
"mark_read": {
|
||||||
|
|||||||
@@ -260,6 +260,15 @@
|
|||||||
"reschedule_prompt": "Informe uma nova data/hora, ex. 2026-05-04T15:30"
|
"reschedule_prompt": "Informe uma nova data/hora, ex. 2026-05-04T15:30"
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
|
"read_receipt": {
|
||||||
|
"prompt": "O remetente solicita uma confirmação de leitura:",
|
||||||
|
"send": "Enviar confirmação",
|
||||||
|
"ignore": "Ignorar",
|
||||||
|
"sent": "Confirmação de leitura enviada.",
|
||||||
|
"send_failed": "Não foi possível enviar a confirmação de leitura",
|
||||||
|
"mdn_subject": "Lido: {subject}",
|
||||||
|
"mdn_body": "Este é um aviso de leitura da mensagem que você enviou para {recipient}.\n\nObservação: este aviso apenas confirma que a mensagem foi exibida no computador do destinatário. Não há garantia de que o destinatário tenha lido ou compreendido o conteúdo."
|
||||||
|
},
|
||||||
"no_email_selected": "Nenhum e-mail selecionado",
|
"no_email_selected": "Nenhum e-mail selecionado",
|
||||||
"no_email_description": "Selecione um e-mail da lista para visualizá-lo aqui",
|
"no_email_description": "Selecione um e-mail da lista para visualizá-lo aqui",
|
||||||
"no_conversation_selected": "Nenhuma conversa selecionada",
|
"no_conversation_selected": "Nenhuma conversa selecionada",
|
||||||
@@ -545,6 +554,8 @@
|
|||||||
"undo_send": "Desfazer envio"
|
"undo_send": "Desfazer envio"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
|
"read_receipt_on": "Confirmação de leitura solicitada (clique para desativar)",
|
||||||
|
"read_receipt_off": "Solicitar confirmação de leitura",
|
||||||
"new_message": "Nova Mensagem",
|
"new_message": "Nova Mensagem",
|
||||||
"reply": "Responder",
|
"reply": "Responder",
|
||||||
"reply_all": "Responder a Todos",
|
"reply_all": "Responder a Todos",
|
||||||
@@ -1006,6 +1017,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"email_behavior": {
|
"email_behavior": {
|
||||||
|
"request_read_receipt": {
|
||||||
|
"label": "Solicitar confirmações de leitura por padrão",
|
||||||
|
"description": "Ativar previamente a solicitação de confirmação de leitura ao redigir uma nova mensagem."
|
||||||
|
},
|
||||||
|
"read_receipt_response": {
|
||||||
|
"label": "Responder a solicitações de confirmação de leitura",
|
||||||
|
"description": "O que fazer quando uma mensagem recebida solicita uma confirmação de leitura.",
|
||||||
|
"ask": "Perguntar sempre",
|
||||||
|
"always": "Enviar sempre",
|
||||||
|
"never": "Nunca enviar"
|
||||||
|
},
|
||||||
"title": "Comportamento de E-mail",
|
"title": "Comportamento de E-mail",
|
||||||
"description": "Configure como os e-mails são manipulados",
|
"description": "Configure como os e-mails são manipulados",
|
||||||
"mark_read": {
|
"mark_read": {
|
||||||
|
|||||||
@@ -260,6 +260,15 @@
|
|||||||
"reschedule_prompt": "Введите новую дату/время, например 2026-05-04T15:30"
|
"reschedule_prompt": "Введите новую дату/время, например 2026-05-04T15:30"
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
|
"read_receipt": {
|
||||||
|
"prompt": "Отправитель запрашивает уведомление о прочтении:",
|
||||||
|
"send": "Отправить уведомление",
|
||||||
|
"ignore": "Игнорировать",
|
||||||
|
"sent": "Уведомление о прочтении отправлено.",
|
||||||
|
"send_failed": "Не удалось отправить уведомление о прочтении",
|
||||||
|
"mdn_subject": "Прочитано: {subject}",
|
||||||
|
"mdn_body": "Это уведомление о прочтении сообщения, отправленного вами на адрес {recipient}.\n\nПримечание: это уведомление лишь подтверждает, что сообщение было показано на компьютере получателя. Оно не гарантирует, что получатель прочитал или понял содержимое."
|
||||||
|
},
|
||||||
"no_email_selected": "Письмо не выбрано",
|
"no_email_selected": "Письмо не выбрано",
|
||||||
"no_email_description": "Выберите письмо из списка, чтобы просмотреть его здесь",
|
"no_email_description": "Выберите письмо из списка, чтобы просмотреть его здесь",
|
||||||
"no_conversation_selected": "Беседа не выбрана",
|
"no_conversation_selected": "Беседа не выбрана",
|
||||||
@@ -545,6 +554,8 @@
|
|||||||
"undo_send": "Отменить отправку"
|
"undo_send": "Отменить отправку"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
|
"read_receipt_on": "Запрошено уведомление о прочтении (нажмите, чтобы отключить)",
|
||||||
|
"read_receipt_off": "Запросить уведомление о прочтении",
|
||||||
"new_message": "Новое письмо",
|
"new_message": "Новое письмо",
|
||||||
"reply": "Ответить",
|
"reply": "Ответить",
|
||||||
"reply_all": "Ответить всем",
|
"reply_all": "Ответить всем",
|
||||||
@@ -1006,6 +1017,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"email_behavior": {
|
"email_behavior": {
|
||||||
|
"request_read_receipt": {
|
||||||
|
"label": "Запрашивать уведомления о прочтении по умолчанию",
|
||||||
|
"description": "Заранее включать запрос уведомления о прочтении при создании нового сообщения."
|
||||||
|
},
|
||||||
|
"read_receipt_response": {
|
||||||
|
"label": "Отвечать на запросы уведомления о прочтении",
|
||||||
|
"description": "Что делать, когда входящее сообщение запрашивает уведомление о прочтении.",
|
||||||
|
"ask": "Спрашивать каждый раз",
|
||||||
|
"always": "Всегда отправлять",
|
||||||
|
"never": "Никогда не отправлять"
|
||||||
|
},
|
||||||
"title": "Поведение почты",
|
"title": "Поведение почты",
|
||||||
"description": "Настройте обработку писем",
|
"description": "Настройте обработку писем",
|
||||||
"mark_read": {
|
"mark_read": {
|
||||||
|
|||||||
@@ -260,6 +260,15 @@
|
|||||||
"reschedule_prompt": "Yeni tarih/saat girin, ör. 2026-05-04T15:30"
|
"reschedule_prompt": "Yeni tarih/saat girin, ör. 2026-05-04T15:30"
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
|
"read_receipt": {
|
||||||
|
"prompt": "Gönderen bir okundu bilgisi istiyor:",
|
||||||
|
"send": "Bilgi gönder",
|
||||||
|
"ignore": "Yoksay",
|
||||||
|
"sent": "Okundu bilgisi gönderildi.",
|
||||||
|
"send_failed": "Okundu bilgisi gönderilemedi",
|
||||||
|
"mdn_subject": "Okundu: {subject}",
|
||||||
|
"mdn_body": "Bu, {recipient} adresine gönderdiğiniz iletinin okundu bilgisidir.\n\nNot: Bu bilgi yalnızca iletinin alıcının bilgisayarında görüntülendiğini belirtir. Alıcının içeriği okuduğunu veya anladığını garanti etmez."
|
||||||
|
},
|
||||||
"no_email_selected": "E-posta seçilmedi",
|
"no_email_selected": "E-posta seçilmedi",
|
||||||
"no_email_description": "Burada görüntülemek için listeden bir e-posta seçin",
|
"no_email_description": "Burada görüntülemek için listeden bir e-posta seçin",
|
||||||
"no_conversation_selected": "Konuşma seçilmedi",
|
"no_conversation_selected": "Konuşma seçilmedi",
|
||||||
@@ -545,6 +554,8 @@
|
|||||||
"undo_send": "Göndermeyi geri al"
|
"undo_send": "Göndermeyi geri al"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
|
"read_receipt_on": "Okundu bilgisi istendi (devre dışı bırakmak için tıklayın)",
|
||||||
|
"read_receipt_off": "Okundu bilgisi iste",
|
||||||
"new_message": "Yeni İleti",
|
"new_message": "Yeni İleti",
|
||||||
"reply": "Yanıtla",
|
"reply": "Yanıtla",
|
||||||
"reply_all": "Tümünü Yanıtla",
|
"reply_all": "Tümünü Yanıtla",
|
||||||
@@ -1006,6 +1017,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"email_behavior": {
|
"email_behavior": {
|
||||||
|
"request_read_receipt": {
|
||||||
|
"label": "Varsayılan olarak okundu bilgisi iste",
|
||||||
|
"description": "Yeni bir ileti yazarken okundu bilgisi isteğini önceden etkinleştir."
|
||||||
|
},
|
||||||
|
"read_receipt_response": {
|
||||||
|
"label": "Okundu bilgisi isteklerine yanıt ver",
|
||||||
|
"description": "Gelen bir ileti okundu bilgisi istediğinde ne yapılacağı.",
|
||||||
|
"ask": "Her seferinde sor",
|
||||||
|
"always": "Her zaman gönder",
|
||||||
|
"never": "Asla gönderme"
|
||||||
|
},
|
||||||
"title": "E-posta Davranışı",
|
"title": "E-posta Davranışı",
|
||||||
"description": "E-postaların nasıl işleneceğini yapılandırın",
|
"description": "E-postaların nasıl işleneceğini yapılandırın",
|
||||||
"mark_read": {
|
"mark_read": {
|
||||||
|
|||||||
@@ -260,6 +260,15 @@
|
|||||||
"reschedule_prompt": "Введіть нову дату/час, напр. 2026-05-04T15:30"
|
"reschedule_prompt": "Введіть нову дату/час, напр. 2026-05-04T15:30"
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
|
"read_receipt": {
|
||||||
|
"prompt": "Відправник запитує сповіщення про прочитання:",
|
||||||
|
"send": "Надіслати сповіщення",
|
||||||
|
"ignore": "Ігнорувати",
|
||||||
|
"sent": "Сповіщення про прочитання надіслано.",
|
||||||
|
"send_failed": "Не вдалося надіслати сповіщення про прочитання",
|
||||||
|
"mdn_subject": "Прочитано: {subject}",
|
||||||
|
"mdn_body": "Це сповіщення про прочитання повідомлення, яке ви надіслали на адресу {recipient}.\n\nПримітка: це сповіщення лише підтверджує, що повідомлення було показано на комп''ютері отримувача. Воно не гарантує, що отримувач прочитав або зрозумів вміст."
|
||||||
|
},
|
||||||
"no_email_selected": "Електронна адреса не вибрана",
|
"no_email_selected": "Електронна адреса не вибрана",
|
||||||
"no_email_description": "Виберіть електронний лист зі списку, щоб переглянути його тут",
|
"no_email_description": "Виберіть електронний лист зі списку, щоб переглянути його тут",
|
||||||
"no_conversation_selected": "Розмова не вибрана",
|
"no_conversation_selected": "Розмова не вибрана",
|
||||||
@@ -545,6 +554,8 @@
|
|||||||
"undo_send": "Скасувати надсилання"
|
"undo_send": "Скасувати надсилання"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
|
"read_receipt_on": "Запитано сповіщення про прочитання (натисніть, щоб вимкнути)",
|
||||||
|
"read_receipt_off": "Запитати сповіщення про прочитання",
|
||||||
"new_message": "Нове повідомлення",
|
"new_message": "Нове повідомлення",
|
||||||
"reply": "Відповісти",
|
"reply": "Відповісти",
|
||||||
"reply_all": "Відповісти всім",
|
"reply_all": "Відповісти всім",
|
||||||
@@ -1006,6 +1017,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"email_behavior": {
|
"email_behavior": {
|
||||||
|
"request_read_receipt": {
|
||||||
|
"label": "Запитувати сповіщення про прочитання за замовчуванням",
|
||||||
|
"description": "Заздалегідь вмикати запит сповіщення про прочитання під час створення нового повідомлення."
|
||||||
|
},
|
||||||
|
"read_receipt_response": {
|
||||||
|
"label": "Відповідати на запити сповіщення про прочитання",
|
||||||
|
"description": "Що робити, коли вхідне повідомлення запитує сповіщення про прочитання.",
|
||||||
|
"ask": "Запитувати щоразу",
|
||||||
|
"always": "Завжди надсилати",
|
||||||
|
"never": "Ніколи не надсилати"
|
||||||
|
},
|
||||||
"title": "Поведінка електронної пошти",
|
"title": "Поведінка електронної пошти",
|
||||||
"description": "Налаштувати спосіб обробки електронних листів",
|
"description": "Налаштувати спосіб обробки електронних листів",
|
||||||
"mark_read": {
|
"mark_read": {
|
||||||
|
|||||||
@@ -260,6 +260,15 @@
|
|||||||
"reschedule_prompt": "输入新的日期/时间,例如 2026-05-04T15:30"
|
"reschedule_prompt": "输入新的日期/时间,例如 2026-05-04T15:30"
|
||||||
},
|
},
|
||||||
"email_viewer": {
|
"email_viewer": {
|
||||||
|
"read_receipt": {
|
||||||
|
"prompt": "发件人请求已读回执:",
|
||||||
|
"send": "发送回执",
|
||||||
|
"ignore": "忽略",
|
||||||
|
"sent": "已读回执已发送。",
|
||||||
|
"send_failed": "无法发送已读回执",
|
||||||
|
"mdn_subject": "已读:{subject}",
|
||||||
|
"mdn_body": "这是您发送给 {recipient} 的邮件的已读回执。\n\n注意:此回执仅表示邮件已在收件人的计算机上显示,并不保证收件人已阅读或理解邮件内容。"
|
||||||
|
},
|
||||||
"no_email_selected": "未选择邮件",
|
"no_email_selected": "未选择邮件",
|
||||||
"no_email_description": "从列表中选择一封邮件以在此查看",
|
"no_email_description": "从列表中选择一封邮件以在此查看",
|
||||||
"no_conversation_selected": "未选择会话",
|
"no_conversation_selected": "未选择会话",
|
||||||
@@ -545,6 +554,8 @@
|
|||||||
"undo_send": "撤销发送"
|
"undo_send": "撤销发送"
|
||||||
},
|
},
|
||||||
"email_composer": {
|
"email_composer": {
|
||||||
|
"read_receipt_on": "已请求已读回执(点击以关闭)",
|
||||||
|
"read_receipt_off": "请求已读回执",
|
||||||
"new_message": "新邮件",
|
"new_message": "新邮件",
|
||||||
"reply": "回复",
|
"reply": "回复",
|
||||||
"reply_all": "全部回复",
|
"reply_all": "全部回复",
|
||||||
@@ -1006,6 +1017,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"email_behavior": {
|
"email_behavior": {
|
||||||
|
"request_read_receipt": {
|
||||||
|
"label": "默认请求已读回执",
|
||||||
|
"description": "撰写新邮件时预先启用已读回执请求。"
|
||||||
|
},
|
||||||
|
"read_receipt_response": {
|
||||||
|
"label": "响应已读回执请求",
|
||||||
|
"description": "当收到的邮件请求已读回执时的处理方式。",
|
||||||
|
"ask": "每次询问",
|
||||||
|
"always": "始终发送",
|
||||||
|
"never": "从不发送"
|
||||||
|
},
|
||||||
"title": "邮件行为",
|
"title": "邮件行为",
|
||||||
"description": "配置邮件的处理方式",
|
"description": "配置邮件的处理方式",
|
||||||
"mark_read": {
|
"mark_read": {
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ interface EmailStore {
|
|||||||
loadMoreEmails: (client: IJMAPClient) => Promise<void>;
|
loadMoreEmails: (client: IJMAPClient) => Promise<void>;
|
||||||
fetchEmailContent: (client: IJMAPClient, emailId: string) => Promise<Email | null>;
|
fetchEmailContent: (client: IJMAPClient, emailId: string) => Promise<Email | null>;
|
||||||
fetchQuota: (client: IJMAPClient) => Promise<void>;
|
fetchQuota: (client: IJMAPClient) => Promise<void>;
|
||||||
sendEmail: (client: IJMAPClient, to: string[], subject: string, body: string, cc?: string[], bcc?: string[], identityId?: string, fromEmail?: string, draftId?: string, fromName?: string, htmlBody?: string, attachments?: Array<{ blobId: string; name: string; type: string; size: number; disposition?: 'attachment' | 'inline'; cid?: string }>, inReplyTo?: string[], references?: string[], delayedUntil?: string, envelopeMailFrom?: string) => Promise<SendEmailResult>;
|
sendEmail: (client: IJMAPClient, to: string[], subject: string, body: string, cc?: string[], bcc?: string[], identityId?: string, fromEmail?: string, draftId?: string, fromName?: string, htmlBody?: string, attachments?: Array<{ blobId: string; name: string; type: string; size: number; disposition?: 'attachment' | 'inline'; cid?: string }>, inReplyTo?: string[], references?: string[], delayedUntil?: string, envelopeMailFrom?: string, options?: { requestReadReceipt?: boolean }) => Promise<SendEmailResult>;
|
||||||
sendRawEmail: (client: IJMAPClient, rawMimeBlob: Blob, identityId: string, delayedUntil?: string, envelopeRecipients?: string[]) => Promise<SendEmailResult>;
|
sendRawEmail: (client: IJMAPClient, rawMimeBlob: Blob, identityId: string, delayedUntil?: string, envelopeRecipients?: string[]) => Promise<SendEmailResult>;
|
||||||
deleteEmail: (client: IJMAPClient, emailId: string, forceDelete?: boolean) => Promise<void>;
|
deleteEmail: (client: IJMAPClient, emailId: string, forceDelete?: boolean) => Promise<void>;
|
||||||
markAsRead: (client: IJMAPClient, emailId: string, read: boolean) => Promise<void>;
|
markAsRead: (client: IJMAPClient, emailId: string, read: boolean) => Promise<void>;
|
||||||
@@ -841,10 +841,10 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
sendEmail: async (client, to, subject, body, cc, bcc, identityId, fromEmail, draftId, fromName, htmlBody, attachments, inReplyTo, references, delayedUntil, envelopeMailFrom) => {
|
sendEmail: async (client, to, subject, body, cc, bcc, identityId, fromEmail, draftId, fromName, htmlBody, attachments, inReplyTo, references, delayedUntil, envelopeMailFrom, options) => {
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
try {
|
try {
|
||||||
const result = await client.sendEmail(to, subject, body, cc, bcc, identityId, fromEmail, draftId, fromName, htmlBody, attachments, inReplyTo, references, delayedUntil, envelopeMailFrom);
|
const result = await client.sendEmail(to, subject, body, cc, bcc, identityId, fromEmail, draftId, fromName, htmlBody, attachments, inReplyTo, references, delayedUntil, envelopeMailFrom, options);
|
||||||
// Refresh handled by UI layer for immediate feedback
|
// Refresh handled by UI layer for immediate feedback
|
||||||
set({
|
set({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export type ListDensity = Density;
|
|||||||
export type DeleteAction = 'trash' | 'trash-and-read' | 'permanent';
|
export type DeleteAction = 'trash' | 'trash-and-read' | 'permanent';
|
||||||
export type ReplyMode = 'reply' | 'replyAll';
|
export type ReplyMode = 'reply' | 'replyAll';
|
||||||
export type SignaturePosition = 'above_quote' | 'below_quote';
|
export type SignaturePosition = 'above_quote' | 'below_quote';
|
||||||
|
/** How to handle an incoming Disposition-Notification-To (read-receipt) request. */
|
||||||
|
export type ReadReceiptResponse = 'ask' | 'always' | 'never';
|
||||||
export type DateFormat = 'smart' | 'relative' | 'full';
|
export type DateFormat = 'smart' | 'relative' | 'full';
|
||||||
export type TimeFormat = '12h' | '24h';
|
export type TimeFormat = '12h' | '24h';
|
||||||
export type FirstDayOfWeek = 0 | 1; // 0 = Sunday, 1 = Monday
|
export type FirstDayOfWeek = 0 | 1; // 0 = Sunday, 1 = Monday
|
||||||
@@ -157,6 +159,8 @@ interface SettingsState {
|
|||||||
sendDelaySeconds: SendDelaySeconds;
|
sendDelaySeconds: SendDelaySeconds;
|
||||||
signaturePosition: SignaturePosition; // Position of the signature relative to quoted text in replies/forwards
|
signaturePosition: SignaturePosition; // Position of the signature relative to quoted text in replies/forwards
|
||||||
signatureSeparatorEnabled: boolean; // Prefix the signature with the RFC 3676 "-- " delimiter
|
signatureSeparatorEnabled: boolean; // Prefix the signature with the RFC 3676 "-- " delimiter
|
||||||
|
requestReadReceiptDefault: boolean; // Pre-check "request read receipt" in the composer
|
||||||
|
readReceiptResponse: ReadReceiptResponse; // How to respond to incoming read-receipt requests
|
||||||
|
|
||||||
// Privacy & Security
|
// Privacy & Security
|
||||||
sessionTimeout: number; // minutes (0 = never)
|
sessionTimeout: number; // minutes (0 = never)
|
||||||
@@ -332,6 +336,8 @@ const DEFAULT_SETTINGS = {
|
|||||||
sendDelaySeconds: 0 as SendDelaySeconds,
|
sendDelaySeconds: 0 as SendDelaySeconds,
|
||||||
signaturePosition: 'below_quote' as SignaturePosition,
|
signaturePosition: 'below_quote' as SignaturePosition,
|
||||||
signatureSeparatorEnabled: true,
|
signatureSeparatorEnabled: true,
|
||||||
|
requestReadReceiptDefault: false,
|
||||||
|
readReceiptResponse: 'ask' as ReadReceiptResponse,
|
||||||
|
|
||||||
// Privacy & Security
|
// Privacy & Security
|
||||||
sessionTimeout: 0, // Never
|
sessionTimeout: 0, // Never
|
||||||
@@ -526,6 +532,8 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
sendDelaySeconds: state.sendDelaySeconds,
|
sendDelaySeconds: state.sendDelaySeconds,
|
||||||
signaturePosition: state.signaturePosition,
|
signaturePosition: state.signaturePosition,
|
||||||
signatureSeparatorEnabled: state.signatureSeparatorEnabled,
|
signatureSeparatorEnabled: state.signatureSeparatorEnabled,
|
||||||
|
requestReadReceiptDefault: state.requestReadReceiptDefault,
|
||||||
|
readReceiptResponse: state.readReceiptResponse,
|
||||||
sessionTimeout: state.sessionTimeout,
|
sessionTimeout: state.sessionTimeout,
|
||||||
emailNotificationsEnabled: state.emailNotificationsEnabled,
|
emailNotificationsEnabled: state.emailNotificationsEnabled,
|
||||||
emailNotificationSound: state.emailNotificationSound,
|
emailNotificationSound: state.emailNotificationSound,
|
||||||
|
|||||||
Reference in New Issue
Block a user