Skip to content

Commit 250fe47

Browse files
feat: implement reply functionality and update docs
1 parent f9fd449 commit 250fe47

6 files changed

Lines changed: 50 additions & 13 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Open-Temp-Mail uses `wrangler.toml` for configuration.
7979
- `ADMIN_PASSWORD`: Secure password for admin access.
8080
- `JWT_TOKEN`: Random string for session security.
8181
- `RESEND_API_KEY`: (Optional) For sending emails via Resend.
82+
> **Note**: Resend offers a free tier with 3000 emails/month, which is perfect for personal use.
8283

8384
## 📦 Deployment
8485

src/components/shared/ComposeEmail.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ import { Loader2, Send } from 'lucide-react';
77
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
88
import toast from 'react-hot-toast';
99

10-
export function ComposeEmail({ onClose }: { onClose?: () => void }) {
10+
export function ComposeEmail({ onClose, initialValues }: {
11+
onClose?: () => void;
12+
initialValues?: { to?: string; subject?: string; body?: string; isHtml?: boolean }
13+
}) {
1114
const { user } = useAuth();
1215
const { sendEmail, isSending } = useSender();
13-
const [to, setTo] = useState('');
14-
const [subject, setSubject] = useState('');
15-
const [body, setBody] = useState('');
16-
const [isHtml, setIsHtml] = useState(false);
16+
const [to, setTo] = useState(initialValues?.to || '');
17+
const [subject, setSubject] = useState(initialValues?.subject || '');
18+
const [body, setBody] = useState(initialValues?.body || '');
19+
const [isHtml, setIsHtml] = useState(initialValues?.isHtml || false);
1720

1821
const handleSubmit = async (e: React.FormEvent) => {
1922
e.preventDefault();

src/pages/mailbox/Compose.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { ComposeEmail } from '@/components/shared/ComposeEmail';
2-
import { useNavigate } from 'react-router-dom';
2+
import { useNavigate, useLocation } from 'react-router-dom';
33

44
export default function ComposePage() {
55
const navigate = useNavigate();
6+
const location = useLocation();
7+
const initialValues = location.state as { to?: string; subject?: string; body?: string } | undefined;
68

79
return (
810
<div className="container max-w-2xl py-8">
9-
<h1 className="text-2xl font-bold mb-6">Compose New Email</h1>
10-
<ComposeEmail onClose={() => navigate('/sent')} />
11+
<h1 className="text-2xl font-bold mb-6">
12+
{initialValues?.to ? 'Reply to Email' : 'Compose New Email'}
13+
</h1>
14+
<ComposeEmail
15+
onClose={() => navigate('/sent')}
16+
initialValues={initialValues}
17+
/>
1118
</div>
1219
);
1320
}

src/pages/mailbox/index.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { useState, useEffect, useCallback, useMemo } from 'react';
2-
import { useSearchParams } from 'react-router-dom';
2+
import { useSearchParams, useNavigate } from 'react-router-dom';
33
import { useAuth } from '@/context/AuthContext';
44
import { apiFetch } from '@/lib/api';
55
import { Button } from '@/components/ui/button';
66
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
7-
import { Trash2, RefreshCw, MailOpen, Mail, Search, KeyRound, Loader2 } from 'lucide-react';
7+
import { Trash2, RefreshCw, MailOpen, Mail, Search, KeyRound, Loader2, Send } from 'lucide-react';
88
import { formatDistanceToNow } from 'date-fns';
99
import toast from 'react-hot-toast';
1010
import { cn } from '@/lib/utils';
@@ -32,6 +32,7 @@ interface EmailDetail extends EmailSummary {
3232

3333
export default function Mailbox() {
3434
const { user } = useAuth();
35+
const navigate = useNavigate();
3536
const [searchParams] = useSearchParams();
3637
const queryMailbox = searchParams.get('mailbox');
3738

@@ -137,6 +138,16 @@ export default function Mailbox() {
137138
}
138139
};
139140

141+
const handleReply = (email: EmailDetail) => {
142+
navigate('/compose', {
143+
state: {
144+
to: email.sender,
145+
subject: email.subject.startsWith('Re:') ? email.subject : `Re: ${email.subject}`,
146+
body: `\n\n\n--- Original Message ---\nFrom: ${email.sender}\nDate: ${new Date(email.received_at).toLocaleString()}\nSubject: ${email.subject}\n\n`
147+
}
148+
});
149+
};
150+
140151
return (
141152
<div className="h-[calc(100vh-4rem)] flex flex-col md:flex-row p-4 gap-4">
142153
{/* Email List */}
@@ -262,6 +273,12 @@ export default function Mailbox() {
262273
</div>
263274
</div>
264275
<div className="flex items-center gap-2">
276+
<Button variant="outline" size="sm" onClick={() => handleReply(selectedEmail)}>
277+
<div className="flex items-center">
278+
<span className="mr-2">Reply</span>
279+
<Send className="h-3 w-3" />
280+
</div>
281+
</Button>
265282
{selectedEmail.download && (
266283
<Button variant="outline" size="sm" asChild>
267284
<a href={selectedEmail.download} download>Download EML</a>

worker/src/api/send.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ async function checkSendPermission(request, db, options) {
2727

2828
// 管理员默认允许
2929
if (payload.role === 'admin') return true;
30+
31+
// 邮箱用户允许(必须匹配 from 地址)
32+
if (payload.role === 'mailbox') {
33+
const fromAddr = options.fromAddress || '';
34+
return fromAddr && payload.mailboxAddress &&
35+
fromAddr.toLowerCase() === payload.mailboxAddress.toLowerCase();
36+
}
3037

3138
// 普通用户检查 can_send 权限(使用缓存)
3239
if (payload.userId) {
@@ -114,9 +121,11 @@ export async function handleSendApi(request, db, url, path, options) {
114121
try {
115122
if (!RESEND_API_KEY) return errorResponse('未配置 Resend API Key', 500);
116123

117-
const allowed = await checkSendPermission(request, db, options);
118-
if (!allowed) return errorResponse('未授权发件或该用户未被授予发件权限', 403);
119124
const sendPayload = await request.json();
125+
126+
// 添加 fromAddress 到 options 以供权限检查使用
127+
const checkOptions = { ...options, fromAddress: sendPayload.from };
128+
const allowed = await checkSendPermission(request, db, checkOptions);
120129
const result = await sendEmailWithAutoResend(RESEND_API_KEY, sendPayload);
121130
await recordSentEmail(db, {
122131
resendId: result.id || null,

worker/src/routes/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export function createRouter() {
110110
success: true,
111111
role: 'mailbox',
112112
mailbox: mailboxInfo.address,
113-
can_send: 0,
113+
can_send: 1,
114114
mailbox_limit: 1
115115
}), { headers });
116116
}

0 commit comments

Comments
 (0)