@bobfrankston/rmfmail 1.2.223 → 1.2.224
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/android-bootstrap.bundle.js +38 -4
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +81 -31
- package/client/app.bundle.js.map +2 -2
- package/client/components/address-book.js +93 -29
- package/client/components/address-book.js.map +1 -1
- package/client/components/address-book.ts +103 -24
- package/client/compose/compose.bundle.js +2 -2
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/lib/api-client.js +2 -2
- package/client/lib/api-client.js.map +1 -1
- package/client/lib/api-client.ts +2 -2
- package/client/lib/mailxapi.js +2 -2
- package/client/styles/components.css +40 -0
- package/package.json +5 -5
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +32 -4
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +34 -5
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/index.d.ts +1 -1
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +33 -4
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +28 -4
- package/packages/mailx-service/jsonrpc.js +1 -1
- package/packages/mailx-service/jsonrpc.js.map +1 -1
- package/packages/mailx-service/jsonrpc.ts +1 -1
- package/packages/mailx-service/package.json +1 -1
- package/packages/mailx-store/db.d.ts +30 -10
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +108 -10
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +126 -13
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store-web/android-bootstrap.d.ts.map +1 -1
- package/packages/mailx-store-web/android-bootstrap.js +17 -2
- package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
- package/packages/mailx-store-web/android-bootstrap.ts +17 -2
- package/packages/mailx-store-web/db.d.ts +1 -1
- package/packages/mailx-store-web/db.d.ts.map +1 -1
- package/packages/mailx-store-web/db.js +17 -2
- package/packages/mailx-store-web/db.js.map +1 -1
- package/packages/mailx-store-web/db.ts +18 -3
- package/packages/mailx-store-web/package.json +1 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-65000 → node_modules.npmglobalize-stash-142920}/.package-lock.json +0 -0
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
* 'manual' (added directly here), 'google' (Google Contacts sync).
|
|
5
5
|
*/
|
|
6
6
|
import { listContacts, upsertContact, deleteContact } from "../lib/api-client.js";
|
|
7
|
+
/** Editable card fields, in the order they appear in the edit dialog.
|
|
8
|
+
* `multiline` gets a textarea; `type` feeds the input element. */
|
|
9
|
+
const CONTACT_FIELDS = [
|
|
10
|
+
{ key: "organization", label: "Company" },
|
|
11
|
+
{ key: "title", label: "Job title" },
|
|
12
|
+
{ key: "phone", label: "Phone", type: "tel" },
|
|
13
|
+
{ key: "website", label: "Website", type: "url", placeholder: "https://" },
|
|
14
|
+
{ key: "birthday", label: "Birthday", placeholder: "YYYY-MM-DD or --MM-DD" },
|
|
15
|
+
{ key: "address", label: "Address", multiline: true },
|
|
16
|
+
{ key: "notes", label: "Notes", multiline: true },
|
|
17
|
+
];
|
|
7
18
|
let isOpen = false;
|
|
8
19
|
export async function openAddressBook(prefillSearch) {
|
|
9
20
|
if (isOpen)
|
|
@@ -61,13 +72,13 @@ export async function openAddressBook(prefillSearch) {
|
|
|
61
72
|
<span class="ab-actions"></span>
|
|
62
73
|
</div>` + items.map(c => `
|
|
63
74
|
<div class="ab-row" data-email="${escapeAttr(c.email)}">
|
|
64
|
-
<span class="ab-name">${escapeHtml(c.name || "")}</span>
|
|
75
|
+
<span class="ab-name" title="${escapeAttr(cardSummary(c))}">${escapeHtml(c.name || "")}${cardSummary(c) ? ' <span class="ab-hascard" aria-hidden="true">•</span>' : ""}</span>
|
|
65
76
|
<span class="ab-email">${escapeHtml(c.email)}</span>
|
|
66
77
|
<span class="ab-source">${escapeHtml(c.source)}</span>
|
|
67
78
|
<span class="ab-count-cell">${c.useCount || 0}</span>
|
|
68
79
|
<span class="ab-last">${fmtDate(c.lastUsed)}</span>
|
|
69
80
|
<span class="ab-actions">
|
|
70
|
-
<button type="button" class="ab-edit" title="Edit
|
|
81
|
+
<button type="button" class="ab-edit" title="Edit contact">✎</button>
|
|
71
82
|
<button type="button" class="ab-del" title="Delete">🗑</button>
|
|
72
83
|
<button type="button" class="ab-mail" title="Compose to">✉</button>
|
|
73
84
|
</span>
|
|
@@ -80,40 +91,83 @@ export async function openAddressBook(prefillSearch) {
|
|
|
80
91
|
row.querySelector(".ab-mail")?.addEventListener("click", () => composeTo(c));
|
|
81
92
|
});
|
|
82
93
|
};
|
|
83
|
-
|
|
94
|
+
// Edit opens the whole card, not just the name. The inline name-only input
|
|
95
|
+
// this replaced was the only editor there was, which is why phone, company,
|
|
96
|
+
// address and the rest had nowhere to be typed even once the store could
|
|
97
|
+
// hold them (Bob 2026-08-07: "extend the contact support for all fields").
|
|
98
|
+
const beginEdit = (_row, c) => {
|
|
84
99
|
if (editingEmail === c.email)
|
|
85
100
|
return;
|
|
86
101
|
editingEmail = c.email;
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
c.name
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
const back = document.createElement("div");
|
|
103
|
+
back.className = "mailx-modal-backdrop";
|
|
104
|
+
const card = document.createElement("div");
|
|
105
|
+
card.className = "mailx-modal";
|
|
106
|
+
card.innerHTML = `
|
|
107
|
+
<div class="mailx-modal-title">
|
|
108
|
+
<span class="mailx-modal-title-text">Edit contact</span>
|
|
109
|
+
<button type="button" class="mailx-modal-close" data-role="x" title="Close (Esc)" aria-label="Close">×</button>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="ab-card">
|
|
112
|
+
<label class="ab-field"><span>Name</span>
|
|
113
|
+
<input type="text" class="mailx-modal-input" data-field="name" value="${escapeAttr(c.name || "")}"></label>
|
|
114
|
+
<label class="ab-field"><span>Email</span>
|
|
115
|
+
<input type="email" class="mailx-modal-input" value="${escapeAttr(c.email)}" disabled
|
|
116
|
+
title="The address identifies the contact — delete and re-add to change it"></label>
|
|
117
|
+
${CONTACT_FIELDS.map(f => `
|
|
118
|
+
<label class="ab-field"><span>${escapeHtml(f.label)}</span>
|
|
119
|
+
${f.multiline
|
|
120
|
+
? `<textarea class="mailx-modal-input" rows="2" data-field="${f.key}">${escapeHtml(c[f.key] || "")}</textarea>`
|
|
121
|
+
: `<input type="${f.type || "text"}" class="mailx-modal-input" data-field="${f.key}"
|
|
122
|
+
value="${escapeAttr(c[f.key] || "")}"
|
|
123
|
+
${f.placeholder ? `placeholder="${escapeAttr(f.placeholder)}"` : ""}>`}
|
|
124
|
+
</label>`).join("")}
|
|
125
|
+
</div>
|
|
126
|
+
<div class="mailx-modal-buttons">
|
|
127
|
+
<span class="mailx-modal-spacer"></span>
|
|
128
|
+
<button type="button" class="mailx-modal-btn" data-role="cancel">Cancel</button>
|
|
129
|
+
<button type="button" class="mailx-modal-btn mailx-modal-btn-primary" data-role="save">Save</button>
|
|
130
|
+
</div>`;
|
|
131
|
+
back.appendChild(card);
|
|
132
|
+
document.body.appendChild(back);
|
|
133
|
+
card.querySelector('[data-field="name"]')?.focus();
|
|
134
|
+
const shut = () => {
|
|
135
|
+
back.remove();
|
|
136
|
+
document.removeEventListener("keydown", onCardKey, true);
|
|
105
137
|
editingEmail = null;
|
|
106
138
|
};
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
139
|
+
// Capture phase + stopPropagation so Escape closes THIS dialog and not
|
|
140
|
+
// the address book behind it.
|
|
141
|
+
const onCardKey = (e) => {
|
|
142
|
+
if (e.key === "Escape") {
|
|
143
|
+
e.stopPropagation();
|
|
110
144
|
e.preventDefault();
|
|
111
|
-
|
|
145
|
+
shut();
|
|
112
146
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
147
|
+
};
|
|
148
|
+
document.addEventListener("keydown", onCardKey, true);
|
|
149
|
+
card.querySelector('[data-role="x"]')?.addEventListener("click", shut);
|
|
150
|
+
card.querySelector('[data-role="cancel"]')?.addEventListener("click", shut);
|
|
151
|
+
back.addEventListener("mousedown", (e) => { if (e.target === back)
|
|
152
|
+
shut(); });
|
|
153
|
+
card.querySelector('[data-role="save"]')?.addEventListener("click", async () => {
|
|
154
|
+
const val = (k) => card.querySelector(`[data-field="${k}"]`)?.value.trim() ?? "";
|
|
155
|
+
const fields = {};
|
|
156
|
+
for (const f of CONTACT_FIELDS)
|
|
157
|
+
fields[f.key] = val(f.key);
|
|
158
|
+
const newName = val("name");
|
|
159
|
+
const bd = fields.birthday;
|
|
160
|
+
if (bd && !/^(\d{4}|-)-\d{2}-\d{2}$/.test(bd)) {
|
|
161
|
+
alert(`Birthday must be YYYY-MM-DD, or --MM-DD when the year is unknown. Got "${bd}".`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
await upsertContact(newName, c.email, fields);
|
|
166
|
+
shut();
|
|
167
|
+
await reload();
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
alert(`Update failed: ${e?.message || e}`);
|
|
117
171
|
}
|
|
118
172
|
});
|
|
119
173
|
};
|
|
@@ -196,6 +250,16 @@ export async function openAddressBook(prefillSearch) {
|
|
|
196
250
|
close(); });
|
|
197
251
|
await reload();
|
|
198
252
|
}
|
|
253
|
+
/** One-line digest of the card fields that are filled in — the row is too
|
|
254
|
+
* narrow for seven more columns, so this rides as the name's tooltip and
|
|
255
|
+
* drives the • marker that says "there is more here than a name". */
|
|
256
|
+
function cardSummary(c) {
|
|
257
|
+
return CONTACT_FIELDS
|
|
258
|
+
.map(f => ({ label: f.label, value: (c[f.key] || "").trim() }))
|
|
259
|
+
.filter(x => x.value)
|
|
260
|
+
.map(x => `${x.label}: ${x.value.replace(/\s+/g, " ")}`)
|
|
261
|
+
.join("\n");
|
|
262
|
+
}
|
|
199
263
|
function escapeHtml(s) {
|
|
200
264
|
return s.replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[c]));
|
|
201
265
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"address-book.js","sourceRoot":"","sources":["address-book.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"address-book.js","sourceRoot":"","sources":["address-book.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,8 +11,29 @@ interface Contact {
|
|
|
11
11
|
source: string;
|
|
12
12
|
useCount: number;
|
|
13
13
|
lastUsed: number;
|
|
14
|
+
// Full contact card (store v3). Google People carries all of these and
|
|
15
|
+
// mailx now keeps them instead of discarding everything but the name.
|
|
16
|
+
organization?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
phone?: string;
|
|
19
|
+
address?: string;
|
|
20
|
+
website?: string;
|
|
21
|
+
notes?: string;
|
|
22
|
+
birthday?: string;
|
|
14
23
|
}
|
|
15
24
|
|
|
25
|
+
/** Editable card fields, in the order they appear in the edit dialog.
|
|
26
|
+
* `multiline` gets a textarea; `type` feeds the input element. */
|
|
27
|
+
const CONTACT_FIELDS: Array<{ key: keyof Contact & string; label: string; type?: string; multiline?: boolean; placeholder?: string }> = [
|
|
28
|
+
{ key: "organization", label: "Company" },
|
|
29
|
+
{ key: "title", label: "Job title" },
|
|
30
|
+
{ key: "phone", label: "Phone", type: "tel" },
|
|
31
|
+
{ key: "website", label: "Website", type: "url", placeholder: "https://" },
|
|
32
|
+
{ key: "birthday", label: "Birthday", placeholder: "YYYY-MM-DD or --MM-DD" },
|
|
33
|
+
{ key: "address", label: "Address", multiline: true },
|
|
34
|
+
{ key: "notes", label: "Notes", multiline: true },
|
|
35
|
+
];
|
|
36
|
+
|
|
16
37
|
let isOpen = false;
|
|
17
38
|
|
|
18
39
|
export async function openAddressBook(prefillSearch?: string): Promise<void> {
|
|
@@ -73,13 +94,13 @@ export async function openAddressBook(prefillSearch?: string): Promise<void> {
|
|
|
73
94
|
<span class="ab-actions"></span>
|
|
74
95
|
</div>` + items.map(c => `
|
|
75
96
|
<div class="ab-row" data-email="${escapeAttr(c.email)}">
|
|
76
|
-
<span class="ab-name">${escapeHtml(c.name || "")}</span>
|
|
97
|
+
<span class="ab-name" title="${escapeAttr(cardSummary(c))}">${escapeHtml(c.name || "")}${cardSummary(c) ? ' <span class="ab-hascard" aria-hidden="true">•</span>' : ""}</span>
|
|
77
98
|
<span class="ab-email">${escapeHtml(c.email)}</span>
|
|
78
99
|
<span class="ab-source">${escapeHtml(c.source)}</span>
|
|
79
100
|
<span class="ab-count-cell">${c.useCount || 0}</span>
|
|
80
101
|
<span class="ab-last">${fmtDate(c.lastUsed)}</span>
|
|
81
102
|
<span class="ab-actions">
|
|
82
|
-
<button type="button" class="ab-edit" title="Edit
|
|
103
|
+
<button type="button" class="ab-edit" title="Edit contact">✎</button>
|
|
83
104
|
<button type="button" class="ab-del" title="Delete">🗑</button>
|
|
84
105
|
<button type="button" class="ab-mail" title="Compose to">✉</button>
|
|
85
106
|
</span>
|
|
@@ -93,32 +114,79 @@ export async function openAddressBook(prefillSearch?: string): Promise<void> {
|
|
|
93
114
|
});
|
|
94
115
|
};
|
|
95
116
|
|
|
96
|
-
|
|
117
|
+
// Edit opens the whole card, not just the name. The inline name-only input
|
|
118
|
+
// this replaced was the only editor there was, which is why phone, company,
|
|
119
|
+
// address and the rest had nowhere to be typed even once the store could
|
|
120
|
+
// hold them (Bob 2026-08-07: "extend the contact support for all fields").
|
|
121
|
+
const beginEdit = (_row: HTMLElement, c: Contact) => {
|
|
97
122
|
if (editingEmail === c.email) return;
|
|
98
123
|
editingEmail = c.email;
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
124
|
+
|
|
125
|
+
const back = document.createElement("div");
|
|
126
|
+
back.className = "mailx-modal-backdrop";
|
|
127
|
+
const card = document.createElement("div");
|
|
128
|
+
card.className = "mailx-modal";
|
|
129
|
+
card.innerHTML = `
|
|
130
|
+
<div class="mailx-modal-title">
|
|
131
|
+
<span class="mailx-modal-title-text">Edit contact</span>
|
|
132
|
+
<button type="button" class="mailx-modal-close" data-role="x" title="Close (Esc)" aria-label="Close">×</button>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="ab-card">
|
|
135
|
+
<label class="ab-field"><span>Name</span>
|
|
136
|
+
<input type="text" class="mailx-modal-input" data-field="name" value="${escapeAttr(c.name || "")}"></label>
|
|
137
|
+
<label class="ab-field"><span>Email</span>
|
|
138
|
+
<input type="email" class="mailx-modal-input" value="${escapeAttr(c.email)}" disabled
|
|
139
|
+
title="The address identifies the contact — delete and re-add to change it"></label>
|
|
140
|
+
${CONTACT_FIELDS.map(f => `
|
|
141
|
+
<label class="ab-field"><span>${escapeHtml(f.label)}</span>
|
|
142
|
+
${f.multiline
|
|
143
|
+
? `<textarea class="mailx-modal-input" rows="2" data-field="${f.key}">${escapeHtml((c[f.key] as string) || "")}</textarea>`
|
|
144
|
+
: `<input type="${f.type || "text"}" class="mailx-modal-input" data-field="${f.key}"
|
|
145
|
+
value="${escapeAttr((c[f.key] as string) || "")}"
|
|
146
|
+
${f.placeholder ? `placeholder="${escapeAttr(f.placeholder)}"` : ""}>`}
|
|
147
|
+
</label>`).join("")}
|
|
148
|
+
</div>
|
|
149
|
+
<div class="mailx-modal-buttons">
|
|
150
|
+
<span class="mailx-modal-spacer"></span>
|
|
151
|
+
<button type="button" class="mailx-modal-btn" data-role="cancel">Cancel</button>
|
|
152
|
+
<button type="button" class="mailx-modal-btn mailx-modal-btn-primary" data-role="save">Save</button>
|
|
153
|
+
</div>`;
|
|
154
|
+
back.appendChild(card);
|
|
155
|
+
document.body.appendChild(back);
|
|
156
|
+
card.querySelector<HTMLInputElement>('[data-field="name"]')?.focus();
|
|
157
|
+
|
|
158
|
+
const shut = () => {
|
|
159
|
+
back.remove();
|
|
160
|
+
document.removeEventListener("keydown", onCardKey, true);
|
|
116
161
|
editingEmail = null;
|
|
117
162
|
};
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
163
|
+
// Capture phase + stopPropagation so Escape closes THIS dialog and not
|
|
164
|
+
// the address book behind it.
|
|
165
|
+
const onCardKey = (e: KeyboardEvent) => {
|
|
166
|
+
if (e.key === "Escape") { e.stopPropagation(); e.preventDefault(); shut(); }
|
|
167
|
+
};
|
|
168
|
+
document.addEventListener("keydown", onCardKey, true);
|
|
169
|
+
card.querySelector('[data-role="x"]')?.addEventListener("click", shut);
|
|
170
|
+
card.querySelector('[data-role="cancel"]')?.addEventListener("click", shut);
|
|
171
|
+
back.addEventListener("mousedown", (e) => { if (e.target === back) shut(); });
|
|
172
|
+
|
|
173
|
+
card.querySelector('[data-role="save"]')?.addEventListener("click", async () => {
|
|
174
|
+
const val = (k: string) => card.querySelector<HTMLInputElement | HTMLTextAreaElement>(`[data-field="${k}"]`)?.value.trim() ?? "";
|
|
175
|
+
const fields: Record<string, string> = {};
|
|
176
|
+
for (const f of CONTACT_FIELDS) fields[f.key] = val(f.key);
|
|
177
|
+
const newName = val("name");
|
|
178
|
+
const bd = fields.birthday;
|
|
179
|
+
if (bd && !/^(\d{4}|-)-\d{2}-\d{2}$/.test(bd)) {
|
|
180
|
+
alert(`Birthday must be YYYY-MM-DD, or --MM-DD when the year is unknown. Got "${bd}".`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
await upsertContact(newName, c.email, fields);
|
|
185
|
+
shut();
|
|
186
|
+
await reload();
|
|
187
|
+
} catch (e: any) {
|
|
188
|
+
alert(`Update failed: ${e?.message || e}`);
|
|
189
|
+
}
|
|
122
190
|
});
|
|
123
191
|
};
|
|
124
192
|
|
|
@@ -202,6 +270,17 @@ export async function openAddressBook(prefillSearch?: string): Promise<void> {
|
|
|
202
270
|
await reload();
|
|
203
271
|
}
|
|
204
272
|
|
|
273
|
+
/** One-line digest of the card fields that are filled in — the row is too
|
|
274
|
+
* narrow for seven more columns, so this rides as the name's tooltip and
|
|
275
|
+
* drives the • marker that says "there is more here than a name". */
|
|
276
|
+
function cardSummary(c: Contact): string {
|
|
277
|
+
return CONTACT_FIELDS
|
|
278
|
+
.map(f => ({ label: f.label, value: ((c[f.key] as string) || "").trim() }))
|
|
279
|
+
.filter(x => x.value)
|
|
280
|
+
.map(x => `${x.label}: ${x.value.replace(/\s+/g, " ")}`)
|
|
281
|
+
.join("\n");
|
|
282
|
+
}
|
|
283
|
+
|
|
205
284
|
function escapeHtml(s: string): string {
|
|
206
285
|
return s.replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }[c]!));
|
|
207
286
|
}
|
|
@@ -1303,8 +1303,8 @@ function hasBccHistoryTo(email) {
|
|
|
1303
1303
|
function listContacts(query, page = 1, pageSize = 100) {
|
|
1304
1304
|
return ipc().listContacts(query, page, pageSize);
|
|
1305
1305
|
}
|
|
1306
|
-
function upsertContact(name, email) {
|
|
1307
|
-
return ipc().upsertContact(name, email);
|
|
1306
|
+
function upsertContact(name, email, fields) {
|
|
1307
|
+
return ipc().upsertContact(name, email, fields);
|
|
1308
1308
|
}
|
|
1309
1309
|
function deleteContact(email) {
|
|
1310
1310
|
return ipc().deleteContact(email);
|