Initial commit: WpywMail 自建邮件系统:.NET 8 原生 SMTP/IMAP 服务端(DKIM 签名、SPF/DKIM/DMARC 入站校验、SQLite 存储、完整账号体系)、Node 服务端、WinUI 3 客户端与 Web 前端
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import crypto from 'node:crypto'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const serverDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const dataDir = process.env.MAIL_DATA_DIR || path.join(serverDir, 'data')
|
||||
const messagesFile = path.join(dataDir, 'messages.json')
|
||||
|
||||
let writeQueue = Promise.resolve()
|
||||
|
||||
async function ensureStore() {
|
||||
await fs.mkdir(dataDir, { recursive: true })
|
||||
try {
|
||||
await fs.access(messagesFile)
|
||||
} catch {
|
||||
await fs.writeFile(messagesFile, '[]', 'utf8')
|
||||
}
|
||||
}
|
||||
|
||||
async function readMessages() {
|
||||
await ensureStore()
|
||||
const raw = await fs.readFile(messagesFile, 'utf8')
|
||||
try {
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function queueWrite(messages) {
|
||||
writeQueue = writeQueue.then(async () => {
|
||||
const tmp = `${messagesFile}.${process.pid}.tmp`
|
||||
await fs.writeFile(tmp, JSON.stringify(messages, null, 2), 'utf8')
|
||||
await fs.rename(tmp, messagesFile)
|
||||
})
|
||||
return writeQueue
|
||||
}
|
||||
|
||||
export async function initStore() {
|
||||
await ensureStore()
|
||||
if (process.env.SEED_DEMO === 'true') {
|
||||
const messages = await readMessages()
|
||||
if (!messages.length) {
|
||||
const now = Date.now()
|
||||
await queueWrite([
|
||||
makeMessage({
|
||||
folder: 'inbox',
|
||||
from: 'Cloudflare <[email protected]>',
|
||||
to: process.env.MAIL_USER || '[email protected]',
|
||||
subject: '你的 wpyw.site 邮件服务已准备就绪',
|
||||
text: '这是本地演示邮件。正式使用时,来自公网的 SMTP 邮件会自动进入这里。',
|
||||
date: new Date(now - 1000 * 60 * 12).toISOString(),
|
||||
unread: true,
|
||||
}),
|
||||
makeMessage({
|
||||
folder: 'inbox',
|
||||
from: '系统管理员 <[email protected]>',
|
||||
to: process.env.MAIL_USER || '[email protected]',
|
||||
subject: '欢迎使用 wpyw.mail',
|
||||
text: '你可以从左侧开始管理收件箱,或点击右上角写信。',
|
||||
date: new Date(now - 1000 * 60 * 60 * 4).toISOString(),
|
||||
unread: false,
|
||||
}),
|
||||
makeMessage({
|
||||
folder: 'sent',
|
||||
from: process.env.MAIL_USER || '[email protected]',
|
||||
to: '[email protected]',
|
||||
subject: '测试发信',
|
||||
text: 'SMTP 提交链路测试。',
|
||||
date: new Date(now - 1000 * 60 * 60 * 22).toISOString(),
|
||||
unread: false,
|
||||
}),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function makeMessage(input) {
|
||||
const text = input.text || ''
|
||||
return {
|
||||
id: input.id || crypto.randomUUID(),
|
||||
folder: input.folder || 'inbox',
|
||||
from: input.from || '',
|
||||
to: input.to || '',
|
||||
subject: input.subject || '(无主题)',
|
||||
text,
|
||||
html: input.html || '',
|
||||
preview: input.preview || text.replace(/\s+/g, ' ').trim().slice(0, 140),
|
||||
date: input.date || new Date().toISOString(),
|
||||
unread: input.unread ?? true,
|
||||
attachments: input.attachments || [],
|
||||
messageId: input.messageId || '',
|
||||
}
|
||||
}
|
||||
|
||||
export async function listMessages(folder = 'inbox', query = '') {
|
||||
const messages = await readMessages()
|
||||
const normalized = query.trim().toLowerCase()
|
||||
return messages
|
||||
.filter((message) => message.folder === folder)
|
||||
.filter((message) => {
|
||||
if (!normalized) return true
|
||||
return [message.from, message.to, message.subject, message.text]
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(normalized)
|
||||
})
|
||||
.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
.map(({ text, html, ...summary }) => summary)
|
||||
}
|
||||
|
||||
export async function getMessage(id) {
|
||||
const messages = await readMessages()
|
||||
return messages.find((message) => message.id === id) || null
|
||||
}
|
||||
|
||||
export async function saveMessage(input) {
|
||||
const messages = await readMessages()
|
||||
const message = makeMessage(input)
|
||||
messages.push(message)
|
||||
await queueWrite(messages)
|
||||
return message
|
||||
}
|
||||
|
||||
export async function markRead(id) {
|
||||
const messages = await readMessages()
|
||||
const index = messages.findIndex((message) => message.id === id)
|
||||
if (index < 0) return null
|
||||
messages[index].unread = false
|
||||
await queueWrite(messages)
|
||||
return messages[index]
|
||||
}
|
||||
|
||||
export async function mailboxStats() {
|
||||
const messages = await readMessages()
|
||||
return {
|
||||
inbox: messages.filter((message) => message.folder === 'inbox').length,
|
||||
unread: messages.filter((message) => message.folder === 'inbox' && message.unread).length,
|
||||
sent: messages.filter((message) => message.folder === 'sent').length,
|
||||
drafts: messages.filter((message) => message.folder === 'drafts').length,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user