feat: let plugins read any message, list a folder and see the folders

This commit is contained in:
Linus Rath
2026-10-14 04:13:33 +02:00
parent 121d8ba30c
commit 8fd6bc42c3
31 changed files with 254 additions and 31 deletions
+119
View File
@@ -0,0 +1,119 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { InstalledPlugin } from '../plugin-types';
import type { IJMAPClient } from '../jmap/client-interface';
import type { Email, Mailbox } from '../jmap/types';
import { useAuthStore } from '@/stores/auth-store';
import { useEmailStore } from '@/stores/email-store';
vi.mock('@/stores/toast-store', () => ({
toast: { success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() },
}));
import { dispatchApiCall } from '../plugin-sandbox/host-api';
function plugin(perms: string[]): InstalledPlugin {
return {
id: 'reader', name: 'Reader', version: '1.0.0', author: 'test', description: '',
type: 'hook', permissions: perms, grantedPermissions: perms, entrypoint: 'index.js',
enabled: true, status: 'running', settings: {},
};
}
const READER = plugin(['email:read']);
const call = (p: InstalledPlugin, method: string, ...args: unknown[]) => dispatchApiCall(p, method, args);
function mail(id: string, over: Partial<Email> = {}): Email {
return {
id, threadId: `t-${id}`, mailboxIds: { inbox: true }, keywords: { $seen: true },
from: [{ name: 'Bob', email: 'bob@example.com' }], to: [{ email: 'ann@example.com' }],
subject: `Subject ${id}`, receivedAt: '2026-05-01T10:00:00Z', preview: 'preview', size: 1234,
hasAttachment: false,
...over,
} as Email;
}
const inbox = { id: 'inbox', name: 'Inbox', role: 'inbox', totalEmails: 12, unreadEmails: 3, sortOrder: 0 } as Mailbox;
const shared = {
id: 'acc2:box', originalId: 'box', name: 'Team', role: undefined, parentId: undefined,
totalEmails: 1, unreadEmails: 0, isShared: true, accountId: 'acc2', sortOrder: 1,
} as unknown as Mailbox;
let client: Record<string, ReturnType<typeof vi.fn>>;
beforeEach(() => {
client = {
getEmail: vi.fn(async (id: string) => (id === 'm1'
? mail('m1', { textBody: [{ partId: '1' }], bodyValues: { 1: { value: 'Full body text' } } } as unknown as Partial<Email>)
: null)),
getEmails: vi.fn(async () => ({ emails: [mail('a'), mail('b')], hasMore: false, total: 2 })),
getMailboxes: vi.fn(async () => [inbox]),
};
useAuthStore.setState({ client: client as unknown as IJMAPClient });
useEmailStore.setState({ mailboxes: [inbox, shared], selectedEmail: null });
});
afterEach(() => {
useAuthStore.setState({ client: null });
useEmailStore.setState({ mailboxes: [], selectedEmail: null });
});
describe('api.email.get', () => {
it('returns any message with its body, not only the open one', async () => {
const view = await call(READER, 'email.get', 'm1') as { id: string; text: string; from: unknown };
expect(view).toEqual(expect.objectContaining({
id: 'm1', subject: 'Subject m1', text: 'Full body text', from: [{ name: 'Bob', email: 'bob@example.com' }],
}));
expect(client.getEmail).toHaveBeenCalledWith('m1');
expect(await call(READER, 'email.get', 'gone')).toBeNull();
});
it('uses the open message when its body is loaded', async () => {
useEmailStore.setState({ selectedEmail: mail('open', { bodyValues: { 1: { value: 'x' } }, textBody: [{ partId: '1' }] } as unknown as Partial<Email>) });
expect(await call(READER, 'email.get', 'open')).toEqual(expect.objectContaining({ id: 'open', text: 'x' }));
expect(client.getEmail).not.toHaveBeenCalled();
});
it('needs email:read and a string id', async () => {
await expect(call(plugin([]), 'email.get', 'm1')).rejects.toThrow(/lacks permission "email:read"/);
await expect(call(READER, 'email.get', { id: 'm1' })).rejects.toThrow(/emailId must be a string/);
expect(client.getEmail).not.toHaveBeenCalled();
});
});
describe('api.email.list', () => {
it('returns summaries of a folder page', async () => {
const list = await call(READER, 'email.list', { mailboxId: 'inbox', limit: 2, position: 10 }) as { id: string }[];
expect(list.map((e) => e.id)).toEqual(['a', 'b']);
expect(list[0]).not.toHaveProperty('text');
expect(client.getEmails).toHaveBeenCalledWith('inbox', undefined, 2, 10);
});
it('asks the owning account for a shared folder', async () => {
await call(READER, 'email.list', { mailboxId: 'acc2:box' });
expect(client.getEmails).toHaveBeenCalledWith('box', 'acc2', 50, 0);
});
it('caps the page at 100 and refuses unknown folders', async () => {
await expect(call(READER, 'email.list', { limit: 101 })).rejects.toThrow(/limit must be a whole number from 1 to 100/);
await expect(call(READER, 'email.list', { mailboxId: 'nope' })).rejects.toThrow(/no mailbox with id "nope"/);
await expect(call(READER, 'email.list', { mailbox: 'inbox' })).rejects.toThrow(/unknown property "mailbox"/);
await expect(call(plugin([]), 'email.list')).rejects.toThrow(/lacks permission "email:read"/);
expect(client.getEmails).not.toHaveBeenCalled();
});
});
describe('api.mailboxes.list', () => {
it('returns plain folder views', async () => {
expect(await call(READER, 'mailboxes.list')).toEqual([
{ id: 'inbox', name: 'Inbox', role: 'inbox', parentId: null, totalEmails: 12, unreadEmails: 3 },
{ id: 'acc2:box', name: 'Team', role: null, parentId: null, totalEmails: 1, unreadEmails: 0 },
]);
await expect(call(plugin(['email:write']), 'mailboxes.list')).rejects.toThrow(/lacks permission "email:read"/);
});
it('asks the server when the app has not loaded folders yet', async () => {
useEmailStore.setState({ mailboxes: [] });
expect(await call(READER, 'mailboxes.list')).toHaveLength(1);
expect(client.getMailboxes).toHaveBeenCalled();
});
});
+76
View File
@@ -0,0 +1,76 @@
// Host side of reading mail for plugins: api.email.get / list and
// api.mailboxes.list. Reads go to the server through the JMAP client and
// never change what the user has open.
//
// Messages are read from the signed-in account itself; mail in folders shared
// from other accounts is not reached by id here.
import type { EmailReadView, EmailSummary, MailboxView } from '../plugin-types';
import type { Email, Mailbox } from '../jmap/types';
import type { IJMAPClient } from '../jmap/client-interface';
import { useEmailStore } from '@/stores/email-store';
import { emailSummary, emailToReadView } from '../plugin-projection';
import { readId, readInteger, readObject, sessionClient } from './host-api-input';
/** Most messages one api.email.list call returns. */
export const MAX_PLUGIN_EMAIL_PAGE = 100;
/** The open message when it is this one and its body is loaded, else the server's copy. */
async function loadEmail(client: IJMAPClient, emailId: string): Promise<Email | null> {
const { selectedEmail } = useEmailStore.getState();
if (selectedEmail?.id === emailId && selectedEmail.bodyValues && !selectedEmail.accountId) {
return selectedEmail;
}
return client.getEmail(emailId);
}
export async function doEmailGet(id: unknown): Promise<EmailReadView | null> {
const method = 'email.get';
const emailId = readId(id, method, 'emailId');
const email = await loadEmail(sessionClient(method), emailId);
return email ? emailToReadView(email) : null;
}
function mailboxToView(mailbox: Mailbox): MailboxView {
return {
id: mailbox.id,
name: mailbox.name,
role: mailbox.role ?? null,
parentId: mailbox.parentId ?? null,
totalEmails: mailbox.totalEmails ?? 0,
unreadEmails: mailbox.unreadEmails ?? 0,
};
}
async function knownMailboxes(client: IJMAPClient): Promise<Mailbox[]> {
const { mailboxes } = useEmailStore.getState();
return mailboxes.length > 0 ? mailboxes : client.getMailboxes();
}
export async function doMailboxesList(): Promise<MailboxView[]> {
return (await knownMailboxes(sessionClient('mailboxes.list'))).map(mailboxToView);
}
/**
* One page of a folder, newest first in the user's list order, as message
* summaries (no body). Without mailboxId it pages through all mail.
*/
export async function doEmailList(options: unknown): Promise<EmailSummary[]> {
const method = 'email.list';
const opts = readObject(options, method, 'options', ['mailboxId', 'limit', 'position'], true);
const limit = opts.limit === undefined ? 50 : readInteger(opts.limit, method, 'limit', 1, MAX_PLUGIN_EMAIL_PAGE);
const position = opts.position === undefined ? 0 : readInteger(opts.position, method, 'position', 0, 1_000_000);
const client = sessionClient(method);
let jmapMailboxId: string | undefined;
let accountId: string | undefined;
if (opts.mailboxId !== undefined) {
const id = readId(opts.mailboxId, method, 'mailboxId');
const mailbox = (await knownMailboxes(client)).find((m) => m.id === id);
if (!mailbox) throw new Error(`${method}: no mailbox with id "${id}"`);
// Shared folders carry a namespaced id in the app; the server knows the original.
jmapMailboxId = mailbox.originalId || mailbox.id;
accountId = mailbox.isShared ? mailbox.accountId : undefined;
}
const { emails } = await client.getEmails(jmapMailboxId, accountId, limit, position);
return emails.map(emailSummary);
}
+9
View File
@@ -41,6 +41,7 @@ import {
doIdentitiesUpdate, doTemplatesCreate, doTemplatesDelete, doTemplatesGet, doTemplatesList,
doTemplatesUpdate, doVacationGet, doVacationSet,
} from './host-api-settings';
import { doEmailGet, doEmailList, doMailboxesList } from './host-api-mail';
/**
* Methods only callable from the privileged (same-origin) tier. These expose
@@ -209,6 +210,10 @@ export const PERM_PER_METHOD: Readonly<Record<string, Permission | null>> = {
// out-of-office reply
'vacation.get': 'vacation:read',
'vacation.set': 'vacation:write',
// reading mail: one message with its text, a page of a folder, the folders
'email.get': 'email:read',
'email.list': 'email:read',
'mailboxes.list': 'email:read',
};
// ─── Cross-origin allow-list (mirrors lib/plugin-api.ts) ──────
@@ -1895,6 +1900,10 @@ export async function dispatchApiCall(
case 'vacation.get': return doVacationGet();
case 'vacation.set': return doVacationSet(args[0]);
case 'email.get': return doEmailGet(args[0]);
case 'email.list': return doEmailList(args[0]);
case 'mailboxes.list': return doMailboxesList();
default:
throw new Error(`Unhandled method "${method}"`);
}
+23 -4
View File
@@ -31,8 +31,9 @@ import { themeSnapshotToCSS, type ThemeSnapshot } from './host-theme';
import { interpolate, lookup, textDirection, type LocaleTables } from './plugin-i18n';
import { isInlineSlot, MAX_INLINE_SLOT_WIDTH } from './slot-layout';
import type {
CalendarEventInput, CalendarEventView, CalendarView, IdentityPatch, IdentityView, SlotName,
TaskInput, TaskView, TemplateInput, TemplateView, VacationInput, VacationView,
CalendarEventInput, CalendarEventView, CalendarView, EmailReadView, EmailSummary, IdentityPatch,
IdentityView, MailboxView, SlotName, TaskInput, TaskView, TemplateInput, TemplateView, VacationInput,
VacationView,
} from '../plugin-types';
import { AddressBook, ContactCard } from '../jmap/types';
import { EncryptionAtRestConfig, PublicKeyInput } from '@/stores/account-security-store';
@@ -425,14 +426,32 @@ function buildPluginApi(manifest: PluginManifest) {
downloadFile: (opts: { content: string; filename: string; contentType?: string }) =>
callApi('ui.downloadFile', [opts]) as Promise<void>,
},
// Email keyword mutations (permission: email:write). Keywords follow JMAP
// syntax, e.g. '$category-promotions' or '$label:<tagId>'.
// Reading mail (permission: email:read) and keyword mutations
// (permission: email:write). Keywords follow JMAP syntax, e.g.
// '$category-promotions' or '$label:<tagId>'.
email: {
/**
* One message with its plain text, headers and source, or null when
* there is none with that id. Any message of the account, not only the
* open one.
*/
get: (emailId: string) => callApi('email.get', [emailId]) as Promise<EmailReadView | null>,
/**
* A page of a folder (or of all mail without mailboxId), in the user's
* list order: summaries without the body. limit 1-100 (default 50).
*/
list: (options?: { mailboxId?: string; limit?: number; position?: number }) =>
callApi('email.list', [options]) as Promise<EmailSummary[]>,
setKeyword: (emailId: string, keyword: string, accountId?: string) =>
callApi('email.setKeyword', [emailId, keyword, accountId]) as Promise<void>,
removeKeyword: (emailId: string, keyword: string, accountId?: string) =>
callApi('email.removeKeyword', [emailId, keyword, accountId]) as Promise<void>,
},
// Folders (permission: email:read).
mailboxes: {
/** Every folder with its role, parent and message counts. */
list: () => callApi('mailboxes.list', []) as Promise<MailboxView[]>,
},
// Native sidebar tag definitions. Definition reads/writes use the existing
// settings permissions; server discovery and message counts use email:read.
// add() is intentionally append-only. reorder() requires a complete
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "تتطلب الحذر",
"email_read": {
"title": "قراءة بريدك",
"body": "عرض المرسلين والمستلمين والمواضيع والنص الكامل للرسائل التي تفتحها ولقائمة الرسائل."
"body": "عرض المرسلين والمستلمين والمواضيع والنص الكامل لأي رسالة في حسابك، ومجلداتك."
},
"email_write": {
"title": "تغيير الوسوم والعلامات على بريدك",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Cal anar amb compte",
"email_read": {
"title": "Llegir el vostre correu",
"body": "Veure remitents, destinataris, assumptes i el text complet dels missatges que obriu i de la llista de missatges."
"body": "Veure remitents, destinataris, assumptes i el text complet de qualsevol missatge del vostre compte, i les vostres carpetes."
},
"email_write": {
"title": "Canviar les etiquetes i marques del correu",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Vyžaduje opatrnost",
"email_read": {
"title": "Číst vaši poštu",
"body": "Vidí odesílatele, příjemce, předměty a celý text zpráv, které otevřete, i seznamu zpráv."
"body": "Vidí odesílatele, příjemce, předměty a celý text kterékoli zprávy ve vašem účtu i vaše složky."
},
"email_write": {
"title": "Měnit štítky a příznaky pošty",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Kræver omtanke",
"email_read": {
"title": "Læse dine e-mails",
"body": "Se afsendere, modtagere, emner og den fulde tekst i de beskeder, du åbner, og i beskedlisten."
"body": "Se afsendere, modtagere, emner og den fulde tekst i alle beskeder på din konto samt dine mapper."
},
"email_write": {
"title": "Ændre tags og markeringer på dine e-mails",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Mit Vorsicht",
"email_read": {
"title": "Ihre E-Mails lesen",
"body": "Absender, Empfänger, Betreff und den vollständigen Text der Nachrichten sehen, die Sie öffnen, sowie der Nachrichtenliste."
"body": "Absender, Empfänger, Betreff und den vollständigen Text jeder Nachricht in Ihrem Konto sowie Ihre Ordner sehen."
},
"email_write": {
"title": "Labels und Markierungen Ihrer E-Mails ändern",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Needs care",
"email_read": {
"title": "Read your email",
"body": "See senders, recipients, subjects and the full text of messages you open and of the message list."
"body": "See senders, recipients, subjects and the full text of any message in your account, and your folders."
},
"email_write": {
"title": "Change tags and flags on your email",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Con cuidado",
"email_read": {
"title": "Leer tu correo",
"body": "Ver remitentes, destinatarios, asuntos y el texto completo de los mensajes que abres y de la lista de mensajes."
"body": "Ver remitentes, destinatarios, asuntos y el texto completo de cualquier mensaje de tu cuenta, y tus carpetas."
},
"email_write": {
"title": "Cambiar etiquetas y marcas de tu correo",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "نیازمند احتیاط",
"email_read": {
"title": "خواندن ایمیل‌های شما",
"body": "دیدن فرستندگان، گیرندگان، موضوع‌ها و متن کامل پیام‌هایی که باز می‌کنید و فهرست پیام‌ها."
"body": "دیدن فرستندگان، گیرندگان، موضوع‌ها و متن کامل هر پیامی در حساب شما، و پوشه‌های شما."
},
"email_write": {
"title": "تغییر برچسب‌ها و پرچم‌های ایمیل‌های شما",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Prudence",
"email_read": {
"title": "Lire vos e-mails",
"body": "Voir les expéditeurs, les destinataires, les objets et le texte complet des messages que vous ouvrez et de la liste des messages."
"body": "Voir les expéditeurs, les destinataires, les objets et le texte complet de n'importe quel message de votre compte, ainsi que vos dossiers."
},
"email_write": {
"title": "Modifier les étiquettes et indicateurs de vos e-mails",
+1 -1
View File
@@ -4035,7 +4035,7 @@
"high_risk": "דורש זהירות",
"email_read": {
"title": "קריאת הדואר שלכם",
"body": "צפייה בשולחים, בנמענים, בנושאים ובטקסט המלא של הודעות שאתם פותחים ושל רשימת ההודעות."
"body": "צפייה בשולחים, בנמענים, בנושאים ובטקסט המלא של כל הודעה בחשבון שלכם, ובתיקיות שלכם."
},
"email_write": {
"title": "שינוי תגים וסימונים בדואר שלכם",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Óvatosan",
"email_read": {
"title": "Leveleid olvasása",
"body": "Látja a megnyitott üzenetek és az üzenetlista feladóit, címzettjeit, tárgyát és teljes szövegét."
"body": "Látja a fiókod bármely üzenetének feladóját, címzettjeit, tárgyát és teljes szövegét, valamint a mappáidat."
},
"email_write": {
"title": "Címkék és jelölők módosítása a leveleiden",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Con cautela",
"email_read": {
"title": "Leggere la tua posta",
"body": "Vedere mittenti, destinatari, oggetti e il testo completo dei messaggi che apri e dell'elenco dei messaggi."
"body": "Vedere mittenti, destinatari, oggetti e il testo completo di qualsiasi messaggio del tuo account, e le tue cartelle."
},
"email_write": {
"title": "Modificare etichette e contrassegni della tua posta",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "注意が必要",
"email_read": {
"title": "メールを読む",
"body": "開いたメッセージとメッセージ一覧の差出人、宛先、件名、本文全体を見ることができます。"
"body": "アカウント内のすべてのメッセージの差出人、宛先、件名、本文全体と、フォルダを見ることができます。"
},
"email_write": {
"title": "メールのタグとフラグを変更",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "주의 필요",
"email_read": {
"title": "메일 읽기",
"body": "열어 본 메시지와 메시지 목록의 보낸 사람, 받는 사람, 제목, 전체 본문을 볼 수 있어요."
"body": "계정에 있는 모든 메시지의 보낸 사람, 받는 사람, 제목, 전체 본문과 폴더를 볼 수 있어요."
},
"email_write": {
"title": "메일의 태그와 플래그 변경",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Nepieciešama piesardzība",
"email_read": {
"title": "Lasīt jūsu pastu",
"body": "Redzēt sūtītājus, adresātus, tēmas un visu tekstu vēstulēm, ko atverat, un vēstulēm sarakstā."
"body": "Redzēt sūtītājus, adresātus, tēmas un visu tekstu jebkurai vēstulei jūsu kontā, kā arī jūsu mapes."
},
"email_write": {
"title": "Mainīt vēstuļu tagus un karodziņus",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Болгоомжтой хандах",
"email_read": {
"title": "Таны имэйлийг унших",
"body": "Таны нээсэн имэйлүүд болон имэйлийн жагсаалтын илгээгч, хүлээн авагч, гарчиг, бүрэн текстийг харах."
"body": "Таны бүртгэл дэх аль ч имэйлийн илгээгч, хүлээн авагч, гарчиг, бүрэн текст болон таны хавтаснуудыг харах."
},
"email_write": {
"title": "Имэйлийн таг болон тугийг өөрчлөх",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Krever varsomhet",
"email_read": {
"title": "Lese e-posten din",
"body": "Se avsendere, mottakere, emner og hele teksten i meldingene du åpner og i meldingslisten."
"body": "Se avsendere, mottakere, emner og hele teksten i alle meldinger på kontoen din, og mappene dine."
},
"email_write": {
"title": "Endre etiketter og flagg på e-posten din",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Let op",
"email_read": {
"title": "Je e-mail lezen",
"body": "Afzenders, ontvangers, onderwerpen en de volledige tekst zien van berichten die je opent en van de berichtenlijst."
"body": "Afzenders, ontvangers, onderwerpen en de volledige tekst zien van elk bericht in je account, en je mappen."
},
"email_write": {
"title": "Labels en markeringen van je e-mail wijzigen",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Wymaga ostrożności",
"email_read": {
"title": "Czytanie poczty",
"body": "Widzi nadawców, odbiorców, tematy i pełną treść otwieranych wiadomości oraz listy wiadomości."
"body": "Widzi nadawców, odbiorców, tematy i pełną treść każdej wiadomości na Twoim koncie oraz Twoje foldery."
},
"email_write": {
"title": "Zmiana etykiet i flag w poczcie",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Requer atenção",
"email_read": {
"title": "Ler seus e-mails",
"body": "Ver remetentes, destinatários, assuntos e o texto completo das mensagens que você abre e da lista de mensagens."
"body": "Ver remetentes, destinatários, assuntos e o texto completo de qualquer mensagem da sua conta, e suas pastas."
},
"email_write": {
"title": "Alterar etiquetas e marcações dos seus e-mails",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Necesită atenție",
"email_read": {
"title": "Citirea e-mailurilor",
"body": "Vede expeditorii, destinatarii, subiectele și textul complet al mesajelor pe care le deschideți și al listei de mesaje."
"body": "Vede expeditorii, destinatarii, subiectele și textul complet al oricărui mesaj din contul dvs., precum și dosarele dvs."
},
"email_write": {
"title": "Modificarea etichetelor și marcajelor e-mailurilor",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Требует осторожности",
"email_read": {
"title": "Читать вашу почту",
"body": "Видеть отправителей, получателей, темы и полный текст открытых писем и писем в списке сообщений."
"body": "Видеть отправителей, получателей, темы и полный текст любого письма в вашей учётной записи, а также ваши папки."
},
"email_write": {
"title": "Менять теги и флажки писем",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Vyžaduje opatrnosť",
"email_read": {
"title": "Čítať vašu poštu",
"body": "Vidí odosielateľov, príjemcov, predmety a celý text správ, ktoré otvoríte, aj zoznamu správ."
"body": "Vidí odosielateľov, príjemcov, predmety a celý text ktorejkoľvek správy vo vašom účte aj vaše priečinky."
},
"email_write": {
"title": "Meniť štítky a príznaky pošty",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Dikkat gerektirir",
"email_read": {
"title": "E-postalarınızı okuma",
"body": "Açtığınız iletilerin ve ileti listesindekilerin gönderenlerini, alıcılarını, konularını ve tam metnini görme."
"body": "Hesabınızdaki herhangi bir iletinin gönderenlerini, alıcılarını, konusunu ve tam metnini, ayrıca klasörlerinizi görme."
},
"email_write": {
"title": "E-postalarınızdaki etiketleri ve bayrakları değiştirme",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "Потребує обережності",
"email_read": {
"title": "Читати вашу пошту",
"body": "Бачити відправників, одержувачів, теми й повний текст відкритих листів і листів у списку повідомлень."
"body": "Бачити відправників, одержувачів, теми й повний текст будь-якого листа у вашому обліковому записі, а також ваші папки."
},
"email_write": {
"title": "Змінювати теги й позначки листів",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "需留意",
"email_read": {
"title": "讀取您的郵件",
"body": "查看您開啟的郵件和郵件清單中的寄件者、收件者、主旨和完整內文。"
"body": "查看您帳戶中任何郵件的寄件者、收件者、主旨和完整內文,以及您的信件匣。"
},
"email_write": {
"title": "變更郵件的標籤和旗標",
+1 -1
View File
@@ -4034,7 +4034,7 @@
"high_risk": "需谨慎",
"email_read": {
"title": "读取您的邮件",
"body": "查看您打开的邮件和邮件列表中的发件人、收件人、主题和完整正文。"
"body": "查看您账户中任何邮件的发件人、收件人、主题和完整正文,以及您的文件夹。"
},
"email_write": {
"title": "更改邮件的标签和旗标",