feat: Add Webhook integration and update README with new features

This commit is contained in:
beilunyang
2024-12-18 02:38:04 +08:00
parent c69947ceae
commit 5d7a6d7bad
2 changed files with 145 additions and 43 deletions
+54
View File
@@ -27,6 +27,8 @@
![邮箱](https://pic.otaku.ren/20241209/AQADw8UxG9k1uVZ-.jpg "邮箱") ![邮箱](https://pic.otaku.ren/20241209/AQADw8UxG9k1uVZ-.jpg "邮箱")
![个人中心](https://pic.otaku.ren/20241217/AQAD9sQxG0g1EVd-.jpg "个人中心")
## 特性 ## 特性
- 🔒 **隐私保护**:保护您的真实邮箱地址,远离垃圾邮件和不必要的订阅 - 🔒 **隐私保护**:保护您的真实邮箱地址,远离垃圾邮件和不必要的订阅
@@ -38,6 +40,7 @@
- 📱 **PWA 支持**:支持 PWA 安装 - 📱 **PWA 支持**:支持 PWA 安装
- 💸 **免费自部署**:基于 Cloudflare 构建, 可实现免费自部署,无需任何费用 - 💸 **免费自部署**:基于 Cloudflare 构建, 可实现免费自部署,无需任何费用
- 🎉 **可爱的 UI**:简洁可爱萌萌哒 UI 界面 - 🎉 **可爱的 UI**:简洁可爱萌萌哒 UI 界面
- 🔔 **Webhook 通知**:支持通过 webhook 接收新邮件通知
## 技术栈 ## 技术栈
@@ -194,6 +197,57 @@ git push origin v1.0.0
- 在 Settings 中选择变量和机密 - 在 Settings 中选择变量和机密
- 添加 AUTH_GITHUB_ID, AUTH_GITHUB_SECRET, AUTH_SECRET - 添加 AUTH_GITHUB_ID, AUTH_GITHUB_SECRET, AUTH_SECRET
## Webhook 集成
当收到新邮件时,系统会向用户配置并且已启用的 Webhook URL 发送 POST 请求。
### 请求头
```http
Content-Type: application/json
X-Webhook-Event: new_message
```
### 请求体
```json
{
"emailId": "email-uuid",
"messageId": "message-uuid",
"fromAddress": "sender@example.com",
"subject": "邮件主题",
"content": "邮件文本内容",
"html": "邮件HTML内容",
"receivedAt": "2024-01-01T12:00:00.000Z",
"toAddress": "your-email@moemail.app"
}
```
### 配置说明
1. 点击个人头像,进入个人中心
2. 在个人中心启用 Webhook
3. 设置接收通知的 URL
4. 点击测试按钮验证配置
5. 保存配置后即可接收新邮件通知
### 测试
项目提供了一个简单的测试服务器, 可以通过如下命令运行:
```bash
pnpm webhook-test-server
```
测试服务器会在本地启动一个 HTTP 服务器,监听 3001 端口(http://localhost:3001),并打印收到的 Webhook 消息详情。
如果需要进行外网测试,可以通过 Cloudflare Tunnel 将服务暴露到外网:
```bash
pnpx cloudflared tunnel --url http://localhost:3001
```
### 注意事项
- Webhook 接口应在 10 秒内响应
- 非 2xx 响应码会触发重试
## 贡献 ## 贡献
欢迎提交 Pull Request 或者 Issue来帮助改进这个项目 欢迎提交 Pull Request 或者 Issue来帮助改进这个项目
+91 -43
View File
@@ -7,7 +7,7 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch" import { Switch } from "@/components/ui/switch"
import { useToast } from "@/components/ui/use-toast" import { useToast } from "@/components/ui/use-toast"
import { Loader2, Send } from "lucide-react" import { Loader2, Send, ChevronDown, ChevronUp } from "lucide-react"
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -20,6 +20,8 @@ export function WebhookConfig() {
const [url, setUrl] = useState("") const [url, setUrl] = useState("")
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [testing, setTesting] = useState(false) const [testing, setTesting] = useState(false)
const [showDocs, setShowDocs] = useState(false)
const [initialLoading, setInitialLoading] = useState(true)
const { toast } = useToast() const { toast } = useToast()
useEffect(() => { useEffect(() => {
@@ -30,8 +32,17 @@ export function WebhookConfig() {
setUrl(data.url) setUrl(data.url)
}) })
.catch(console.error) .catch(console.error)
.finally(() => setInitialLoading(false))
}, []) }, [])
if (initialLoading) {
return (
<div className="flex justify-center py-8">
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
</div>
)
}
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (!url) return if (!url) return
@@ -105,49 +116,86 @@ export function WebhookConfig() {
</div> </div>
{enabled && ( {enabled && (
<div className="space-y-2"> <div className="space-y-4">
<Label htmlFor="webhook-url">Webhook URL</Label> <div className="space-y-2">
<div className="flex gap-2"> <Label htmlFor="webhook-url">Webhook URL</Label>
<Input <div className="flex gap-2">
id="webhook-url" <Input
placeholder="https://example.com/webhook" id="webhook-url"
value={url} placeholder="https://example.com/webhook"
onChange={(e) => setUrl(e.target.value)} value={url}
type="url" onChange={(e) => setUrl(e.target.value)}
required type="url"
/> required
<Button type="submit" disabled={loading} className="w-20"> />
{loading ? ( <Button type="submit" disabled={loading} className="w-20">
<Loader2 className="w-4 h-4 animate-spin" /> {loading ? (
) : ( <Loader2 className="w-4 h-4 animate-spin" />
"保存" ) : (
)} "保存"
</Button> )}
<TooltipProvider> </Button>
<Tooltip> <TooltipProvider>
<TooltipTrigger asChild> <Tooltip>
<Button <TooltipTrigger asChild>
type="button" <Button
variant="outline" type="button"
onClick={handleTest} variant="outline"
disabled={testing || !url} onClick={handleTest}
> disabled={testing || !url}
{testing ? ( >
<Loader2 className="w-4 h-4 animate-spin" /> {testing ? (
) : ( <Loader2 className="w-4 h-4 animate-spin" />
<Send className="w-4 h-4" /> ) : (
)} <Send className="w-4 h-4" />
</Button> )}
</TooltipTrigger> </Button>
<TooltipContent> </TooltipTrigger>
<p> Webhook</p> <TooltipContent>
</TooltipContent> <p> Webhook</p>
</Tooltip> </TooltipContent>
</TooltipProvider> </Tooltip>
</TooltipProvider>
</div>
<p className="text-xs text-muted-foreground">
URL POST ,
</p>
</div>
<div className="space-y-2">
<button
type="button"
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
onClick={() => setShowDocs(!showDocs)}
>
{showDocs ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
{showDocs && (
<div className="rounded-md bg-muted p-4 text-sm space-y-3">
<p> URL POST :</p>
<pre className="bg-background p-2 rounded text-xs">
Content-Type: application/json{'\n'}
X-Webhook-Event: new_message
</pre>
<p>:</p>
<pre className="bg-background p-2 rounded text-xs overflow-auto">
{`{
"emailId": "email-uuid",
"messageId": "message-uuid",
"fromAddress": "sender@example.com",
"subject": "邮件主题",
"content": "邮件文本内容",
"html": "邮件HTML内容",
"receivedAt": "2024-01-01T12:00:00.000Z",
"toAddress": "your-email@${window.location.host}"
}`}
</pre>
</div>
)}
</div> </div>
<p className="text-xs text-muted-foreground">
URL POST ,
</p>
</div> </div>
)} )}
</form> </form>