feat: email a contact or group via the in-app composer

Adds a "Send email to group" action (To / Cc / Bcc) that opens the composer
pre-filled with the group's members in the chosen field, preserving each
member's display name. It is available both in the group context menu (between
"Edit Group" and "Delete") and in the group detail panel's header (shown when
the group has at least one member with an email). The single-contact "Send
email" button in the contact detail panel uses the same path.

Routing is internal, not via mailto:. Contacts is its own route and the composer
lives in the mail route, so the handoff stashes the recipients
(savePendingMailto) and does a client-side router.push("/"); the main route's
existing consumePendingMailto effect opens the composer in the current account.
This avoids the OS mailto handler (which could open a different mail app) and
the protocol round-trip's full-page reload, which dropped the in-memory
per-account JMAP clients of a multi-account session (a logout).

- contacts/page.tsx: openComposeInApp(recipients, field) shared helper;
  handleComposeGroupFromSidebar (deduped "Name <email>" members, empty -> toast)
  and handleComposeContact; wired to the sidebar, group detail, contact detail.
- contact-group-detail.tsx: onComposeGroup(field) prop + To/Cc/Bcc header control
  (shown when the group has emailable members).
- contacts-sidebar.tsx: onComposeGroup(groupId, field) prop + "Send email to
  group" submenu between Edit and Delete.
- contact-detail.tsx: onCompose() prop; the button is no longer a mailto: link.
- mailto.ts: recipient splitter is quote-aware (reuses the composer's
  splitRecipients) so a comma in a display name survives — still useful for real
  OS mailto: links.
- i18n: contacts.groups.send_email{,_to,_cc,_bcc} and no_member_emails across all
  locales.

