Files

103 lines
3.8 KiB
JavaScript

import 'dotenv/config'
import crypto from 'node:crypto'
import express from 'express'
import { initStore, listMessages, getMessage, markRead, mailboxStats, saveMessage } from './store.mjs'
import { mailConfig, sendMail, startMailServers } from './mail.mjs'
const app = express()
const port = Number(process.env.WEB_PORT || 8787)
const sessions = new Map()
const account = (process.env.MAIL_USER || `admin@${mailConfig.domain}`).toLowerCase()
const password = process.env.MAIL_PASSWORD
if (!password) {
throw new Error('MAIL_PASSWORD is required. Copy server/.env.example to server/.env and set it before starting.')
}
app.use(express.json({ limit: '2mb' }))
app.use((req, res, next) => {
const allowedOrigin = process.env.CLIENT_ORIGIN || '*'
res.setHeader('Access-Control-Allow-Origin', allowedOrigin)
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, OPTIONS')
if (req.method === 'OPTIONS') return res.sendStatus(204)
next()
})
function auth(req, res, next) {
const token = req.headers.authorization?.replace(/^Bearer\s+/i, '')
if (!token || !sessions.has(token)) return res.status(401).json({ error: '登录已失效' })
req.user = sessions.get(token)
next()
}
app.get('/api/health', (_req, res) => {
res.json({ ok: true, service: 'wpyw.mail', hostname: mailConfig.hostname, domain: mailConfig.domain })
})
app.get('/api/config', auth, (_req, res) => {
res.json({
domain: mailConfig.domain,
hostname: mailConfig.hostname,
account,
protocols: {
smtp: Number(process.env.SMTP_PORT || 25),
submission: Number(process.env.SUBMISSION_PORT || 587),
api: port,
},
})
})
app.post('/api/login', (req, res) => {
const email = String(req.body?.email || '').toLowerCase().trim()
const pass = String(req.body?.password || '')
if (email !== account || pass !== password) return res.status(401).json({ error: '邮箱或密码不正确' })
const token = crypto.randomBytes(32).toString('hex')
sessions.set(token, { email: account, createdAt: Date.now() })
res.json({ token, user: { email: account, domain: mailConfig.domain } })
})
app.post('/api/logout', auth, (req, res) => {
const token = req.headers.authorization.replace(/^Bearer\s+/i, '')
sessions.delete(token)
res.json({ ok: true })
})
app.get('/api/me', auth, async (req, res) => {
res.json({ user: req.user, stats: await mailboxStats() })
})
app.get('/api/messages', auth, async (req, res) => {
const folder = ['inbox', 'sent', 'drafts', 'archive'].includes(req.query.folder) ? req.query.folder : 'inbox'
res.json({ messages: await listMessages(folder, String(req.query.q || '')) })
})
app.get('/api/messages/:id', auth, async (req, res) => {
const message = await getMessage(req.params.id)
if (!message) return res.status(404).json({ error: '邮件不存在' })
await markRead(req.params.id)
res.json({ message: { ...message, unread: false } })
})
app.post('/api/send', auth, async (req, res) => {
const { to, subject, text, html } = req.body || {}
if (!to || !subject || !text) return res.status(400).json({ error: '收件人、主题和正文不能为空' })
try {
const result = await sendMail({ to, subject, text, html })
await saveMessage({ folder: 'sent', from: account, to, subject, text, html, unread: false })
res.json({ ok: true, result })
} catch (error) {
console.error('[send]', error)
res.status(502).json({ error: `发信失败:${error.message}` })
}
})
app.use('/api', (_req, res) => res.status(404).json({ error: 'API endpoint not found' }))
await initStore()
app.listen(port, '0.0.0.0', () => console.log(`[web] wpyw.mail listening on ${port}`))
startMailServers()
process.on('SIGINT', () => process.exit(0))
process.on('SIGTERM', () => process.exit(0))