Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
142 commits
Select commit Hold shift + click to select a range
6c1ba81
Switch outbound email delivery to Resend API
guiming99 Aug 24, 2026
125bd92
feat(auth): add isolated user auth durable object
guiming99 Aug 24, 2026
7f3a55a
feat(auth): add session and mailbox authorization helpers
guiming99 Aug 24, 2026
a07b7a8
feat(auth): add auth bindings to worker types
guiming99 Aug 24, 2026
ea0211a
feat(auth): add user auth durable object binding
guiming99 Aug 24, 2026
bd1731c
feat(auth): add login session and mailbox ACL middleware
guiming99 Aug 24, 2026
d14e2b1
fix(auth): enforce mailbox directory permissions at outer router
guiming99 Aug 24, 2026
a6fecdb
feat(auth): add browser auth client
guiming99 Aug 24, 2026
c002749
feat(auth): add employee login and registration screen
guiming99 Aug 24, 2026
51ff13c
feat(auth): add login and registration routes
guiming99 Aug 24, 2026
7bd04b2
feat(auth): gate application routes behind user session
guiming99 Aug 24, 2026
8ed87b8
fix(auth): remove unused Hono context variable
guiming99 Aug 24, 2026
207d79a
feat(auth): add admin user management operations
guiming99 Aug 24, 2026
e3ae9e0
feat(auth): add admin user management API
guiming99 Aug 24, 2026
c830b89
feat(auth): add admin user management client
guiming99 Aug 24, 2026
00dc358
feat(auth): add administrator user management screen
guiming99 Aug 24, 2026
8e87fe0
feat(auth): add admin route
guiming99 Aug 24, 2026
5c3c9d4
fix(auth): read password hash from SQLite column name
guiming99 Aug 24, 2026
28ebda6
Harden session gates for agents and MCP
guiming99 Aug 24, 2026
49492b2
Clean up auth route imports
guiming99 Aug 24, 2026
6dabe7a
Ensure seeded admin has a mailbox
guiming99 Aug 24, 2026
52793f3
Use explicit MCP middleware routes
guiming99 Aug 24, 2026
d2f5abc
Add admin-managed login background
guiming99 Aug 24, 2026
1c0b7c5
Add login background admin API
guiming99 Aug 24, 2026
d2104c4
Add login background management to admin
guiming99 Aug 24, 2026
60d91eb
Polish login screen with configurable background
guiming99 Aug 24, 2026
67f97f8
Harden admin seeding and prevent admin address hijacking
guiming99 Aug 24, 2026
1f52799
Reserve administrator email from employee registration
guiming99 Aug 24, 2026
15dabb1
feat: add resilient post-delivery forwarding and Telegram notifications
guiming99 Aug 24, 2026
35a5573
feat: harden post-delivery settings and forwarding loop checks
guiming99 Aug 24, 2026
bc91bd0
feat: add forwarding and Telegram notification settings UI
guiming99 Aug 24, 2026
7726d4e
feat: unify inbound delivery with post-delivery notifications
guiming99 Aug 24, 2026
ca8c0a3
feat: route inbound mail through unified post-delivery pipeline
guiming99 Aug 24, 2026
1fdaf98
fix: avoid relying on PostalMime namespace type export
guiming99 Aug 24, 2026
c8b3271
fix: detect forwarded messages by transport header
guiming99 Aug 24, 2026
9bac28b
fix: propagate forwarding marker from inbound headers
guiming99 Aug 24, 2026
0908cc5
refactor: keep notifications on external delivery path only
guiming99 Aug 24, 2026
9f3b619
feat: add employee authentication and mailbox permissions
guiming99 Aug 24, 2026
1cc20df
feat: add external email forwarding and Telegram notifications
guiming99 Aug 24, 2026
f2ca847
fix: give login and register unique route modules
guiming99 Aug 24, 2026
cb8184b
fix: add dedicated register route module
guiming99 Aug 24, 2026
0d7a887
fix: refresh admin credential from ADMIN_PASSWORD
guiming99 Aug 24, 2026
7efcb98
fix: synchronize admin password from ADMIN_PASSWORD
guiming99 Aug 24, 2026
0d0467f
fix: surface admin bootstrap configuration failures
guiming99 Aug 24, 2026
313c84d
fix: report admin bootstrap errors during login
guiming99 Aug 24, 2026
0772ecc
fix: use Workers-supported PBKDF2 iteration count
guiming99 Aug 24, 2026
abc7483
fix: render inline email images correctly
guiming99 Aug 24, 2026
56f8268
fix: close search route after inline image header fix
guiming99 Aug 24, 2026
0f02bdb
feat: add attachment selection to compose
guiming99 Aug 24, 2026
f4661c7
feat: add attachment picker to compose dialog
guiming99 Aug 24, 2026
cecdd37
fix: repair malformed thread route syntax in worker index
guiming99 Aug 24, 2026
37b937f
fix: render CID inline images from stored attachments
guiming99 Aug 24, 2026
683548f
fix: treat Content-ID attachments as inline resources
guiming99 Aug 24, 2026
fcc914e
fix inline CID attachment response and persistence
guiming99 Aug 24, 2026
6608682
fix: do not classify Content-ID alone as inline
guiming99 Aug 24, 2026
fd14a19
fix: show real attachments even when sender adds Content-ID
guiming99 Aug 24, 2026
ebebe6b
fix: restore original agentic-inbox attachment MIME semantics
guiming99 Aug 24, 2026
a14ce8a
Ensure compose attachment picker is visible
guiming99 Aug 25, 2026
e878996
Fix Gmail CID inline images inside sandboxed email iframe
guiming99 Aug 25, 2026
929a7d4
Fix authenticated CID image loading in sandbox
guiming99 Aug 25, 2026
6d5b4c0
Fix authenticated inline attachment images
guiming99 Aug 25, 2026
e66399c
Fix absolute attachment URLs used by inline image rendering
guiming99 Aug 25, 2026
ef75dc8
Add attachment upload component for compose form
guiming99 Aug 25, 2026
a756919
Fix: Attachment download endpoint respects disposition field for inli…
guiming99 Aug 25, 2026
000b3d5
Feature: Add attachment support to useComposeForm hook
guiming99 Aug 25, 2026
1b10893
Add attachment upload UI to sidebar compose panel
guiming99 Aug 25, 2026
32faa3a
Fix compose form syntax error while preserving attachment support
guiming99 Aug 25, 2026
2041d55
Use native Cloudflare EmailMessage.forward for inbound mailbox forwar…
guiming99 Aug 25, 2026
c6f8210
Pass native EmailMessage.forward into post-delivery forwarding
guiming99 Aug 25, 2026
e4fde2c
Use Cloudflare EmailMessage.forward for native inbound forwarding
guiming99 Aug 25, 2026
c86fbe9
Bind native EmailMessage.forward for safe post-delivery forwarding
guiming99 Aug 25, 2026
e77b276
Fix native email forwarding lifecycle and keep Telegram independent
guiming99 Aug 25, 2026
56ee55b
Await post-delivery so native EmailMessage.forward runs during email …
guiming99 Aug 25, 2026
35d9346
Fix native email forwarding binding without changing mail storage or …
guiming99 Aug 25, 2026
37bb2da
Remove Cloudflare Access gate and use application login
guiming99 Aug 25, 2026
12ade6d
Fix company email registration to use TEAM_DOMAIN
guiming99 Aug 25, 2026
e341e7c
feat: notify admin when a user registers
guiming99 Aug 25, 2026
6859537
fix: preserve employee password reset query
guiming99 Aug 25, 2026
05ad631
Brand login screen as Astra Trade Mail
guiming99 Aug 25, 2026
bc3fac7
Add visible mailbox logout button
guiming99 Aug 25, 2026
e49c55d
Allow admin to initialize login credentials for pre-existing mailboxes
guiming99 Aug 25, 2026
a1664d9
Allow admin password reset to initialize legacy mailboxes
guiming99 Aug 25, 2026
6a1e2af
Expose mailbox login initialization in admin API
guiming99 Aug 25, 2026
a7a2253
Add admin setup for pre-existing mailbox passwords
guiming99 Aug 25, 2026
38985a1
Use browser-session auth cookie and keep explicit logout
guiming99 Aug 25, 2026
2e410ba
Make application brand configurable via APP_NAME
guiming99 Aug 25, 2026
e5498c3
Make login branding configurable via APP_NAME and expose public auth …
guiming99 Aug 25, 2026
ff71161
Use configurable application name and company domain on auth pages
guiming99 Aug 25, 2026
f648ffe
feat: add admin branding API client
guiming99 Aug 25, 2026
aec8693
feat: complete admin branding and approval UI
guiming99 Aug 25, 2026
a73a8e5
feat: persist configurable mailbox brand name
guiming99 Aug 25, 2026
09e57a5
feat: add public branding resource route
guiming99 Aug 25, 2026
69feeca
fix: connect admin branding UI to persistent branding route
guiming99 Aug 25, 2026
7082ab5
feat: load persisted brand on login screen
guiming99 Aug 25, 2026
337fb8c
feat: expose administration from mailbox header
guiming99 Aug 25, 2026
7c258f7
feat: protect and complete administration route
guiming99 Aug 25, 2026
bf3d88a
feat: split mailbox login into username and domain
guiming99 Aug 25, 2026
b35e586
feat: support managed multiple email domains
guiming99 Aug 25, 2026
c28ca10
feat: add admin email domain management
guiming99 Aug 25, 2026
bf36d4b
feat: complete admin domains and navigation UI
guiming99 Aug 25, 2026
d5cce6d
chore: type multi-domain configuration
guiming99 Aug 25, 2026
db0561f
Fix Agent WebSocket auth routing for custom login
guiming99 Aug 25, 2026
c3a437f
Add temporary workflow to switch Agent chat model
guiming99 Aug 25, 2026
a89d1f1
[ai-model-hotfix] Use GLM-4.7-Flash for Agent chat
github-actions[bot] Aug 25, 2026
9db0b9a
[ai-model-hotfix] Remove temporary workflow
github-actions[bot] Aug 25, 2026
adbb9cb
debug Workers AI stream errors
guiming99 Aug 25, 2026
c62d4e2
Optimize AI draft-reply flow: skip secondary draft verification and r…
guiming99 Aug 25, 2026
d47dd91
feat: add independent Cloudmail to Agentic Inbox migration tool
guiming99 Aug 25, 2026
135ba57
refactor: make migration resources deployment-time bindings
guiming99 Aug 25, 2026
8a870f7
docs: document deployment-time Cloudflare bindings
guiming99 Aug 25, 2026
8a28db5
fix: add npm lockfile for Cloudflare build
guiming99 Aug 25, 2026
7e0b0c2
fix: let Cloudflare install Wrangler via deploy command
guiming99 Aug 25, 2026
cebcc5f
fix: make migration lockfile match package manifest
guiming99 Aug 25, 2026
ebc7f75
fix: provide Cloudflare Builds build script
guiming99 Aug 25, 2026
c3c9298
chore: trigger migration tool redeploy from latest main
guiming99 Aug 25, 2026
57e7d97
feat: add contacts module with contact email history
guiming99 Aug 25, 2026
2d73e63
feat: register contacts route
guiming99 Aug 25, 2026
9d7f19a
feat: add contacts navigation to sidebar
guiming99 Aug 25, 2026
01f8ae0
feat: add inline-logo email signature settings
guiming99 Aug 25, 2026
1c18dfc
feat: render signature logo as CID inline image
guiming99 Aug 25, 2026
3e25487
feat: attach signature logo as hidden inline CID resource
guiming99 Aug 25, 2026
e3f1c20
fix: send inline signature images using Resend content_id
guiming99 Aug 25, 2026
a845d40
feat: type contact and inline signature settings
guiming99 Aug 25, 2026
d1fe503
fix: correct contacts route dialog and folder field
guiming99 Aug 25, 2026
5bf4301
fix: contacts page runtime crash by removing unsupported Dialog API
guiming99 Aug 25, 2026
c15abd8
fix: robust signature logo preview and data-url validation
guiming99 Aug 25, 2026
328997c
fix: show signature logo as data URL in compose editor
guiming99 Aug 25, 2026
2fcf5e6
fix: preview signature logo in compose and convert to CID only on send
guiming99 Aug 25, 2026
af491e9
feat: add configurable R2 binding for signature assets
guiming99 Aug 25, 2026
050303e
feat: add R2 signature asset upload and delivery endpoints
guiming99 Aug 25, 2026
27e2a4f
feat: add signature logo R2 API client
guiming99 Aug 25, 2026
9e2ddec
feat: upload signature logo to R2 and preview from Worker
guiming99 Aug 25, 2026
bc25b35
feat: render signature logo from R2 in composer
guiming99 Aug 25, 2026
7bff17d
feat: send R2 signature logo as inline CID
guiming99 Aug 25, 2026
9c0cc00
feat: type signature logo R2 key
guiming99 Aug 25, 2026
5748a32
Update useComposeForm.ts
guiming99 Aug 25, 2026
82498d8
Update useComposeForm.ts
guiming99 Aug 25, 2026
cd931de
refactor: remove hardcoded company domain from auth flow
guiming99 Aug 26, 2026
b5dbfc6
Update useComposeForm.ts
guiming99 Aug 27, 2026
21c374e
Update ThreadMessage.tsx
guiming99 Aug 27, 2026
ef8d246
Update SingleMessageView.tsx
guiming99 Aug 27, 2026
d917034
Update utils.ts
guiming99 Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions app/components/AttachmentUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright (c) 2026 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