Display names round-trip via formatRecipient -> parseRecipientList.
This commit is contained in:
Stefan Hildebrandt
2026-06-19 12:29:43 +02:00
committed by Linus Rath
parent c9eae3b3a1
commit ab3e0e717a
25 changed files with 324 additions and 32 deletions
+6 -1
View File
@@ -2359,7 +2359,12 @@
"members_label": "Členové",
"search_members": "Hledat kontakty k přidání...",
"no_members": "Tato skupina nemá žádné členy",
"member_count": "{count, plural, =0 {Žádní členové} one {1 člen} few {# členové} other {# členů}}"
"member_count": "{count, plural, =0 {Žádní členové} one {1 člen} few {# členové} other {# členů}}",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
},
"import": {
"title": "Importovat kontakty",
+6 -1
View File
@@ -2359,7 +2359,12 @@
"members_label": "Medlemmer",
"search_members": "Søg efter kontakter at tilføje...",
"no_members": "Ingen medlemmer i denne gruppe",
"member_count": "{count, plural, =0 {Ingen medlemmer} one {1 medlem} other {# medlemmer}}"
"member_count": "{count, plural, =0 {Ingen medlemmer} one {1 medlem} other {# medlemmer}}",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
},
"import": {
"title": "Importér kontakter",
+6 -1
View File
@@ -2359,7 +2359,12 @@
"members_label": "Mitglieder",
"search_members": "Kontakte zum Hinzufügen suchen...",
"no_members": "Keine Mitglieder in dieser Gruppe",
"member_count": "{count, plural, =0 {Keine Mitglieder} one {1 Mitglied} other {# Mitglieder}}"
"member_count": "{count, plural, =0 {Keine Mitglieder} one {1 Mitglied} other {# Mitglieder}}",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
},
"import": {
"title": "Kontakte importieren",
+6 -1
View File
@@ -2360,7 +2360,12 @@
"members_label": "Members",
"search_members": "Search contacts to add...",
"no_members": "No members in this group",
"member_count": "{count, plural, =0 {No members} one {1 member} other {# members}}"
"member_count": "{count, plural, =0 {No members} one {1 member} other {# members}}",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
},
"import": {
"title": "Import Contacts",
+6 -1
View File
@@ -2359,7 +2359,12 @@
"members_label": "Miembros",
"search_members": "Buscar contactos para agregar...",
"no_members": "No hay miembros en este grupo",
"member_count": "{count, plural, =0 {Sin miembros} one {1 miembro} other {# miembros}}"
"member_count": "{count, plural, =0 {Sin miembros} one {1 miembro} other {# miembros}}",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
},
"import": {
"title": "Importar contactos",
+6 -1
View File
@@ -2359,7 +2359,12 @@
"members_label": "Membres",
"search_members": "Rechercher des contacts à ajouter...",
"no_members": "Aucun membre dans ce groupe",
"member_count": "{count, plural, =0 {Aucun membre} one {1 membre} other {# membres}}"
"member_count": "{count, plural, =0 {Aucun membre} one {1 membre} other {# membres}}",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
},
"import": {
"title": "Importer des contacts",
+6 -1
View File
@@ -2360,7 +2360,12 @@
"members_label": "Tagok",
"search_members": "Névjegyek keresése a hozzáadáshoz...",
"no_members": "Nincs tag ebben a csoportban",
"member_count": "{count, plural, =0 {Nincs tag} one {1 tag} other {# tag}}"
"member_count": "{count, plural, =0 {Nincs tag} one {1 tag} other {# tag}}",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
},
"import": {
"title": "Névjegyek importálása",
+6 -1
View File
@@ -2359,7 +2359,12 @@
"members_label": "Membri",
"search_members": "Cerca contatti da aggiungere...",
"no_members": "Nessun membro in questo gruppo",
"member_count": "{count, plural, =0 {Nessun membro} one {1 membro} other {# membri}}"
"member_count": "{count, plural, =0 {Nessun membro} one {1 membro} other {# membri}}",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
},
"import": {
"title": "Importa contatti",
+6 -1
View File
@@ -2359,7 +2359,12 @@
"members_label": "メンバー",
"search_members": "追加する連絡先を検索...",
"no_members": "このグループにメンバーがいません",
"member_count": "{count, plural, =0 {メンバーなし} other {#人のメンバー}}"
"member_count": "{count, plural, =0 {メンバーなし} other {#人のメンバー}}",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
},
"import": {
"title": "連絡先をインポート",
+6 -1
View File
@@ -2359,7 +2359,12 @@
"members_label": "멤버",
"search_members": "추가할 연락처 검색...",
"no_members": "이 그룹에는 멤버가 없어요",
"member_count": "멤버 {count}명"
"member_count": "멤버 {count}명",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
},
"import": {
"title": "연락처 가져오기",
+6 -1
View File
@@ -2355,7 +2355,12 @@
"members_label": "Dalībnieki",
"search_members": "Meklēt kontaktus, ko pievienot...",
"no_members": "Šajā grupā nav dalībnieku",
"member_count": "{count, plural, =0 {Nav dalībnieku} one {1 dalībnieks} other {# dalībnieki}}"
"member_count": "{count, plural, =0 {Nav dalībnieku} one {1 dalībnieks} other {# dalībnieki}}",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
},
"import": {
"title": "Kontaktu imports",
+6 -1
View File
@@ -2359,7 +2359,12 @@
"members_label": "Leden",
"search_members": "Contacten zoeken om toe te voegen...",
"no_members": "Geen leden in deze groep",
"member_count": "{count, plural, =0 {Geen leden} one {1 lid} other {# leden}}"
"member_count": "{count, plural, =0 {Geen leden} one {1 lid} other {# leden}}",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
},
"import": {
"title": "Contacten importeren",
+6 -1
View File
@@ -2359,7 +2359,12 @@
"members_label": "Członkowie",
"search_members": "Szukaj kontaktów do dodania...",
"no_members": "Brak członków w tej grupie",
"member_count": "{count, plural, =0 {Brak członków} one {1 członek} other {# członków}}"
"member_count": "{count, plural, =0 {Brak członków} one {1 członek} other {# członków}}",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
},
"import": {
"title": "Importuj kontakty",
+6 -1
View File
@@ -2359,7 +2359,12 @@
"members_label": "Membros",
"search_members": "Pesquisar contatos para adicionar...",
"no_members": "Nenhum membro neste grupo",
"member_count": "{count, plural, =0 {Nenhum membro} one {1 membro} other {# membros}}"
"member_count": "{count, plural, =0 {Nenhum membro} one {1 membro} other {# membros}}",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
},
"import": {
"title": "Importar contatos",
+6 -1
View File
@@ -2359,7 +2359,12 @@
"members_label": "Участники",
"search_members": "Поиск контактов для добавления...",
"no_members": "В этой группе нет участников",
"member_count": "{count, plural, =0 {Нет участников} one {1 участник} other {# участников}}"
"member_count": "{count, plural, =0 {Нет участников} one {1 участник} other {# участников}}",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
},
"import": {
"title": "Импорт контактов",
+6 -1
View File
@@ -2359,7 +2359,12 @@
"members_label": "Üyeler",
"search_members": "Eklemek için kişileri arayın...",
"no_members": "Bu grupta üye yok",
"member_count": "{count, plural, =0 {Üye yok} one {1 üye} other {# üye}}"
"member_count": "{count, plural, =0 {Üye yok} one {1 üye} other {# üye}}",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
},
"import": {
"title": "Kişileri İçe Aktar",
+6 -1
View File
@@ -2359,7 +2359,12 @@
"members_label": "Члени",
"search_members": "Пошук контактів для додавання...",
"no_members": "У цій групі немає учасників",
"member_count": "{count, plural, =0 {Немає учасників} one {1 учасник} few {# учасники} many {# учасників} other {# учасників}}"
"member_count": "{count, plural, =0 {Немає учасників} one {1 учасник} few {# учасники} many {# учасників} other {# учасників}}",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
},
"import": {
"title": "Імпортувати контакти",
+6 -1
View File
@@ -2359,7 +2359,12 @@
"members_label": "成员",
"search_members": "搜索联系人以添加...",
"no_members": "该群组中没有成员",
"member_count": "{count, plural, =0 {无成员} one {1 位成员} other {# 位成员}}"
"member_count": "{count, plural, =0 {无成员} one {1 位成员} other {# 位成员}}",
"send_email": "Send email to group",
"send_email_to": "To",
"send_email_cc": "Cc",
"send_email_bcc": "Bcc",
"no_member_emails": "This group has no members with an email address."
},
"import": {
"title": "导入联系人",