msmail / index.ts
github-actions[bot]
Update from GitHub Actions
9dbf793
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<any>) => {
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