feat: add auto reply (#49)

* feat: add auto reply

* feat: add auto reply

* feat: update readme

* feat: add auto reply
This commit is contained in:
Dream Hunter
2023-12-18 21:41:15 +08:00
committed by GitHub
parent af78248145
commit 95ae65dc03
14 changed files with 349 additions and 33 deletions

View File

@@ -3,6 +3,7 @@
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"start": "wrangler dev"
},
@@ -12,6 +13,7 @@
"dependencies": {
"hono": "^3.11.7",
"mailparser": "^3.6.5",
"mimetext": "^3.0.16",
"postal-mime": "^1.1.0"
},
"pnpm": {

71
worker/pnpm-lock.yaml generated
View File

@@ -16,6 +16,9 @@ dependencies:
mailparser:
specifier: ^3.6.5
version: 3.6.5(patch_hash=ykuld7ytssxm3j6t6ehwmtonk4)
mimetext:
specifier: ^3.0.16
version: 3.0.16
postal-mime:
specifier: ^1.1.0
version: 1.1.0
@@ -27,6 +30,21 @@ devDependencies:
packages:
/@babel/runtime-corejs3@7.23.6:
resolution: {integrity: sha512-Djs/ZTAnpyj0nyg7p1J6oiE/tZ9G2stqAFlLGZynrW+F3k2w2jGK2mLOBxzYIOcZYA89+c3d3wXKpYLcpwcU6w==}
engines: {node: '>=6.9.0'}
dependencies:
core-js-pure: 3.34.0
regenerator-runtime: 0.14.1
dev: false
/@babel/runtime@7.23.6:
resolution: {integrity: sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.1
dev: false
/@cloudflare/kv-asset-handler@0.2.0:
resolution: {integrity: sha512-MVbXLbTcAotOPUj0pAMhVtJ+3/kFkwJqc5qNOleOZTv6QkZZABDMS21dSrSlVswEHwrpWC03e4fWytjqKvuE2A==}
dependencies:
@@ -306,6 +324,19 @@ packages:
selderee: 0.11.0
dev: false
/@tsconfig/esm@1.0.5:
resolution: {integrity: sha512-JzoZ0h299JRLPfV5VBsMq1TuMy+OmU9bdV/7NcjfRojL0eIcA1k5ESrtjWrDwJRJnk9B0QmgR0rq04LERbdfWw==}
deprecated: this package has been deprecated
dev: false
/@tsconfig/node18@2.0.1:
resolution: {integrity: sha512-UqdfvuJK0SArA2CxhKWwwAWfnVSXiYe63bVpMutc27vpngCntGUZQETO24pEJ46zU6XM+7SpqYoMgcO3bM11Ew==}
dev: false
/@tsconfig/strictest@2.0.2:
resolution: {integrity: sha512-jt4jIsWKvUvuY6adJnQJlb/UR7DdjC8CjHI/OaSQruj2yX9/K6+KOvDt/vD6udqos/FUk5Op66CvYT7TBLYO5Q==}
dev: false
/@types/node-forge@1.3.10:
resolution: {integrity: sha512-y6PJDYN4xYBxwd22l+OVH35N+1fCYWiuC3aiP2SlXVE6Lo7SS+rSx9r89hLxrP4pn6n1lBGhHJ12pj3F3Mpttw==}
dependencies:
@@ -316,7 +347,6 @@ packages:
resolution: {integrity: sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==}
dependencies:
undici-types: 5.26.5
dev: true
/acorn-walk@8.3.1:
resolution: {integrity: sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==}
@@ -392,6 +422,11 @@ packages:
engines: {node: '>= 0.6'}
dev: true
/core-js-pure@3.34.0:
resolution: {integrity: sha512-pmhivkYXkymswFfbXsANmBAewXx86UBfmagP+w0wkK06kLsLlTK5oQmsURPivzMkIBQiYq2cjamcZExIwlFQIg==}
requiresBuild: true
dev: false
/data-uri-to-buffer@2.0.2:
resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==}
dev: true
@@ -588,6 +623,10 @@ packages:
engines: {node: '>=0.12.0'}
dev: true
/js-base64@3.7.5:
resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==}
dev: false
/leac@0.6.0:
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
dev: false
@@ -653,12 +692,37 @@ packages:
libqp: 2.0.1
dev: false
/mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
dev: false
/mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
dev: false
/mime@3.0.0:
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
engines: {node: '>=10.0.0'}
hasBin: true
dev: true
/mimetext@3.0.16:
resolution: {integrity: sha512-uqtsQ2eNEpqoLUBSGSF6Y9eSbbNNeiknv7M6k5nzcs7M0uWtSjqWINYe6ZY/CowxnVDtNFSX+k9K6IicPboNBg==}
dependencies:
'@babel/runtime': 7.23.6
'@babel/runtime-corejs3': 7.23.6
'@tsconfig/esm': 1.0.5
'@tsconfig/node18': 2.0.1
'@tsconfig/strictest': 2.0.2
'@types/node': 20.10.4
js-base64: 3.7.5
mime-types: 2.1.35
dev: false
/miniflare@3.20231030.4:
resolution: {integrity: sha512-7MBz0ArLuDop1WJGZC6tFgN6c5MRyDOIlxbm3yp0TRBpvDS/KsTuWCQcCjsxN4QQ5zvL3JTkuIZbQzRRw/j6ow==}
engines: {node: '>=16.13'}
@@ -747,6 +811,10 @@ packages:
picomatch: 2.3.1
dev: true
/regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
dev: false
/resolve.exports@2.0.2:
resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==}
engines: {node: '>=10'}
@@ -842,7 +910,6 @@ packages:
/undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
dev: true
/undici@5.28.2:
resolution: {integrity: sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==}

View File

