@rapidmx/web-client 0.25.1 → 0.26.0
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/apps/shared/components/calendar/EventDetails.tsx +5 -2
- package/apps/shared/components/contacts/ContactCard.tsx +330 -0
- package/apps/shared/components/contacts/ContactCardProvider.tsx +72 -0
- package/apps/shared/components/contacts/ParticipantLink.tsx +42 -0
- package/apps/shared/components/contacts/contactCardData.ts +220 -0
- package/apps/shared/components/contacts/participantDetails.ts +169 -0
- package/apps/shared/components/layout/AppShell.tsx +4 -0
- package/apps/shared/components/mail/InviteCard.tsx +27 -5
- package/apps/shared/components/mail/MailAddress.tsx +8 -3
- package/apps/shared/components/mail/MessageDetailPane.tsx +63 -18
- package/apps/shared/components/mail/VCardAttachmentChip.tsx +85 -0
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +112 -1
- package/apps/shared/components/mail/compose/RecipientInput.tsx +1 -0
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +15 -2
- package/apps/shared/components/mail/compose/dropFiles.ts +21 -0
- package/dist/apps/shared/components/calendar/EventDetails.js +2 -1
- package/dist/apps/shared/components/contacts/ContactCard.d.ts +19 -0
- package/dist/apps/shared/components/contacts/ContactCard.js +132 -0
- package/dist/apps/shared/components/contacts/ContactCardProvider.d.ts +25 -0
- package/dist/apps/shared/components/contacts/ContactCardProvider.js +41 -0
- package/dist/apps/shared/components/contacts/ParticipantLink.d.ts +18 -0
- package/dist/apps/shared/components/contacts/ParticipantLink.js +16 -0
- package/dist/apps/shared/components/contacts/contactCardData.d.ts +69 -0
- package/dist/apps/shared/components/contacts/contactCardData.js +158 -0
- package/dist/apps/shared/components/contacts/participantDetails.d.ts +64 -0
- package/dist/apps/shared/components/contacts/participantDetails.js +129 -0
- package/dist/apps/shared/components/layout/AppShell.js +11 -10
- package/dist/apps/shared/components/mail/InviteCard.js +5 -3
- package/dist/apps/shared/components/mail/MailAddress.d.ts +5 -2
- package/dist/apps/shared/components/mail/MailAddress.js +5 -3
- package/dist/apps/shared/components/mail/MessageDetailPane.js +19 -4
- package/dist/apps/shared/components/mail/VCardAttachmentChip.d.ts +18 -0
- package/dist/apps/shared/components/mail/VCardAttachmentChip.js +53 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +81 -4
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +1 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +5 -2
- package/dist/apps/shared/components/mail/compose/RichTextEditor.js +10 -1
- package/dist/apps/shared/components/mail/compose/dropFiles.d.ts +8 -0
- package/dist/apps/shared/components/mail/compose/dropFiles.js +18 -0
- package/dist/lib/contacts/contactsApi.d.ts +3 -0
- package/dist/lib/contacts/contactsApi.js +1 -1
- package/dist/lib/mail/directoryApi.d.ts +16 -10
- package/dist/lib/mail/directoryApi.js +20 -12
- package/lib/contacts/contactsApi.ts +1 -1
- package/lib/mail/directoryApi.ts +30 -12
- package/package.json +1 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useContext, useEffect, useRef, useState } from "react";
|
|
7
|
+
import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
|
|
8
|
+
import ContactAvatar from "../../../../lib/components/avatar/ContactAvatar.js";
|
|
9
|
+
import Button from "../../../../lib/components/buttons/Button.js";
|
|
10
|
+
import CopyButton from "../../../../lib/components/buttons/CopyButton.js";
|
|
11
|
+
import Skeleton from "../../../../lib/components/feedback/Skeleton.js";
|
|
12
|
+
import Modal from "../../../../lib/components/overlays/Modal.js";
|
|
13
|
+
import { formatMailAddress } from "../../../../lib/mail/mailAddress.js";
|
|
14
|
+
import { listMailboxes } from "../../../../lib/mail/mailApi.js";
|
|
15
|
+
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
16
|
+
import { MAILBOX_LIST_LIMIT, MailConnectionContext } from "../../mail/useMailConnection.js";
|
|
17
|
+
import { notify } from "../../notifications/store.js";
|
|
18
|
+
import { notifyApiError } from "../../notifications/apiErrors.js";
|
|
19
|
+
import { APP_HREFS } from "../../navigation/appHrefs.js";
|
|
20
|
+
import { useCompose } from "../mail/compose/ComposeContext.js";
|
|
21
|
+
import { formatInviteWhen } from "../mail/invite/inviteFormat.js";
|
|
22
|
+
import { contactScope, createContactFor, findContact, loadFolders, loadParticipantCard, loadRecentMessages, loadUpcomingEvents, scopeMailboxes, } from "./contactCardData.js";
|
|
23
|
+
import { contactInputFor, isExternalAddress, ownDomains, participantName } from "./participantDetails.js";
|
|
24
|
+
/**
|
|
25
|
+
* One piece of the card's data. `load` is `undefined` while what it needs is still coming (it stays loading), and runs once when it is there; a
|
|
26
|
+
* failure is this section's error and nothing else's. `upstream` is the section this one needs: its error is this one's too.
|
|
27
|
+
*/
|
|
28
|
+
function useSection(load, upstream) {
|
|
29
|
+
const [state, setState] = useState({ status: "loading" });
|
|
30
|
+
const ready = !!load;
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!load) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let cancelled = false;
|
|
36
|
+
load().then((data) => !cancelled && setState({ status: "ready", data }), () => !cancelled && setState({ status: "error" }));
|
|
37
|
+
return () => {
|
|
38
|
+
cancelled = true;
|
|
39
|
+
};
|
|
40
|
+
}, [ready]);
|
|
41
|
+
return upstream?.status === "error" ? { status: "error" } : state;
|
|
42
|
+
}
|
|
43
|
+
const KIND_LABEL = { work: "Work", home: "Home", other: "" };
|
|
44
|
+
function kindLabel(kind) {
|
|
45
|
+
return KIND_LABEL[kind];
|
|
46
|
+
}
|
|
47
|
+
function Detail({ label, children }) {
|
|
48
|
+
return (_jsxs("div", { className: "flex gap-3 text-sm", children: [_jsx("dt", { className: "w-24 shrink-0 text-text-muted", children: label }), _jsx("dd", { className: "min-w-0 break-words whitespace-pre-line", children: children })] }));
|
|
49
|
+
}
|
|
50
|
+
/** A card section: its heading, and while loading a skeleton, on failure the reason, when empty the empty text, else what `render` draws. */
|
|
51
|
+
function CardSection({ title, section, empty, failure, isEmpty, render, }) {
|
|
52
|
+
return (_jsxs("section", { className: "mt-5", children: [_jsx("h3", { className: "text-sm font-bold mb-2", children: title }), section.status === "loading" ? (_jsxs("div", { role: "status", "aria-label": `Loading ${title.toLowerCase()}`, className: "flex flex-col gap-2", children: [_jsx(Skeleton, { width: "w-3/4" }), _jsx(Skeleton, { width: "w-1/2" })] })) : section.status === "error" ? (_jsx("p", { role: "alert", className: "text-sm text-danger", children: failure })) : isEmpty(section.data) ? (_jsx("p", { className: "text-sm text-text-muted", children: empty })) : (render(section.data))] }));
|
|
53
|
+
}
|
|
54
|
+
const ACTION_CLASS = "!w-auto !py-1 !px-3 text-xs";
|
|
55
|
+
const ROW_LINK_CLASS = "block rounded-sm px-2 py-1.5 -mx-2 hover:bg-surface-alt focus-visible:outline-2 focus-visible:outline-primary";
|
|
56
|
+
/**
|
|
57
|
+
* A person's contact card, as Outlook's people card: a big avatar, their name, an "External" badge when the address is outside the user's own
|
|
58
|
+
* domains, the actions (email them, copy the address, create the contact or open it), then their contact details, the last messages to or from
|
|
59
|
+
* them and the events coming up that they organize or were invited to. Each section loads on its own, with a skeleton, an empty text and an
|
|
60
|
+
* error that touches nothing else on the card.
|
|
61
|
+
*
|
|
62
|
+
* When the address is not in the user's address book the details are what is known: the name and address, and everything a vCard attached to the
|
|
63
|
+
* message they were met in says. "Create contact" stores that. The lookups are made once, when the card opens.
|
|
64
|
+
*/
|
|
65
|
+
export default function ContactCard({ participant, context = {}, onClose, userUid }) {
|
|
66
|
+
const client = useApiClient();
|
|
67
|
+
const { openCompose } = useCompose();
|
|
68
|
+
const connection = useContext(MailConnectionContext);
|
|
69
|
+
const [listed, setListed] = useState({ status: "loading" });
|
|
70
|
+
const [created, setCreated] = useState();
|
|
71
|
+
const [creating, setCreating] = useState(false);
|
|
72
|
+
const mounted = useRef(true);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
mounted.current = true;
|
|
75
|
+
return () => void (mounted.current = false);
|
|
76
|
+
}, []);
|
|
77
|
+
// The mailboxes come from the app frame's connection when there is one; otherwise (a page rendered outside it) they are asked for.
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (connection) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
listMailboxes({ limit: MAILBOX_LIST_LIMIT }, client).then((data) => mounted.current && setListed({ status: "ready", data }), () => mounted.current && setListed({ status: "error" }));
|
|
83
|
+
}, []);
|
|
84
|
+
const mailboxes = connection
|
|
85
|
+
? connection.status === "ready"
|
|
86
|
+
? { status: "ready", data: connection.mailboxes }
|
|
87
|
+
: connection.status === "error"
|
|
88
|
+
? { status: "error" }
|
|
89
|
+
: { status: "loading" }
|
|
90
|
+
: listed;
|
|
91
|
+
const scope = mailboxes.status === "ready" ? scopeMailboxes(mailboxes.data, userUid, context.message) : undefined;
|
|
92
|
+
const now = useRef(new Date());
|
|
93
|
+
const folders = useSection(scope && (() => loadFolders(scope, client)), mailboxes);
|
|
94
|
+
const contact = useSection(folders.status === "ready" ? () => findContact(folders.data, participant.address, client) : undefined, folders);
|
|
95
|
+
const messages = useSection(scope && (() => loadRecentMessages(scope, participant.address, client)), mailboxes);
|
|
96
|
+
const events = useSection(folders.status === "ready" ? () => loadUpcomingEvents(folders.data, participant.address, now.current, client) : undefined, folders);
|
|
97
|
+
const card = useSection(() => loadParticipantCard(participant, context, client));
|
|
98
|
+
const name = participantName(participant);
|
|
99
|
+
const stored = created ?? (contact.status === "ready" ? contact.data : undefined);
|
|
100
|
+
const external = mailboxes.status === "ready" && isExternalAddress(participant.address, ownDomains(mailboxes.data));
|
|
101
|
+
// What is known when there is no contact: the name, the address and the message's vCard.
|
|
102
|
+
const known = stored ?? contactInputFor(participant, card.status === "ready" ? card.data : undefined, { mailboxUid: "", folderUid: "" });
|
|
103
|
+
// What "Create contact" needs, once it is all there.
|
|
104
|
+
const createWith = folders.status === "ready" && mailboxes.status === "ready" && card.status === "ready"
|
|
105
|
+
? { folders: folders.data, mailboxes: mailboxes.data, card: card.data }
|
|
106
|
+
: undefined;
|
|
107
|
+
async function handleCreate({ folders: folderList, mailboxes: mailboxList, card: parsed }) {
|
|
108
|
+
setCreating(true);
|
|
109
|
+
try {
|
|
110
|
+
const target = contactScope(mailboxList, folderList, userUid, context.message);
|
|
111
|
+
const made = await createContactFor(participant, parsed, target, client);
|
|
112
|
+
clearPinnedSignerCache();
|
|
113
|
+
notify({ kind: "success", title: "Contact created", message: `${made.displayName} was added to your contacts.` });
|
|
114
|
+
if (mounted.current) {
|
|
115
|
+
setCreated(made);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
notifyApiError(err, "Couldn't create this contact");
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
if (mounted.current) {
|
|
123
|
+
setCreating(false);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function handleEmail() {
|
|
128
|
+
openCompose({ to: formatMailAddress(participant) });
|
|
129
|
+
onClose();
|
|
130
|
+
}
|
|
131
|
+
return (_jsxs(Modal, { open: true, onClose: onClose, title: name, children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsx(ContactAvatar, { displayName: name, size: 72 }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-sm text-text-muted break-words", children: participant.address }), external && (_jsx("span", { className: "inline-block mt-1 text-xs font-semibold py-0.5 px-2 rounded-pill bg-warning/15 text-text", children: "External" })), (known.jobTitle || known.company) && (_jsx("p", { className: "mt-1 text-sm break-words", children: [known.jobTitle, known.company].filter(Boolean).join(", ") }))] })] }), _jsxs("div", { className: "mt-4 flex flex-wrap items-center gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: ACTION_CLASS, onClick: handleEmail, children: "Email" }), _jsx(CopyButton, { value: participant.address, label: "Copy address", children: "Copy address" }), stored ? (_jsx("a", { href: `${APP_HREFS.contacts}/${encodeURIComponent(stored.uid)}`, onClick: onClose, className: "inline-flex items-center text-xs font-semibold py-1 px-3 rounded-sm border border-border hover:border-accent hover:text-accent-dark focus-visible:outline-2 focus-visible:outline-primary", children: "Open contact" })) : (_jsx(Button, { type: "button", variant: "secondary", className: ACTION_CLASS, disabled: !createWith || creating, loading: creating, onClick: () => void handleCreate(createWith), children: "Create contact" }))] }), _jsxs("section", { className: "mt-5", children: [_jsx("h3", { className: "text-sm font-bold mb-2", children: "Contact" }), !stored && contact.status === "loading" && _jsx(Skeleton, { width: "w-1/2", className: "mb-2" }), !stored && contact.status === "error" && (_jsx("p", { role: "alert", className: "text-sm text-danger mb-2", children: "Couldn\u2019t check your address book." })), !stored && contact.status === "ready" && _jsx("p", { className: "text-sm text-text-muted mb-2", children: "Not in your address book." }), _jsxs("dl", { className: "flex flex-col gap-1.5", children: [known.emails.map((email) => (_jsx(Detail, { label: ["Email", kindLabel(email.type)].filter(Boolean).join(" "), children: email.address }, `e:${email.address}`))), known.phones.map((phone) => (_jsx(Detail, { label: ["Phone", kindLabel(phone.type)].filter(Boolean).join(" "), children: phone.phoneNumber }, `p:${phone.phoneNumber}`))), known.company && _jsx(Detail, { label: "Company", children: known.company }), known.jobTitle && _jsx(Detail, { label: "Job title", children: known.jobTitle }), known.addresses.map((address, index) => (_jsx(Detail, { label: ["Address", kindLabel(address.type)].filter(Boolean).join(" "), children: [address.street, [address.postalCode, address.city].filter(Boolean).join(" "), address.state, address.country].filter(Boolean).join("\n") }, `a:${index}`))), known.notes && _jsx(Detail, { label: "Notes", children: known.notes })] })] }), _jsx(CardSection, { title: "Recent messages", section: messages, empty: "No recent messages", failure: "Couldn't load recent messages.", isEmpty: (list) => list.length === 0, render: (list) => (_jsx("ul", { className: "flex flex-col", children: list.map((message) => (_jsx("li", { children: _jsxs("a", { href: `/messages/${encodeURIComponent(message.uid)}`, onClick: onClose, className: ROW_LINK_CLASS, children: [_jsxs("span", { className: "flex items-baseline justify-between gap-3 text-sm", children: [_jsx("span", { className: "font-medium truncate", children: message.subject || "(No subject)" }), _jsx("time", { dateTime: message.receivedDate, className: "text-xs text-text-muted shrink-0", children: new Date(message.receivedDate).toLocaleDateString() })] }), _jsx("span", { className: "block text-xs text-text-muted truncate", children: message.bodyPreview })] }) }, message.uid))) })) }), _jsx(CardSection, { title: "Upcoming events", section: events, empty: "No upcoming events", failure: "Couldn't load upcoming events.", isEmpty: (list) => list.length === 0, render: (list) => (_jsx("ul", { className: "flex flex-col", children: list.map((event) => (_jsx("li", { children: _jsxs("a", { href: APP_HREFS.calendar, onClick: onClose, className: ROW_LINK_CLASS, children: [_jsx("span", { className: "block text-sm font-medium truncate", children: event.title }), _jsx("span", { className: "block text-xs text-text-muted", children: formatInviteWhen(event) })] }) }, event.occurrenceKey))) })) })] }));
|
|
132
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React, { PropsWithChildren } from "react";
|
|
2
|
+
import { type Message } from "../../../../lib/mail/mailApi.js";
|
|
3
|
+
import { type AddCardsResult, type ContactCardContext } from "./contactCardData.js";
|
|
4
|
+
import type { Participant } from "./participantDetails.js";
|
|
5
|
+
export interface ContactCardValue {
|
|
6
|
+
/** Opens the contact card of `participant`; `context` is the message they were met in, when there is one (its vCard attachments and mailbox). */
|
|
7
|
+
show: (participant: Participant, context?: ContactCardContext) => void;
|
|
8
|
+
/**
|
|
9
|
+
* Adds every card of a vCard file's `text` to the user's address book - in the default contacts folder of `message`'s mailbox when it is one of
|
|
10
|
+
* theirs, else of their primary mailbox - skipping the ones whose email is already a contact's. Rejects if a card cannot be stored (an
|
|
11
|
+
* `AddCardsError`) or the folders cannot be read. Does nothing useful outside a provider (`available` is `false`).
|
|
12
|
+
*/
|
|
13
|
+
addVCards: (text: string, message?: Message) => Promise<AddCardsResult>;
|
|
14
|
+
/** `false` outside a `ContactCardProvider`, where `show()` does nothing: a caller then draws plain text instead of a button. */
|
|
15
|
+
available: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Shows a person's contact card from anywhere inside `AppShell` - see `ContactCard`. */
|
|
18
|
+
export declare function useContactCard(): ContactCardValue;
|
|
19
|
+
/**
|
|
20
|
+
* Owns the one contact card that can be open. Mounted once in `AppShell` (inside the compose provider, whose windows the card's "Email" opens), so
|
|
21
|
+
* a participant is clickable in every app. The card is a modal: focus goes into it, and back to the participant that was clicked when it closes.
|
|
22
|
+
*/
|
|
23
|
+
export default function ContactCardProvider({ children, userUid }: PropsWithChildren<{
|
|
24
|
+
userUid?: string;
|
|
25
|
+
}>): React.JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { createContext, useCallback, useContext, useMemo, useState } from "react";
|
|
7
|
+
import { parseVCards } from "../../../../lib/contacts/vcard.js";
|
|
8
|
+
import { listMailboxes } from "../../../../lib/mail/mailApi.js";
|
|
9
|
+
import { useApiClient } from "../../../../lib/util/apiClientContext.js";
|
|
10
|
+
import { MAILBOX_LIST_LIMIT, MailConnectionContext } from "../../mail/useMailConnection.js";
|
|
11
|
+
import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
|
|
12
|
+
import ContactCard from "./ContactCard.js";
|
|
13
|
+
import { addCardsToAddressBook, contactScope, loadFolders, scopeMailboxes } from "./contactCardData.js";
|
|
14
|
+
const CardContext = createContext({ show: () => undefined, addVCards: () => Promise.resolve({ added: 0, skipped: 0 }), available: false });
|
|
15
|
+
/** Shows a person's contact card from anywhere inside `AppShell` - see `ContactCard`. */
|
|
16
|
+
export function useContactCard() {
|
|
17
|
+
return useContext(CardContext);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Owns the one contact card that can be open. Mounted once in `AppShell` (inside the compose provider, whose windows the card's "Email" opens), so
|
|
21
|
+
* a participant is clickable in every app. The card is a modal: focus goes into it, and back to the participant that was clicked when it closes.
|
|
22
|
+
*/
|
|
23
|
+
export default function ContactCardProvider({ children, userUid }) {
|
|
24
|
+
const client = useApiClient();
|
|
25
|
+
const connection = useContext(MailConnectionContext);
|
|
26
|
+
const [shown, setShown] = useState(null);
|
|
27
|
+
const show = useCallback((participant, context) => {
|
|
28
|
+
setShown((previous) => ({ participant, context, id: (previous?.id ?? 0) + 1 }));
|
|
29
|
+
}, []);
|
|
30
|
+
const addVCards = useCallback(async (text, message) => {
|
|
31
|
+
const cards = parseVCards(text);
|
|
32
|
+
// The frame's mailboxes when it has them; a page rendered outside it asks.
|
|
33
|
+
const mailboxes = connection?.status === "ready" ? connection.mailboxes : await listMailboxes({ limit: MAILBOX_LIST_LIMIT }, client);
|
|
34
|
+
const folders = await loadFolders(scopeMailboxes(mailboxes, userUid, message), client);
|
|
35
|
+
const result = await addCardsToAddressBook(cards, contactScope(mailboxes, folders, userUid, message), client);
|
|
36
|
+
clearPinnedSignerCache();
|
|
37
|
+
return result;
|
|
38
|
+
}, [connection, client, userUid]);
|
|
39
|
+
const value = useMemo(() => ({ show, addVCards, available: true }), [show, addVCards]);
|
|
40
|
+
return (_jsxs(CardContext.Provider, { value: value, children: [children, shown && _jsx(ContactCard, { participant: shown.participant, context: shown.context, userUid: userUid, onClose: () => setShown(null) }, shown.id)] }));
|
|
41
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
import type { ContactCardContext } from "./contactCardData.js";
|
|
3
|
+
import type { Participant } from "./participantDetails.js";
|
|
4
|
+
export interface ParticipantLinkProps {
|
|
5
|
+
participant: Participant;
|
|
6
|
+
/** The message the participant was met in - see `ContactCardValue.show`. */
|
|
7
|
+
context?: ContactCardContext;
|
|
8
|
+
/** The button's accessible name, when what it shows has none of its own (an avatar). */
|
|
9
|
+
label?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A participant of a message or an event (a sender, a recipient, an organizer, a guest) that opens their contact card when clicked: a real
|
|
15
|
+
* button, underlined on hover and with a visible focus ring, named by what it shows. Outside a `ContactCardProvider` it is plain text. Never
|
|
16
|
+
* put one inside another button or link.
|
|
17
|
+
*/
|
|
18
|
+
export default function ParticipantLink({ participant, context, label, className, children }: ParticipantLinkProps): React.JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useContactCard } from "./ContactCardProvider.js";
|
|
3
|
+
/**
|
|
4
|
+
* A participant of a message or an event (a sender, a recipient, an organizer, a guest) that opens their contact card when clicked: a real
|
|
5
|
+
* button, underlined on hover and with a visible focus ring, named by what it shows. Outside a `ContactCardProvider` it is plain text. Never
|
|
6
|
+
* put one inside another button or link.
|
|
7
|
+
*/
|
|
8
|
+
export default function ParticipantLink({ participant, context, label, className, children }) {
|
|
9
|
+
const { show, available } = useContactCard();
|
|
10
|
+
if (!available) {
|
|
11
|
+
return _jsx("span", { className: className, children: children });
|
|
12
|
+
}
|
|
13
|
+
return (_jsx("button", { type: "button", "aria-label": label, onClick: () => show(participant, context), className: ["text-left rounded-sm hover:underline focus-visible:underline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary", className]
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join(" "), children: children }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type CalendarOccurrence } from "../../../../lib/calendar/recurrence.js";
|
|
2
|
+
import { type Contact } from "../../../../lib/contacts/contactsApi.js";
|
|
3
|
+
import { type ParsedVCardContact } from "../../../../lib/contacts/vcard.js";
|
|
4
|
+
import { type Attachment, type Folder, type Mailbox, type Message } from "../../../../lib/mail/mailApi.js";
|
|
5
|
+
import { type ApiClient } from "../../../../lib/util/api.js";
|
|
6
|
+
import { type Participant } from "./participantDetails.js";
|
|
7
|
+
/** What the card is opened for besides who: the message the participant was met in, and that message's attachments when the caller already has them. */
|
|
8
|
+
export interface ContactCardContext {
|
|
9
|
+
message?: Message;
|
|
10
|
+
attachments?: Attachment[];
|
|
11
|
+
}
|
|
12
|
+
/** How many recent messages and upcoming events the card lists. */
|
|
13
|
+
export declare const CARD_LIST_SIZE = 5;
|
|
14
|
+
/** How far ahead "upcoming" looks, in days. */
|
|
15
|
+
export declare const UPCOMING_DAYS = 60;
|
|
16
|
+
/** The most vCard attachments and the most bytes of one that are read to fill a contact in. */
|
|
17
|
+
export declare const MAX_VCARD_FILES = 3;
|
|
18
|
+
export declare const MAX_VCARD_BYTES: number;
|
|
19
|
+
/** The mailboxes whose data the card reads: the user's own, else the primary/first one, plus the message's mailbox when it is one they can reach. */
|
|
20
|
+
export declare function scopeMailboxes(mailboxes: readonly Mailbox[], userUid: string | undefined, message?: Message): Mailbox[];
|
|
21
|
+
/** Every folder of `mailboxes`. A mailbox whose folders cannot be listed is left out rather than failing the rest. */
|
|
22
|
+
export declare function loadFolders(mailboxes: readonly Mailbox[], client?: ApiClient): Promise<Folder[]>;
|
|
23
|
+
/** The contact the address belongs to in `folders`' contacts folders, if any. */
|
|
24
|
+
export declare function findContact(folders: readonly Folder[], address: string, client?: ApiClient): Promise<Contact | undefined>;
|
|
25
|
+
/** The last `CARD_LIST_SIZE` messages to or from `address` in `mailboxes`, newest first. */
|
|
26
|
+
export declare function loadRecentMessages(mailboxes: readonly Mailbox[], address: string, client?: ApiClient): Promise<Message[]>;
|
|
27
|
+
/** The next `CARD_LIST_SIZE` events within `UPCOMING_DAYS` from `now` that `address` organizes or is invited to, soonest first. */
|
|
28
|
+
export declare function loadUpcomingEvents(folders: readonly Folder[], address: string, now: Date, client?: ApiClient): Promise<CalendarOccurrence[]>;
|
|
29
|
+
/** A vCard attachment's text. */
|
|
30
|
+
export declare function fetchVCardText(attachmentUid: string): Promise<string>;
|
|
31
|
+
/**
|
|
32
|
+
* The vCard of the context message that describes `participant`: the first attachment holding a card with their address, else - only for the
|
|
33
|
+
* message's sender, whose own vCard it presumably is - the first card there is. `undefined` when there is none, and for any failure to read one:
|
|
34
|
+
* a vCard only adds detail, it is never needed.
|
|
35
|
+
*/
|
|
36
|
+
export declare function loadParticipantCard(participant: Participant, context: ContactCardContext, client?: ApiClient): Promise<ParsedVCardContact | undefined>;
|
|
37
|
+
/** Where a new contact goes: the message's mailbox when it is one of the user's own, else their primary mailbox - in that mailbox's contacts folder. */
|
|
38
|
+
export declare function contactScope(mailboxes: readonly Mailbox[], folders: readonly Folder[], userUid: string | undefined, message?: Message): {
|
|
39
|
+
mailboxUid: string;
|
|
40
|
+
folderUid: string;
|
|
41
|
+
};
|
|
42
|
+
/** Stores a contact for `participant` with everything known - the name, the address and whatever `card` says. */
|
|
43
|
+
export declare function createContactFor(participant: Participant, card: ParsedVCardContact | undefined, scope: {
|
|
44
|
+
mailboxUid: string;
|
|
45
|
+
folderUid: string;
|
|
46
|
+
}, client?: ApiClient): Promise<Contact>;
|
|
47
|
+
/**
|
|
48
|
+
* The sender's own contact card as a file to attach, `<display name>.vcf`: the mailbox being sent from (its name and primary address) with the phones, company and
|
|
49
|
+
* title of the user's own contact entry for that address, when they have one in their own mailboxes' address books. A lookup that fails just leaves those out.
|
|
50
|
+
*/
|
|
51
|
+
export declare function buildOwnCardFile(mailbox: Mailbox, mailboxes: readonly Mailbox[], userUid: string | undefined, client?: ApiClient): Promise<File>;
|
|
52
|
+
/** The outcome of adding a vCard file's cards to the address book. */
|
|
53
|
+
export interface AddCardsResult {
|
|
54
|
+
added: number;
|
|
55
|
+
skipped: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Adds every card of a vCard file to the contacts folder `scope` names, skipping the ones whose email is already a stored contact's (or an earlier
|
|
59
|
+
* card's in the same file). A card that cannot be stored fails the whole call after the earlier ones were added - `AddCardsError` says how many.
|
|
60
|
+
*/
|
|
61
|
+
export declare function addCardsToAddressBook(cards: readonly ParsedVCardContact[], scope: {
|
|
62
|
+
mailboxUid: string;
|
|
63
|
+
folderUid: string;
|
|
64
|
+
}, client?: ApiClient): Promise<AddCardsResult>;
|
|
65
|
+
/** A card failed to store: the message is the failure's and `added` how many cards were stored before it. */
|
|
66
|
+
export declare class AddCardsError extends Error {
|
|
67
|
+
readonly added: number;
|
|
68
|
+
constructor(failure: unknown, added: number);
|
|
69
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/**
|
|
6
|
+
* What the contact card (`ContactCard`) and the vCard attachment actions read and write: the user's contact and calendar folders, the contact
|
|
7
|
+
* behind an address, the last messages to or from it, its upcoming events, and the calls that make a contact from a participant or a vCard.
|
|
8
|
+
* Every function takes the `ApiClient` to use (`useApiClient()`), like the API modules under `lib/`.
|
|
9
|
+
*/
|
|
10
|
+
import { addDays } from "date-fns";
|
|
11
|
+
import { listCalendarEvents } from "../../../../lib/calendar/calendarApi.js";
|
|
12
|
+
import { expandAllOccurrences } from "../../../../lib/calendar/recurrence.js";
|
|
13
|
+
import { createContact, listContactsInFolders } from "../../../../lib/contacts/contactsApi.js";
|
|
14
|
+
import { parseVCards } from "../../../../lib/contacts/vcard.js";
|
|
15
|
+
import { attachmentContentUrl, getMessage, listAttachments, listFolders, } from "../../../../lib/mail/mailApi.js";
|
|
16
|
+
import { search } from "../../../../lib/search/searchApi.js";
|
|
17
|
+
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
18
|
+
import { ownMailboxes, primaryMailbox } from "../../mail/primaryMailbox.js";
|
|
19
|
+
import { cardForAddress, contactInputFor, contactInputFromCard, findContactByAddress, isVCardAttachment, normalizeAddress, ownContactCard, withoutDuplicates, } from "./participantDetails.js";
|
|
20
|
+
/** How many recent messages and upcoming events the card lists. */
|
|
21
|
+
export const CARD_LIST_SIZE = 5;
|
|
22
|
+
/** How far ahead "upcoming" looks, in days. */
|
|
23
|
+
export const UPCOMING_DAYS = 60;
|
|
24
|
+
/** The most vCard attachments and the most bytes of one that are read to fill a contact in. */
|
|
25
|
+
export const MAX_VCARD_FILES = 3;
|
|
26
|
+
export const MAX_VCARD_BYTES = 256 * 1024;
|
|
27
|
+
/** The mailboxes whose data the card reads: the user's own, else the primary/first one, plus the message's mailbox when it is one they can reach. */
|
|
28
|
+
export function scopeMailboxes(mailboxes, userUid, message) {
|
|
29
|
+
const own = ownMailboxes(mailboxes, userUid);
|
|
30
|
+
const scope = own.length > 0 ? own : [primaryMailbox(mailboxes, userUid)].filter((mailbox) => !!mailbox);
|
|
31
|
+
const messageMailbox = mailboxes.find((mailbox) => mailbox.uid === message?.mailboxUid);
|
|
32
|
+
return messageMailbox && !scope.includes(messageMailbox) ? [...scope, messageMailbox] : scope;
|
|
33
|
+
}
|
|
34
|
+
/** Every folder of `mailboxes`. A mailbox whose folders cannot be listed is left out rather than failing the rest. */
|
|
35
|
+
export async function loadFolders(mailboxes, client) {
|
|
36
|
+
const lists = await Promise.all(mailboxes.map((mailbox) => listFolders(mailbox.uid, client).catch(() => [])));
|
|
37
|
+
return lists.flat();
|
|
38
|
+
}
|
|
39
|
+
/** The contact the address belongs to in `folders`' contacts folders, if any. */
|
|
40
|
+
export async function findContact(folders, address, client) {
|
|
41
|
+
const uids = folders.filter((folder) => folder.type === "contacts").map((folder) => folder.uid);
|
|
42
|
+
return findContactByAddress(await listContactsInFolders(uids, client), address);
|
|
43
|
+
}
|
|
44
|
+
/** The last `CARD_LIST_SIZE` messages to or from `address` in `mailboxes`, newest first. */
|
|
45
|
+
export async function loadRecentMessages(mailboxes, address, client) {
|
|
46
|
+
const pages = await Promise.all(mailboxes.flatMap((mailbox) => ["from", "to"].map((role) => search("", { types: ["message"], [role]: address, limit: CARD_LIST_SIZE, mailboxUid: mailbox.uid }, client))));
|
|
47
|
+
const uids = [...new Set(pages.flatMap((page) => page.results.map((result) => result.entityUid)))];
|
|
48
|
+
// A hit can briefly outlive its message: one that no longer resolves is dropped.
|
|
49
|
+
const messages = (await Promise.all(uids.map((uid) => getMessage(uid, client).catch(() => null)))).filter((message) => !!message);
|
|
50
|
+
return messages.sort((a, b) => Date.parse(b.receivedDate) - Date.parse(a.receivedDate)).slice(0, CARD_LIST_SIZE);
|
|
51
|
+
}
|
|
52
|
+
/** The next `CARD_LIST_SIZE` events within `UPCOMING_DAYS` from `now` that `address` organizes or is invited to, soonest first. */
|
|
53
|
+
export async function loadUpcomingEvents(folders, address, now, client) {
|
|
54
|
+
const wanted = normalizeAddress(address);
|
|
55
|
+
const calendars = folders.filter((folder) => folder.type === "calendar");
|
|
56
|
+
const lists = await Promise.all(calendars.map((folder) => listCalendarEvents(folder.uid, client)));
|
|
57
|
+
const involved = lists
|
|
58
|
+
.flat()
|
|
59
|
+
.filter((event) => event.status !== "cancelled" &&
|
|
60
|
+
(normalizeAddress(event.organizer.address) === wanted ||
|
|
61
|
+
(event.attendees ?? []).some((attendee) => normalizeAddress(attendee.address) === wanted)));
|
|
62
|
+
return expandAllOccurrences(involved, now, addDays(now, UPCOMING_DAYS))
|
|
63
|
+
.sort((a, b) => Date.parse(a.startDate) - Date.parse(b.startDate))
|
|
64
|
+
.slice(0, CARD_LIST_SIZE);
|
|
65
|
+
}
|
|
66
|
+
/** A vCard attachment's text. */
|
|
67
|
+
export async function fetchVCardText(attachmentUid) {
|
|
68
|
+
const res = await fetch(attachmentContentUrl(attachmentUid), { credentials: "include" });
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
throw new ApiRequestError(res.statusText || "Could not load this contact card.", res.status);
|
|
71
|
+
}
|
|
72
|
+
return res.text();
|
|
73
|
+
}
|
|
74
|
+
/** The message's vCard attachments: `context.attachments` when given, else the message's own list. */
|
|
75
|
+
async function vCardAttachments(context, client) {
|
|
76
|
+
const { message } = context;
|
|
77
|
+
const attachments = context.attachments ?? (message?.hasAttachments ? await listAttachments(message.folderUid, message.uid, client) : []);
|
|
78
|
+
return attachments.filter((attachment) => isVCardAttachment(attachment) && attachment.sizeBytes <= MAX_VCARD_BYTES).slice(0, MAX_VCARD_FILES);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The vCard of the context message that describes `participant`: the first attachment holding a card with their address, else - only for the
|
|
82
|
+
* message's sender, whose own vCard it presumably is - the first card there is. `undefined` when there is none, and for any failure to read one:
|
|
83
|
+
* a vCard only adds detail, it is never needed.
|
|
84
|
+
*/
|
|
85
|
+
export async function loadParticipantCard(participant, context, client) {
|
|
86
|
+
try {
|
|
87
|
+
const isSender = !!context.message && normalizeAddress(context.message.from.address) === normalizeAddress(participant.address);
|
|
88
|
+
let fallback;
|
|
89
|
+
for (const attachment of await vCardAttachments(context, client)) {
|
|
90
|
+
const cards = parseVCards(await fetchVCardText(attachment.uid));
|
|
91
|
+
const match = cardForAddress(cards, participant.address, false);
|
|
92
|
+
if (match) {
|
|
93
|
+
return match;
|
|
94
|
+
}
|
|
95
|
+
fallback ?? (fallback = cards[0]);
|
|
96
|
+
}
|
|
97
|
+
return isSender ? fallback : undefined;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Where a new contact goes: the message's mailbox when it is one of the user's own, else their primary mailbox - in that mailbox's contacts folder. */
|
|
104
|
+
export function contactScope(mailboxes, folders, userUid, message) {
|
|
105
|
+
const own = ownMailboxes(mailboxes, userUid);
|
|
106
|
+
const mailbox = own.find((candidate) => candidate.uid === message?.mailboxUid) ?? primaryMailbox(mailboxes, userUid);
|
|
107
|
+
const folder = folders.find((candidate) => candidate.mailboxUid === mailbox?.uid && candidate.type === "contacts");
|
|
108
|
+
if (!mailbox || !folder) {
|
|
109
|
+
throw new Error("There is no contacts folder to add this contact to.");
|
|
110
|
+
}
|
|
111
|
+
return { mailboxUid: mailbox.uid, folderUid: folder.uid };
|
|
112
|
+
}
|
|
113
|
+
/** Stores a contact for `participant` with everything known - the name, the address and whatever `card` says. */
|
|
114
|
+
export function createContactFor(participant, card, scope, client) {
|
|
115
|
+
return createContact(contactInputFor(participant, card, scope), client);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The sender's own contact card as a file to attach, `<display name>.vcf`: the mailbox being sent from (its name and primary address) with the phones, company and
|
|
119
|
+
* title of the user's own contact entry for that address, when they have one in their own mailboxes' address books. A lookup that fails just leaves those out.
|
|
120
|
+
*/
|
|
121
|
+
export async function buildOwnCardFile(mailbox, mailboxes, userUid, client) {
|
|
122
|
+
let own;
|
|
123
|
+
try {
|
|
124
|
+
const mine = ownMailboxes(mailboxes, userUid);
|
|
125
|
+
own = await findContact(await loadFolders(mine.length > 0 ? mine : [mailbox], client), mailbox.primarySmtpAddress, client);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// The card is still worth sending without them.
|
|
129
|
+
}
|
|
130
|
+
const { displayName, vcard } = ownContactCard(mailbox, own);
|
|
131
|
+
return new File([vcard], `${displayName.replace(/[\\/:*?"<>|]/g, "").trim() || "Contact"}.vcf`, { type: "text/vcard" });
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Adds every card of a vCard file to the contacts folder `scope` names, skipping the ones whose email is already a stored contact's (or an earlier
|
|
135
|
+
* card's in the same file). A card that cannot be stored fails the whole call after the earlier ones were added - `AddCardsError` says how many.
|
|
136
|
+
*/
|
|
137
|
+
export async function addCardsToAddressBook(cards, scope, client) {
|
|
138
|
+
const { fresh, skipped } = withoutDuplicates(cards, await listContactsInFolders([scope.folderUid], client));
|
|
139
|
+
let added = 0;
|
|
140
|
+
for (const card of fresh) {
|
|
141
|
+
try {
|
|
142
|
+
await createContact(contactInputFromCard(card, scope), client);
|
|
143
|
+
added += 1;
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
throw new AddCardsError(err, added);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { added, skipped };
|
|
150
|
+
}
|
|
151
|
+
/** A card failed to store: the message is the failure's and `added` how many cards were stored before it. */
|
|
152
|
+
export class AddCardsError extends Error {
|
|
153
|
+
constructor(failure, added) {
|
|
154
|
+
super(failure instanceof Error ? failure.message : "Could not add the contact.");
|
|
155
|
+
this.name = "AddCardsError";
|
|
156
|
+
this.added = added;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure rules behind the contact card (`ContactCard`), the "Add to address book" action of a vCard attachment and the compose window's
|
|
3
|
+
* "Attach my contact card": who a participant is, whether they are external, which contact (or vCard) describes them and what a new contact
|
|
4
|
+
* made from them holds. No network and no React - the calls that use these are in `contactCardData.ts`.
|
|
5
|
+
*/
|
|
6
|
+
import type { Contact, ContactEmail, ContactInput, ContactPhone, ContactPostalAddress } from "../../../../lib/contacts/contactsApi.js";
|
|
7
|
+
import { type ParsedVCardContact } from "../../../../lib/contacts/vcard.js";
|
|
8
|
+
import type { Mailbox } from "../../../../lib/mail/mailApi.js";
|
|
9
|
+
/** Someone who took part in a message or an event: a sender, a recipient, an organizer, a guest. */
|
|
10
|
+
export interface Participant {
|
|
11
|
+
address: string;
|
|
12
|
+
displayName?: string;
|
|
13
|
+
}
|
|
14
|
+
/** Whether an attachment is a vCard: by its MIME type (parameters such as `; charset=utf-8` ignored), else by a `.vcf`/`.vcard` name. */
|
|
15
|
+
export declare function isVCardAttachment(attachment: {
|
|
16
|
+
filename?: string;
|
|
17
|
+
mimeType?: string;
|
|
18
|
+
}): boolean;
|
|
19
|
+
/** An address in the form two of them are compared by. */
|
|
20
|
+
export declare function normalizeAddress(address: string): string;
|
|
21
|
+
/** The name to show for a participant: their own when distinct from the address, else the address's local part. */
|
|
22
|
+
export declare function participantName(participant: Participant): string;
|
|
23
|
+
/** A display name as given name + surname: `Doe, Jane` and `Jane Doe` are both Jane Doe; a single word is only a given name, and a middle name stays with the given. */
|
|
24
|
+
export declare function splitPersonName(displayName: string): {
|
|
25
|
+
givenName?: string;
|
|
26
|
+
surname?: string;
|
|
27
|
+
};
|
|
28
|
+
/** The first of `contacts` with an email equal (case-insensitively) to `address`. */
|
|
29
|
+
export declare function findContactByAddress(contacts: readonly Contact[], address: string): Contact | undefined;
|
|
30
|
+
/** The domains (lower case) of every address of `mailboxes` - primary and aliases. */
|
|
31
|
+
export declare function ownDomains(mailboxes: readonly Pick<Mailbox, "primarySmtpAddress" | "aliasAddresses">[]): Set<string>;
|
|
32
|
+
/** Whether `address` is outside the user's own domains. An address with no domain at all is not called external. */
|
|
33
|
+
export declare function isExternalAddress(address: string, domains: ReadonlySet<string>): boolean;
|
|
34
|
+
/** The card of `cards` that carries `address`; when none does and `orFirst` (the message's sender - a vCard a person attaches is their own), the first card. */
|
|
35
|
+
export declare function cardForAddress(cards: readonly ParsedVCardContact[], address: string, orFirst: boolean): ParsedVCardContact | undefined;
|
|
36
|
+
/** A `ContactInput` whose lists are always there. */
|
|
37
|
+
export type KnownPerson = ContactInput & {
|
|
38
|
+
emails: ContactEmail[];
|
|
39
|
+
phones: ContactPhone[];
|
|
40
|
+
addresses: ContactPostalAddress[];
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* What a new contact for `participant` holds: everything the vCard `card` says (when there is one) and always the participant's address. The
|
|
44
|
+
* name is the vCard's, else the participant's, else the address's local part; given name and surname are the vCard's `N`, else split from the name.
|
|
45
|
+
*/
|
|
46
|
+
export declare function contactInputFor(participant: Participant, card: ParsedVCardContact | undefined, scope: {
|
|
47
|
+
mailboxUid: string;
|
|
48
|
+
folderUid: string;
|
|
49
|
+
}): KnownPerson;
|
|
50
|
+
/** A contact made from a vCard on its own, for "Add to address book": the card's own name and details, with the given/surname split when it lacks them. */
|
|
51
|
+
export declare function contactInputFromCard(card: ParsedVCardContact, scope: {
|
|
52
|
+
mailboxUid: string;
|
|
53
|
+
folderUid: string;
|
|
54
|
+
}): KnownPerson;
|
|
55
|
+
/** The cards that are not yet in the address book, and how many were: a card is a duplicate when one of its emails is a stored contact's or an earlier card's. */
|
|
56
|
+
export declare function withoutDuplicates(cards: readonly ParsedVCardContact[], existing: readonly Contact[]): {
|
|
57
|
+
fresh: ParsedVCardContact[];
|
|
58
|
+
skipped: number;
|
|
59
|
+
};
|
|
60
|
+
/** The sender's own vCard, and the name it is filed under. It is built from - the mailbox's name and primary address, with the phones, company and title of the user's own contact entry when they have one (never its other emails or postal addresses: a card that is sent out says only what it should). */
|
|
61
|
+
export declare function ownContactCard(mailbox: Pick<Mailbox, "displayName" | "primarySmtpAddress">, own: Contact | undefined): {
|
|
62
|
+
displayName: string;
|
|
63
|
+
vcard: string;
|
|
64
|
+
};
|