Initial commit: WpywMail 自建邮件系统:.NET 8 原生 SMTP/IMAP 服务端(DKIM 签名、SPF/DKIM/DMARC 入站校验、SQLite 存储、完整账号体系)、Node 服务端、WinUI 3 客户端与 Web 前端
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
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))
|
||||
Reference in New Issue
Block a user