import { Button } from "@cloudflare/kumo";
import { PaperclipIcon, XIcon } from "@phosphor-icons/react";
import { useRef } from "react";
import { formatBytes } from "~/lib/utils";

export interface AttachmentFile {
file: File;
id: string; // temporary id for display
}

interface AttachmentUploadProps {
attachments: AttachmentFile[];
onAdd: (files: File[]) => void;
onRemove: (id: string) => void;
maxSize?: number; // in bytes, default 25MB
disabled?: boolean;
}

const DEFAULT_MAX_SIZE = 25 * 1024 * 1024; // 25MB

export default function AttachmentUpload({
attachments,
onAdd,
onRemove,
maxSize = DEFAULT_MAX_SIZE,
disabled = false,
}: AttachmentUploadProps) {
const inputRef = useRef<HTMLInputElement>(null);

const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || []);

// Validate files
const validFiles: File[] = [];
for (const file of files) {
if (file.size > maxSize) {
console.warn(`File ${file.name} exceeds size limit (${formatBytes(file.size)} > ${formatBytes(maxSize)})`);
continue;
}
validFiles.push(file);
}

if (validFiles.length > 0) {
onAdd(validFiles);
}

// Reset input so the same file can be selected again
if (inputRef.current) {
inputRef.current.value = "";
}
};

