mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-02 16:15:39 +00:00
feat: let plugins read any message, list a folder and see the folders
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}"`);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4034,7 +4034,7 @@
|
||||
"high_risk": "تتطلب الحذر",
|
||||
"email_read": {
|
||||
"title": "قراءة بريدك",
|
||||
"body": "عرض المرسلين والمستلمين والمواضيع والنص الكامل للرسائل التي تفتحها ولقائمة الرسائل."
|
||||
"body": "عرض المرسلين والمستلمين والمواضيع والنص الكامل لأي رسالة في حسابك، ومجلداتك."
|
||||
},
|
||||
"email_write": {
|
||||
"title": "تغيير الوسوم والعلامات على بريدك",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -4034,7 +4034,7 @@
|
||||
"high_risk": "نیازمند احتیاط",
|
||||
"email_read": {
|
||||
"title": "خواندن ایمیلهای شما",
|
||||
"body": "دیدن فرستندگان، گیرندگان، موضوعها و متن کامل پیامهایی که باز میکنید و فهرست پیامها."
|
||||
"body": "دیدن فرستندگان، گیرندگان، موضوعها و متن کامل هر پیامی در حساب شما، و پوشههای شما."
|
||||
},
|
||||
"email_write": {
|
||||
"title": "تغییر برچسبها و پرچمهای ایمیلهای شما",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -4035,7 +4035,7 @@
|
||||
"high_risk": "דורש זהירות",
|
||||
"email_read": {
|
||||
"title": "קריאת הדואר שלכם",
|
||||
"body": "צפייה בשולחים, בנמענים, בנושאים ובטקסט המלא של הודעות שאתם פותחים ושל רשימת ההודעות."
|
||||
"body": "צפייה בשולחים, בנמענים, בנושאים ובטקסט המלא של כל הודעה בחשבון שלכם, ובתיקיות שלכם."
|
||||
},
|
||||
"email_write": {
|
||||
"title": "שינוי תגים וסימונים בדואר שלכם",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -4034,7 +4034,7 @@
|
||||
"high_risk": "注意が必要",
|
||||
"email_read": {
|
||||
"title": "メールを読む",
|
||||
"body": "開いたメッセージとメッセージ一覧の差出人、宛先、件名、本文全体を見ることができます。"
|
||||
"body": "アカウント内のすべてのメッセージの差出人、宛先、件名、本文全体と、フォルダを見ることができます。"
|
||||
},
|
||||
"email_write": {
|
||||
"title": "メールのタグとフラグを変更",
|
||||
|
||||
@@ -4034,7 +4034,7 @@
|
||||
"high_risk": "주의 필요",
|
||||
"email_read": {
|
||||
"title": "메일 읽기",
|
||||
"body": "열어 본 메시지와 메시지 목록의 보낸 사람, 받는 사람, 제목, 전체 본문을 볼 수 있어요."
|
||||
"body": "계정에 있는 모든 메시지의 보낸 사람, 받는 사람, 제목, 전체 본문과 폴더를 볼 수 있어요."
|
||||
},
|
||||
"email_write": {
|
||||
"title": "메일의 태그와 플래그 변경",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -4034,7 +4034,7 @@
|
||||
"high_risk": "Болгоомжтой хандах",
|
||||
"email_read": {
|
||||
"title": "Таны имэйлийг унших",
|
||||
"body": "Таны нээсэн имэйлүүд болон имэйлийн жагсаалтын илгээгч, хүлээн авагч, гарчиг, бүрэн текстийг харах."
|
||||
"body": "Таны бүртгэл дэх аль ч имэйлийн илгээгч, хүлээн авагч, гарчиг, бүрэн текст болон таны хавтаснуудыг харах."
|
||||
},
|
||||
"email_write": {
|
||||
"title": "Имэйлийн таг болон тугийг өөрчлөх",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -4034,7 +4034,7 @@
|
||||
"high_risk": "Требует осторожности",
|
||||
"email_read": {
|
||||
"title": "Читать вашу почту",
|
||||
"body": "Видеть отправителей, получателей, темы и полный текст открытых писем и писем в списке сообщений."
|
||||
"body": "Видеть отправителей, получателей, темы и полный текст любого письма в вашей учётной записи, а также ваши папки."
|
||||
},
|
||||
"email_write": {
|
||||
"title": "Менять теги и флажки писем",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -4034,7 +4034,7 @@
|
||||
"high_risk": "Потребує обережності",
|
||||
"email_read": {
|
||||
"title": "Читати вашу пошту",
|
||||
"body": "Бачити відправників, одержувачів, теми й повний текст відкритих листів і листів у списку повідомлень."
|
||||
"body": "Бачити відправників, одержувачів, теми й повний текст будь-якого листа у вашому обліковому записі, а також ваші папки."
|
||||
},
|
||||
"email_write": {
|
||||
"title": "Змінювати теги й позначки листів",
|
||||
|
||||
@@ -4034,7 +4034,7 @@
|
||||
"high_risk": "需留意",
|
||||
"email_read": {
|
||||
"title": "讀取您的郵件",
|
||||
"body": "查看您開啟的郵件和郵件清單中的寄件者、收件者、主旨和完整內文。"
|
||||
"body": "查看您帳戶中任何郵件的寄件者、收件者、主旨和完整內文,以及您的信件匣。"
|
||||
},
|
||||
"email_write": {
|
||||
"title": "變更郵件的標籤和旗標",
|
||||
|
||||
@@ -4034,7 +4034,7 @@
|
||||
"high_risk": "需谨慎",
|
||||
"email_read": {
|
||||
"title": "读取您的邮件",
|
||||
"body": "查看您打开的邮件和邮件列表中的发件人、收件人、主题和完整正文。"
|
||||
"body": "查看您账户中任何邮件的发件人、收件人、主题和完整正文,以及您的文件夹。"
|
||||
},
|
||||
"email_write": {
|
||||
"title": "更改邮件的标签和旗标",
|
||||
|
||||
Reference in New Issue
Block a user