Skip to content

Commit 7bcdcba

Browse files
added
1 parent f9d44f7 commit 7bcdcba

36 files changed

Lines changed: 5542 additions & 12 deletions

version 2/.env.example

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Database Configuration
2+
TEMP_MAIL_DB_ID=c82663ac-62b6-4ba6-be81-cbe297f2a3dd
3+
4+
# Mail Configuration
5+
MAIL_DOMAIN=temp.example.com
6+
RESEND_API_KEY=your_resend_api_key_here
7+
8+
# Security
9+
ADMIN_PASSWORD=change_me_secure_password
10+
ADMIN_NAME=admin
11+
JWT_TOKEN=change_me_jwt_secret_key
12+
SESSION_EXPIRE_DAYS=7
13+
14+
# Optional: Forwarding
15+
# FORWARD_RULES="[{\"prefix\":\"vip\",\"email\":\"a@example.com\"},{\"prefix\":\"*\",\"email\":\"admin@example.com\"}]"

version 2/DEPLOY.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Deploy Guide
2+
3+
## Prerequisites
4+
5+
- Node.js & npm installed
6+
- Cloudflare Wrangler CLI installed (`npm install -g wrangler`)
7+
- A Cloudflare account
8+
9+
## Setup
10+
11+
1. **Install dependencies**:
12+
```bash
13+
npm install
14+
```
15+
16+
2. **Configure Environment**:
17+
- Copy `.env.example` to `.env` (for local development using Vite's proxy, though `wrangler.toml` handles production vars).
18+
- Update `wrangler.toml` with your D1 Database ID and Name.
19+
- Set secrets in Cloudflare dashboard OR via wrangler:
20+
```bash
21+
wrangler secret put ADMIN_PASSWORD
22+
wrangler secret put JWT_TOKEN
23+
wrangler secret put RESEND_API_KEY
24+
# ... other variables
25+
```
26+
27+
3. **Database Initialization**:
28+
- If you haven't created the database yet:
29+
```bash
30+
wrangler d1 create maill_free_db
31+
```
32+
- Initialize the schema:
33+
```bash
34+
wrangler d1 execute maill_free_db --file=d1-init.sql
35+
```
36+
37+
## Development
38+
39+
1. **Frontend & Backend**:
40+
- Run `npm run dev` to start the Vite dev server.
41+
- Note: The Vite dev server proxies API requests to `http://localhost:8787`. You will need to run the worker locally separately or adjust the proxy target.
42+
- **Better Approach**: Run `wrangler dev` to serve both the worker and the static assets (after building).
43+
44+
```bash
45+
npm run build
46+
wrangler dev
47+
```
48+
49+
## Deployment
50+
51+
1. **Build Frontend**:
52+
```bash
53+
npm run build
54+
```
55+
56+
2. **Deploy to Cloudflare Workers**:
57+
```bash
58+
wrangler deploy
59+
```
60+
61+
## Verification
62+
63+
- Visit your worker URL (e.g., `https://freemail-v2.your-subdomain.workers.dev`).
64+
- Log in with the credentials you set.

version 2/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ A modern, responsive frontend for the Freemail application, built with React, Vi
66
- **Modern UI**: Clean interface built with Tailwind CSS v4.
77
- **Authentication**: Secure login for Admins, Users, and Mailbox accounts.
88
- **Dashboard**: Overview of system status and mailboxes.
9+
- **Mailbox Management**:
10+
- **Favorites**: Star important mailboxes for quick access.
11+
- **Filtering**: Easily filter by All, Favorites, or Forwarding status.
12+
- **Forwarding**: Set up auto-forwarding rules for mailboxes.
913
- **Mailbox**: Real-time email listing and reading with sanitized HTML view.
1014
- **Responsive**: Fully optimized for mobile and desktop.
1115

@@ -33,8 +37,10 @@ The frontend is configured to proxy API requests to `http://localhost:8787`.
3337
To change this, edit `vite.config.ts`.
3438

3539
## Deployment
40+
See [DEPLOY.md](DEPLOY.md) for detailed deployment instructions using Cloudflare Workers.
41+
3642
To build for production:
3743
```bash
3844
npm run build
3945
```
40-
The output will be in the `dist` directory.
46+
The output will be in the `dist` directory, which is served by the Cloudflare Worker.

version 2/d1-init.sql

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
-- Cloudflare D1 数据库初始化脚本
2+
-- 首次部署时执行:wrangler d1 execute DB --file=./d1-init.sql
3+
4+
-- 启用外键约束
5+
PRAGMA foreign_keys = ON;
6+
7+
-- 邮箱地址表
8+
CREATE TABLE IF NOT EXISTS mailboxes (
9+
id INTEGER PRIMARY KEY AUTOINCREMENT,
10+
address TEXT NOT NULL UNIQUE,
11+
local_part TEXT NOT NULL,
12+
domain TEXT NOT NULL,
13+
password_hash TEXT,
14+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
15+
last_accessed_at TEXT,
16+
expires_at TEXT,
17+
is_pinned INTEGER DEFAULT 0,
18+
can_login INTEGER DEFAULT 0,
19+
forward_to TEXT DEFAULT NULL,
20+
is_favorite INTEGER DEFAULT 0
21+
);
22+
23+
-- 邮件消息表
24+
CREATE TABLE IF NOT EXISTS messages (
25+
id INTEGER PRIMARY KEY AUTOINCREMENT,
26+
mailbox_id INTEGER NOT NULL,
27+
sender TEXT NOT NULL,
28+
to_addrs TEXT NOT NULL DEFAULT '',
29+
subject TEXT NOT NULL,
30+
verification_code TEXT,
31+
preview TEXT,
32+
r2_bucket TEXT NOT NULL DEFAULT 'mail-eml',
33+
r2_object_key TEXT NOT NULL DEFAULT '',
34+
received_at TEXT DEFAULT CURRENT_TIMESTAMP,
35+
is_read INTEGER DEFAULT 0,
36+
FOREIGN KEY(mailbox_id) REFERENCES mailboxes(id)
37+
);
38+
39+
-- 用户表
40+
CREATE TABLE IF NOT EXISTS users (
41+
id INTEGER PRIMARY KEY AUTOINCREMENT,
42+
username TEXT NOT NULL UNIQUE,
43+
password_hash TEXT,
44+
role TEXT NOT NULL DEFAULT 'user',
45+
can_send INTEGER NOT NULL DEFAULT 0,
46+
mailbox_limit INTEGER NOT NULL DEFAULT 10,
47+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
48+
);
49+
50+
-- 用户-邮箱关联表
51+
CREATE TABLE IF NOT EXISTS user_mailboxes (
52+
id INTEGER PRIMARY KEY AUTOINCREMENT,
53+
user_id INTEGER NOT NULL,
54+
mailbox_id INTEGER NOT NULL,
55+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
56+
is_pinned INTEGER NOT NULL DEFAULT 0,
57+
UNIQUE(user_id, mailbox_id),
58+
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
59+
FOREIGN KEY(mailbox_id) REFERENCES mailboxes(id) ON DELETE CASCADE
60+
);
61+
62+
-- 发送邮件记录表
63+
CREATE TABLE IF NOT EXISTS sent_emails (
64+
id INTEGER PRIMARY KEY AUTOINCREMENT,
65+
resend_id TEXT,
66+
from_name TEXT,
67+
from_addr TEXT NOT NULL,
68+
to_addrs TEXT NOT NULL,
69+
subject TEXT NOT NULL,
70+
html_content TEXT,
71+
text_content TEXT,
72+
status TEXT DEFAULT 'queued',
73+
scheduled_at TEXT,
74+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
75+
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
76+
);
77+
78+
-- 创建索引
79+
80+
-- mailboxes 索引
81+
CREATE INDEX IF NOT EXISTS idx_mailboxes_address ON mailboxes(address);
82+
CREATE INDEX IF NOT EXISTS idx_mailboxes_is_pinned ON mailboxes(is_pinned DESC);
83+
CREATE INDEX IF NOT EXISTS idx_mailboxes_address_created ON mailboxes(address, created_at DESC);
84+
CREATE INDEX IF NOT EXISTS idx_mailboxes_is_favorite ON mailboxes(is_favorite DESC);
85+
86+
-- messages 索引
87+
CREATE INDEX IF NOT EXISTS idx_messages_mailbox_id ON messages(mailbox_id);
88+
CREATE INDEX IF NOT EXISTS idx_messages_received_at ON messages(received_at DESC);
89+
CREATE INDEX IF NOT EXISTS idx_messages_r2_object_key ON messages(r2_object_key);
90+
CREATE INDEX IF NOT EXISTS idx_messages_mailbox_received ON messages(mailbox_id, received_at DESC);
91+
CREATE INDEX IF NOT EXISTS idx_messages_mailbox_received_read ON messages(mailbox_id, received_at DESC, is_read);
92+
93+
-- users 索引
94+
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
95+
96+
-- user_mailboxes 索引
97+
CREATE INDEX IF NOT EXISTS idx_user_mailboxes_user ON user_mailboxes(user_id);
98+
CREATE INDEX IF NOT EXISTS idx_user_mailboxes_mailbox ON user_mailboxes(mailbox_id);
99+
CREATE INDEX IF NOT EXISTS idx_user_mailboxes_user_pinned ON user_mailboxes(user_id, is_pinned DESC);
100+
CREATE INDEX IF NOT EXISTS idx_user_mailboxes_composite ON user_mailboxes(user_id, mailbox_id, is_pinned);
101+
102+
-- sent_emails 索引
103+
CREATE INDEX IF NOT EXISTS idx_sent_emails_resend_id ON sent_emails(resend_id);
104+
CREATE INDEX IF NOT EXISTS idx_sent_emails_status_created ON sent_emails(status, created_at DESC);
105+
CREATE INDEX IF NOT EXISTS idx_sent_emails_from_addr ON sent_emails(from_addr);
106+

version 2/src/pages/dashboard/index.tsx

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { apiFetch } from '@/lib/api';
44
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
55
import { Button } from '@/components/ui/button';
66
import { Link } from 'react-router-dom';
7-
import { Mail, Users, Trash2, ExternalLink } from 'lucide-react';
7+
import { Mail, Users, Trash2, ExternalLink, Star } from 'lucide-react';
88
import toast from 'react-hot-toast';
99
import { formatDistanceToNow } from 'date-fns';
1010
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -15,12 +15,15 @@ interface Mailbox {
1515
address: string;
1616
created_at: string;
1717
email_count?: number;
18+
is_favorite?: boolean;
19+
forward_to?: string;
1820
}
1921

2022
export default function Dashboard() {
2123
const { user } = useAuth();
2224
const [mailboxes, setMailboxes] = useState<Mailbox[]>([]);
2325
const [isLoading, setIsLoading] = useState(false);
26+
const [filter, setFilter] = useState<'all' | 'favorites' | 'forwarding'>('all');
2427

2528
const isMailboxUser = user?.role === 'mailbox';
2629
const isAdmin = user?.role === 'admin';
@@ -29,9 +32,13 @@ export default function Dashboard() {
2932
if (isMailboxUser) return;
3033
setIsLoading(true);
3134
try {
32-
const data = await apiFetch<any>('/api/mailboxes?limit=10');
33-
if (data.success && Array.isArray(data.results)) {
34-
setMailboxes(data.results);
35+
let url = '/api/mailboxes?limit=20';
36+
if (filter === 'favorites') url += '&favorite=true';
37+
if (filter === 'forwarding') url += '&forward=true';
38+
39+
const data = await apiFetch<any>(url);
40+
if (data.success && Array.isArray(data.results || data.list)) {
41+
setMailboxes(data.results || data.list);
3542
}
3643
} catch (error) {
3744
console.error('Failed to fetch mailboxes');
@@ -42,7 +49,22 @@ export default function Dashboard() {
4249

4350
useEffect(() => {
4451
fetchMailboxes();
45-
}, [user]);
52+
}, [user, filter]);
53+
54+
const toggleFavorite = async (mailbox: Mailbox) => {
55+
try {
56+
await apiFetch('/api/mailbox/favorite', {
57+
method: 'POST',
58+
body: JSON.stringify({ mailbox_id: mailbox.id })
59+
});
60+
setMailboxes(mailboxes.map(m =>
61+
m.id === mailbox.id ? { ...m, is_favorite: !m.is_favorite } : m
62+
));
63+
toast.success(mailbox.is_favorite ? 'Removed from favorites' : 'Added to favorites');
64+
} catch (error) {
65+
toast.error('Failed to update favorite status');
66+
}
67+
};
4668

4769
const handleDeleteMailbox = async (id: number) => {
4870
if (!confirm('Delete this mailbox?')) return;
@@ -121,11 +143,37 @@ export default function Dashboard() {
121143
</div>
122144

123145
<Card className="col-span-3">
124-
<CardHeader>
125-
<CardTitle>Recent Mailboxes</CardTitle>
126-
<CardDescription>
127-
Manage your temporary mailboxes here.
128-
</CardDescription>
146+
<CardHeader className="flex flex-row items-center justify-between">
147+
<div>
148+
<CardTitle>Recent Mailboxes</CardTitle>
149+
<CardDescription>
150+
Manage your temporary mailboxes here.
151+
</CardDescription>
152+
</div>
153+
<div className="flex bg-muted p-1 rounded-md">
154+
<Button
155+
variant={filter === 'all' ? 'secondary' : 'ghost'}
156+
size="sm"
157+
onClick={() => setFilter('all')}
158+
>
159+
All
160+
</Button>
161+
<Button
162+
variant={filter === 'favorites' ? 'secondary' : 'ghost'}
163+
size="sm"
164+
onClick={() => setFilter('favorites')}
165+
className="gap-2"
166+
>
167+
<Star className="h-3 w-3" /> Favorites
168+
</Button>
169+
<Button
170+
variant={filter === 'forwarding' ? 'secondary' : 'ghost'}
171+
size="sm"
172+
onClick={() => setFilter('forwarding')}
173+
>
174+
Forwarding
175+
</Button>
176+
</div>
129177
</CardHeader>
130178
<CardContent>
131179
<div className="space-y-4">
@@ -137,11 +185,26 @@ export default function Dashboard() {
137185
<Mail className="h-4 w-4 text-primary" />
138186
</div>
139187
<div>
140-
<p className="font-medium">{mb.address}</p>
188+
<div className="flex items-center gap-2">
189+
<p className="font-medium">{mb.address}</p>
190+
{mb.forward_to && (
191+
<span className="text-[10px] bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300 px-1.5 py-0.5 rounded-full">
192+
Forwarding
193+
</span>
194+
)}
195+
</div>
141196
<p className="text-xs text-muted-foreground">Created {formatDistanceToNow(new Date(mb.created_at), { addSuffix: true })}</p>
142197
</div>
143198
</div>
144199
<div className="flex items-center gap-2">
200+
<Button
201+
variant="ghost"
202+
size="icon"
203+
onClick={() => toggleFavorite(mb)}
204+
className={mb.is_favorite ? "text-yellow-500 hover:text-yellow-600 hover:bg-yellow-100/50" : "text-muted-foreground hover:text-yellow-500"}
205+
>
206+
<Star className={`h-4 w-4 ${mb.is_favorite ? "fill-current" : ""}`} />
207+
</Button>
145208
<Button variant="ghost" size="icon" asChild>
146209
<Link to={`/mailbox?mailbox=${mb.address}`}><ExternalLink className="h-4 w-4" /></Link>
147210
</Button>

0 commit comments

Comments
 (0)