const handleClick = () => {
inputRef.current?.click();
};

return (
<div className="space-y-2">
<div>
<Button
type="button"
variant="ghost"
size="sm"
icon={<PaperclipIcon size={14} />}
onClick={handleClick}
disabled={disabled}
>
Add Attachment
</Button>
<input
ref={inputRef}
type="file"
multiple
onChange={handleFileSelect}
disabled={disabled}
className="hidden"
aria-label="Select files to attach"
/>
</div>

{attachments.length > 0 && (
<div className="bg-kumo-tint rounded p-3">
<div className="text-xs font-medium text-kumo-subtle mb-2">
{attachments.length} attachment{attachments.length !== 1 ? "s" : ""}
</div>
<div className="space-y-1.5">
{attachments.map((att) => (
<div
key={att.id}
className="flex items-center justify-between bg-kumo-base rounded px-2 py-1.5"
>
<div className="flex items-center gap-2 min-w-0 flex-1">
<PaperclipIcon size={12} className="text-kumo-subtle shrink-0" />
<div className="min-w-0 flex-1">
<div className="text-xs text-kumo-default truncate font-medium">
{att.file.name}
</div>
<div className="text-xs text-kumo-subtle">
{formatBytes(att.file.size)}
</div>
</div>
</div>
<button
type="button"
onClick={() => onRemove(att.id)}
disabled={disabled}
className="ml-2 p-1 text-kumo-subtle hover:text-kumo-default transition-colors shrink-0"
aria-label={`Remove ${att.file.name}`}
>
<XIcon size={12} />
</button>
</div>
))}
</div>
</div>
)}
</div>
);
}
163 changes: 48 additions & 115 deletions app/components/ComposeEmail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,141 +3,74 @@
// https://opensource.org/licenses/Apache-2.0

