import { Context, Hono } from 'hono' import * as dotenv from 'dotenv' import { cors } from "hono/cors"; import { compress } from "hono/compress"; import { prettyJSON } from "hono/pretty-json"; import { trimTrailingSlash } from "hono/trailing-slash"; import { serve } from '@hono/node-server' import { createStorage } from "unstorage"; import cloudflareKVHTTPDriver from "unstorage/drivers/cloudflare-kv-http"; import { serveStatic } from '@hono/node-server/serve-static' import path from 'path' import { fileURLToPath } from 'url' import { dirname } from 'path' import { getTenantAccessToken, sendFeishuMessageText } from './functions/utils/feishu.js' import { get_access_token, getEmails } from './functions/utils/mail.js' // 导入所有路由处理函数 import { onRequest as handleAccount } from './functions/api/account.js' import { onRequest as handleLogin } from './functions/api/login.js' import { onRequest as handleSetting } from './functions/api/setting.js' import { onRequest as handleMailAuth } from './functions/api/mail/auth.js' import { onRequest as handleMailCallback } from './functions/api/mail/callback.js' import { onRequest as handleMailAll } from './functions/api/mail/all.js' import { onRequest as handleMailNew } from './functions/api/mail/new.js' import { onRequest as handleMailSend } from './functions/api/mail/send.js' import { onRequest as handleBatch } from './functions/api/mail/batch.js' import { onRequest as handleMailStatus } from './functions/api/mail/status.js' dotenv.config({ path: ['.env', '.env.local'], override: true }); const isDev = process.env.NODE_ENV === 'development' const app = new Hono<{ Bindings: Env }>() app.use(compress()); app.use(prettyJSON()); app.use(trimTrailingSlash()); app.use('*', cors({ origin: '*', allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowHeaders: ['Content-Type', 'Authorization'], exposeHeaders: ['Content-Length'], credentials: true, })); const storage = createStorage({ driver: cloudflareKVHTTPDriver({ accountId: process.env.CLOUDFLARE_ACCOUNT_ID || "", namespaceId: process.env.CLOUDFLARE_NAMESPACE_ID || "", apiToken: process.env.CLOUDFLARE_API_TOKEN || "", }), }); const kv: KVNamespace = { get: async (key: string) => { const value = await storage.getItemRaw(key); return value as string; }, put: async (key: string, value: string) => { await storage.setItem(key, value); }, delete: async (key: string) => { await storage.removeItem(key); } }; app.use('*', async (c, next) => { c.env.KV = kv; await next() }) const scriptPath = fileURLToPath(import.meta.url) const scriptDir = dirname(scriptPath) const rootDir = isDev ? dirname(scriptPath) : dirname(scriptDir) const currentDir = process.cwd(); let staticPath = path.relative(currentDir, rootDir); if (!isDev) { staticPath = path.relative(currentDir, path.join(rootDir, "dist")) } console.log('Script dir:', scriptDir) console.log('Root dir:', rootDir) console.log('Current dir:', currentDir); console.log('Relative path for static files:', staticPath || '.'); const createContext = (c: Context) => { const eventContext: RouteContext = { request: c.req.raw, functionPath: c.req.path, waitUntil: (promise: Promise) => { if (c.executionCtx?.waitUntil) { c.executionCtx.waitUntil(promise); } }, passThroughOnException: () => { if (c.executionCtx?.passThroughOnException) { c.executionCtx.passThroughOnException(); } }, next: async (input?: Request | string, init?: RequestInit) => { if (typeof input === 'string') { return fetch(input, init); } else if (input instanceof Request) { return fetch(input); } return new Response('Not Found', { status: 404 }); }, env: { ...c.env, ASSETS: { fetch: fetch.bind(globalThis) } }, params: c.req.param(), // 可以从 c.get() 获取数据,或者传入空对象 data: c.get('data') || {} }; return eventContext; } app.all('/api/*', async (c) => { try { const context = createContext(c); const path = c.req.path; // 根据路径匹配对应的处理函数 let response: Response; switch (path) { case '/api/account': response = await handleAccount(context); break; case '/api/login': response = await handleLogin(context); break; case '/api/setting': response = await handleSetting(context); break; case '/api/mail/auth': response = await handleMailAuth(context); break; case '/api/mail/callback': response = await handleMailCallback(context); break; case '/api/mail/all': response = await handleMailAll(context); break; case '/api/mail/new': response = await handleMailNew(context); break; case '/api/mail/send': response = await handleMailSend(context); break; case '/api/mail/status': response = await handleMailStatus(context); break; case '/api/mail/batch': response = await handleBatch(context); break; default: return c.json({ error: 'Route not found' }, 404); } return response; } catch (error) { return c.json({ error: (error as Error).message }, 500); } }) // 中间件配置 app.get('/*', serveStatic({ root: staticPath, rewriteRequestPath: (path) => { return path === '/' ? '/index.html' : path; }, onFound: async (path, c) => { console.log('Found:', path) }, onNotFound: async (path, c) => { console.log('Not Found:', path) } })) // 邮箱监控功能 async function checkEmailsForNewMessages(env: Env) { try { // 获取设置 const KV_KEY = "settings"; const settingsStr = await env.KV.get(KV_KEY); if (!settingsStr) { console.log('未找到设置信息'); return; } const settings = JSON.parse(settingsStr); if (!settings.emailMonitor || !Array.isArray(settings.emailMonitor) || settings.emailMonitor.length === 0) { console.log('未配置监控邮箱'); return; } // 获取飞书访问令牌 const feishuConfig = settings.feishu; if (!feishuConfig || !feishuConfig.app_id || !feishuConfig.app_secret || !feishuConfig.receive_id) { console.log('飞书配置不完整'); return; } const tokenResponse = await getTenantAccessToken(feishuConfig.app_id, feishuConfig.app_secret); if (tokenResponse.code !== 0) { console.log('获取飞书访问令牌失败:', tokenResponse.msg); return; } const feishuToken = tokenResponse.tenant_access_token; const now = new Date(); const currentTime = now.toISOString(); // 遍历所有监控邮箱 for (const monitor of settings.emailMonitor) { if (!monitor.email) continue; // 检查是否到了检查间隔 let shouldCheck = false; if (monitor.lastCheckTime) { const lastCheck = new Date(monitor.lastCheckTime); const diffMinutes = Math.floor((now.getTime() - lastCheck.getTime()) / (1000 * 60)); shouldCheck = diffMinutes >= (monitor.checkInterval || 60); } else { shouldCheck = true; // 如果从未检查过,则立即检查 } if (shouldCheck) { try { // 获取邮箱的访问令牌 const tokenInfoStr = await env.KV.get(`refresh_token_${monitor.email}`); if (!tokenInfoStr) { console.log(`邮箱 ${monitor.email} 未授权`); continue; } const tokenInfo = JSON.parse(tokenInfoStr); const access_token = await get_access_token(tokenInfo, env.ENTRA_CLIENT_ID, env.ENTRA_CLIENT_SECRET); // 获取最新邮件 const emails = await getEmails(access_token, 1); if (emails.length === 0) { console.log(`邮箱 ${monitor.email} 没有邮件`); continue; } const latestEmail = emails[0]; // 更新检查时间 monitor.lastCheckTime = currentTime; // 获取最新邮件的时间戳 const latestEmailTime = new Date(latestEmail.date.received || latestEmail.date.created || latestEmail.date.sent || latestEmail.date.modified).getTime(); const lastEmailTime = monitor.lastEmailTimestamp ? new Date(monitor.lastEmailTimestamp).getTime() : 0; //console.log(JSON.stringify(latestEmail),latestEmailTime,lastEmailTime) // 如果最新邮件的时间戳大于用户设置的时间戳,且ID不同,则认为有新邮件 if (latestEmailTime > lastEmailTime && (!monitor.lastEmailId || monitor.lastEmailId !== latestEmail.id)) { // 只更新最新邮件ID,不更新lastEmailTimestamp(该值由用户手动设置) monitor.lastEmailId = latestEmail.id; // 发送飞书通知 const notificationContent = `邮箱 ${monitor.email} 收到新邮件\n主题: ${latestEmail.subject}\n发件人: ${latestEmail.from}\n时间: ${latestEmail.date.received}\n预览: ${latestEmail.preview || '无预览内容'}`; await sendFeishuMessageText(feishuToken, feishuConfig.receive_id, notificationContent); console.log(`已发送邮箱 ${monitor.email} 的新邮件通知`); } else { console.log(`邮箱 ${monitor.email} 没有新邮件`); } } catch (error) { console.error(`检查邮箱 ${monitor.email} 失败:`, error); } } } // 保存更新后的设置 await env.KV.put(KV_KEY, JSON.stringify(settings)); console.log('邮箱监控完成'); } catch (error) { console.error('邮箱监控出错:', error); } } // 启动服务器 const port = parseInt(process.env.PORT || '8788') serve({ fetch: (request: Request, env) => app.fetch(request, { ...env, ...process.env }), port }, () => { console.log(`Server running at http://localhost:${port}`) // 启动定时监控邮箱 setInterval(() => { const envWithKV = { ...process.env, KV: kv }; checkEmailsForNewMessages(envWithKV as unknown as Env); }, 60000); // 每分钟检查一次 }) export default app