@rapidmx/web-client 0.2.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/LICENSE +382 -0
- package/apps/admin/_404.tsx +9 -0
- package/apps/admin/_500.tsx +9 -0
- package/apps/admin/_layout.tsx +41 -0
- package/apps/admin/audit-log/index.tsx +187 -0
- package/apps/admin/branding/index.tsx +404 -0
- package/apps/admin/distribution-lists/[uid].tsx +70 -0
- package/apps/admin/distribution-lists/index.tsx +115 -0
- package/apps/admin/distribution-lists/new/index.tsx +114 -0
- package/apps/admin/domains/[uid].tsx +259 -0
- package/apps/admin/domains/index.tsx +125 -0
- package/apps/admin/domains/new/index.tsx +84 -0
- package/apps/admin/index.tsx +82 -0
- package/apps/admin/ingest-queue/index.tsx +116 -0
- package/apps/admin/mailboxes/[uid].tsx +126 -0
- package/apps/admin/mailboxes/new/index.tsx +291 -0
- package/apps/admin/quarantine/index.tsx +171 -0
- package/apps/admin/transport-rules/[uid].tsx +143 -0
- package/apps/admin/transport-rules/_transportRuleConfig.tsx +75 -0
- package/apps/admin/transport-rules/index.tsx +117 -0
- package/apps/admin/transport-rules/new/index.tsx +105 -0
- package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +117 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +186 -0
- package/apps/shared/components/admin/mailboxes/MailboxTable.tsx +73 -0
- package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +154 -0
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +124 -0
- package/apps/shared/components/booking/AvailabilityEditor.tsx +118 -0
- package/apps/shared/components/calendar/CalendarListSidebar.tsx +119 -0
- package/apps/shared/components/calendar/EventModal.tsx +513 -0
- package/apps/shared/components/calendar/MonthView.tsx +139 -0
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +180 -0
- package/apps/shared/components/calendar/ResourcePicker.tsx +80 -0
- package/apps/shared/components/calendar/SplitDayView.tsx +137 -0
- package/apps/shared/components/calendar/TimeGridView.tsx +198 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +211 -0
- package/apps/shared/components/contacts/ContactDetailPane.tsx +99 -0
- package/apps/shared/components/contacts/ContactForm.tsx +314 -0
- package/apps/shared/components/contacts/ContactsSidebar.tsx +242 -0
- package/apps/shared/components/contacts/ContactsToolbar.tsx +115 -0
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +197 -0
- package/apps/shared/components/layout/AppShell.tsx +165 -0
- package/apps/shared/components/layout/BrandingChrome.tsx +25 -0
- package/apps/shared/components/layout/MailboxProvisioning.tsx +110 -0
- package/apps/shared/components/layout/UserMenu.tsx +150 -0
- package/apps/shared/components/mail/ConversationList.tsx +61 -0
- package/apps/shared/components/mail/ConversationThreadPane.tsx +174 -0
- package/apps/shared/components/mail/MessageDetailPane.tsx +390 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +114 -0
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +418 -0
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +518 -0
- package/apps/shared/components/mail/compose/EmojiPicker.tsx +64 -0
- package/apps/shared/components/mail/compose/GifPicker.tsx +82 -0
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +83 -0
- package/apps/shared/components/mail/compose/ScheduleSendPicker.tsx +64 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +287 -0
- package/apps/shared/components/rules/RuleBuilder.tsx +276 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +223 -0
- package/apps/shared/components/tasks/TasksSidebar.tsx +201 -0
- package/apps/shared/components/tasks/TasksToolbar.tsx +68 -0
- package/apps/shared/components/tasks/layout/TasksShell.tsx +199 -0
- package/apps/shared/styles/app.css +265 -0
- package/apps/www/_404.tsx +9 -0
- package/apps/www/_500.tsx +9 -0
- package/apps/www/_layout.tsx +45 -0
- package/apps/www/calendar/index.tsx +407 -0
- package/apps/www/contacts/[uid].tsx +88 -0
- package/apps/www/contacts/index.tsx +436 -0
- package/apps/www/index.tsx +375 -0
- package/apps/www/messages/[uid].tsx +70 -0
- package/apps/www/settings/auto-reply/index.tsx +129 -0
- package/apps/www/settings/booking-types/[uid].tsx +300 -0
- package/apps/www/settings/booking-types/index.tsx +98 -0
- package/apps/www/settings/booking-types/new/index.tsx +212 -0
- package/apps/www/settings/filters/[uid].tsx +155 -0
- package/apps/www/settings/filters/_mailFilterRuleConfig.tsx +102 -0
- package/apps/www/settings/filters/index.tsx +99 -0
- package/apps/www/settings/filters/new/index.tsx +126 -0
- package/apps/www/settings/focused-inbox/index.tsx +148 -0
- package/apps/www/settings/read-receipts/index.tsx +125 -0
- package/apps/www/settings/signatures/[uid].tsx +167 -0
- package/apps/www/settings/signatures/index.tsx +85 -0
- package/apps/www/settings/signatures/new/index.tsx +129 -0
- package/apps/www/settings/signatures/signatureDefaults.ts +36 -0
- package/apps/www/tasks/index.tsx +530 -0
- package/dist/apps/admin/_404.js +4 -0
- package/dist/apps/admin/_500.js +4 -0
- package/dist/apps/admin/_layout.js +10 -0
- package/dist/apps/admin/audit-log/index.js +66 -0
- package/dist/apps/admin/branding/index.js +126 -0
- package/dist/apps/admin/distribution-lists/[uid].js +34 -0
- package/dist/apps/admin/distribution-lists/index.js +31 -0
- package/dist/apps/admin/distribution-lists/new/index.js +51 -0
- package/dist/apps/admin/domains/[uid].js +102 -0
- package/dist/apps/admin/domains/index.js +31 -0
- package/dist/apps/admin/domains/new/index.js +41 -0
- package/dist/apps/admin/index.js +32 -0
- package/dist/apps/admin/ingest-queue/index.js +48 -0
- package/dist/apps/admin/mailboxes/[uid].js +59 -0
- package/dist/apps/admin/mailboxes/new/index.js +93 -0
- package/dist/apps/admin/quarantine/index.js +66 -0
- package/dist/apps/admin/transport-rules/[uid].js +87 -0
- package/dist/apps/admin/transport-rules/_transportRuleConfig.js +37 -0
- package/dist/apps/admin/transport-rules/index.js +32 -0
- package/dist/apps/admin/transport-rules/new/index.js +50 -0
- package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +64 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.js +94 -0
- package/dist/apps/shared/components/admin/mailboxes/MailboxTable.js +16 -0
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +59 -0
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +67 -0
- package/dist/apps/shared/components/booking/AvailabilityEditor.js +45 -0
- package/dist/apps/shared/components/calendar/CalendarListSidebar.js +44 -0
- package/dist/apps/shared/components/calendar/EventModal.js +167 -0
- package/dist/apps/shared/components/calendar/MonthView.js +38 -0
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +55 -0
- package/dist/apps/shared/components/calendar/ResourcePicker.js +36 -0
- package/dist/apps/shared/components/calendar/SplitDayView.js +44 -0
- package/dist/apps/shared/components/calendar/TimeGridView.js +58 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +96 -0
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -0
- package/dist/apps/shared/components/contacts/ContactForm.js +85 -0
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +105 -0
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +32 -0
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +93 -0
- package/dist/apps/shared/components/layout/AppShell.js +68 -0
- package/dist/apps/shared/components/layout/BrandingChrome.js +18 -0
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +75 -0
- package/dist/apps/shared/components/layout/UserMenu.js +59 -0
- package/dist/apps/shared/components/mail/ConversationList.js +17 -0
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +112 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +153 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +55 -0
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +88 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +261 -0
- package/dist/apps/shared/components/mail/compose/EmojiPicker.js +25 -0
- package/dist/apps/shared/components/mail/compose/GifPicker.js +36 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.js +60 -0
- package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.js +33 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +139 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +78 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +100 -0
- package/dist/apps/shared/components/tasks/TasksSidebar.js +81 -0
- package/dist/apps/shared/components/tasks/TasksToolbar.js +15 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +92 -0
- package/dist/apps/shared/styles/app.css +265 -0
- package/dist/apps/www/_404.js +4 -0
- package/dist/apps/www/_500.js +4 -0
- package/dist/apps/www/_layout.js +8 -0
- package/dist/apps/www/calendar/index.js +228 -0
- package/dist/apps/www/contacts/[uid].js +62 -0
- package/dist/apps/www/contacts/index.js +262 -0
- package/dist/apps/www/index.js +228 -0
- package/dist/apps/www/messages/[uid].js +47 -0
- package/dist/apps/www/settings/auto-reply/index.js +54 -0
- package/dist/apps/www/settings/booking-types/[uid].js +129 -0
- package/dist/apps/www/settings/booking-types/index.js +29 -0
- package/dist/apps/www/settings/booking-types/new/index.js +86 -0
- package/dist/apps/www/settings/filters/[uid].js +94 -0
- package/dist/apps/www/settings/filters/_mailFilterRuleConfig.js +65 -0
- package/dist/apps/www/settings/filters/index.js +30 -0
- package/dist/apps/www/settings/filters/new/index.js +61 -0
- package/dist/apps/www/settings/focused-inbox/index.js +69 -0
- package/dist/apps/www/settings/read-receipts/index.js +51 -0
- package/dist/apps/www/settings/signatures/[uid].js +96 -0
- package/dist/apps/www/settings/signatures/index.js +34 -0
- package/dist/apps/www/settings/signatures/new/index.js +63 -0
- package/dist/apps/www/settings/signatures/signatureDefaults.js +30 -0
- package/dist/apps/www/tasks/index.js +280 -0
- package/package.json +87 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { useEffect, useState } from "react";
|
|
6
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
|
+
import { IngestQueueEntry, listIngestQueue } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
8
|
+
import AdminShell, { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
|
|
9
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
10
|
+
|
|
11
|
+
export function readMailboxUid(): string | null {
|
|
12
|
+
if (typeof window === "undefined") return null;
|
|
13
|
+
return new URLSearchParams(window.location.search).get("mailboxUid");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const STATUS_STYLES: Record<IngestQueueEntry["status"], string> = {
|
|
17
|
+
pending: "bg-surface-alt text-text-muted",
|
|
18
|
+
scanning: "bg-surface-alt text-text-muted",
|
|
19
|
+
delivered: "bg-success text-white",
|
|
20
|
+
failed: "bg-danger text-white",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default function IngestQueuePage(props: Omit<AdminShellProps, "active">) {
|
|
24
|
+
return (
|
|
25
|
+
<AdminShell {...props} active="ingestQueue">
|
|
26
|
+
<IngestQueueContent />
|
|
27
|
+
</AdminShell>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function IngestQueueContent() {
|
|
32
|
+
const [mailboxUid, setMailboxUid] = useState<string | null>(null);
|
|
33
|
+
const [entries, setEntries] = useState<IngestQueueEntry[]>([]);
|
|
34
|
+
const [loading, setLoading] = useState(true);
|
|
35
|
+
const [error, setError] = useState<string | null>(null);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
setMailboxUid(readMailboxUid());
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!mailboxUid) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
setLoading(true);
|
|
46
|
+
setError(null);
|
|
47
|
+
listIngestQueue(mailboxUid)
|
|
48
|
+
.then(setEntries)
|
|
49
|
+
.catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load the ingest queue."))
|
|
50
|
+
.finally(() => setLoading(false));
|
|
51
|
+
}, [mailboxUid]);
|
|
52
|
+
|
|
53
|
+
if (!mailboxUid) {
|
|
54
|
+
return <Alert>No mailbox specified. Open a mailbox's detail page and choose "View ingest queue".</Alert>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="max-w-4xl flex flex-col gap-4">
|
|
59
|
+
<div>
|
|
60
|
+
<a
|
|
61
|
+
href={`/admin/mailboxes/${encodeURIComponent(mailboxUid)}`}
|
|
62
|
+
className="text-sm text-primary-dark hover:underline"
|
|
63
|
+
>
|
|
64
|
+
← Back to mailbox
|
|
65
|
+
</a>
|
|
66
|
+
<h1 className="text-xl font-bold uppercase tracking-wide mt-1">Ingest queue</h1>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{error && <Alert>{error}</Alert>}
|
|
70
|
+
|
|
71
|
+
{loading ? (
|
|
72
|
+
<p className="text-sm text-text-muted">Loading…</p>
|
|
73
|
+
) : entries.length === 0 ? (
|
|
74
|
+
<p className="text-sm text-text-muted">Nothing pending or failed for this mailbox.</p>
|
|
75
|
+
) : (
|
|
76
|
+
<div className="overflow-x-auto">
|
|
77
|
+
<table className="w-full text-sm border-collapse">
|
|
78
|
+
<thead>
|
|
79
|
+
<tr>
|
|
80
|
+
{["From", "To", "Received", "Status", "Error"].map((h) => (
|
|
81
|
+
<th
|
|
82
|
+
key={h}
|
|
83
|
+
className="text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border"
|
|
84
|
+
>
|
|
85
|
+
{h}
|
|
86
|
+
</th>
|
|
87
|
+
))}
|
|
88
|
+
</tr>
|
|
89
|
+
</thead>
|
|
90
|
+
<tbody>
|
|
91
|
+
{entries.map((entry) => (
|
|
92
|
+
<tr key={entry.uid}>
|
|
93
|
+
<td className="py-2.5 px-2.5 border-b border-border">{entry.envelopeFrom}</td>
|
|
94
|
+
<td className="py-2.5 px-2.5 border-b border-border">{entry.envelopeTo.join(", ")}</td>
|
|
95
|
+
<td className="py-2.5 px-2.5 border-b border-border">
|
|
96
|
+
{new Date(entry.dateCreated).toLocaleString()}
|
|
97
|
+
</td>
|
|
98
|
+
<td className="py-2.5 px-2.5 border-b border-border">
|
|
99
|
+
<span
|
|
100
|
+
className={`inline-block text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${STATUS_STYLES[entry.status]}`}
|
|
101
|
+
>
|
|
102
|
+
{entry.status}
|
|
103
|
+
</span>
|
|
104
|
+
</td>
|
|
105
|
+
<td className="py-2.5 px-2.5 border-b border-border text-text-muted">
|
|
106
|
+
{entry.errorMessage ?? ""}
|
|
107
|
+
</td>
|
|
108
|
+
</tr>
|
|
109
|
+
))}
|
|
110
|
+
</tbody>
|
|
111
|
+
</table>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { useEffect, useState } from "react";
|
|
6
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
|
+
import { getMailbox, impersonateUser, Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
8
|
+
import AdminShell, { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
|
|
9
|
+
import ShareAccessCard from "../../shared/components/admin/mailboxes/ShareAccessCard.js";
|
|
10
|
+
import ResourceSettingsCard from "../../shared/components/admin/mailboxes/ResourceSettingsCard.js";
|
|
11
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
12
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
13
|
+
|
|
14
|
+
function formatBytes(bytes: number): string {
|
|
15
|
+
if (bytes >= 1_000_000_000) return `${(bytes / 1_000_000_000).toFixed(2)} GB`;
|
|
16
|
+
if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)} MB`;
|
|
17
|
+
if (bytes >= 1_000) return `${(bytes / 1_000).toFixed(1)} KB`;
|
|
18
|
+
return `${bytes} B`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function MailboxDetailPage(props: Omit<AdminShellProps, "active"> & { params: { uid: string } }) {
|
|
22
|
+
return (
|
|
23
|
+
<AdminShell {...props} active="mailboxes">
|
|
24
|
+
<MailboxDetailContent uid={props.params.uid} impersonationBaseUrl={props.impersonationBaseUrl} />
|
|
25
|
+
</AdminShell>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function MailboxDetailContent({ uid, impersonationBaseUrl }: { uid: string } & Pick<AdminShellProps, "impersonationBaseUrl">) {
|
|
30
|
+
const [mailbox, setMailbox] = useState<Mailbox | null>(null);
|
|
31
|
+
const [loading, setLoading] = useState(true);
|
|
32
|
+
const [error, setError] = useState<string | null>(null);
|
|
33
|
+
const [impersonating, setImpersonating] = useState(false);
|
|
34
|
+
|
|
35
|
+
async function handleAccessMailbox(ownerUserUid: string) {
|
|
36
|
+
setImpersonating(true);
|
|
37
|
+
try {
|
|
38
|
+
await impersonateUser(impersonationBaseUrl ?? "", ownerUserUid);
|
|
39
|
+
window.location.href = "/";
|
|
40
|
+
} catch (err) {
|
|
41
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not access this mailbox.");
|
|
42
|
+
setImpersonating(false);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
setLoading(true);
|
|
48
|
+
setError(null);
|
|
49
|
+
getMailbox(uid)
|
|
50
|
+
.then(setMailbox)
|
|
51
|
+
.catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this mailbox."))
|
|
52
|
+
.finally(() => setLoading(false));
|
|
53
|
+
}, [uid]);
|
|
54
|
+
|
|
55
|
+
if (loading) {
|
|
56
|
+
return <p className="text-sm text-text-muted">Loading…</p>;
|
|
57
|
+
}
|
|
58
|
+
if (error || !mailbox) {
|
|
59
|
+
return <Alert>{error ?? "Mailbox not found."}</Alert>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const quarantineHref = `/admin/quarantine?mailboxUid=${encodeURIComponent(mailbox.uid)}`;
|
|
63
|
+
const ingestQueueHref = `/admin/ingest-queue?mailboxUid=${encodeURIComponent(mailbox.uid)}`;
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className="max-w-3xl flex flex-col gap-5">
|
|
67
|
+
<div className="flex items-start justify-between gap-4">
|
|
68
|
+
<div>
|
|
69
|
+
<a href="/admin" className="text-sm text-primary-dark hover:underline">
|
|
70
|
+
← All mailboxes
|
|
71
|
+
</a>
|
|
72
|
+
<h1 className="text-xl font-bold tracking-tight mt-1">{mailbox.primarySmtpAddress}</h1>
|
|
73
|
+
</div>
|
|
74
|
+
{mailbox.ownerUserUid && (
|
|
75
|
+
<Button
|
|
76
|
+
type="button"
|
|
77
|
+
variant="secondary"
|
|
78
|
+
className="!w-auto shrink-0"
|
|
79
|
+
loading={impersonating}
|
|
80
|
+
disabled={impersonating}
|
|
81
|
+
onClick={() => handleAccessMailbox(mailbox.ownerUserUid as string)}
|
|
82
|
+
>
|
|
83
|
+
Access this mailbox
|
|
84
|
+
</Button>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div className="bg-surface border border-border rounded-md p-6">
|
|
89
|
+
<dl className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
|
90
|
+
<dt className="text-text-muted">Display name</dt>
|
|
91
|
+
<dd>{mailbox.displayName}</dd>
|
|
92
|
+
<dt className="text-text-muted">Owner</dt>
|
|
93
|
+
<dd>{mailbox.ownerUserUid ?? "None (shared mailbox)"}</dd>
|
|
94
|
+
<dt className="text-text-muted">Timezone</dt>
|
|
95
|
+
<dd>{mailbox.timezone}</dd>
|
|
96
|
+
<dt className="text-text-muted">Quota</dt>
|
|
97
|
+
<dd>
|
|
98
|
+
{formatBytes(mailbox.usedBytes)} / {formatBytes(mailbox.quotaBytes)}
|
|
99
|
+
</dd>
|
|
100
|
+
<dt className="text-text-muted">Alias addresses</dt>
|
|
101
|
+
<dd>{mailbox.aliasAddresses.length > 0 ? mailbox.aliasAddresses.join(", ") : "None"}</dd>
|
|
102
|
+
{mailbox.isResource && (
|
|
103
|
+
<>
|
|
104
|
+
<dt className="text-text-muted">Resource type</dt>
|
|
105
|
+
<dd className="capitalize">{mailbox.resourceType ?? "room"}</dd>
|
|
106
|
+
</>
|
|
107
|
+
)}
|
|
108
|
+
<dt className="text-text-muted">Created</dt>
|
|
109
|
+
<dd>{new Date(mailbox.dateCreated).toLocaleString()}</dd>
|
|
110
|
+
</dl>
|
|
111
|
+
<div className="flex gap-4 mt-5 pt-5 border-t border-border text-sm font-medium">
|
|
112
|
+
<a href={quarantineHref} className="text-primary-dark hover:underline">
|
|
113
|
+
View quarantine
|
|
114
|
+
</a>
|
|
115
|
+
<a href={ingestQueueHref} className="text-primary-dark hover:underline">
|
|
116
|
+
View ingest queue
|
|
117
|
+
</a>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<ShareAccessCard mailboxUid={mailbox.uid} />
|
|
122
|
+
|
|
123
|
+
{mailbox.isResource && <ResourceSettingsCard mailbox={mailbox} onUpdate={setMailbox} />}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { FormEvent, useEffect, useState } from "react";
|
|
6
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
|
+
import { createMailbox, listMailboxDomains } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
8
|
+
import AdminShell, { AdminShellProps } from "../../../shared/components/admin/layout/AdminShell.js";
|
|
9
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
10
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
11
|
+
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
12
|
+
|
|
13
|
+
const INPUT_CLASS =
|
|
14
|
+
"w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
15
|
+
const SELECT_CLASS =
|
|
16
|
+
"text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
17
|
+
|
|
18
|
+
export default function NewMailboxPage(props: Omit<AdminShellProps, "active">) {
|
|
19
|
+
return (
|
|
20
|
+
<AdminShell {...props} active="mailboxes">
|
|
21
|
+
<NewMailboxForm />
|
|
22
|
+
</AdminShell>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function NewMailboxForm() {
|
|
27
|
+
const [primarySmtpAddress, setPrimarySmtpAddress] = useState("");
|
|
28
|
+
const [localPart, setLocalPart] = useState("");
|
|
29
|
+
const [domains, setDomains] = useState<string[]>([]);
|
|
30
|
+
const [domain, setDomain] = useState("");
|
|
31
|
+
const [displayName, setDisplayName] = useState("");
|
|
32
|
+
const [ownerUserUid, setOwnerUserUid] = useState("");
|
|
33
|
+
const [timezone, setTimezone] = useState("UTC");
|
|
34
|
+
const [quotaGb, setQuotaGb] = useState(5);
|
|
35
|
+
const [isResource, setIsResource] = useState(false);
|
|
36
|
+
const [resourceType, setResourceType] = useState<"room" | "equipment">("room");
|
|
37
|
+
const [resourceCapacity, setResourceCapacity] = useState("");
|
|
38
|
+
const [autoAcceptBookings, setAutoAcceptBookings] = useState(false);
|
|
39
|
+
const [allowConflicts, setAllowConflicts] = useState(false);
|
|
40
|
+
const [bookingWindowDays, setBookingWindowDays] = useState("");
|
|
41
|
+
const [maxDurationMinutes, setMaxDurationMinutes] = useState("");
|
|
42
|
+
const [error, setError] = useState<string | null>(null);
|
|
43
|
+
const [saving, setSaving] = useState(false);
|
|
44
|
+
|
|
45
|
+
// An empty `mail:domains` list (the default) means this server enforces no domain restriction —
|
|
46
|
+
// the address field stays free text, exactly as before. Once an admin configures at least one
|
|
47
|
+
// domain, every mailbox this server creates must be on one of them (enforced server-side too, in
|
|
48
|
+
// `BaseMailboxRoute.create()`), so the form switches to a local-part input + domain dropdown to
|
|
49
|
+
// make that restriction visible rather than let an admin discover it only via a rejected submit.
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
listMailboxDomains()
|
|
52
|
+
.then((list) => {
|
|
53
|
+
setDomains(list);
|
|
54
|
+
setDomain(list[0] ?? "");
|
|
55
|
+
})
|
|
56
|
+
.catch(() => undefined);
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
const constrained = domains.length > 0;
|
|
60
|
+
|
|
61
|
+
async function handleSubmit(e: FormEvent) {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
setError(null);
|
|
64
|
+
|
|
65
|
+
const address = constrained ? `${localPart.trim()}@${domain}` : primarySmtpAddress.trim();
|
|
66
|
+
if (constrained ? !localPart.trim() : !primarySmtpAddress.trim()) {
|
|
67
|
+
setError("A primary SMTP address is required.");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!displayName.trim()) {
|
|
71
|
+
setError("A display name is required.");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setSaving(true);
|
|
76
|
+
try {
|
|
77
|
+
// ownerUserUid left blank creates a true ownerless shared mailbox (e.g. support@example.com);
|
|
78
|
+
// delegates are then granted access from the mailbox's detail page. A resource mailbox is
|
|
79
|
+
// ownerless the same way — leave "Owner user uid" blank for one too.
|
|
80
|
+
const mailbox = await createMailbox({
|
|
81
|
+
primarySmtpAddress: address,
|
|
82
|
+
displayName: displayName.trim(),
|
|
83
|
+
ownerUserUid: ownerUserUid.trim() || undefined,
|
|
84
|
+
timezone,
|
|
85
|
+
quotaBytes: Math.round(quotaGb * 1_000_000_000),
|
|
86
|
+
...(isResource && {
|
|
87
|
+
isResource: true,
|
|
88
|
+
resourceType,
|
|
89
|
+
resourceCapacity: resourceCapacity ? Number(resourceCapacity) : undefined,
|
|
90
|
+
autoAcceptBookings,
|
|
91
|
+
allowConflicts,
|
|
92
|
+
bookingWindowDays: bookingWindowDays ? Number(bookingWindowDays) : undefined,
|
|
93
|
+
maxDurationMinutes: maxDurationMinutes ? Number(maxDurationMinutes) : undefined,
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
window.location.href = `/admin/mailboxes/${encodeURIComponent(mailbox.uid)}`;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not create the mailbox.");
|
|
99
|
+
} finally {
|
|
100
|
+
setSaving(false);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="max-w-xl">
|
|
106
|
+
<h1 className="text-xl font-bold uppercase tracking-wide mb-1">New mailbox</h1>
|
|
107
|
+
<p className="text-sm text-text-muted mb-5">
|
|
108
|
+
Leave "Owner user uid" blank to create a true ownerless shared mailbox (the Exchange "shared
|
|
109
|
+
mailbox" concept) — access is then granted entirely to delegates afterward, not to a single
|
|
110
|
+
owner. Set an owner to create a mailbox for a specific user instead.
|
|
111
|
+
</p>
|
|
112
|
+
|
|
113
|
+
{error && <Alert>{error}</Alert>}
|
|
114
|
+
|
|
115
|
+
<form onSubmit={handleSubmit} className="bg-surface border border-border rounded-md p-6">
|
|
116
|
+
{constrained ? (
|
|
117
|
+
<FormField label="Local part" htmlFor="localPart">
|
|
118
|
+
<div className="flex gap-2 items-center">
|
|
119
|
+
<input
|
|
120
|
+
id="localPart"
|
|
121
|
+
type="text"
|
|
122
|
+
className={INPUT_CLASS}
|
|
123
|
+
value={localPart}
|
|
124
|
+
onChange={(e) => setLocalPart(e.target.value)}
|
|
125
|
+
placeholder="support"
|
|
126
|
+
/>
|
|
127
|
+
<span className="text-text-muted shrink-0">@</span>
|
|
128
|
+
<select
|
|
129
|
+
aria-label="Domain"
|
|
130
|
+
className={SELECT_CLASS}
|
|
131
|
+
value={domain}
|
|
132
|
+
onChange={(e) => setDomain(e.target.value)}
|
|
133
|
+
>
|
|
134
|
+
{domains.map((d) => (
|
|
135
|
+
<option key={d} value={d}>
|
|
136
|
+
{d}
|
|
137
|
+
</option>
|
|
138
|
+
))}
|
|
139
|
+
</select>
|
|
140
|
+
</div>
|
|
141
|
+
</FormField>
|
|
142
|
+
) : (
|
|
143
|
+
<FormField label="Primary SMTP address" htmlFor="primarySmtpAddress">
|
|
144
|
+
<input
|
|
145
|
+
id="primarySmtpAddress"
|
|
146
|
+
type="email"
|
|
147
|
+
className={INPUT_CLASS}
|
|
148
|
+
value={primarySmtpAddress}
|
|
149
|
+
onChange={(e) => setPrimarySmtpAddress(e.target.value)}
|
|
150
|
+
placeholder="support@example.com"
|
|
151
|
+
/>
|
|
152
|
+
</FormField>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
<FormField label="Display name" htmlFor="displayName">
|
|
156
|
+
<input
|
|
157
|
+
id="displayName"
|
|
158
|
+
type="text"
|
|
159
|
+
className={INPUT_CLASS}
|
|
160
|
+
value={displayName}
|
|
161
|
+
onChange={(e) => setDisplayName(e.target.value)}
|
|
162
|
+
placeholder="Support"
|
|
163
|
+
/>
|
|
164
|
+
</FormField>
|
|
165
|
+
|
|
166
|
+
<FormField label="Owner user uid (optional)" htmlFor="ownerUserUid">
|
|
167
|
+
<input
|
|
168
|
+
id="ownerUserUid"
|
|
169
|
+
type="text"
|
|
170
|
+
className={INPUT_CLASS}
|
|
171
|
+
value={ownerUserUid}
|
|
172
|
+
onChange={(e) => setOwnerUserUid(e.target.value)}
|
|
173
|
+
placeholder="Leave blank for a shared mailbox"
|
|
174
|
+
/>
|
|
175
|
+
</FormField>
|
|
176
|
+
|
|
177
|
+
<FormField label="Timezone" htmlFor="timezone">
|
|
178
|
+
<input
|
|
179
|
+
id="timezone"
|
|
180
|
+
type="text"
|
|
181
|
+
className={INPUT_CLASS}
|
|
182
|
+
value={timezone}
|
|
183
|
+
onChange={(e) => setTimezone(e.target.value)}
|
|
184
|
+
placeholder="America/Los_Angeles"
|
|
185
|
+
/>
|
|
186
|
+
</FormField>
|
|
187
|
+
|
|
188
|
+
<FormField label="Quota (GB)" htmlFor="quotaGb">
|
|
189
|
+
<input
|
|
190
|
+
id="quotaGb"
|
|
191
|
+
type="number"
|
|
192
|
+
min={1}
|
|
193
|
+
step={1}
|
|
194
|
+
className={INPUT_CLASS}
|
|
195
|
+
value={quotaGb}
|
|
196
|
+
onChange={(e) => setQuotaGb(Number(e.target.value))}
|
|
197
|
+
/>
|
|
198
|
+
</FormField>
|
|
199
|
+
|
|
200
|
+
<label className="flex items-center gap-2 text-sm mb-4">
|
|
201
|
+
<input type="checkbox" checked={isResource} onChange={(e) => setIsResource(e.target.checked)} />
|
|
202
|
+
This is a resource mailbox (a bookable room or piece of equipment)
|
|
203
|
+
</label>
|
|
204
|
+
|
|
205
|
+
{isResource && (
|
|
206
|
+
<div className="border border-border rounded-sm p-4 mb-4 flex flex-col gap-4">
|
|
207
|
+
<FormField label="Resource type" htmlFor="resourceType">
|
|
208
|
+
<select
|
|
209
|
+
id="resourceType"
|
|
210
|
+
className={SELECT_CLASS}
|
|
211
|
+
value={resourceType}
|
|
212
|
+
onChange={(e) => setResourceType(e.target.value as "room" | "equipment")}
|
|
213
|
+
>
|
|
214
|
+
<option value="room">Room</option>
|
|
215
|
+
<option value="equipment">Equipment</option>
|
|
216
|
+
</select>
|
|
217
|
+
</FormField>
|
|
218
|
+
|
|
219
|
+
<FormField label="Capacity (optional)" htmlFor="resourceCapacity">
|
|
220
|
+
<input
|
|
221
|
+
id="resourceCapacity"
|
|
222
|
+
type="number"
|
|
223
|
+
min={0}
|
|
224
|
+
step={1}
|
|
225
|
+
className={INPUT_CLASS}
|
|
226
|
+
value={resourceCapacity}
|
|
227
|
+
onChange={(e) => setResourceCapacity(e.target.value)}
|
|
228
|
+
placeholder="Informational only"
|
|
229
|
+
/>
|
|
230
|
+
</FormField>
|
|
231
|
+
|
|
232
|
+
<label className="flex items-center gap-2 text-sm">
|
|
233
|
+
<input
|
|
234
|
+
type="checkbox"
|
|
235
|
+
checked={autoAcceptBookings}
|
|
236
|
+
onChange={(e) => setAutoAcceptBookings(e.target.checked)}
|
|
237
|
+
/>
|
|
238
|
+
Automatically accept booking requests
|
|
239
|
+
</label>
|
|
240
|
+
|
|
241
|
+
<label className="flex items-center gap-2 text-sm">
|
|
242
|
+
<input
|
|
243
|
+
type="checkbox"
|
|
244
|
+
checked={allowConflicts}
|
|
245
|
+
onChange={(e) => setAllowConflicts(e.target.checked)}
|
|
246
|
+
/>
|
|
247
|
+
Allow conflicting bookings (skip conflict checking entirely)
|
|
248
|
+
</label>
|
|
249
|
+
|
|
250
|
+
<FormField label="Booking window, in days (optional)" htmlFor="bookingWindowDays">
|
|
251
|
+
<input
|
|
252
|
+
id="bookingWindowDays"
|
|
253
|
+
type="number"
|
|
254
|
+
min={0}
|
|
255
|
+
step={1}
|
|
256
|
+
className={INPUT_CLASS}
|
|
257
|
+
value={bookingWindowDays}
|
|
258
|
+
onChange={(e) => setBookingWindowDays(e.target.value)}
|
|
259
|
+
placeholder="No limit"
|
|
260
|
+
/>
|
|
261
|
+
</FormField>
|
|
262
|
+
|
|
263
|
+
<FormField label="Maximum duration, in minutes (optional)" htmlFor="maxDurationMinutes">
|
|
264
|
+
<input
|
|
265
|
+
id="maxDurationMinutes"
|
|
266
|
+
type="number"
|
|
267
|
+
min={0}
|
|
268
|
+
step={1}
|
|
269
|
+
className={INPUT_CLASS}
|
|
270
|
+
value={maxDurationMinutes}
|
|
271
|
+
onChange={(e) => setMaxDurationMinutes(e.target.value)}
|
|
272
|
+
placeholder="No limit"
|
|
273
|
+
/>
|
|
274
|
+
</FormField>
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
|
|
278
|
+
<div className="flex gap-3 mt-2">
|
|
279
|
+
<Button type="submit" loading={saving} disabled={saving} className="!w-auto">
|
|
280
|
+
Create mailbox
|
|
281
|
+
</Button>
|
|
282
|
+
<a href="/admin">
|
|
283
|
+
<Button type="button" variant="secondary" className="!w-auto">
|
|
284
|
+
Cancel
|
|
285
|
+
</Button>
|
|
286
|
+
</a>
|
|
287
|
+
</div>
|
|
288
|
+
</form>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}
|