103 lines
3.8 KiB
JavaScript
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))
|