mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-07 00:17:12 +08:00
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:
@@ -34,6 +34,7 @@
|
|||||||
- [x] 获取自定义名字的邮箱
|
- [x] 获取自定义名字的邮箱
|
||||||
- [x] 支持多语言
|
- [x] 支持多语言
|
||||||
- [x] 增加访问授权,可作为私人站点
|
- [x] 增加访问授权,可作为私人站点
|
||||||
|
- [x] 增加自动回复功能
|
||||||
- [ ] 免费版附件过大会造成 Exceeded CPU Limit 错误
|
- [ ] 免费版附件过大会造成 Exceeded CPU Limit 错误
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ This is a temporary email service that uses Cloudflare Workers to create a tempo
|
|||||||
- [x] Get Custom Name Email
|
- [x] Get Custom Name Email
|
||||||
- [x] Support multiple languages
|
- [x] Support multiple languages
|
||||||
- [x] Add access authorization, which can be used as a private site
|
- [x] Add access authorization, which can be used as a private site
|
||||||
|
- [x] Add auto reply feature
|
||||||
- [ ] Exceeded CPU Limit error caused by the free version of the attachment
|
- [ ] Exceeded CPU Limit error caused by the free version of the attachment
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
+25
-4
@@ -1,4 +1,25 @@
|
|||||||
DROP TABLE IF EXISTS mails;
|
CREATE TABLE IF NOT EXISTS mails (
|
||||||
DROP TABLE IF EXISTS address;
|
id INTEGER PRIMARY KEY,
|
||||||
CREATE TABLE IF NOT EXISTS mails (id INTEGER PRIMARY KEY, source TEXT, address TEXT, subject TEXT, message TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);
|
source TEXT,
|
||||||
CREATE TABLE IF NOT EXISTS address (id INTEGER PRIMARY KEY, name TEXT UNIQUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP);
|
address TEXT,
|
||||||
|
subject TEXT,
|
||||||
|
message TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS address (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT UNIQUE,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS auto_reply_mails (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
source_prefix TEXT,
|
||||||
|
name TEXT,
|
||||||
|
address TEXT UNIQUE,
|
||||||
|
subject TEXT,
|
||||||
|
message TEXT,
|
||||||
|
enabled INTEGER DEFAULT 1,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { useGlobalState } from '../store'
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
||||||
const { loading, auth, jwt, openSettings, showAuth, adminAuth, showAdminAuth } = useGlobalState();
|
const { loading, auth, jwt, settings, openSettings } = useGlobalState();
|
||||||
|
const { showAuth, adminAuth, showAdminAuth } = useGlobalState();
|
||||||
|
|
||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
baseURL: API_BASE,
|
baseURL: API_BASE,
|
||||||
@@ -65,9 +66,11 @@ const getSettings = async () => {
|
|||||||
if (typeof jwt.value != 'string' || jwt.value.trim() === '' || jwt.value === 'undefined') {
|
if (typeof jwt.value != 'string' || jwt.value.trim() === '' || jwt.value === 'undefined') {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
loading.value = true;
|
|
||||||
const res = await apiFetch("/api/settings");;
|
const res = await apiFetch("/api/settings");;
|
||||||
return res["address"];
|
settings.value = {
|
||||||
|
address: res["address"],
|
||||||
|
auto_reply: res["auto_reply"]
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminShowPassword = async (id) => {
|
const adminShowPassword = async (id) => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import Index from '../views/Index.vue'
|
import Index from '../views/Index.vue'
|
||||||
|
import Settings from '../views/Settings.vue'
|
||||||
import Admin from '../views/Admin.vue'
|
import Admin from '../views/Admin.vue'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -9,6 +10,10 @@ const router = createRouter({
|
|||||||
path: '/',
|
path: '/',
|
||||||
component: Index
|
component: Index
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
component: Settings
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/admin',
|
path: '/admin',
|
||||||
component: Admin
|
component: Admin
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ export const useGlobalState = createGlobalState(
|
|||||||
value: 'test.com'
|
value: 'test.com'
|
||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
|
const settings = ref({
|
||||||
|
address: '',
|
||||||
|
auto_reply: {
|
||||||
|
subject: '',
|
||||||
|
message: '',
|
||||||
|
enabled: false,
|
||||||
|
source_prefix: '',
|
||||||
|
name: '',
|
||||||
|
}
|
||||||
|
})
|
||||||
const showAuth = ref(false);
|
const showAuth = ref(false);
|
||||||
const showAdminAuth = ref(false);
|
const showAdminAuth = ref(false);
|
||||||
const auth = useStorage('auth', '');
|
const auth = useStorage('auth', '');
|
||||||
@@ -21,6 +31,7 @@ export const useGlobalState = createGlobalState(
|
|||||||
const themeSwitch = useStorage('themeSwitch', false);
|
const themeSwitch = useStorage('themeSwitch', false);
|
||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
|
settings,
|
||||||
openSettings,
|
openSettings,
|
||||||
showAuth,
|
showAuth,
|
||||||
auth,
|
auth,
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import { api } from '../api'
|
|||||||
const { toClipboard } = useClipboard()
|
const { toClipboard } = useClipboard()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const address = ref("")
|
const { jwt, settings, openSettings } = useGlobalState()
|
||||||
const { jwt, openSettings } = useGlobalState()
|
|
||||||
const autoRefresh = ref(false)
|
const autoRefresh = ref(false)
|
||||||
const data = ref([])
|
const data = ref([])
|
||||||
const timer = ref(null)
|
const timer = ref(null)
|
||||||
@@ -84,7 +83,7 @@ watch([page, pageSize], async ([page, pageSize], [oldPage, oldPageSize]) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
if (typeof address.value != 'string' || address.value.trim() === '') {
|
if (typeof settings.value.address != 'string' || settings.value.address.trim() === '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -105,7 +104,7 @@ const refresh = async () => {
|
|||||||
|
|
||||||
const copy = async () => {
|
const copy = async () => {
|
||||||
try {
|
try {
|
||||||
await toClipboard(address.value)
|
await toClipboard(settings.value.address)
|
||||||
message.success('Copied');
|
message.success('Copied');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
message.error(e.message || "error");
|
message.error(e.message || "error");
|
||||||
@@ -120,7 +119,7 @@ const newEmail = async () => {
|
|||||||
+ `&domain=${emailDomain.value || ''}`
|
+ `&domain=${emailDomain.value || ''}`
|
||||||
);
|
);
|
||||||
jwt.value = res["jwt"];
|
jwt.value = res["jwt"];
|
||||||
address.value = await api.getSettings();
|
await api.getSettings();
|
||||||
await refresh();
|
await refresh();
|
||||||
showNewEmail.value = false;
|
showNewEmail.value = false;
|
||||||
showPassword.value = true;
|
showPassword.value = true;
|
||||||
@@ -132,7 +131,7 @@ const newEmail = async () => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await api.getOpenSettings(message);
|
await api.getOpenSettings(message);
|
||||||
emailDomain.value = openSettings.value.domains ? openSettings.value.domains[0].value : "";
|
emailDomain.value = openSettings.value.domains ? openSettings.value.domains[0].value : "";
|
||||||
address.value = await api.getSettings();
|
await api.getSettings();
|
||||||
await refresh();
|
await refresh();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -140,9 +139,9 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-layout>
|
<n-layout>
|
||||||
<n-alert :type='address ? "info" : "warning"' show-icon>
|
<n-alert :type='settings.address ? "info" : "warning"' show-icon>
|
||||||
<span v-if="address">
|
<span v-if="settings.address">
|
||||||
{{ t('yourAddress') }} <b>{{ address }}</b>
|
{{ t('yourAddress') }} <b>{{ settings.address }}</b>
|
||||||
<n-button @click="copy" size="small" tertiary round type="primary">
|
<n-button @click="copy" size="small" tertiary round type="primary">
|
||||||
{{ t('copy') }}
|
{{ t('copy') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
@@ -151,7 +150,7 @@ onMounted(async () => {
|
|||||||
{{ t('pleaseGetNewEmail') }}
|
{{ t('pleaseGetNewEmail') }}
|
||||||
</span>
|
</span>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
<n-button v-if="address" class="center" @click="showPassword = true" tertiary round type="primary">
|
<n-button v-if="settings.address" class="center" @click="showPassword = true" tertiary round type="primary">
|
||||||
{{ t('showPassword') }}
|
{{ t('showPassword') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button v-else class="center" @click="showNewEmail = true" tertiary round type="primary">
|
<n-button v-else class="center" @click="showNewEmail = true" tertiary round type="primary">
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ const { t } = useI18n({
|
|||||||
logoutConfirm: 'Are you sure to logout?',
|
logoutConfirm: 'Are you sure to logout?',
|
||||||
auth: 'Auth',
|
auth: 'Auth',
|
||||||
authTip: 'Please enter the correct auth code',
|
authTip: 'Please enter the correct auth code',
|
||||||
|
settings: 'Settings',
|
||||||
|
home: 'Home',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
title: 'Cloudflare 临时邮件',
|
title: 'Cloudflare 临时邮件',
|
||||||
@@ -69,12 +71,26 @@ const { t } = useI18n({
|
|||||||
logoutConfirm: '确定要登出吗?',
|
logoutConfirm: '确定要登出吗?',
|
||||||
auth: '授权',
|
auth: '授权',
|
||||||
authTip: '请输入正确的授权码',
|
authTip: '请输入正确的授权码',
|
||||||
|
settings: '设置',
|
||||||
|
home: '主页',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const menuOptions = computed(() => [
|
const menuOptions = computed(() => [
|
||||||
|
{
|
||||||
|
label: () => h(
|
||||||
|
NButton,
|
||||||
|
{
|
||||||
|
tertiary: true,
|
||||||
|
ghost: true,
|
||||||
|
onClick: () => router.push('/')
|
||||||
|
},
|
||||||
|
{ default: () => t('home') }
|
||||||
|
),
|
||||||
|
key: "home"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
@@ -86,7 +102,7 @@ const menuOptions = computed(() => [
|
|||||||
{ default: () => "Admin" }
|
{ default: () => "Admin" }
|
||||||
),
|
),
|
||||||
show: !!adminAuth.value,
|
show: !!adminAuth.value,
|
||||||
key: "home"
|
key: "admin"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: () => h(
|
label: () => h(
|
||||||
@@ -127,6 +143,18 @@ const menuOptions = computed(() => [
|
|||||||
),
|
),
|
||||||
show: !!jwt.value,
|
show: !!jwt.value,
|
||||||
key: "logout"
|
key: "logout"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: () => h(
|
||||||
|
NButton,
|
||||||
|
{
|
||||||
|
tertiary: true,
|
||||||
|
ghost: true,
|
||||||
|
onClick: () => { router.push('/settings') }
|
||||||
|
},
|
||||||
|
{ default: () => t('settings') }
|
||||||
|
),
|
||||||
|
key: "settings"
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -186,7 +214,7 @@ const menuOptionsMobile = [
|
|||||||
</template>
|
</template>
|
||||||
<p>{{ t('logoutConfirm') }}</p>
|
<p>{{ t('logoutConfirm') }}</p>
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button @click="login" size="small" tertiary round type="primary">
|
<n-button @click="logout" size="small" tertiary round type="primary">
|
||||||
{{ t('logout') }}
|
{{ t('logout') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<script setup>
|
||||||
|
import { NSpace, NFormItem, NInput, NSwitch, NButton } from 'naive-ui'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import Header from './Header.vue'
|
||||||
|
import { useGlobalState } from '../store'
|
||||||
|
import { api } from '../api'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const sourcePrefix = ref("")
|
||||||
|
const enableAutoReply = ref(false)
|
||||||
|
const autoReplyMessage = ref("")
|
||||||
|
const subject = ref("")
|
||||||
|
const name = ref("")
|
||||||
|
|
||||||
|
const { settings } = useGlobalState()
|
||||||
|
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
success: 'Success',
|
||||||
|
settings: 'Settings',
|
||||||
|
sourcePrefix: 'Source Mail Prefix',
|
||||||
|
name: 'Name',
|
||||||
|
enableAutoReply: 'Enable Auto Reply',
|
||||||
|
subject: 'Subject',
|
||||||
|
autoReply: 'Auto Reply',
|
||||||
|
save: 'Save',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
success: '成功',
|
||||||
|
settings: '设置',
|
||||||
|
sourcePrefix: '来源邮件前缀',
|
||||||
|
name: '名称',
|
||||||
|
enableAutoReply: '启用自动回复',
|
||||||
|
subject: '主题',
|
||||||
|
autoReply: '自动回复',
|
||||||
|
save: '保存',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const getSettings = async () => {
|
||||||
|
await api.getSettings()
|
||||||
|
sourcePrefix.value = settings.value.auto_reply.source_prefix || ""
|
||||||
|
enableAutoReply.value = settings.value.auto_reply.enabled || false
|
||||||
|
name.value = settings.value.auto_reply.name || ""
|
||||||
|
autoReplyMessage.value = settings.value.auto_reply.message || ""
|
||||||
|
subject.value = settings.value.auto_reply.subject || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveSettings = async () => {
|
||||||
|
try {
|
||||||
|
await api.fetch("/api/settings", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
auto_reply: {
|
||||||
|
enabled: enableAutoReply.value,
|
||||||
|
source_prefix: sourcePrefix.value,
|
||||||
|
name: name.value,
|
||||||
|
message: autoReplyMessage.value,
|
||||||
|
subject: subject.value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
message.success(t("success"))
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getSettings()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-space vertical>
|
||||||
|
<Header />
|
||||||
|
<h1>{{ t("settings") }}</h1>
|
||||||
|
<n-button type="primary" @click="saveSettings">{{ t('save') }}</n-button>
|
||||||
|
<n-form-item :label="t('enableAutoReply')" label-placement="left">
|
||||||
|
<n-switch v-model:value="enableAutoReply" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="t('name')" label-placement="left">
|
||||||
|
<n-input :disabled="!enableAutoReply" v-model:value="name" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="t('sourcePrefix')" label-placement="left">
|
||||||
|
<n-input :disabled="!enableAutoReply" v-model:value="sourcePrefix" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="t('subject')" label-placement="left">
|
||||||
|
<n-input :disabled="!enableAutoReply" v-model:value="subject" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="t('autoReply')" label-placement="left">
|
||||||
|
<n-input :disabled="!enableAutoReply" type="textarea" v-model:value="autoReplyMessage" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"dev": "wrangler dev",
|
||||||
"deploy": "wrangler deploy",
|
"deploy": "wrangler deploy",
|
||||||
"start": "wrangler dev"
|
"start": "wrangler dev"
|
||||||
},
|
},
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hono": "^3.11.7",
|
"hono": "^3.11.7",
|
||||||
"mailparser": "^3.6.5",
|
"mailparser": "^3.6.5",
|
||||||
|
"mimetext": "^3.0.16",
|
||||||
"postal-mime": "^1.1.0"
|
"postal-mime": "^1.1.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
|
|||||||
Generated
+69
-2
@@ -16,6 +16,9 @@ dependencies:
|
|||||||
mailparser:
|
mailparser:
|
||||||
specifier: ^3.6.5
|
specifier: ^3.6.5
|
||||||
version: 3.6.5(patch_hash=ykuld7ytssxm3j6t6ehwmtonk4)
|
version: 3.6.5(patch_hash=ykuld7ytssxm3j6t6ehwmtonk4)
|
||||||
|
mimetext:
|
||||||
|
specifier: ^3.0.16
|
||||||
|
version: 3.0.16
|
||||||
postal-mime:
|
postal-mime:
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
@@ -27,6 +30,21 @@ devDependencies:
|
|||||||
|
|
||||||
packages:
|
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:
|
/@cloudflare/kv-asset-handler@0.2.0:
|
||||||
resolution: {integrity: sha512-MVbXLbTcAotOPUj0pAMhVtJ+3/kFkwJqc5qNOleOZTv6QkZZABDMS21dSrSlVswEHwrpWC03e4fWytjqKvuE2A==}
|
resolution: {integrity: sha512-MVbXLbTcAotOPUj0pAMhVtJ+3/kFkwJqc5qNOleOZTv6QkZZABDMS21dSrSlVswEHwrpWC03e4fWytjqKvuE2A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -306,6 +324,19 @@ packages:
|
|||||||
selderee: 0.11.0
|
selderee: 0.11.0
|
||||||
dev: false
|
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:
|
/@types/node-forge@1.3.10:
|
||||||
resolution: {integrity: sha512-y6PJDYN4xYBxwd22l+OVH35N+1fCYWiuC3aiP2SlXVE6Lo7SS+rSx9r89hLxrP4pn6n1lBGhHJ12pj3F3Mpttw==}
|
resolution: {integrity: sha512-y6PJDYN4xYBxwd22l+OVH35N+1fCYWiuC3aiP2SlXVE6Lo7SS+rSx9r89hLxrP4pn6n1lBGhHJ12pj3F3Mpttw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -316,7 +347,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==}
|
resolution: {integrity: sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 5.26.5
|
undici-types: 5.26.5
|
||||||
dev: true
|
|
||||||
|
|
||||||
/acorn-walk@8.3.1:
|
/acorn-walk@8.3.1:
|
||||||
resolution: {integrity: sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==}
|
resolution: {integrity: sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==}
|
||||||
@@ -392,6 +422,11 @@ packages:
|
|||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/core-js-pure@3.34.0:
|
||||||
|
resolution: {integrity: sha512-pmhivkYXkymswFfbXsANmBAewXx86UBfmagP+w0wkK06kLsLlTK5oQmsURPivzMkIBQiYq2cjamcZExIwlFQIg==}
|
||||||
|
requiresBuild: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/data-uri-to-buffer@2.0.2:
|
/data-uri-to-buffer@2.0.2:
|
||||||
resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==}
|
resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -588,6 +623,10 @@ packages:
|
|||||||
engines: {node: '>=0.12.0'}
|
engines: {node: '>=0.12.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/js-base64@3.7.5:
|
||||||
|
resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/leac@0.6.0:
|
/leac@0.6.0:
|
||||||
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
|
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -653,12 +692,37 @@ packages:
|
|||||||
libqp: 2.0.1
|
libqp: 2.0.1
|
||||||
dev: false
|
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:
|
/mime@3.0.0:
|
||||||
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: 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:
|
/miniflare@3.20231030.4:
|
||||||
resolution: {integrity: sha512-7MBz0ArLuDop1WJGZC6tFgN6c5MRyDOIlxbm3yp0TRBpvDS/KsTuWCQcCjsxN4QQ5zvL3JTkuIZbQzRRw/j6ow==}
|
resolution: {integrity: sha512-7MBz0ArLuDop1WJGZC6tFgN6c5MRyDOIlxbm3yp0TRBpvDS/KsTuWCQcCjsxN4QQ5zvL3JTkuIZbQzRRw/j6ow==}
|
||||||
engines: {node: '>=16.13'}
|
engines: {node: '>=16.13'}
|
||||||
@@ -747,6 +811,10 @@ packages:
|
|||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/regenerator-runtime@0.14.1:
|
||||||
|
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/resolve.exports@2.0.2:
|
/resolve.exports@2.0.2:
|
||||||
resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==}
|
resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -842,7 +910,6 @@ packages:
|
|||||||
|
|
||||||
/undici-types@5.26.5:
|
/undici-types@5.26.5:
|
||||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/undici@5.28.2:
|
/undici@5.28.2:
|
||||||
resolution: {integrity: sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==}
|
resolution: {integrity: sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { createMimeMessage } from "mimetext";
|
||||||
|
import { EmailMessage } from "cloudflare:email";
|
||||||
|
|
||||||
const PostalMime = require("postal-mime");
|
const PostalMime = require("postal-mime");
|
||||||
const simpleParser = require('mailparser').simpleParser;
|
const simpleParser = require('mailparser').simpleParser;
|
||||||
global.setImmediate = (callback) => callback();
|
global.setImmediate = (callback) => callback();
|
||||||
@@ -44,6 +47,34 @@ async function email(message, env, ctx) {
|
|||||||
message.setReject(`Failed save message to ${message.to}`);
|
message.setReject(`Failed save message to ${message.to}`);
|
||||||
console.log(`Failed save message from ${message.from} 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 {
|
} else {
|
||||||
message.setReject(`Unknown address ${message.to}`);
|
message.setReject(`Unknown address ${message.to}`);
|
||||||
console.log(`Unknown address ${message.to}`);
|
console.log(`Unknown address ${message.to}`);
|
||||||
|
|||||||
+55
-10
@@ -32,14 +32,59 @@ api.get('/api/mails', async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
api.get('/api/settings', 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) => {
|
api.get('/open_api/settings', async (c) => {
|
||||||
// check header x-custom-auth
|
// check header x-custom-auth
|
||||||
let needAuth = false;
|
let needAuth = false;
|
||||||
if (c.env.PASSWORDS && c.env.PASSWORDS.length > 0) {
|
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);
|
needAuth = !c.env.PASSWORDS.includes(auth);
|
||||||
}
|
}
|
||||||
return c.json({
|
return c.json({
|
||||||
@@ -63,7 +108,7 @@ api.get('/api/new_address', async (c) => {
|
|||||||
const emailAddress = c.env.PREFIX + name + "@" + domain
|
const emailAddress = c.env.PREFIX + name + "@" + domain
|
||||||
try {
|
try {
|
||||||
const { success } = await c.env.DB.prepare(
|
const { success } = await c.env.DB.prepare(
|
||||||
`INSERT INTO address (name) VALUES (?)`
|
`INSERT INTO address(name) VALUES(?)`
|
||||||
).bind(name + "@" + domain).run();
|
).bind(name + "@" + domain).run();
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return c.text("Failed to create address", 500)
|
return c.text("Failed to create address", 500)
|
||||||
@@ -92,7 +137,7 @@ api.get('/admin/address', async (c) => {
|
|||||||
return c.text("Invalid offset", 400)
|
return c.text("Invalid offset", 400)
|
||||||
}
|
}
|
||||||
const { results } = await c.env.DB.prepare(
|
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();
|
).bind(limit, offset).all();
|
||||||
let count = 0;
|
let count = 0;
|
||||||
if (offset == 0) {
|
if (offset == 0) {
|
||||||
@@ -113,7 +158,7 @@ api.get('/admin/address', async (c) => {
|
|||||||
api.delete('/admin/delete_address/:id', async (c) => {
|
api.delete('/admin/delete_address/:id', async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const { success } = await c.env.DB.prepare(
|
const { success } = await c.env.DB.prepare(
|
||||||
`DELETE FROM address WHERE id = ?`
|
`DELETE FROM address WHERE id = ? `
|
||||||
).bind(id).run();
|
).bind(id).run();
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return c.text("Failed to delete address", 500)
|
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) => {
|
api.get('/admin/show_password/:id', async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const name = await c.env.DB.prepare(
|
const name = await c.env.DB.prepare(
|
||||||
`SELECT name FROM address WHERE id = ?`
|
`SELECT name FROM address WHERE id = ? `
|
||||||
).bind(id).first("name");
|
).bind(id).first("name");
|
||||||
// compute address
|
// compute address
|
||||||
const emailAddress = c.env.PREFIX + name
|
const emailAddress = c.env.PREFIX + name
|
||||||
@@ -148,12 +193,12 @@ api.get('/admin/mails', async (c) => {
|
|||||||
return c.text("Invalid offset", 400)
|
return c.text("Invalid offset", 400)
|
||||||
}
|
}
|
||||||
const { results } = await c.env.DB.prepare(
|
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();
|
).bind(address, limit, offset).all();
|
||||||
let count = 0;
|
let count = 0;
|
||||||
if (offset == 0) {
|
if (offset == 0) {
|
||||||
const { count: mailCount } = await c.env.DB.prepare(
|
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();
|
).bind(address).first();
|
||||||
count = mailCount;
|
count = mailCount;
|
||||||
}
|
}
|
||||||
@@ -173,7 +218,7 @@ api.get('/admin/mails_unknow', async (c) => {
|
|||||||
}
|
}
|
||||||
const { results } = await c.env.DB.prepare(`
|
const { results } = await c.env.DB.prepare(`
|
||||||
SELECT id, source, subject, message FROM mails
|
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 ? `
|
order by id desc limit ? offset ? `
|
||||||
).bind(limit, offset).all();
|
).bind(limit, offset).all();
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -181,7 +226,7 @@ api.get('/admin/mails_unknow', async (c) => {
|
|||||||
const { count: mailCount } = await c.env.DB.prepare(`
|
const { count: mailCount } = await c.env.DB.prepare(`
|
||||||
SELECT count(*) as count FROM mails
|
SELECT count(*) as count FROM mails
|
||||||
where address NOT IN
|
where address NOT IN
|
||||||
(select concat('${c.env.PREFIX}', name) from address)`
|
(select concat('${c.env.PREFIX}', name) from address)`
|
||||||
).first();
|
).first();
|
||||||
count = mailCount;
|
count = mailCount;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ app.use('/*', cors());
|
|||||||
app.use('/api/*', async (c, next) => {
|
app.use('/api/*', async (c, next) => {
|
||||||
// check header x-custom-auth
|
// check header x-custom-auth
|
||||||
if (c.env.PASSWORDS && c.env.PASSWORDS.length > 0) {
|
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)) {
|
if (!auth || !c.env.PASSWORDS.includes(auth)) {
|
||||||
return c.text("Need Password", 401)
|
return c.text("Need Password", 401)
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ app.use('/api/*', async (c, next) => {
|
|||||||
app.use('/admin/*', async (c, next) => {
|
app.use('/admin/*', async (c, next) => {
|
||||||
// check header x-admin-auth
|
// check header x-admin-auth
|
||||||
if (c.env.ADMIN_PASSWORDS && c.env.ADMIN_PASSWORDS.length > 0) {
|
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)) {
|
if (adminAuth && c.env.ADMIN_PASSWORDS.includes(adminAuth)) {
|
||||||
await next();
|
await next();
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user