feat(sharing): add email and message sharing functionality

This commit is contained in:
beilunyang
2025-10-18 20:08:42 +08:00
parent 47d555eaf5
commit dbe8c42b11
45 changed files with 5669 additions and 38 deletions

View File

@@ -0,0 +1,55 @@
import { createDb } from "@/lib/db"
import { messageShares, messages, emails } from "@/lib/schema"
import { eq, and } from "drizzle-orm"
import { NextResponse } from "next/server"
import { getUserId } from "@/lib/apiKey"
export const runtime = "edge"
// 删除消息分享链接
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string; messageId: string; shareId: string }> }
) {
const userId = await getUserId()
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const { id: emailId, messageId, shareId } = await params
const db = createDb()
try {
// 验证邮箱所有权
const email = await db.query.emails.findFirst({
where: and(eq(emails.id, emailId), eq(emails.userId, userId))
})
if (!email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 403 })
}
// 获取消息并验证
const message = await db.query.messages.findFirst({
where: and(eq(messages.id, messageId), eq(messages.emailId, emailId))
})
if (!message) {
return NextResponse.json({ error: "Message not found" }, { status: 404 })
}
// 删除分享记录
await db.delete(messageShares).where(
and(eq(messageShares.id, shareId), eq(messageShares.messageId, messageId))
)
return NextResponse.json({ success: true })
} catch (error) {
console.error("Failed to delete message share:", error)
return NextResponse.json(
{ error: "Failed to delete share" },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,119 @@
import { createDb } from "@/lib/db"
import { messageShares, messages, emails } from "@/lib/schema"
import { eq, and } from "drizzle-orm"
import { NextResponse } from "next/server"
import { getUserId } from "@/lib/apiKey"
import { nanoid } from "nanoid"
export const runtime = "edge"
// 获取消息的所有分享链接
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string; messageId: string }> }
) {
const userId = await getUserId()
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const { id: emailId, messageId } = await params
const db = createDb()
try {
// 验证邮箱所有权
const email = await db.query.emails.findFirst({
where: and(eq(emails.id, emailId), eq(emails.userId, userId))
})
if (!email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 403 })
}
// 获取消息
const message = await db.query.messages.findFirst({
where: and(eq(messages.id, messageId), eq(messages.emailId, emailId))
})
if (!message) {
return NextResponse.json({ error: "Message not found" }, { status: 404 })
}
// 获取该消息的所有分享链接
const shares = await db.query.messageShares.findMany({
where: eq(messageShares.messageId, messageId),
orderBy: (messageShares, { desc }) => [desc(messageShares.createdAt)]
})
return NextResponse.json({ shares, total: shares.length })
} catch (error) {
console.error("Failed to fetch message shares:", error)
return NextResponse.json(
{ error: "Failed to fetch shares" },
{ status: 500 }
)
}
}
// 创建新的消息分享链接
export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string; messageId: string }> }
) {
const userId = await getUserId()
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const { id: emailId, messageId } = await params
const db = createDb()
try {
// 验证邮箱所有权
const email = await db.query.emails.findFirst({
where: and(eq(emails.id, emailId), eq(emails.userId, userId))
})
if (!email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 403 })
}
// 获取消息并验证
const message = await db.query.messages.findFirst({
where: and(eq(messages.id, messageId), eq(messages.emailId, emailId))
})
if (!message) {
return NextResponse.json({ error: "Message not found" }, { status: 404 })
}
// 解析请求体
const body = await request.json() as { expiresIn: number }
const { expiresIn } = body // expiresIn 单位为毫秒0表示永久
// 生成简短的分享token (16个字符)
const token = nanoid(16)
// 计算过期时间
let expiresAt = null
if (expiresIn && expiresIn > 0) {
expiresAt = new Date(Date.now() + expiresIn)
}
// 创建分享记录
const [share] = await db.insert(messageShares).values({
messageId,
token,
expiresAt
}).returning()
return NextResponse.json(share, { status: 201 })
} catch (error) {
console.error("Failed to create message share:", error)
return NextResponse.json(
{ error: "Failed to create share" },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,46 @@
import { createDb } from "@/lib/db"
import { emailShares, emails } from "@/lib/schema"
import { eq, and } from "drizzle-orm"
import { NextResponse } from "next/server"
import { getUserId } from "@/lib/apiKey"
export const runtime = "edge"
// 删除分享链接
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string; shareId: string }> }
) {
const userId = await getUserId()
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const { id: emailId, shareId } = await params
const db = createDb()
try {
// 验证邮箱所有权
const email = await db.query.emails.findFirst({
where: and(eq(emails.id, emailId), eq(emails.userId, userId))
})
if (!email) {
return NextResponse.json({ error: "Email not found" }, { status: 404 })
}
// 删除分享记录
await db.delete(emailShares).where(
and(eq(emailShares.id, shareId), eq(emailShares.emailId, emailId))
)
return NextResponse.json({ success: true })
} catch (error) {
console.error("Failed to delete email share:", error)
return NextResponse.json(
{ error: "Failed to delete share" },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,101 @@
import { createDb } from "@/lib/db"
import { emailShares, emails } from "@/lib/schema"
import { eq, and } from "drizzle-orm"
import { NextResponse } from "next/server"
import { getUserId } from "@/lib/apiKey"
import { nanoid } from "nanoid"
export const runtime = "edge"
// 获取邮箱的所有分享链接
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const userId = await getUserId()
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const { id: emailId } = await params
const db = createDb()
try {
// 验证邮箱所有权
const email = await db.query.emails.findFirst({
where: and(eq(emails.id, emailId), eq(emails.userId, userId))
})
if (!email) {
return NextResponse.json({ error: "Email not found" }, { status: 404 })
}
// 获取该邮箱的所有分享链接
const shares = await db.query.emailShares.findMany({
where: eq(emailShares.emailId, emailId),
orderBy: (emailShares, { desc }) => [desc(emailShares.createdAt)]
})
return NextResponse.json({ shares, total: shares.length })
} catch (error) {
console.error("Failed to fetch email shares:", error)
return NextResponse.json(
{ error: "Failed to fetch shares" },
{ status: 500 }
)
}
}
// 创建新的分享链接
export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const userId = await getUserId()
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const { id: emailId } = await params
const db = createDb()
try {
// 验证邮箱所有权
const email = await db.query.emails.findFirst({
where: and(eq(emails.id, emailId), eq(emails.userId, userId))
})
if (!email) {
return NextResponse.json({ error: "Email not found" }, { status: 404 })
}
// 解析请求体
const body = await request.json() as { expiresIn: number }
const { expiresIn } = body // expiresIn 单位为毫秒0表示永久
// 生成简短的分享token (16个字符)
const token = nanoid(16)
// 计算过期时间
let expiresAt = null
if (expiresIn && expiresIn > 0) {
expiresAt = new Date(Date.now() + expiresIn)
}
// 创建分享记录
const [share] = await db.insert(emailShares).values({
emailId,
token,
expiresAt
}).returning()
return NextResponse.json(share, { status: 201 })
} catch (error) {
console.error("Failed to create email share:", error)
return NextResponse.json(
{ error: "Failed to create share" },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,83 @@
import { createDb } from "@/lib/db"
import { emailShares, messages } from "@/lib/schema"
import { eq, and } from "drizzle-orm"
import { NextResponse } from "next/server"
export const runtime = "edge"
// 通过分享token获取消息详情
export async function GET(
request: Request,
{ params }: { params: Promise<{ token: string; messageId: string }> }
) {
const { token, messageId } = await params
const db = createDb()
try {
// 验证分享token
const share = await db.query.emailShares.findFirst({
where: eq(emailShares.token, token),
with: {
email: true
}
})
if (!share) {
return NextResponse.json(
{ error: "Share link not found or expired" },
{ status: 404 }
)
}
// 检查分享是否过期
if (share.expiresAt && share.expiresAt < new Date()) {
return NextResponse.json(
{ error: "Share link has expired" },
{ status: 410 }
)
}
// 检查邮箱是否过期
if (share.email.expiresAt < new Date()) {
return NextResponse.json(
{ error: "Email has expired" },
{ status: 410 }
)
}
// 获取消息详情
const message = await db.query.messages.findFirst({
where: and(
eq(messages.id, messageId),
eq(messages.emailId, share.email.id)
)
})
if (!message) {
return NextResponse.json(
{ error: "Message not found" },
{ status: 404 }
)
}
return NextResponse.json({
message: {
id: message.id,
from_address: message.fromAddress,
to_address: message.toAddress,
subject: message.subject,
content: message.content,
html: message.html,
received_at: message.receivedAt,
sent_at: message.sentAt
}
})
} catch (error) {
console.error("Failed to fetch shared message:", error)
return NextResponse.json(
{ error: "Failed to fetch message" },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,124 @@
import { createDb } from "@/lib/db"
import { emailShares, messages } from "@/lib/schema"
import { eq, and, lt, or, sql, ne, isNull } from "drizzle-orm"
import { NextResponse } from "next/server"
import { encodeCursor, decodeCursor } from "@/lib/cursor"
export const runtime = "edge"
const PAGE_SIZE = 20
// 通过分享token获取邮箱的消息列表
export async function GET(
request: Request,
{ params }: { params: Promise<{ token: string }> }
) {
const { token } = await params
const db = createDb()
const { searchParams } = new URL(request.url)
const cursor = searchParams.get('cursor')
try {
// 验证分享token
const share = await db.query.emailShares.findFirst({
where: eq(emailShares.token, token),
with: {
email: true
}
})
if (!share) {
return NextResponse.json(
{ error: "Share link not found or expired" },
{ status: 404 }
)
}
// 检查分享是否过期
if (share.expiresAt && share.expiresAt < new Date()) {
return NextResponse.json(
{ error: "Share link has expired" },
{ status: 410 }
)
}
// 检查邮箱是否过期
if (share.email.expiresAt < new Date()) {
return NextResponse.json(
{ error: "Email has expired" },
{ status: 410 }
)
}
const emailId = share.email.id
// 只显示接收的邮件,不显示发送的邮件
const baseConditions = and(
eq(messages.emailId, emailId),
or(
ne(messages.type, "sent"),
isNull(messages.type)
)
)
// 获取消息总数(只统计接收的邮件)
const totalResult = await db.select({ count: sql<number>`count(*)` })
.from(messages)
.where(baseConditions)
const totalCount = Number(totalResult[0].count)
const conditions = [baseConditions]
if (cursor) {
const { timestamp, id } = decodeCursor(cursor)
const cursorCondition = or(
lt(messages.receivedAt, new Date(timestamp)),
and(
eq(messages.receivedAt, new Date(timestamp)),
lt(messages.id, id)
)
)
if (cursorCondition) {
conditions.push(cursorCondition)
}
}
const results = await db.query.messages.findMany({
where: and(...conditions),
orderBy: (messages, { desc }) => [
desc(messages.receivedAt),
desc(messages.id)
],
limit: PAGE_SIZE + 1
})
const hasMore = results.length > PAGE_SIZE
const nextCursor = hasMore
? encodeCursor(
results[PAGE_SIZE - 1].receivedAt.getTime(),
results[PAGE_SIZE - 1].id
)
: null
const messageList = hasMore ? results.slice(0, PAGE_SIZE) : results
return NextResponse.json({
messages: messageList.map(msg => ({
id: msg.id,
from_address: msg.fromAddress,
to_address: msg.toAddress,
subject: msg.subject,
received_at: msg.receivedAt,
sent_at: msg.sentAt
})),
nextCursor,
total: totalCount
})
} catch (error) {
console.error("Failed to fetch shared messages:", error)
return NextResponse.json(
{ error: "Failed to fetch messages" },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,64 @@
import { createDb } from "@/lib/db"
import { emailShares } from "@/lib/schema"
import { eq } from "drizzle-orm"
import { NextResponse } from "next/server"
export const runtime = "edge"
// 通过分享token获取邮箱信息
export async function GET(
request: Request,
{ params }: { params: Promise<{ token: string }> }
) {
const { token } = await params
const db = createDb()
try {
// 查找分享记录
const share = await db.query.emailShares.findFirst({
where: eq(emailShares.token, token),
with: {
email: true
}
})
if (!share) {
return NextResponse.json(
{ error: "Share link not found or expired" },
{ status: 404 }
)
}
// 检查分享是否过期
if (share.expiresAt && share.expiresAt < new Date()) {
return NextResponse.json(
{ error: "Share link has expired" },
{ status: 410 }
)
}
// 检查邮箱是否过期
if (share.email.expiresAt < new Date()) {
return NextResponse.json(
{ error: "Email has expired" },
{ status: 410 }
)
}
return NextResponse.json({
email: {
id: share.email.id,
address: share.email.address,
createdAt: share.email.createdAt,
expiresAt: share.email.expiresAt
}
})
} catch (error) {
console.error("Failed to fetch shared email:", error)
return NextResponse.json(
{ error: "Failed to fetch shared email" },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,69 @@
import { createDb } from "@/lib/db"
import { messageShares, messages } from "@/lib/schema"
import { eq } from "drizzle-orm"
import { NextResponse } from "next/server"
export const runtime = "edge"
// 通过分享token获取消息详情
export async function GET(
request: Request,
{ params }: { params: Promise<{ token: string }> }
) {
const { token } = await params
const db = createDb()
try {
// 验证分享token
const share = await db.query.messageShares.findFirst({
where: eq(messageShares.token, token)
})
if (!share) {
return NextResponse.json(
{ error: "Share link not found or disabled" },
{ status: 404 }
)
}
// 检查分享是否过期
if (share.expiresAt && share.expiresAt < new Date()) {
return NextResponse.json(
{ error: "Share link has expired" },
{ status: 410 }
)
}
// 获取消息详情
const message = await db.query.messages.findFirst({
where: eq(messages.id, share.messageId)
})
if (!message) {
return NextResponse.json(
{ error: "Message not found" },
{ status: 404 }
)
}
return NextResponse.json({
message: {
id: message.id,
from_address: message.fromAddress,
to_address: message.toAddress,
subject: message.subject,
content: message.content,
html: message.html,
received_at: message.receivedAt,
sent_at: message.sentAt
}
})
} catch (error) {
console.error("Failed to fetch shared message:", error)
return NextResponse.json(
{ error: "Failed to fetch message" },
{ status: 500 }
)
}
}