import { Banner, Button, Dialog, Input, Text } from "@cloudflare/kumo";
import { FloppyDiskIcon, PaperPlaneTiltIcon } from "@phosphor-icons/react";
import { FileIcon, PaperclipIcon, PaperPlaneTiltIcon, XIcon, FloppyDiskIcon } from "@phosphor-icons/react";
import { useParams } from "react-router";
import { useComposeForm } from "~/hooks/useComposeForm";
import RichTextEditor from "./RichTextEditor";
import { useUIStore } from "~/hooks/useUIStore";

function formatSize(bytes: number) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}

export default function ComposeEmail() {
const { mailboxId, folder } = useParams<{
mailboxId: string;
folder: string;
}>();

const { mailboxId, folder } = useParams<{ mailboxId: string; folder: string }>();
const { isComposeModalOpen, closeComposeModal } = useUIStore();

const {
to,
setTo,
cc,
setCc,
bcc,
setBcc,
showCcBcc,
setShowCcBcc,
subject,
setSubject,
body,
setBody,
error,
isSavingDraft,
isSending,
formTitle,
handleSaveDraft,
handleSend,
to, setTo, cc, setCc, bcc, setBcc, showCcBcc, setShowCcBcc,
subject, setSubject, body, setBody, attachments, addAttachments, removeAttachment,
error, isSavingDraft, isSending, formTitle, handleSaveDraft, handleSend,
} = useComposeForm(mailboxId, folder);

return (
<Dialog.Root
open={isComposeModalOpen}
onOpenChange={(open) => !open && !isSending && closeComposeModal()}
>
<Dialog.Root open={isComposeModalOpen} onOpenChange={(open) => !open && !isSending && closeComposeModal()}>
<Dialog size="lg" className="p-6 max-h-[85vh] overflow-y-auto">
<Dialog.Title className="text-lg font-semibold mb-5">
{formTitle}
</Dialog.Title>
<Dialog.Title className="text-lg font-semibold mb-5">{formTitle}</Dialog.Title>
<form onSubmit={(e) => handleSend(e, closeComposeModal)} className="space-y-4">
{error && <Banner variant="error" text={error} />}
<div className="flex items-center gap-2">
<div className="flex-1">
<Input
label="To"
type="text"
placeholder="recipient@example.com, another@example.com"
size="sm"
value={to}
onChange={(e) => setTo(e.target.value)}
required
<div className="flex-1"><Input label="To" type="text" placeholder="recipient@example.com, another@example.com" size="sm" value={to} onChange={(e) => setTo(e.target.value)} required /></div>
{!showCcBcc && <button type="button" onClick={() => setShowCcBcc(true)} className="shrink-0 text-xs text-kumo-link hover:text-kumo-link-hover font-medium mt-5">CC / BCC</button>}
</div>
{showCcBcc && <Input label="CC" type="text" size="sm" value={cc} onChange={(e) => setCc(e.target.value)} placeholder="Separate multiple addresses with commas" />}
{showCcBcc && <Input label="BCC" type="text" size="sm" value={bcc} onChange={(e) => setBcc(e.target.value)} placeholder="Separate multiple addresses with commas" />}
<Input label="Subject" type="text" placeholder="Email subject" size="sm" value={subject} onChange={(e) => setSubject(e.target.value)} required />
<div><Text size="sm" DANGEROUS_className="font-medium mb-1.5 block">Message</Text><RichTextEditor value={body} onChange={setBody} /></div>

{/* Attachment UI: the hook already converts files to the base64 format expected by SendEmailRequestSchema. */}
<div className="space-y-2">
<label className="inline-flex items-center gap-2 cursor-pointer text-sm text-kumo-default rounded-md border border-kumo-line px-3 py-2 hover:bg-kumo-tint">
<PaperclipIcon size={16} />
<span>Attach files</span>
<input
type="file"
multiple
className="hidden"
onChange={(e) => { void addAttachments(e.target.files); e.currentTarget.value = ""; }}
disabled={isSending}
/>
</div>
{!showCcBcc && (
<button
type="button"
onClick={() => setShowCcBcc(true)}
className="shrink-0 text-xs text-kumo-link hover:text-kumo-link-hover font-medium mt-5"
>
CC / BCC
</button>
</label>
{attachments.length > 0 && (
<div className="space-y-1.5">
{attachments.map((attachment, index) => (
<div key={`${attachment.filename}-${index}`} className="flex items-center gap-2 rounded-md border border-kumo-line px-3 py-2 text-sm">
<FileIcon size={16} className="text-kumo-subtle shrink-0" />
<span className="truncate flex-1">{attachment.filename}</span>
<span className="text-kumo-subtle shrink-0">{formatSize(attachment.size)}</span>
<button type="button" title="Remove attachment" onClick={() => removeAttachment(index)} disabled={isSending} className="p-1 rounded hover:bg-kumo-tint"><XIcon size={14} /></button>
</div>
))}
</div>
)}
</div>
{showCcBcc && (
<Input
label="CC"
type="text"
size="sm"
value={cc}
onChange={(e) => setCc(e.target.value)}
placeholder="Separate multiple addresses with commas"
/>
)}
{showCcBcc && (
<Input
label="BCC"
type="text"
size="sm"
value={bcc}
onChange={(e) => setBcc(e.target.value)}
placeholder="Separate multiple addresses with commas"
/>
)}
<Input
label="Subject"
type="text"
placeholder="Email subject"
size="sm"
value={subject}
onChange={(e) => setSubject(e.target.value)}
required
/>
<div>
<Text size="sm" DANGEROUS_className="font-medium mb-1.5 block">
Message
</Text>
<RichTextEditor value={body} onChange={setBody} />
</div>

<div className="flex justify-between items-center pt-2">
<Button
type="button"
variant="ghost"
size="sm"
onClick={closeComposeModal}
disabled={isSending}
>
Discard
</Button>
<Button type="button" variant="ghost" size="sm" onClick={closeComposeModal} disabled={isSending}>Discard</Button>
<div className="flex items-center gap-2">
<Button
type="button"
variant="secondary"
size="sm"
loading={isSavingDraft}
disabled={isSending}
icon={<FloppyDiskIcon size={14} />}
onClick={handleSaveDraft}
>
{isSavingDraft ? "Saving..." : "Save as Draft"}
</Button>
<Button
type="submit"
variant="primary"
size="sm"
loading={isSending}
disabled={isSavingDraft || isSending}
icon={<PaperPlaneTiltIcon size={14} />}
>
{isSending ? "Sending..." : "Send"}
</Button>
<Button type="button" variant="secondary" size="sm" loading={isSavingDraft} disabled={isSending} icon={<FloppyDiskIcon size={14} />} onClick={handleSaveDraft}>{isSavingDraft ? "Saving..." : "Save as Draft"}</Button>
<Button type="submit" variant="primary" size="sm" loading={isSending} disabled={isSavingDraft || isSending} icon={<PaperPlaneTiltIcon size={14} />}>{isSending ? "Sending..." : "Send"}</Button>
</div>
</div>
</form>
Expand Down
Loading