feat: use rust mail-parser (#104)

* feat: imp worker v2

* feat: add rust mail-parser

* feat: imp frontend v2

* feat: imp frontend v2

* feat: update doc

* feat: add mailV1Alert

* feat: remove unused
This commit is contained in:
Dream Hunter
2024-04-09 14:58:19 +08:00
committed by GitHub
parent 105d51ff91
commit def400eb09
27 changed files with 856 additions and 533 deletions
+1 -8
View File
@@ -13,13 +13,6 @@
},
"dependencies": {
"hono": "^4.2.2",
"mailparser": "^3.6.9",
"mimetext": "^3.0.24",
"postal-mime": "^2.2.1"
},
"pnpm": {
"patchedDependencies": {
"mailparser@3.6.9": "patches/mailparser@3.6.9.patch"
}
"mimetext": "^3.0.24"
}
}
-24
View File
@@ -1,24 +0,0 @@
diff --git a/lib/stream-hash.js b/lib/stream-hash.js
index 3f9b44133766c04866ab2ab178c061f35dbf8f42..368ed6d94da4401909b7eddc87d18354947daf33 100644
--- a/lib/stream-hash.js
+++ b/lib/stream-hash.js
@@ -7,19 +7,15 @@ class StreamHash extends Transform {
constructor(attachment, algo) {
super();
this.attachment = attachment;
- this.algo = (algo || 'md5').toLowerCase();
- this.hash = crypto.createHash(algo);
this.byteCount = 0;
}
_transform(chunk, encoding, done) {
- this.hash.update(chunk);
this.byteCount += chunk.length;
done(null, chunk);
}
_flush(done) {
- this.attachment.checksum = this.hash.digest('hex');
this.attachment.size = this.byteCount;
done();
}
-204
View File
@@ -4,24 +4,13 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
patchedDependencies:
mailparser@3.6.9:
hash: vtv6mupuxeqjidadcgidi322su
path: patches/mailparser@3.6.9.patch
dependencies:
hono:
specifier: ^4.2.2
version: 4.2.2
mailparser:
specifier: ^3.6.9
version: 3.6.9(patch_hash=vtv6mupuxeqjidadcgidi322su)
mimetext:
specifier: ^3.0.24
version: 3.0.24
postal-mime:
specifier: ^2.2.1
version: 2.2.1
devDependencies:
wrangler:
@@ -340,13 +329,6 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.15
dev: true
/@selderee/plugin-htmlparser2@0.11.0:
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
dependencies:
domhandler: 5.0.3
selderee: 0.11.0
dev: false
/@types/node-forge@1.3.11:
resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
dependencies:
@@ -450,48 +432,6 @@ packages:
ms: 2.1.2
dev: true
/deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
dev: false
/dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
entities: 4.5.0
dev: false
/domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
dev: false
/domhandler@5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dependencies:
domelementtype: 2.3.0
dev: false
/domutils@3.1.0:
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
dev: false
/encoding-japanese@2.0.0:
resolution: {integrity: sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==}
engines: {node: '>=8.10.0'}
dev: false
/entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
dev: false
/esbuild@0.17.19:
resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==}
engines: {node: '>=12'}
@@ -580,43 +520,11 @@ packages:
function-bind: 1.1.2
dev: true
/he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
dev: false
/hono@4.2.2:
resolution: {integrity: sha512-mDmjBHF6uBNN3TASdAbDCFsN9FLbrlgXyFZkhLEkU7hUgk0+T9hcsUrL/nho4qV+Xk0RDHx7gop4Q1gelZZVRw==}
engines: {node: '>=16.0.0'}
dev: false
/html-to-text@9.0.5:
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
engines: {node: '>=14'}
dependencies:
'@selderee/plugin-htmlparser2': 0.11.0
deepmerge: 4.3.1
dom-serializer: 2.0.0
htmlparser2: 8.0.2
selderee: 0.11.0
dev: false
/htmlparser2@8.0.2:
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.1.0
entities: 4.5.0
dev: false
/iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
dependencies:
safer-buffer: 2.1.2
dev: false
/is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
@@ -651,80 +559,12 @@ packages:
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
dev: false
/leac@0.6.0:
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
dev: false
/libbase64@1.2.1:
resolution: {integrity: sha512-l+nePcPbIG1fNlqMzrh68MLkX/gTxk/+vdvAb388Ssi7UuUN31MI44w4Yf33mM3Cm4xDfw48mdf3rkdHszLNew==}
dev: false
/libbase64@1.3.0:
resolution: {integrity: sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==}
dev: false
/libmime@5.2.0:
resolution: {integrity: sha512-X2U5Wx0YmK0rXFbk67ASMeqYIkZ6E5vY7pNWRKtnNzqjvdYYG8xtPDpCnuUEnPU9vlgNev+JoSrcaKSUaNvfsw==}
dependencies:
encoding-japanese: 2.0.0
iconv-lite: 0.6.3
libbase64: 1.2.1
libqp: 2.0.1
dev: false
/libmime@5.3.4:
resolution: {integrity: sha512-TsqPdercr6DHrnoQx1F0nS2Y4yPT+fWuOjEP2rqzvV77hMYWomTe/rpm0u9JORQ/FavEXybAGcBJsQbLr9+hjA==}
dependencies:
encoding-japanese: 2.0.0
iconv-lite: 0.6.3
libbase64: 1.3.0
libqp: 2.1.0
dev: false
/libqp@2.0.1:
resolution: {integrity: sha512-Ka0eC5LkF3IPNQHJmYBWljJsw0UvM6j+QdKRbWyCdTmYwvIDE6a7bCm0UkTAL/K+3KXK5qXT/ClcInU01OpdLg==}
dev: false
/libqp@2.1.0:
resolution: {integrity: sha512-O6O6/fsG5jiUVbvdgT7YX3xY3uIadR6wEZ7+vy9u7PKHAlSEB6blvC1o5pHBjgsi95Uo0aiBBdkyFecj6jtb7A==}
dev: false
/linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
dependencies:
uc.micro: 2.1.0
dev: false
/magic-string@0.25.9:
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
dependencies:
sourcemap-codec: 1.4.8
dev: true
/mailparser@3.6.9(patch_hash=vtv6mupuxeqjidadcgidi322su):
resolution: {integrity: sha512-1fIDZlgN1NnuzmTSEUxkaViquXYkw5NbQehVc+kz55QRy98QgLdTtRSKv289Jy4NrCiDchRx6zAijB4HrPsvkA==}
dependencies:
encoding-japanese: 2.0.0
he: 1.2.0
html-to-text: 9.0.5
iconv-lite: 0.6.3
libmime: 5.3.4
linkify-it: 5.0.0
mailsplit: 5.4.0
nodemailer: 6.9.11
punycode: 2.3.1
tlds: 1.250.0
dev: false
patched: true
/mailsplit@5.4.0:
resolution: {integrity: sha512-wnYxX5D5qymGIPYLwnp6h8n1+6P6vz/MJn5AzGjZ8pwICWssL+CCQjWBIToOVHASmATot4ktvlLo6CyLfOXWYA==}
dependencies:
libbase64: 1.2.1
libmime: 5.2.0
libqp: 2.0.1
dev: false
/mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
@@ -795,23 +635,11 @@ packages:
engines: {node: '>= 6.13.0'}
dev: true
/nodemailer@6.9.11:
resolution: {integrity: sha512-UiAkgiERuG94kl/3bKfE8o10epvDnl0vokNEtZDPTq9BWzIl6EFT9336SbIT4oaTBD8NmmUTLsQyXHV82eXSWg==}
engines: {node: '>=6.0.0'}
dev: false
/normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
dev: true
/parseley@0.12.1:
resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==}
dependencies:
leac: 0.6.0
peberminta: 0.9.0
dev: false
/path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
dev: true
@@ -820,28 +648,15 @@ packages:
resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==}
dev: true
/peberminta@0.9.0:
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
dev: false
/picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
dev: true
/postal-mime@2.2.1:
resolution: {integrity: sha512-YqGeFmiKXUxv32hOy2t47VX67mYydC47CTCc7+HKd3xlNKPDhivnO/ZovN3iWXxvyyL2TRTxusuuq3etWeCKsw==}
dev: false
/printable-characters@1.0.42:
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
dev: true
/punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
dev: false
/readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
@@ -888,16 +703,6 @@ packages:
estree-walker: 0.6.1
dev: true
/safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
dev: false
/selderee@0.11.0:
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
dependencies:
parseley: 0.12.1
dev: false
/selfsigned@2.4.1:
resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==}
engines: {node: '>=10'}
@@ -933,11 +738,6 @@ packages:
engines: {node: '>= 0.4'}
dev: true
/tlds@1.250.0:
resolution: {integrity: sha512-rWsBfFCWKrjM/o2Q1TTUeYQv6tHSd/umUutDjVs6taTuEgRDIreVYIBgWRWW4ot7jp6n0UVUuxhTLWBtUmPu/w==}
hasBin: true
dev: false
/to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -949,10 +749,6 @@ packages:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
dev: true
/uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
dev: false
/undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
dev: true
+158
View File
@@ -0,0 +1,158 @@
import { Hono } from 'hono'
import { Jwt } from 'hono/utils/jwt'
const api = new Hono()
api.get('/admin/address', async (c) => {
const { limit, offset, query } = c.req.query();
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
if (query) {
const { results } = await c.env.DB.prepare(
`SELECT * FROM address where concat('${c.env.PREFIX}', name) like ? order by id desc limit ? offset ? `
).bind(`%${query}%`, limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: addressCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM address where concat('${c.env.PREFIX}', name) like ?`
).bind(`%${query}%`).first();
count = addressCount;
}
return c.json({
results: results.map((r) => {
r.name = c.env.PREFIX + r.name;
return r;
}),
count: count
})
}
const { results } = await c.env.DB.prepare(
`SELECT * FROM address order by id desc limit ? offset ? `
).bind(limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: addressCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM address`
).first();
count = addressCount;
}
return c.json({
results: results.map((r) => {
r.name = c.env.PREFIX + r.name;
return r;
}),
count: count
})
})
api.delete('/admin/delete_address/:id', async (c) => {
const { id } = c.req.param();
const { success } = await c.env.DB.prepare(
`DELETE FROM address WHERE id = ? `
).bind(id).run();
if (!success) {
return c.text("Failed to delete address", 500)
}
const { success: mailSuccess } = await c.env.DB.prepare(
`DELETE FROM mails WHERE address IN
(select concat('${c.env.PREFIX}', name) from address where id = ?) `
).bind(id).run();
if (!mailSuccess) {
return c.text("Failed to delete mails", 500)
}
return c.json({
success: success
})
})
api.get('/admin/show_password/:id', async (c) => {
const { id } = c.req.param();
const name = await c.env.DB.prepare(
`SELECT name FROM address WHERE id = ? `
).bind(id).first("name");
// compute address
const emailAddress = c.env.PREFIX + name
const jwt = await Jwt.sign({
address: emailAddress,
address_id: id
}, c.env.JWT_SECRET)
return c.json({
password: jwt
})
})
api.get('/admin/mails', async (c) => {
const { address, limit, offset } = c.req.query();
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
const { results } = await c.env.DB.prepare(
`SELECT id, source, raw, created_at FROM raw_mails where address = ? order by id desc limit ? offset ?`
).bind(address, limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM raw_mails where address = ? `
).bind(address).first();
count = mailCount;
}
return c.json({
results: results,
count: count
})
});
api.get('/admin/mails_unknow', async (c) => {
const { limit, offset } = c.req.query();
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
const { results } = await c.env.DB.prepare(`
SELECT id, source, raw, created_at FROM raw_mails
where address NOT IN(select concat('${c.env.PREFIX}', name) from address)
order by id desc limit ? offset ? `
).bind(limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(`
SELECT count(*) as count FROM raw_mails
where address NOT IN
(select concat('${c.env.PREFIX}', name) from address)`
).first();
count = mailCount;
}
return c.json({
results: results,
count: count
})
});
api.get('/admin/statistics', async (c) => {
const { count: mailCount } = await c.env.DB.prepare(`
SELECT count(*) as count FROM mails`
).first();
const { count: addressCount } = await c.env.DB.prepare(`
SELECT count(*) as count FROM address`
).first();
const { count: activeUserCount7days } = await c.env.DB.prepare(`
SELECT count(*) as count FROM address where updated_at > datetime('now', '-7 day')`
).first();
return c.json({
mailCount: mailCount,
userCount: addressCount,
activeUserCount7days: activeUserCount7days
})
});
export { api }
+114
View File
@@ -0,0 +1,114 @@
import { Hono } from 'hono'
// api v1 is deprecated
const api = new Hono()
api.get('/admin/v1/mails', async (c) => {
const { address, limit, offset } = c.req.query();
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
const { results } = await c.env.DB.prepare(
`SELECT id, source, subject, message FROM mails where address = ? order by id desc limit ? offset ? `
).bind(address, limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM mails where address = ? `
).bind(address).first();
count = mailCount;
}
return c.json({
results: results,
count: count
})
});
api.get('/admin/v1/mails_unknow', async (c) => {
const { limit, offset } = c.req.query();
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
const { results } = await c.env.DB.prepare(`
SELECT id, source, subject, message FROM mails
where address NOT IN(select concat('${c.env.PREFIX}', name) from address)
order by id desc limit ? offset ? `
).bind(limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(`
SELECT count(*) as count FROM mails
where address NOT IN
(select concat('${c.env.PREFIX}', name) from address)`
).first();
count = mailCount;
}
return c.json({
results: results,
count: count
})
});
api.get('/api/v1/mails', async (c) => {
const { address } = c.get("jwtPayload")
if (!address) {
return c.json({ "error": "No address" }, 400)
}
const { limit, offset } = c.req.query();
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
const { results } = await c.env.DB.prepare(
`SELECT id, source, subject, message, message_id, created_at FROM mails where address = ? order by id desc limit ? offset ?`
).bind(address, limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM mails where address = ?`
).bind(address).first();
count = mailCount;
}
// add attachments
let attachmentResults = [];
const message_ids = results.map((r) => r.message_id).filter((r) => r);
if (message_ids && message_ids.length > 0) {
const { results: innerAttachmentResults } = await c.env.DB.prepare(
`SELECT id, message_id FROM attachments where message_id in (${message_ids.map((id) => `'${id}'`).join(",")})`
).all();
attachmentResults = innerAttachmentResults || [];
}
results.forEach((r) => {
const attachment_id = attachmentResults.filter((ar) => ar.message_id == r.message_id).map((ar) => ar.id);
if (attachment_id && attachment_id.length > 0) {
r.attachment_id = attachment_id[0];
}
delete r.message_id;
})
return c.json({
results: results,
count: count
})
})
// attachments
api.get("/api/v1/attachment/:attachment_id", async (c) => {
const { attachment_id } = c.req.param();
const { data } = await c.env.DB.prepare(
`SELECT data FROM attachments where id = ? `
).bind(attachment_id).first();
if (!data) {
return c.text("Not found", 404)
}
return c.json(JSON.parse(data))
})
export { api }
+4 -46
View File
@@ -1,8 +1,5 @@
import { createMimeMessage } from "mimetext";
import { EmailMessage } from "cloudflare:email";
import { simpleParser } from 'mailparser';
import PostalMime from 'postal-mime';
global.setImmediate = (callback) => callback();
async function email(message, env, ctx) {
if (env.BLACK_LIST && env.BLACK_LIST.split(",").some(word => message.from.includes(word))) {
@@ -23,34 +20,17 @@ async function email(message, env, ctx) {
}
const message_id = message.headers.get("Message-ID");
let parsedEmail = {};
// todo fix this
if (!message.from.endsWith("@mega.nz")) {
try {
parsedEmail = await simpleParser(rawEmail)
} catch (error) {
console.log(error)
}
}
if (!parsedEmail.html && !parsedEmail.textAsHtml && !parsedEmail.text) {
console.log("Failed parse email, try postal-mime");
parsedEmail = await PostalMime.parse(rawEmail);
}
// process email
// save email
const { success } = await env.DB.prepare(
`INSERT INTO mails (source, address, subject, message, message_id) VALUES (?, ?, ?, ?, ?)`
`INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
).bind(
message.from, message.to,
parsedEmail.subject || "",
parsedEmail.html || parsedEmail.textAsHtml || parsedEmail.text || "",
message_id
message.from, message.to, rawEmail, message_id
).run();
if (!success) {
message.setReject(`Failed save message to ${message.to}`);
console.log(`Failed save message from ${message.from} to ${message.to}`);
}
// auto reply email
try {
const results = await env.DB.prepare(
@@ -80,28 +60,6 @@ async function email(message, env, ctx) {
} catch (error) {
console.log("reply email error", error);
}
// process attachments
try {
if (
env.ENABLE_ATTACHMENT
&& parsedEmail.attachments
&& parsedEmail.attachments.length > 0
) {
const { success } = await env.DB.prepare(
`INSERT INTO attachments (source, address, message_id, data) VALUES (?, ?, ?, ?)`
).bind(
message.from, message.to, message_id,
JSON.stringify(parsedEmail.attachments)
).run();
if (!success) {
message.setReject(`Failed save attachment to ${message.to}`);
console.log(`Failed save attachment from ${message.from} to ${message.to}`);
}
}
}
catch (error) {
console.log("save attachment error", error);
}
} else {
message.setReject(`Unknown address ${message.to}`);
console.log(`Unknown address ${message.to}`);
+14 -194
View File
@@ -16,31 +16,15 @@ api.get('/api/mails', async (c) => {
return c.text("Invalid offset", 400)
}
const { results } = await c.env.DB.prepare(
`SELECT id, source, subject, message, message_id, created_at FROM mails where address = ? order by id desc limit ? offset ?`
`SELECT id, source, raw, created_at FROM raw_mails where address = ? order by id desc limit ? offset ?`
).bind(address, limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM mails where address = ?`
`SELECT count(*) as count FROM raw_mails where address = ?`
).bind(address).first();
count = mailCount;
}
// add attachments
let attachmentResults = [];
const message_ids = results.map((r) => r.message_id).filter((r) => r);
if (message_ids && message_ids.length > 0) {
const { results: innerAttachmentResults } = await c.env.DB.prepare(
`SELECT id, message_id FROM attachments where message_id in (${message_ids.map((id) => `'${id}'`).join(",")})`
).all();
attachmentResults = innerAttachmentResults || [];
}
results.forEach((r) => {
const attachment_id = attachmentResults.filter((ar) => ar.message_id == r.message_id).map((ar) => ar.id);
if (attachment_id && attachment_id.length > 0) {
r.attachment_id = attachment_id[0];
}
delete r.message_id;
})
return c.json({
results: results,
count: count
@@ -84,28 +68,29 @@ api.get('/api/settings', async (c) => {
console.warn("Failed to update address")
}
}
let auto_reply = {};
const results = await c.env.DB.prepare(
`SELECT * FROM auto_reply_mails where address = ? `
).bind(address).first();
if (!results) {
return c.json({
auto_reply: {},
address: address
});
}
return c.json({
auto_reply: {
if (results) {
auto_reply = {
subject: results.subject,
message: results.message,
enabled: results.enabled == 1,
source_prefix: results.source_prefix,
name: results.name,
},
address: address
}
}
const { count: mailCountV1 } = await c.env.DB.prepare(
`SELECT count(*) as count FROM mails where address = ?`
).bind(address).first();
return c.json({
auto_reply: auto_reply,
address: address,
has_v1_mails: mailCountV1 > 0
});
})
api.post('/api/settings', async (c) => {
const { address } = c.get("jwtPayload")
const { auto_reply } = await c.req.json();
@@ -211,169 +196,4 @@ api.delete('/api/delete_address', async (c) => {
})
})
api.get('/admin/address', async (c) => {
const { limit, offset, query } = c.req.query();
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
if (query) {
const { results } = await c.env.DB.prepare(
`SELECT * FROM address where concat('${c.env.PREFIX}', name) like ? order by id desc limit ? offset ? `
).bind(`%${query}%`, limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: addressCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM address where concat('${c.env.PREFIX}', name) like ?`
).bind(`%${query}%`).first();
count = addressCount;
}
return c.json({
results: results.map((r) => {
r.name = c.env.PREFIX + r.name;
return r;
}),
count: count
})
}
const { results } = await c.env.DB.prepare(
`SELECT * FROM address order by id desc limit ? offset ? `
).bind(limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: addressCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM address`
).first();
count = addressCount;
}
return c.json({
results: results.map((r) => {
r.name = c.env.PREFIX + r.name;
return r;
}),
count: count
})
})
api.delete('/admin/delete_address/:id', async (c) => {
const { id } = c.req.param();
const { success } = await c.env.DB.prepare(
`DELETE FROM address WHERE id = ? `
).bind(id).run();
if (!success) {
return c.text("Failed to delete address", 500)
}
const { success: mailSuccess } = await c.env.DB.prepare(
`DELETE FROM mails WHERE address IN
(select concat('${c.env.PREFIX}', name) from address where id = ?) `
).bind(id).run();
if (!mailSuccess) {
return c.text("Failed to delete mails", 500)
}
return c.json({
success: success
})
})
api.get('/admin/show_password/:id', async (c) => {
const { id } = c.req.param();
const name = await c.env.DB.prepare(
`SELECT name FROM address WHERE id = ? `
).bind(id).first("name");
// compute address
const emailAddress = c.env.PREFIX + name
const jwt = await Jwt.sign({
address: emailAddress,
address_id: id
}, c.env.JWT_SECRET)
return c.json({
password: jwt
})
})
api.get('/admin/mails', async (c) => {
const { address, limit, offset } = c.req.query();
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
const { results } = await c.env.DB.prepare(
`SELECT id, source, subject, message FROM mails where address = ? order by id desc limit ? offset ? `
).bind(address, limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM mails where address = ? `
).bind(address).first();
count = mailCount;
}
return c.json({
results: results,
count: count
})
});
api.get('/admin/mails_unknow', async (c) => {
const { limit, offset } = c.req.query();
if (!limit || limit < 0 || limit > 100) {
return c.text("Invalid limit", 400)
}
if (!offset || offset < 0) {
return c.text("Invalid offset", 400)
}
const { results } = await c.env.DB.prepare(`
SELECT id, source, subject, message FROM mails
where address NOT IN(select concat('${c.env.PREFIX}', name) from address)
order by id desc limit ? offset ? `
).bind(limit, offset).all();
let count = 0;
if (offset == 0) {
const { count: mailCount } = await c.env.DB.prepare(`
SELECT count(*) as count FROM mails
where address NOT IN
(select concat('${c.env.PREFIX}', name) from address)`
).first();
count = mailCount;
}
return c.json({
results: results,
count: count
})
});
api.get('/admin/statistics', async (c) => {
const { count: mailCount } = await c.env.DB.prepare(`
SELECT count(*) as count FROM mails`
).first();
const { count: addressCount } = await c.env.DB.prepare(`
SELECT count(*) as count FROM address`
).first();
const { count: activeUserCount7days } = await c.env.DB.prepare(`
SELECT count(*) as count FROM address where updated_at > datetime('now', '-7 day')`
).first();
return c.json({
mailCount: mailCount,
userCount: addressCount,
activeUserCount7days: activeUserCount7days
})
});
// attachments
api.get("/api/attachment/:attachment_id", async (c) => {
const { attachment_id } = c.req.param();
const { data } = await c.env.DB.prepare(
`SELECT data FROM attachments where id = ? `
).bind(attachment_id).first();
if (!data) {
return c.text("Not found", 404)
}
return c.json(JSON.parse(data))
})
export { api }
+5 -1
View File
@@ -3,6 +3,8 @@ import { cors } from 'hono/cors';
import { jwt } from 'hono/jwt'
import { api } from './router';
import { api as adminApi } from './admin_api';
import { api as apiV1 } from './api_v1';
import { email } from './email';
const app = new Hono()
@@ -36,8 +38,10 @@ app.use('/admin/*', async (c, next) => {
app.route('/', api)
app.route('/', adminApi)
app.route('/', apiV1)
app.all('/*', async c => c.html(`<h1>Hello World</h1>`))
app.all('/*', async c => c.text("Not Found", 404))
export default {
+1 -3
View File
@@ -1,6 +1,6 @@
name = "cloudflare_temp_email"
main = "src/worker.js"
compatibility_date = "2023-08-14"
compatibility_date = "2023-12-01"
node_compat = true
[vars]
@@ -12,8 +12,6 @@ PREFIX = "tmp"
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"]
JWT_SECRET = "xxx"
BLACK_LIST = ""
# IF YOU WANT DISABLE ATTACHMENT, SET IT TO false or COMMENT IT
ENABLE_ATTACHMENT = true
[[d1_databases]]
binding = "DB"