@@ -1,3 +1,6 @@
import { createMimeMessage } from "mimetext";
import { EmailMessage } from "cloudflare:email";
const PostalMime = require("postal-mime");
const simpleParser = require('mailparser').simpleParser;
global.setImmediate = (callback) => callback();
@@ -44,6 +47,34 @@ async function email(message, env, ctx) {
message.setReject(`Failed save message to ${message.to}`);
console.log(`Failed save message from ${message.from} to ${message.to}`);
}
try {
const results = await env.DB.prepare(
`SELECT * FROM auto_reply_mails where address = ? and enabled = 1`
).bind(message.to).first();
if (results && results.source_prefix && message.from.startsWith(results.source_prefix)) {
const msg = createMimeMessage();
msg.setHeader("In-Reply-To", message.headers.get("Message-ID"));
msg.setSender({
name: results.name || results.address,
addr: results.address
});
msg.setRecipient(message.from);
msg.setSubject(results.subject || "Auto-reply");
msg.addMessage({
contentType: 'text/plain',
data: results.message || "This is an auto-reply message, please reconact later."
});
const replyMessage = new EmailMessage(
message.to,
message.from,
msg.asRaw()
);
await message.reply(replyMessage);
}
} catch (error) {
console.log("reply email error", error);
}
} else {
message.setReject(`Unknown address ${message.to}`);
console.log(`Unknown address ${message.to}`);

View File

@@ -32,14 +32,59 @@ api.get('/api/mails', async (c) => {
})
api.get('/api/settings', async (c) => {
return c.json(c.get("jwtPayload"));
const { address } = c.get("jwtPayload")
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: {
subject: results.subject,
message: results.message,
enabled: results.enabled == 1,
source_prefix: results.source_prefix,
name: results.name,
},
address: address
});
})
api.post('/api/settings', async (c) => {
const { address } = c.get("jwtPayload")
const { auto_reply } = await c.req.json();
const { name, subject, source_prefix, message, enabled } = auto_reply;
if ((!subject || !message) && enabled) {
return c.text("Invalid subject or message", 400)
}
else if (subject.length > 255 || message.length > 255) {
return c.text("Subject or message too long", 400)
}
const { success } = await c.env.DB.prepare(
`INSERT OR REPLACE INTO
auto_reply_mails
(name, address, source_prefix, subject, message, enabled)
VALUES
(?, ?, ?, ?, ?, ?)`
).bind(name || '', address, source_prefix || '', subject || '', message || '', enabled ? 1 : 0).run();
if (!success) {
return c.text("Failed to save settings", 500)
}
return c.json({
success: success
})
})
api.get('/open_api/settings', async (c) => {
// check header x-custom-auth
let needAuth = false;
if (c.env.PASSWORDS && c.env.PASSWORDS.length > 0) {
const auth = c.req.headers.get("x-custom-auth");
const auth = c.req.raw.headers.get("x-custom-auth");
needAuth = !c.env.PASSWORDS.includes(auth);
}
return c.json({
@@ -63,7 +108,7 @@ api.get('/api/new_address', async (c) => {
const emailAddress = c.env.PREFIX + name + "@" + domain
try {
const { success } = await c.env.DB.prepare(
`INSERT INTO address (name) VALUES (?)`
`INSERT INTO address(name) VALUES(?)`
).bind(name + "@" + domain).run();
if (!success) {
return c.text("Failed to create address", 500)
@@ -92,7 +137,7 @@ api.get('/admin/address', async (c) => {
return c.text("Invalid offset", 400)
}
const { results } = await c.env.DB.prepare(
`SELECT * FROM address order by id desc limit ? offset ?`
`SELECT * FROM address order by id desc limit ? offset ? `
).bind(limit, offset).all();
let count = 0;
if (offset == 0) {
@@ -113,7 +158,7 @@ api.get('/admin/address', async (c) => {
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 = ?`
`DELETE FROM address WHERE id = ? `
).bind(id).run();
if (!success) {
return c.text("Failed to delete address", 500)
@@ -126,7 +171,7 @@ api.delete('/admin/delete_address/:id', async (c) => {
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 = ?`
`SELECT name FROM address WHERE id = ? `
).bind(id).first("name");
// compute address
const emailAddress = c.env.PREFIX + name
@@ -148,12 +193,12 @@ api.get('/admin/mails', async (c) => {
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 ?`
`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 = ?`
`SELECT count(*) as count FROM mails where address = ? `
).bind(address).first();
count = mailCount;
}
@@ -173,7 +218,7 @@ api.get('/admin/mails_unknow', async (c) => {
}
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)
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;
@@ -181,7 +226,7 @@ api.get('/admin/mails_unknow', async (c) => {
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)`
(select concat('${c.env.PREFIX}', name) from address)`
).first();
count = mailCount;
}

View File

@@ -10,7 +10,7 @@ app.use('/*', cors());
app.use('/api/*', async (c, next) => {
// check header x-custom-auth
if (c.env.PASSWORDS && c.env.PASSWORDS.length > 0) {
const auth = c.req.headers.get("x-custom-auth");
const auth = c.req.raw.headers.get("x-custom-auth");
if (!auth || !c.env.PASSWORDS.includes(auth)) {
return c.text("Need Password", 401)
}
@@ -25,7 +25,7 @@ app.use('/api/*', async (c, next) => {
app.use('/admin/*', async (c, next) => {
// check header x-admin-auth
if (c.env.ADMIN_PASSWORDS && c.env.ADMIN_PASSWORDS.length > 0) {
const adminAuth = c.req.headers.get("x-admin-auth");
const adminAuth = c.req.raw.headers.get("x-admin-auth");
if (adminAuth && c.env.ADMIN_PASSWORDS.includes(adminAuth)) {
await next();
return;