mirror of
https://github.com/beilunyang/moemail.git
synced 2026-06-11 02:20:11 +08:00
feat(sharing): add email and message sharing functionality
This commit is contained in:
@@ -40,8 +40,14 @@ export async function handleApiKeyAuth(apiKey: string, pathname: string) {
|
||||
)
|
||||
}
|
||||
|
||||
const response = NextResponse.next()
|
||||
response.headers.set("X-User-Id", user.id)
|
||||
const requestHeaders = new Headers(await headers())
|
||||
requestHeaders.set("X-User-Id", user.id)
|
||||
|
||||
const response = NextResponse.next({
|
||||
request: {
|
||||
headers: requestHeaders
|
||||
}
|
||||
})
|
||||
return response
|
||||
}
|
||||
|
||||
|
||||
@@ -114,6 +114,36 @@ export const apiKeys = sqliteTable('api_keys', {
|
||||
nameUserIdUnique: uniqueIndex('name_user_id_unique').on(table.name, table.userId)
|
||||
}));
|
||||
|
||||
export const emailShares = sqliteTable('email_share', {
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
emailId: text('email_id')
|
||||
.notNull()
|
||||
.references(() => emails.id, { onDelete: "cascade" }),
|
||||
token: text('token').notNull().unique(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }),
|
||||
}, (table) => ({
|
||||
emailIdIdx: index('email_share_email_id_idx').on(table.emailId),
|
||||
tokenIdx: index('email_share_token_idx').on(table.token),
|
||||
}));
|
||||
|
||||
export const messageShares = sqliteTable('message_share', {
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
messageId: text('message_id')
|
||||
.notNull()
|
||||
.references(() => messages.id, { onDelete: "cascade" }),
|
||||
token: text('token').notNull().unique(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }),
|
||||
}, (table) => ({
|
||||
messageIdIdx: index('message_share_message_id_idx').on(table.messageId),
|
||||
tokenIdx: index('message_share_token_idx').on(table.token),
|
||||
}));
|
||||
|
||||
|
||||
|
||||
export const apiKeysRelations = relations(apiKeys, ({ one }) => ({
|
||||
@@ -141,4 +171,18 @@ export const usersRelations = relations(users, ({ many }) => ({
|
||||
|
||||
export const rolesRelations = relations(roles, ({ many }) => ({
|
||||
userRoles: many(userRoles),
|
||||
}));
|
||||
|
||||
export const emailSharesRelations = relations(emailShares, ({ one }) => ({
|
||||
email: one(emails, {
|
||||
fields: [emailShares.emailId],
|
||||
references: [emails.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const messageSharesRelations = relations(messageShares, ({ one }) => ({
|
||||
message: one(messages, {
|
||||
fields: [messageShares.messageId],
|
||||
references: [messages.id],
|
||||
}),
|
||||
}));
|
||||
192
app/lib/shared-data.ts
Normal file
192
app/lib/shared-data.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { createDb } from "@/lib/db"
|
||||
import { emailShares, messageShares, messages, emails } from "@/lib/schema"
|
||||
import { eq, desc, and, or, ne, isNull } from "drizzle-orm"
|
||||
|
||||
export interface SharedEmail {
|
||||
id: string
|
||||
address: string
|
||||
createdAt: Date
|
||||
expiresAt: Date
|
||||
}
|
||||
|
||||
export interface SharedMessage {
|
||||
id: string
|
||||
from_address?: string
|
||||
to_address?: string
|
||||
subject: string
|
||||
content?: string
|
||||
html?: string
|
||||
received_at?: Date
|
||||
sent_at?: Date
|
||||
expiresAt?: Date
|
||||
emailAddress?: string
|
||||
emailExpiresAt?: Date
|
||||
}
|
||||
|
||||
export async function getSharedEmail(token: string): Promise<SharedEmail | null> {
|
||||
const db = createDb()
|
||||
|
||||
try {
|
||||
const share = await db.query.emailShares.findFirst({
|
||||
where: eq(emailShares.token, token),
|
||||
with: {
|
||||
email: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!share) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 检查分享是否过期
|
||||
if (share.expiresAt && share.expiresAt < new Date()) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 检查邮箱是否过期
|
||||
if (share.email.expiresAt < new Date()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
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 null
|
||||
}
|
||||
}
|
||||
|
||||
export interface SharedMessagesResult {
|
||||
messages: SharedMessage[]
|
||||
nextCursor: string | null
|
||||
total: number
|
||||
}
|
||||
|
||||
export async function getSharedEmailMessages(token: string, limit = 20): Promise<SharedMessagesResult> {
|
||||
const db = createDb()
|
||||
|
||||
try {
|
||||
const share = await db.query.emailShares.findFirst({
|
||||
where: eq(emailShares.token, token),
|
||||
with: {
|
||||
email: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!share) {
|
||||
return { messages: [], nextCursor: null, total: 0 }
|
||||
}
|
||||
|
||||
// 检查分享是否过期
|
||||
if (share.expiresAt && share.expiresAt < new Date()) {
|
||||
return { messages: [], nextCursor: null, total: 0 }
|
||||
}
|
||||
|
||||
// 只显示接收的邮件,不显示发送的邮件
|
||||
const baseConditions = and(
|
||||
eq(messages.emailId, share.emailId),
|
||||
or(
|
||||
ne(messages.type, "sent"),
|
||||
isNull(messages.type)
|
||||
)
|
||||
)
|
||||
|
||||
// 获取消息总数(只统计接收的邮件)
|
||||
const { sql } = await import("drizzle-orm")
|
||||
const totalResult = await db.select({ count: sql<number>`count(*)` })
|
||||
.from(messages)
|
||||
.where(baseConditions)
|
||||
const totalCount = Number(totalResult[0].count)
|
||||
|
||||
// 获取邮箱的消息列表(多获取一条用于判断是否有更多)
|
||||
const messageList = await db.query.messages.findMany({
|
||||
where: baseConditions,
|
||||
orderBy: [desc(messages.receivedAt), desc(messages.id)],
|
||||
limit: limit + 1
|
||||
})
|
||||
|
||||
const hasMore = messageList.length > limit
|
||||
const results = hasMore ? messageList.slice(0, limit) : messageList
|
||||
|
||||
// 生成下一页的cursor
|
||||
let nextCursor: string | null = null
|
||||
if (hasMore) {
|
||||
const { encodeCursor } = await import("@/lib/cursor")
|
||||
const lastMessage = results[results.length - 1]
|
||||
nextCursor = encodeCursor(
|
||||
lastMessage.receivedAt.getTime(),
|
||||
lastMessage.id
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
messages: results.map(msg => ({
|
||||
id: msg.id,
|
||||
from_address: msg.fromAddress ?? undefined,
|
||||
to_address: msg.toAddress ?? undefined,
|
||||
subject: msg.subject,
|
||||
received_at: msg.receivedAt,
|
||||
sent_at: msg.sentAt
|
||||
})),
|
||||
nextCursor,
|
||||
total: totalCount
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch shared email messages:", error)
|
||||
return { messages: [], nextCursor: null, total: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSharedMessage(token: string): Promise<SharedMessage | null> {
|
||||
const db = createDb()
|
||||
|
||||
try {
|
||||
const share = await db.query.messageShares.findFirst({
|
||||
where: eq(messageShares.token, token)
|
||||
})
|
||||
|
||||
if (!share) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 检查分享是否过期
|
||||
if (share.expiresAt && share.expiresAt < new Date()) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 获取消息详情
|
||||
const message = await db.query.messages.findFirst({
|
||||
where: eq(messages.id, share.messageId)
|
||||
})
|
||||
|
||||
if (!message) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 获取邮箱信息
|
||||
const email = await db.query.emails.findFirst({
|
||||
where: eq(emails.id, message.emailId)
|
||||
})
|
||||
|
||||
return {
|
||||
id: message.id,
|
||||
from_address: message.fromAddress ?? undefined,
|
||||
to_address: message.toAddress ?? undefined,
|
||||
subject: message.subject,
|
||||
content: message.content ?? undefined,
|
||||
html: message.html ?? undefined,
|
||||
received_at: message.receivedAt,
|
||||
sent_at: message.sentAt,
|
||||
expiresAt: share.expiresAt ?? undefined,
|
||||
emailAddress: email?.address,
|
||||
emailExpiresAt: email?.expiresAt
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch shared message:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user