diff --git a/web/src/locales/de.ts b/web/src/locales/de.ts index a34ad66..1aec786 100644 --- a/web/src/locales/de.ts +++ b/web/src/locales/de.ts @@ -474,6 +474,11 @@ export const catalog: Catalog = { "Bcc": "Bcc", "Subject": "Betreff", "Subject (optional)": "Betreff (optional)", + "To (optional)": "An (optional)", + "Cc (optional)": "Cc (optional)", + "Bcc (optional)": "Bcc (optional)", + "Adds: {recipients}": "Fügt hinzu: {recipients}", + "Added to the message when you insert the template, beside anyone already on it.": "Wird der Nachricht beim Einfügen der Vorlage hinzugefügt, neben allen, die schon darauf stehen.", "Body": "Text", "Body text": "Fließtext", "Attach files": "Dateien anhängen", diff --git a/web/src/locales/es.ts b/web/src/locales/es.ts index 40c427b..802c322 100644 --- a/web/src/locales/es.ts +++ b/web/src/locales/es.ts @@ -466,6 +466,11 @@ export const catalog: Catalog = { "Bcc": "Cco", "Subject": "Asunto", "Subject (optional)": "Asunto (opcional)", + "To (optional)": "Para (opcional)", + "Cc (optional)": "Cc (opcional)", + "Bcc (optional)": "Cco (opcional)", + "Adds: {recipients}": "Añade: {recipients}", + "Added to the message when you insert the template, beside anyone already on it.": "Se añaden al mensaje al insertar la plantilla, junto a quienes ya figuren en él.", "Body": "Cuerpo", "Body text": "Texto normal", "Attach files": "Adjuntar archivos", diff --git a/web/src/locales/fr.ts b/web/src/locales/fr.ts index 4f0159d..e709951 100644 --- a/web/src/locales/fr.ts +++ b/web/src/locales/fr.ts @@ -471,6 +471,11 @@ export const catalog: Catalog = { "Bcc": "Cci", "Subject": "Objet", "Subject (optional)": "Objet (facultatif)", + "To (optional)": "À (facultatif)", + "Cc (optional)": "Cc (facultatif)", + "Bcc (optional)": "Cci (facultatif)", + "Adds: {recipients}": "Ajoute : {recipients}", + "Added to the message when you insert the template, beside anyone already on it.": "Ajoutés au message quand vous insérez le modèle, à côté des destinataires déjà présents.", "Body": "Corps", "Body text": "Texte courant", "Attach files": "Joindre des fichiers", diff --git a/web/src/locales/ja.ts b/web/src/locales/ja.ts index 40aa6c8..d895d3f 100644 --- a/web/src/locales/ja.ts +++ b/web/src/locales/ja.ts @@ -465,6 +465,11 @@ export const catalog: Catalog = { "Bcc": "Bcc", "Subject": "件名", "Subject (optional)": "件名(省略可)", + "To (optional)": "宛先(省略可)", + "Cc (optional)": "Cc(省略可)", + "Bcc (optional)": "Bcc(省略可)", + "Adds: {recipients}": "追加: {recipients}", + "Added to the message when you insert the template, beside anyone already on it.": "テンプレートを挿入するとメッセージに追加されます。すでにある宛先はそのまま残ります。", "Body": "本文", "Body text": "本文テキスト", "Attach files": "ファイルを添付", diff --git a/web/src/locales/nl.ts b/web/src/locales/nl.ts index ced3326..414f05f 100644 --- a/web/src/locales/nl.ts +++ b/web/src/locales/nl.ts @@ -465,6 +465,11 @@ export const catalog: Catalog = { "Bcc": "Bcc", "Subject": "Onderwerp", "Subject (optional)": "Onderwerp (optioneel)", + "To (optional)": "Aan (optioneel)", + "Cc (optional)": "Cc (optioneel)", + "Bcc (optional)": "Bcc (optioneel)", + "Adds: {recipients}": "Voegt toe: {recipients}", + "Added to the message when you insert the template, beside anyone already on it.": "Worden aan het bericht toegevoegd als je de sjabloon invoegt, naast wie er al op staat.", "Body": "Tekst", "Body text": "Bodytekst", "Attach files": "Bestanden bijvoegen", diff --git a/web/src/locales/pt-BR.ts b/web/src/locales/pt-BR.ts index 1cfb8d7..cf39983 100644 --- a/web/src/locales/pt-BR.ts +++ b/web/src/locales/pt-BR.ts @@ -469,6 +469,11 @@ export const catalog: Catalog = { "Bcc": "Cco", "Subject": "Assunto", "Subject (optional)": "Assunto (opcional)", + "To (optional)": "Para (opcional)", + "Cc (optional)": "Cc (opcional)", + "Bcc (optional)": "Cco (opcional)", + "Adds: {recipients}": "Adiciona: {recipients}", + "Added to the message when you insert the template, beside anyone already on it.": "Adicionados à mensagem quando você insere o modelo, junto de quem já estiver nela.", "Body": "Corpo", "Body text": "Texto normal", "Attach files": "Anexar arquivos", diff --git a/web/src/locales/ru.ts b/web/src/locales/ru.ts index c84fe65..c9238a2 100644 --- a/web/src/locales/ru.ts +++ b/web/src/locales/ru.ts @@ -468,6 +468,11 @@ export const catalog: Catalog = { "Bcc": "Скрытая копия", "Subject": "Тема", "Subject (optional)": "Тема (необязательно)", + "To (optional)": "Кому (необязательно)", + "Cc (optional)": "Копия (необязательно)", + "Bcc (optional)": "Скрытая копия (необязательно)", + "Adds: {recipients}": "Добавляет: {recipients}", + "Added to the message when you insert the template, beside anyone already on it.": "Добавляются в письмо при вставке шаблона, рядом с уже указанными получателями.", "Body": "Текст", "Body text": "Обычный текст", "Attach files": "Прикрепить файлы", diff --git a/web/src/locales/tr.ts b/web/src/locales/tr.ts index e60428a..5feefbb 100644 --- a/web/src/locales/tr.ts +++ b/web/src/locales/tr.ts @@ -1180,6 +1180,11 @@ export const catalog: Catalog = { "Strikethrough": "Üstü çizili", "Subject": "Konu", "Subject (optional)": "Konu (isteğe bağlı)", + "To (optional)": "Kime (isteğe bağlı)", + "Cc (optional)": "Bilgi (isteğe bağlı)", + "Bcc (optional)": "Gizli (isteğe bağlı)", + "Adds: {recipients}": "Ekler: {recipients}", + "Added to the message when you insert the template, beside anyone already on it.": "Şablonu eklediğinizde iletiye, zaten içinde olanların yanına eklenir.", "Subject: {subject}": "Konu: {subject}", "Subscribe to a calendar": "Bir takvime abone ol", "Subscribed calendar": "Abone olunan takvim", diff --git a/web/src/locales/uk.ts b/web/src/locales/uk.ts index 00d38ce..2c54b86 100644 --- a/web/src/locales/uk.ts +++ b/web/src/locales/uk.ts @@ -462,6 +462,11 @@ export const catalog: Catalog = { "Bcc": "Прихована копія", "Subject": "Тема", "Subject (optional)": "Тема (необов'язково)", + "To (optional)": "Кому (необов'язково)", + "Cc (optional)": "Копія (необов'язково)", + "Bcc (optional)": "Прихована копія (необов'язково)", + "Adds: {recipients}": "Додає: {recipients}", + "Added to the message when you insert the template, beside anyone already on it.": "Додаються до листа, коли ви вставляєте шаблон, поряд з уже вказаними одержувачами.", "Body": "Текст", "Body text": "Звичайний текст", "Attach files": "Прикріпити файли", diff --git a/web/src/locales/zh-Hans.ts b/web/src/locales/zh-Hans.ts index 9dd0d73..18dc416 100644 --- a/web/src/locales/zh-Hans.ts +++ b/web/src/locales/zh-Hans.ts @@ -464,6 +464,11 @@ export const catalog: Catalog = { "Bcc": "密送", "Subject": "主题", "Subject (optional)": "主题(可选)", + "To (optional)": "收件人(可选)", + "Cc (optional)": "抄送(可选)", + "Bcc (optional)": "密送(可选)", + "Adds: {recipients}": "添加:{recipients}", + "Added to the message when you insert the template, beside anyone already on it.": "插入模板时添加到邮件中,已有的收件人保持不变。", "Body": "正文", "Body text": "正文文本", "Attach files": "添加附件", diff --git a/web/src/store/__tests__/template-recipients.test.ts b/web/src/store/__tests__/template-recipients.test.ts new file mode 100644 index 0000000..3fac7db --- /dev/null +++ b/web/src/store/__tests__/template-recipients.test.ts @@ -0,0 +1,83 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { CAP, client } from "@/jmap/client"; +import { useCompose } from "@/store/compose"; +import { useMail } from "@/store/mail"; +import { DEFAULT_SETTINGS, useSettings } from "@/store/settings"; +import type { JmapSession } from "@/jmap/types"; + +/* A template's saved recipients join the message it is inserted into (#58). */ + +beforeEach(() => { + client.session = { + capabilities: { [CAP.core]: {}, [CAP.mail]: {}, [CAP.submission]: {} }, + accounts: { a1: { accountCapabilities: { [CAP.mail]: {}, [CAP.submission]: {} } } }, + primaryAccounts: {}, + state: "s1", + } as unknown as JmapSession; + // Nothing here should reach the server; autosave is answered with nothing. + vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, status: 200, json: async () => ({ methodResponses: [], sessionState: "1" }) }) as unknown as Response)); + useSettings.setState({ settings: { ...DEFAULT_SETTINGS } }); + useCompose.setState({ drafts: [], activeKey: null, pendingSends: {} }); + useMail.setState({ + accountId: "a1", + identities: [{ id: "i1", name: "Me", email: "me@example.org", replyTo: null }] as never, + mailboxes: {} as never, + }); +}); + +afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); +}); + +const draft = (key: string) => useCompose.getState().drafts.find((d) => d.key === key)!; + +describe("inserting a template with recipients", () => { + it("fills To, Cc and Bcc, and shows the Cc and Bcc fields it used", () => { + const key = useCompose.getState().open({}); + useCompose.getState().insertTemplate(key, { + html: "
Invoice attached.
", + subject: "Invoice", + to: [{ name: "Accounts", email: "accounts@client.example" }], + cc: [{ name: null, email: "boss@client.example" }], + bcc: [{ name: null, email: "archive@example.org" }], + }); + const d = draft(key); + expect(d.to.map((a) => a.email)).toEqual(["accounts@client.example"]); + expect(d.cc.map((a) => a.email)).toEqual(["boss@client.example"]); + expect(d.bcc.map((a) => a.email)).toEqual(["archive@example.org"]); + expect(d.showCc).toBe(true); + expect(d.showBcc).toBe(true); + expect(d.subject).toBe("Invoice"); + }); + + it("adds to the recipients already there, never twice", () => { + const key = useCompose.getState().open({ to: [{ name: "Ann", email: "ann@example.com" }], cc: [{ name: null, email: "bob@example.com" }] }); + useCompose.getState().insertTemplate(key, { + html: "Hi
", + subject: "", + to: [{ name: null, email: "ANN@example.com" }, { name: "Cat", email: "cat@example.com" }], + cc: [{ name: null, email: "cat@example.com" }], + bcc: [{ name: null, email: "bob@example.com" }], + }); + const d = draft(key); + expect(d.to.map((a) => a.email)).toEqual(["ann@example.com", "cat@example.com"]); + expect(d.cc.map((a) => a.email)).toEqual(["bob@example.com"]); + expect(d.bcc).toEqual([]); + }); + + it("leaves the recipients alone for a template without any", () => { + const key = useCompose.getState().open({ to: [{ name: "Ann", email: "ann@example.com" }] }); + useCompose.getState().insertTemplate(key, { html: "Thanks
", subject: "" }); + const d = draft(key); + expect(d.to.map((a) => a.email)).toEqual(["ann@example.com"]); + expect(d.showCc).toBe(false); + expect(d.showBcc).toBe(false); + }); + + it("names the template's own recipient in a placeholder", () => { + const key = useCompose.getState().open({}); + useCompose.getState().insertTemplate(key, { html: "Dear {{recipientName}},
", subject: "", to: [{ name: "Jane Smith", email: "jane@example.com" }] }); + expect(draft(key).html).toContain("Dear Jane Smith,"); + }); +}); diff --git a/web/src/store/compose.ts b/web/src/store/compose.ts index 200a6c9..f5881ae 100644 --- a/web/src/store/compose.ts +++ b/web/src/store/compose.ts @@ -13,7 +13,7 @@ import { ensureScheduledMailbox, useScheduled } from "./scheduled"; import { formatScheduleTime, holdUntil } from "@/lib/schedule"; import { t as translate } from "@/lib/i18n"; import { BASE_PATH } from "@/lib/basePath"; -import { settings } from "./settings"; +import { settings, type Template } from "./settings"; import { useContacts } from "./contacts"; import { emlFilename } from "@/lib/text/emlName"; import { fillPlaceholders, type PlaceholderContext } from "@/lib/templatePlaceholders"; @@ -160,7 +160,7 @@ interface ComposeState { send(key: string): Promise{translate("Added to the message when you insert the template, beside anyone already on it.")}