mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-09 18:52:40 +08:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd477fe2c8 | ||
|
|
0db611bb3e | ||
|
|
6225f6521a | ||
|
|
da2e72e523 | ||
|
|
c5d01e09e8 | ||
|
|
201c7658be | ||
|
|
77155299e0 | ||
|
|
9725407c77 | ||
|
|
e91bbe273a | ||
|
|
b792c196c1 | ||
|
|
7a368d7b23 | ||
|
|
f882e4cf97 | ||
|
|
00abf79417 | ||
|
|
1f8edbc295 | ||
|
|
268f3d6446 |
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,12 +1,31 @@
|
||||
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
||||
# CHANGE LOG
|
||||
|
||||
## main branch
|
||||
## v0.5.1
|
||||
|
||||
- UI lazy load
|
||||
- telegram bot 添加用户全局推送功能
|
||||
- 添加 `mail-parser-wasm-worker` 用于 worker 解析邮件, [文档](https://temp-mail-docs.awsl.uk/zh/guide/feature/mail_parser_wasm_worker.html)
|
||||
- 添加校验用户邮箱长度配置 `MIN_ADDRESS_LEN` 和 `MAX_ADDRESS_LEN`
|
||||
- 修复 `pages function` 未转发 `telegram` api 问题
|
||||
|
||||
## v0.5.0
|
||||
|
||||
- UI: 增加本地缓存进行地址管理
|
||||
- worker: 增加 `FORWARD_ADDRESS_LIST` 全局邮件转发地址(等同于 `catch all`)
|
||||
- UI: 多语言使用路由进行切换
|
||||
- 添加保存附件到 S3 的功能
|
||||
- UI: 增加收取邮件列表 `批量删除` 和 `批量下载`
|
||||
|
||||
## v0.4.6
|
||||
|
||||
- worker 配置文件添加 `TITLE = "Custom Title"`, 可自定义网站标题
|
||||
- 修复 KV 未绑定无法删除地址的问题
|
||||
|
||||
## v0.4.5
|
||||
|
||||
- UI lazy load 懒加载
|
||||
- telegram bot 添加用户全局推送功能(admin 用户)
|
||||
- 增加对 cloudflare verified 用户发送邮件
|
||||
- 增加使用 `resend` 发送邮件
|
||||
- 增加使用 `resend` 发送邮件, `resend` 提供 http 和 smtp api, 使用更加方便, 文档: https://temp-mail-docs.awsl.uk/zh/guide/config-send-mail.html
|
||||
|
||||
## v0.4.4
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudflare_temp_email",
|
||||
"version": "0.4.5",
|
||||
"version": "0.5.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -15,12 +15,14 @@
|
||||
"deploy": "npm run build && wrangler pages deploy ./dist --branch production"
|
||||
},
|
||||
"dependencies": {
|
||||
"@unhead/vue": "^1.9.12",
|
||||
"@vicons/material": "^0.12.0",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"@vueuse/core": "^10.10.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"axios": "^1.7.2",
|
||||
"mail-parser-wasm": "^0.1.6",
|
||||
"jszip": "^3.10.1",
|
||||
"mail-parser-wasm": "^0.1.8",
|
||||
"naive-ui": "^2.38.2",
|
||||
"postal-mime": "^2.2.5",
|
||||
"vooks": "^0.2.12",
|
||||
@@ -31,14 +33,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vicons/fa": "^0.12.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"unplugin-auto-import": "^0.17.6",
|
||||
"unplugin-vue-components": "^0.27.0",
|
||||
"vite": "^5.2.11",
|
||||
"vite": "^5.2.12",
|
||||
"vite-plugin-pwa": "^0.19.8",
|
||||
"vite-plugin-top-level-await": "^1.4.1",
|
||||
"vite-plugin-wasm": "^3.3.0",
|
||||
"workbox-window": "^7.1.0",
|
||||
"wrangler": "^3.57.1"
|
||||
"wrangler": "^3.58.0"
|
||||
}
|
||||
}
|
||||
|
||||
1828
frontend/pnpm-lock.yaml
generated
1828
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -9,18 +9,14 @@ import Footer from './views/Footer.vue';
|
||||
|
||||
|
||||
const {
|
||||
localeCache, isDark, loading, useSideMargin,
|
||||
telegramApp, isTelegram
|
||||
isDark, loading, useSideMargin, telegramApp, isTelegram
|
||||
} = useGlobalState()
|
||||
const { locale } = useI18n({});
|
||||
const theme = computed(() => isDark.value ? darkTheme : null)
|
||||
const localeConfig = computed(() => localeCache.value == 'zh' ? zhCN : null)
|
||||
const localeConfig = computed(() => locale.value == 'zh' ? zhCN : null)
|
||||
const isMobile = useIsMobile()
|
||||
const showSideMargin = computed(() => !isMobile.value && useSideMargin.value);
|
||||
|
||||
const { locale } = useI18n({
|
||||
useScope: 'global',
|
||||
});
|
||||
locale.value = localeCache.value;
|
||||
|
||||
onMounted(async () => {
|
||||
const token = import.meta.env.VITE_CF_WEB_ANALY_TOKEN;
|
||||
|
||||
@@ -54,7 +54,10 @@ const getOpenSettings = async (message) => {
|
||||
try {
|
||||
const res = await api.fetch("/open_api/settings");
|
||||
Object.assign(openSettings.value, {
|
||||
title: res["title"] || "",
|
||||
prefix: res["prefix"] || "",
|
||||
minAddressLen: res["minAddressLen"] || 1,
|
||||
maxAddressLen: res["maxAddressLen"] || 30,
|
||||
needAuth: res["needAuth"] || false,
|
||||
domains: res["domains"].map((domain) => {
|
||||
return {
|
||||
@@ -70,6 +73,7 @@ const getOpenSettings = async (message) => {
|
||||
copyright: res["copyright"] || openSettings.value.copyright,
|
||||
cfTurnstileSiteKey: res["cfTurnstileSiteKey"] || "",
|
||||
enableWebhook: res["enableWebhook"] || false,
|
||||
isS3Enabled: res["isS3Enabled"] || false,
|
||||
});
|
||||
if (openSettings.value.needAuth) {
|
||||
showAuth.value = true;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup>
|
||||
import { watch, onMounted, ref, onBeforeUnmount } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from '../store'
|
||||
@@ -10,7 +9,6 @@ import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
|
||||
|
||||
const message = useMessage()
|
||||
const isMobile = useIsMobile()
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
enableUserDeleteEmail: {
|
||||
@@ -37,11 +35,21 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
requried: false
|
||||
}
|
||||
},
|
||||
showSaveS3: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
requried: false
|
||||
},
|
||||
saveToS3: {
|
||||
type: Function,
|
||||
default: (mail_id, filename, blob) => { },
|
||||
requried: false
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
localeCache, isDark, mailboxSplitSize, indexTab,
|
||||
isDark, mailboxSplitSize, indexTab, loading,
|
||||
useIframeShowMail, sendMailModel, preferShowTextMail
|
||||
} = useGlobalState()
|
||||
const autoRefresh = ref(false)
|
||||
@@ -58,8 +66,13 @@ const curAttachments = ref([])
|
||||
const curMail = ref(null);
|
||||
const showTextMail = ref(preferShowTextMail.value)
|
||||
|
||||
const multiActionMode = ref(false)
|
||||
const showMultiActionDownload = ref(false)
|
||||
const showMultiActionDelete = ref(false)
|
||||
const multiActionDownloadZip = ref({})
|
||||
const multiActionDeleteProgress = ref({ percentage: 0, tip: '0/0' })
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
success: 'Success',
|
||||
@@ -68,12 +81,17 @@ const { t } = useI18n({
|
||||
refresh: 'Refresh',
|
||||
attachments: 'Show Attachments',
|
||||
downloadMail: 'Download Mail',
|
||||
pleaseSelectMail: "Please select a mail to view.",
|
||||
pleaseSelectMail: "Please select mail",
|
||||
delete: 'Delete',
|
||||
deleteMailTip: 'Are you sure you want to delete this mail?',
|
||||
deleteMailTip: 'Are you sure you want to delete mail?',
|
||||
reply: 'Reply',
|
||||
showTextMail: 'Show Text Mail',
|
||||
showHtmlMail: 'Show Html Mail'
|
||||
showHtmlMail: 'Show Html Mail',
|
||||
saveToS3: 'Save to S3',
|
||||
multiAction: 'Multi Action',
|
||||
cancelMultiAction: 'Cancel Multi Action',
|
||||
selectAll: 'Select All',
|
||||
unselectAll: 'Unselect All',
|
||||
},
|
||||
zh: {
|
||||
success: '成功',
|
||||
@@ -82,12 +100,17 @@ const { t } = useI18n({
|
||||
refresh: '刷新',
|
||||
downloadMail: '下载邮件',
|
||||
attachments: '查看附件',
|
||||
pleaseSelectMail: "请选择一封邮件查看。",
|
||||
pleaseSelectMail: "请选择邮件",
|
||||
delete: '删除',
|
||||
deleteMailTip: '确定要删除这封邮件吗?',
|
||||
deleteMailTip: '确定要删除邮件吗?',
|
||||
reply: '回复',
|
||||
showTextMail: '显示纯文本邮件',
|
||||
showHtmlMail: '显示HTML邮件'
|
||||
showHtmlMail: '显示HTML邮件',
|
||||
saveToS3: '保存到S3',
|
||||
multiAction: '多选',
|
||||
cancelMultiAction: '取消多选',
|
||||
selectAll: '全选',
|
||||
unselectAll: '取消全选',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -125,12 +148,14 @@ const refresh = async () => {
|
||||
pageSize.value, (page.value - 1) * pageSize.value
|
||||
);
|
||||
data.value = await Promise.all(results.map(async (item) => {
|
||||
item.checked = false;
|
||||
return await processItem(item);
|
||||
}));
|
||||
if (totalCount > 0) {
|
||||
count.value = totalCount;
|
||||
}
|
||||
if (!isMobile.value && !curMail.value && data.value.length > 0) {
|
||||
curMail.value = null;
|
||||
if (!isMobile.value && data.value.length > 0) {
|
||||
curMail.value = data.value[0];
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -140,6 +165,10 @@ const refresh = async () => {
|
||||
};
|
||||
|
||||
const clickRow = async (row) => {
|
||||
if (multiActionMode.value) {
|
||||
row.checked = !row.checked;
|
||||
return;
|
||||
}
|
||||
curMail.value = row;
|
||||
};
|
||||
|
||||
@@ -186,6 +215,92 @@ const onSpiltSizeChange = (size) => {
|
||||
mailboxSplitSize.value = size;
|
||||
}
|
||||
|
||||
const attachmentLoding = ref(false)
|
||||
const saveToS3Proxy = async (filename, blob) => {
|
||||
attachmentLoding.value = true
|
||||
try {
|
||||
await props.saveToS3(curMail.value.id, filename, blob);
|
||||
} finally {
|
||||
attachmentLoding.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const multiActionModeClick = (enableMulti) => {
|
||||
if (enableMulti) {
|
||||
data.value.forEach((item) => {
|
||||
item.checked = false;
|
||||
});
|
||||
multiActionMode.value = true;
|
||||
} else {
|
||||
multiActionMode.value = false;
|
||||
data.value.forEach((item) => {
|
||||
item.checked = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const multiActionSelectAll = (checked) => {
|
||||
data.value.forEach((item) => {
|
||||
item.checked = checked;
|
||||
});
|
||||
}
|
||||
|
||||
const multiActionDeleteMail = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const selectedMails = data.value.filter((item) => item.checked);
|
||||
if (selectedMails.length === 0) {
|
||||
message.error(t('pleaseSelectMail'));
|
||||
return;
|
||||
}
|
||||
multiActionDeleteProgress.value = {
|
||||
percentage: 0,
|
||||
tip: `0/${selectedMails.length}`
|
||||
};
|
||||
for (const [index, mail] of selectedMails.entries()) {
|
||||
await props.deleteMail(mail.id);
|
||||
showMultiActionDelete.value = true;
|
||||
multiActionDeleteProgress.value = {
|
||||
percentage: Math.floor((index + 1) / selectedMails.length * 100),
|
||||
tip: `${index + 1}/${selectedMails.length}`
|
||||
};
|
||||
}
|
||||
message.success(t("success"));
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
showMultiActionDelete.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
const multiActionDownload = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const selectedMails = data.value.filter((item) => item.checked);
|
||||
if (selectedMails.length === 0) {
|
||||
message.error(t('pleaseSelectMail'));
|
||||
return;
|
||||
}
|
||||
const JSZipModlue = await import('jszip');
|
||||
const JSZip = JSZipModlue.default;
|
||||
const zip = new JSZip();
|
||||
for (const mail of selectedMails) {
|
||||
zip.file(`${mail.id}.eml`, mail.raw);
|
||||
}
|
||||
multiActionDownloadZip.value = {
|
||||
url: URL.createObjectURL(await zip.generateAsync({ type: "blob" })),
|
||||
filename: `mails-${new Date().toISOString().replace(/:/g, '-')}.zip`
|
||||
}
|
||||
showMultiActionDownload.value = true;
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await refresh();
|
||||
});
|
||||
@@ -197,14 +312,38 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-split class="left" v-if="!isMobile" direction="horizontal" :max="0.75" :min="0.25"
|
||||
:default-size="mailboxSplitSize" :on-update:size="onSpiltSizeChange">
|
||||
<template #1>
|
||||
<div class="center">
|
||||
<div style="display: inline-block;">
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
||||
</div>
|
||||
<n-switch v-model:value="autoRefresh" size="small" :round="false">
|
||||
<div v-if="!isMobile" class="left">
|
||||
<div style="margin-bottom: 10px;">
|
||||
<n-space v-if="multiActionMode">
|
||||
<n-button @click="multiActionModeClick(false)" tertiary>
|
||||
{{ t('cancelMultiAction') }}
|
||||
</n-button>
|
||||
<n-button @click="multiActionSelectAll(true)" tertiary>
|
||||
{{ t('selectAll') }}
|
||||
</n-button>
|
||||
<n-button @click="multiActionSelectAll(false)" tertiary>
|
||||
{{ t('unselectAll') }}
|
||||
</n-button>
|
||||
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="multiActionDeleteMail">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="error">{{ t('delete') }}</n-button>
|
||||
</template>
|
||||
{{ t('deleteMailTip') }}
|
||||
</n-popconfirm>
|
||||
<n-button @click="multiActionDownload" tertiary type="info">
|
||||
<template #icon>
|
||||
<n-icon :component="CloudDownloadRound" />
|
||||
</template>
|
||||
{{ t('downloadMail') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
<n-space v-else>
|
||||
<n-button @click="multiActionModeClick(true)" type="primary" tertiary>
|
||||
{{ t('multiAction') }}
|
||||
</n-button>
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" :page-sizes="[20, 50, 100]"
|
||||
show-size-picker />
|
||||
<n-switch v-model:value="autoRefresh" :round="false">
|
||||
<template #checked>
|
||||
{{ t('refreshAfter', { msg: autoRefreshInterval }) }}
|
||||
</template>
|
||||
@@ -212,90 +351,98 @@ onBeforeUnmount(() => {
|
||||
{{ t('autoRefresh') }}
|
||||
</template>
|
||||
</n-switch>
|
||||
<n-button @click="refresh" size="small" type="primary" tertiary>
|
||||
<n-button @click="refresh" type="primary" tertiary>
|
||||
{{ t('refresh') }}
|
||||
</n-button>
|
||||
</div>
|
||||
<div style="overflow: auto; height: 80vh;">
|
||||
<n-list hoverable clickable>
|
||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
||||
:class="mailItemClass(row)">
|
||||
<n-thing :title="row.subject">
|
||||
<template #description>
|
||||
<n-tag type="info">
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ row.created_at }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ row.source }}
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailTo" type="info">
|
||||
TO: {{ row.address }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</div>
|
||||
<n-split class="left" direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize"
|
||||
:on-update:size="onSpiltSizeChange">
|
||||
<template #1>
|
||||
<div style="overflow: auto; height: 80vh;">
|
||||
<n-list hoverable clickable>
|
||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
||||
:class="mailItemClass(row)">
|
||||
<template #prefix v-if="multiActionMode">
|
||||
<n-checkbox v-model:checked="row.checked" />
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</div>
|
||||
</template>
|
||||
<template #2>
|
||||
<n-card v-if="curMail" class="mail-item" :title="curMail.subject" style="overflow: auto; max-height: 100vh;">
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
ID: {{ curMail.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ curMail.created_at }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.source }}
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailTo" type="info">
|
||||
TO: {{ curMail.address }}
|
||||
</n-tag>
|
||||
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="deleteMail">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
|
||||
</template>
|
||||
{{ t('deleteMailTip') }}
|
||||
</n-popconfirm>
|
||||
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
||||
@click="getAttachments(curMail.attachments)">
|
||||
{{ t('attachments') }}
|
||||
</n-button>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
|
||||
:href="getDownloadEmlUrl(curMail.raw)">
|
||||
<template #icon>
|
||||
<n-icon :component="CloudDownloadRound" />
|
||||
</template>
|
||||
{{ t('downloadMail') }}
|
||||
</n-button>
|
||||
<n-button v-if="showReply" size="small" tertiary type="info" @click="replyMail">
|
||||
<template #icon>
|
||||
<n-icon :component="ReplyFilled" />
|
||||
</template>
|
||||
{{ t('reply') }}
|
||||
</n-button>
|
||||
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
|
||||
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
<pre v-if="showTextMail" style="margin-top: 10px;">{{ curMail.text }}</pre>
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
|
||||
style="margin-top: 10px;width: 100%; height: 100%;">
|
||||
</iframe>
|
||||
<div v-else v-html="curMail.message" style="margin-top: 10px;"></div>
|
||||
</n-card>
|
||||
<n-card class="mail-item" v-else>
|
||||
<n-result status="info" :title="t('pleaseSelectMail')">
|
||||
</n-result>
|
||||
</n-card>
|
||||
</template>
|
||||
</n-split>
|
||||
<n-thing :title="row.subject">
|
||||
<template #description>
|
||||
<n-tag type="info">
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ `${row.created_at} UTC` }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ row.source }}
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailTo" type="info">
|
||||
TO: {{ row.address }}
|
||||
</n-tag>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</div>
|
||||
</template>
|
||||
<template #2>
|
||||
<n-card v-if="curMail" class="mail-item" :title="curMail.subject" style="overflow: auto; max-height: 100vh;">
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
ID: {{ curMail.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ `${curMail.created_at} UTC` }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.source }}
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailTo" type="info">
|
||||
TO: {{ curMail.address }}
|
||||
</n-tag>
|
||||
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="deleteMail">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
|
||||
</template>
|
||||
{{ t('deleteMailTip') }}
|
||||
</n-popconfirm>
|
||||
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
||||
@click="getAttachments(curMail.attachments)">
|
||||
{{ t('attachments') }}
|
||||
</n-button>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
|
||||
:href="getDownloadEmlUrl(curMail.raw)">
|
||||
<template #icon>
|
||||
<n-icon :component="CloudDownloadRound" />
|
||||
</template>
|
||||
{{ t('downloadMail') }}
|
||||
</n-button>
|
||||
<n-button v-if="showReply" size="small" tertiary type="info" @click="replyMail">
|
||||
<template #icon>
|
||||
<n-icon :component="ReplyFilled" />
|
||||
</template>
|
||||
{{ t('reply') }}
|
||||
</n-button>
|
||||
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
|
||||
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
<pre v-if="showTextMail" style="margin-top: 10px;">{{ curMail.text }}</pre>
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
|
||||
style="margin-top: 10px;width: 100%; height: 100%;">
|
||||
</iframe>
|
||||
<div v-else v-html="curMail.message" style="margin-top: 10px;"></div>
|
||||
</n-card>
|
||||
<n-card class="mail-item" v-else>
|
||||
<n-result status="info" :title="t('pleaseSelectMail')">
|
||||
</n-result>
|
||||
</n-card>
|
||||
</template>
|
||||
</n-split>
|
||||
</div>
|
||||
<div class="left" v-else>
|
||||
<div class="center">
|
||||
<n-space justify="center">
|
||||
<div style="display: inline-block;">
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
||||
</div>
|
||||
@@ -307,10 +454,10 @@ onBeforeUnmount(() => {
|
||||
{{ t('autoRefresh') }}
|
||||
</template>
|
||||
</n-switch>
|
||||
<n-button @click="refresh" size="small" type="primary">
|
||||
<n-button @click="refresh" tertiary size="small" type="primary">
|
||||
{{ t('refresh') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</n-space>
|
||||
<div style="overflow: auto; height: 80vh;">
|
||||
<n-list hoverable clickable>
|
||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)">
|
||||
@@ -320,7 +467,7 @@ onBeforeUnmount(() => {
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ row.created_at }}
|
||||
{{ `${row.created_at} UTC` }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ row.source }}
|
||||
@@ -342,7 +489,7 @@ onBeforeUnmount(() => {
|
||||
ID: {{ curMail.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ curMail.created_at }}
|
||||
{{ `${curMail.created_at} UTC` }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.source }}
|
||||
@@ -388,25 +535,51 @@ onBeforeUnmount(() => {
|
||||
<template #header>
|
||||
<div>{{ t("attachments") }}</div>
|
||||
</template>
|
||||
<n-list hoverable clickable>
|
||||
<n-list-item v-for="row in curAttachments" v-bind:key="row.id">
|
||||
<n-thing class="center" :title="row.filename">
|
||||
<template #description>
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
Size: {{ row.size }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
<n-spin v-model:show="attachmentLoding">
|
||||
<n-list hoverable clickable>
|
||||
<n-list-item v-for="row in curAttachments" v-bind:key="row.id">
|
||||
<n-thing class="center" :title="row.filename">
|
||||
<template #description>
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
Size: {{ row.size }}
|
||||
</n-tag>
|
||||
<n-button v-if="showSaveS3" @click="saveToS3Proxy(row.filename, row.blob)" ghost type="info"
|
||||
size="small">
|
||||
{{ t('saveToS3') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-thing>
|
||||
<template #suffix>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="row.filename"
|
||||
:href="row.url">
|
||||
<n-icon :component="CloudDownloadRound" />
|
||||
</n-button>
|
||||
</template>
|
||||
</n-thing>
|
||||
<template #suffix>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="row.filename"
|
||||
:href="row.url">
|
||||
<n-icon :component="CloudDownloadRound" />
|
||||
</n-button>
|
||||
</template>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-spin>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showMultiActionDownload" preset="dialog" :title="t('downloadMail')">
|
||||
<n-tag type="info">
|
||||
{{ multiActionDownloadZip.filename }}
|
||||
</n-tag>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="multiActionDownloadZip.filename"
|
||||
:href="multiActionDownloadZip.url">
|
||||
<n-icon :component="CloudDownloadRound" />
|
||||
{{ t('downloadMail') + " zip" }}
|
||||
</n-button>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showMultiActionDelete" preset="dialog" :title="t('delete') + t('success')"
|
||||
negative-text="OK">
|
||||
<n-space justify="center">
|
||||
<n-progress type="circle" status="error" :percentage="multiActionDeleteProgress.percentage">
|
||||
<span style="text-align: center">
|
||||
{{ multiActionDeleteProgress.tip }}
|
||||
</span>
|
||||
</n-progress>
|
||||
</n-space>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -20,7 +20,7 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const { localeCache, isDark, mailboxSplitSize } = useGlobalState()
|
||||
const { isDark, mailboxSplitSize } = useGlobalState()
|
||||
const data = ref([])
|
||||
|
||||
const count = ref(0)
|
||||
@@ -31,7 +31,6 @@ const curMail = ref(null);
|
||||
const showCode = ref(false)
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
success: 'Success',
|
||||
@@ -134,7 +133,7 @@ onMounted(async () => {
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ row.created_at }}
|
||||
{{ `${row.created_at} UTC` }}
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailFrom" type="info">
|
||||
FROM: {{ row.address }}
|
||||
@@ -155,7 +154,7 @@ onMounted(async () => {
|
||||
ID: {{ curMail.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ curMail.created_at }}
|
||||
{{ `${curMail.created_at} UTC` }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.address }}
|
||||
@@ -195,7 +194,7 @@ onMounted(async () => {
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ row.created_at }}
|
||||
{{ `${row.created_at} UTC` }}
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailFrom" type="info">
|
||||
FROM: {{ row.address }}
|
||||
@@ -217,7 +216,7 @@ onMounted(async () => {
|
||||
ID: {{ curMail.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ curMail.created_at }}
|
||||
{{ `${curMail.created_at} UTC` }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.address }}
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
import { ref, watch, defineModel, onMounted } from "vue";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from '../store'
|
||||
const { localeCache, openSettings, isDark } = useGlobalState()
|
||||
const { openSettings, isDark } = useGlobalState()
|
||||
|
||||
const cfToken = defineModel('value')
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
const { locale, t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
refresh: 'Refresh'
|
||||
@@ -42,7 +41,7 @@ const checkCfTurnstile = async (remove) => {
|
||||
"#cf-turnstile",
|
||||
{
|
||||
sitekey: openSettings.value.cfTurnstileSiteKey,
|
||||
language: localeCache.value == 'zh' ? 'zh-CN' : 'en-US',
|
||||
language: locale.value == 'zh' ? 'zh-CN' : 'en-US',
|
||||
theme: isDark.value ? 'dark' : 'light',
|
||||
callback: function (token) {
|
||||
cfToken.value = token;
|
||||
|
||||
@@ -3,6 +3,7 @@ import App from './App.vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import router from './router'
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
import { createHead } from '@unhead/vue'
|
||||
|
||||
registerSW({ immediate: true })
|
||||
const i18n = createI18n({
|
||||
@@ -16,7 +17,19 @@ const i18n = createI18n({
|
||||
messages: {}
|
||||
}
|
||||
})
|
||||
|
||||
router.beforeEach((to, from) => {
|
||||
if (to.params.lang && ['en', 'zh'].includes(to.params.lang)) {
|
||||
i18n.global.locale.value = to.params.lang
|
||||
} else {
|
||||
i18n.global.locale.value = 'zh'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const head = createHead()
|
||||
const app = createApp(App)
|
||||
app.use(i18n)
|
||||
app.use(router)
|
||||
app.use(head)
|
||||
app.mount('#app')
|
||||
|
||||
@@ -1,26 +1,36 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Index from '../views/Index.vue'
|
||||
import User from '../views/User.vue'
|
||||
import { useGlobalState } from '../store'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
alias: "/:lang/",
|
||||
component: Index
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
alias: "/:lang/user",
|
||||
component: User
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
alias: "/:lang/admin",
|
||||
component: () => import('../views/Admin.vue')
|
||||
},
|
||||
{
|
||||
path: '/telegram_mail',
|
||||
alias: "/:lang/telegram_mail",
|
||||
component: () => import('../views/telegram/Mail.vue')
|
||||
},
|
||||
{
|
||||
name: 'not-found',
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export const useGlobalState = createGlobalState(
|
||||
const toggleDark = useToggle(isDark)
|
||||
const loading = ref(false);
|
||||
const openSettings = ref({
|
||||
title: '',
|
||||
prefix: '',
|
||||
needAuth: false,
|
||||
adminContact: '',
|
||||
@@ -17,6 +18,8 @@ export const useGlobalState = createGlobalState(
|
||||
domains: [],
|
||||
copyright: 'Dream Hunter',
|
||||
cfTurnstileSiteKey: '',
|
||||
enableWebhook: false,
|
||||
isS3Enabled: false,
|
||||
})
|
||||
const settings = ref({
|
||||
fetched: false,
|
||||
@@ -44,7 +47,6 @@ export const useGlobalState = createGlobalState(
|
||||
const auth = useStorage('auth', '');
|
||||
const adminAuth = useStorage('adminAuth', '');
|
||||
const jwt = useStorage('jwt', '');
|
||||
const localeCache = useStorage('locale', 'zh');
|
||||
const adminTab = ref("account");
|
||||
const adminMailTabAddress = ref("");
|
||||
const adminSendBoxTabAddress = ref("");
|
||||
@@ -81,7 +83,6 @@ export const useGlobalState = createGlobalState(
|
||||
showAddressCredential,
|
||||
auth,
|
||||
jwt,
|
||||
localeCache,
|
||||
adminAuth,
|
||||
showAdminAuth,
|
||||
adminTab,
|
||||
|
||||
@@ -16,11 +16,11 @@ export async function processItem(item) {
|
||||
item.message = parsedEmail.body_html || parsedEmail.text || '';
|
||||
item.text = parsedEmail.text || '';
|
||||
item.attachments = parsedEmail.attachments?.map((a_item) => {
|
||||
const blob_url = URL.createObjectURL(
|
||||
new Blob(
|
||||
[a_item.content],
|
||||
{ type: a_item.content_type || 'application/octet-stream' }
|
||||
))
|
||||
const blob = new Blob(
|
||||
[a_item.content],
|
||||
{ type: a_item.content_type || 'application/octet-stream' }
|
||||
);
|
||||
const blob_url = URL.createObjectURL(blob);
|
||||
if (a_item.content_id && a_item.content_id.length > 0) {
|
||||
item.message = item.message.replace(`cid:${a_item.content_id}`, blob_url);
|
||||
}
|
||||
@@ -28,7 +28,8 @@ export async function processItem(item) {
|
||||
id: a_item.content_id || Math.random().toString(36).substring(2, 15),
|
||||
filename: a_item.filename || a_item.content_id || "",
|
||||
size: humanFileSize(a_item.content?.length || 0),
|
||||
url: blob_url
|
||||
url: blob_url,
|
||||
blob: blob
|
||||
}
|
||||
}) || [];
|
||||
} catch (error) {
|
||||
@@ -49,11 +50,11 @@ export async function processItem(item) {
|
||||
item.message = parsedEmail.html || parsedEmail.text || item.raw;
|
||||
item.text = parsedEmail.text || '';
|
||||
item.attachments = parsedEmail.attachments?.map((a_item) => {
|
||||
const blob_url = URL.createObjectURL(
|
||||
new Blob(
|
||||
[a_item.content],
|
||||
{ type: a_item.mimeType || 'application/octet-stream' }
|
||||
))
|
||||
const blob = new Blob(
|
||||
[a_item.content],
|
||||
{ type: a_item.mimeType || 'application/octet-stream' }
|
||||
);
|
||||
const blob_url = URL.createObjectURL(blob)
|
||||
if (a_item.contentId && a_item.contentId.length > 0) {
|
||||
item.message = item.message.replace(`cid:${a_item.contentId}`, blob_url);
|
||||
}
|
||||
@@ -61,7 +62,8 @@ export async function processItem(item) {
|
||||
id: a_item.contentId || Math.random().toString(36).substring(2, 15),
|
||||
filename: a_item.filename || a_item.contentId || "",
|
||||
size: humanFileSize(a_item.content?.length || 0),
|
||||
url: blob_url
|
||||
url: blob_url,
|
||||
blob: blob
|
||||
}
|
||||
}) || [];
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
export const hashPassword = async (password) => {
|
||||
export const hashPassword = async (password: string) => {
|
||||
// user crypto to hash password
|
||||
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(password));
|
||||
const hashArray = Array.from(new Uint8Array(digest));
|
||||
return hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
export const getRouterPathWithLang = (path: string, lang: string) => {
|
||||
if (!lang || lang === 'zh') {
|
||||
return path;
|
||||
}
|
||||
return `/${lang}${path}`;
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import Telegram from './admin/Telegram.vue';
|
||||
import Webhook from './admin/Webhook.vue';
|
||||
|
||||
const {
|
||||
localeCache, adminAuth, showAdminAuth, adminTab, loading, globalTabplacement
|
||||
adminAuth, showAdminAuth, adminTab, loading, globalTabplacement
|
||||
} = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
@@ -34,7 +34,6 @@ const authFunc = async () => {
|
||||
}
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
accessHeader: 'Admin Password',
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from '../store'
|
||||
const { localeCache, openSettings } = useGlobalState()
|
||||
const { openSettings } = useGlobalState()
|
||||
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
copyright: "Copyright"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { ref, h, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useHead } from '@unhead/vue'
|
||||
import { useRoute, useRouter, RouterLink } from 'vue-router'
|
||||
import { useIsMobile } from '../utils/composables'
|
||||
import {
|
||||
@@ -11,11 +12,13 @@ import { GithubAlt, Language, User, Home } from '@vicons/fa'
|
||||
|
||||
import { useGlobalState } from '../store'
|
||||
import { api } from '../api'
|
||||
import { getRouterPathWithLang } from '../utils'
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const {
|
||||
localeCache, toggleDark, isDark, isTelegram,
|
||||
showAuth, adminAuth, auth, loading
|
||||
toggleDark, isDark, isTelegram,
|
||||
showAuth, adminAuth, auth, loading, openSettings
|
||||
} = useGlobalState()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -36,13 +39,15 @@ const authFunc = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const changeLocale = (locale) => {
|
||||
localeCache.value = locale;
|
||||
location.reload()
|
||||
const changeLocale = async (lang) => {
|
||||
if (lang == 'zh') {
|
||||
await router.push(route.fullPath.replace('/en', ''));
|
||||
} else {
|
||||
await router.push(`/${lang}${route.fullPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
const { locale, t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
title: 'Cloudflare Temp Email',
|
||||
@@ -79,7 +84,10 @@ const menuOptions = computed(() => [
|
||||
size: "small",
|
||||
type: menuValue.value == "home" ? "primary" : "default",
|
||||
style: "width: 100%",
|
||||
onClick: async () => { await router.push('/'); showMobileMenu.value = false; }
|
||||
onClick: async () => {
|
||||
await router.push(getRouterPathWithLang('/', locale.value));
|
||||
showMobileMenu.value = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => t('home'),
|
||||
@@ -95,7 +103,10 @@ const menuOptions = computed(() => [
|
||||
size: "small",
|
||||
type: menuValue.value == "user" ? "primary" : "default",
|
||||
style: "width: 100%",
|
||||
onClick: async () => { await router.push("/user"); showMobileMenu.value = false; }
|
||||
onClick: async () => {
|
||||
await router.push(getRouterPathWithLang("/user", locale.value));
|
||||
showMobileMenu.value = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => t('user'),
|
||||
@@ -113,7 +124,10 @@ const menuOptions = computed(() => [
|
||||
size: "small",
|
||||
type: menuValue.value == "admin" ? "primary" : "default",
|
||||
style: "width: 100%",
|
||||
onClick: async () => { await router.push('/admin'); showMobileMenu.value = false; }
|
||||
onClick: async () => {
|
||||
await router.push(getRouterPathWithLang('/admin', locale.value));
|
||||
showMobileMenu.value = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => "Admin",
|
||||
@@ -148,13 +162,13 @@ const menuOptions = computed(() => [
|
||||
text: true,
|
||||
size: "small",
|
||||
style: "width: 100%",
|
||||
onClick: () => {
|
||||
localeCache.value == 'zh' ? changeLocale('en') : changeLocale('zh');
|
||||
onClick: async () => {
|
||||
locale.value == 'zh' ? await changeLocale('en') : await changeLocale('zh');
|
||||
showMobileMenu.value = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => localeCache.value == 'zh' ? "English" : "中文",
|
||||
default: () => locale.value == 'zh' ? "English" : "中文",
|
||||
icon: () => h(
|
||||
NIcon, { component: Language }
|
||||
)
|
||||
@@ -182,6 +196,13 @@ const menuOptions = computed(() => [
|
||||
}
|
||||
]);
|
||||
|
||||
useHead({
|
||||
title: () => openSettings.value.title || t('title'),
|
||||
meta: [
|
||||
{ name: "description", content: openSettings.value.description || t('title') },
|
||||
]
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await api.getOpenSettings(message);
|
||||
});
|
||||
@@ -191,7 +212,7 @@ onMounted(async () => {
|
||||
<div>
|
||||
<n-page-header>
|
||||
<template #title>
|
||||
<h3>{{ t('title') }}</h3>
|
||||
<h3>{{ openSettings.title || t('title') }}</h3>
|
||||
</template>
|
||||
<template #avatar>
|
||||
<n-avatar style="margin-left: 10px;" src="/logo.png" />
|
||||
|
||||
@@ -10,14 +10,15 @@ import MailBox from '../components/MailBox.vue';
|
||||
import SendBox from '../components/SendBox.vue';
|
||||
import AutoReply from './index/AutoReply.vue';
|
||||
import AccountSettings from './index/AccountSettings.vue';
|
||||
import WenHook from './index/Webhook.vue';
|
||||
import Webhook from './index/Webhook.vue';
|
||||
import Attachment from './index/Attachment.vue';
|
||||
import About from './common/About.vue';
|
||||
|
||||
const SendMail = defineAsyncComponent(() => import('./index/SendMail.vue'));
|
||||
const { localeCache, settings, openSettings, indexTab, globalTabplacement } = useGlobalState()
|
||||
const { settings, openSettings, indexTab, globalTabplacement } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
mailbox: 'Mail Box',
|
||||
@@ -26,6 +27,8 @@ const { t } = useI18n({
|
||||
auto_reply: 'Auto Reply',
|
||||
accountSettings: 'Account Settings',
|
||||
about: 'About',
|
||||
s3Attachment: 'S3 Attachment',
|
||||
saveToS3Success: 'save to s3 success',
|
||||
},
|
||||
zh: {
|
||||
mailbox: '收件箱',
|
||||
@@ -34,6 +37,8 @@ const { t } = useI18n({
|
||||
auto_reply: '自动回复',
|
||||
accountSettings: '账户设置',
|
||||
about: '关于',
|
||||
s3Attachment: 'S3附件',
|
||||
saveToS3Success: '保存到s3成功',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -49,6 +54,26 @@ const deleteMail = async (curMailId) => {
|
||||
const fetchSenboxData = async (limit, offset) => {
|
||||
return await api.fetch(`/api/sendbox?limit=${limit}&offset=${offset}`);
|
||||
};
|
||||
|
||||
const saveToS3 = async (mail_id, filename, blob) => {
|
||||
try {
|
||||
const { url } = await api.fetch(`/api/attachment/put_url`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ key: `${mail_id}/${filename}` })
|
||||
});
|
||||
// upload to s3 by formdata
|
||||
const formData = new FormData();
|
||||
formData.append(filename, blob);
|
||||
await fetch(url, {
|
||||
method: 'PUT',
|
||||
body: formData
|
||||
});
|
||||
message.success(t('saveToS3Success'));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error(error.message || "save to s3 error");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -56,8 +81,9 @@ const fetchSenboxData = async (limit, offset) => {
|
||||
<AddressBar />
|
||||
<n-tabs v-if="settings.address" type="card" v-model:value="indexTab" :placement="globalTabplacement">
|
||||
<n-tab-pane name="mailbox" :tab="t('mailbox')">
|
||||
<MailBox :showEMailTo="false" :showReply="true" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||
:fetchMailData="fetchMailData" :deleteMail="deleteMail" />
|
||||
<MailBox :showEMailTo="false" :showReply="true" :showSaveS3="openSettings.isS3Enabled" :saveToS3="saveToS3"
|
||||
:enableUserDeleteEmail="openSettings.enableUserDeleteEmail" :fetchMailData="fetchMailData"
|
||||
:deleteMail="deleteMail" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="sendbox" :tab="t('sendbox')">
|
||||
<SendBox :fetchMailData="fetchSenboxData" />
|
||||
@@ -72,7 +98,10 @@ const fetchSenboxData = async (limit, offset) => {
|
||||
<AutoReply />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="openSettings.enableWebhook" name="webhook" :tab="t('webhook')">
|
||||
<WenHook />
|
||||
<Webhook />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="openSettings.isS3Enabled" name="s3_attachment" :tab="t('s3Attachment')">
|
||||
<Attachment />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="openSettings.enableIndexAbout" name="about" :tab="t('about')">
|
||||
<About />
|
||||
|
||||
@@ -9,11 +9,10 @@ import UserBar from './user/UserBar.vue';
|
||||
import BindAddress from './user/BindAddress.vue';
|
||||
|
||||
const {
|
||||
localeCache, userTab, globalTabplacement, userSettings
|
||||
userTab, globalTabplacement, userSettings
|
||||
} = useGlobalState()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
address_management: 'Address Management',
|
||||
|
||||
@@ -9,13 +9,12 @@ import { NButton, NMenu } from 'naive-ui';
|
||||
import { MenuFilled } from '@vicons/material'
|
||||
|
||||
const {
|
||||
localeCache, adminAuth, showAdminAuth, loading,
|
||||
adminAuth, showAdminAuth, loading,
|
||||
adminTab, adminMailTabAddress, adminSendBoxTabAddress
|
||||
} = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
name: 'Name',
|
||||
|
||||
@@ -5,11 +5,10 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { localeCache, loading } = useGlobalState()
|
||||
const { loading } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
save: 'Save',
|
||||
|
||||
@@ -6,12 +6,11 @@ import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const {
|
||||
localeCache, loading, openSettings,
|
||||
loading, openSettings,
|
||||
} = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
address: 'Address',
|
||||
|
||||
@@ -7,12 +7,11 @@ import { api } from '../../api'
|
||||
import MailBox from '../../components/MailBox.vue';
|
||||
|
||||
const {
|
||||
localeCache, adminAuth, showAdminAuth,
|
||||
adminAuth, showAdminAuth,
|
||||
adminMailTabAddress
|
||||
} = useGlobalState()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
addressQueryTip: 'Leave blank to query all addresses',
|
||||
@@ -66,6 +65,7 @@ onMounted(async () => {
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<div style="margin-top: 10px;"></div>
|
||||
<MailBox :key="mailBoxKey" :enableUserDeleteEmail="false" :fetchMailData="fetchMailData" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CleaningServicesFilled } from '@vicons/material'
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { localeCache, adminAuth, showAdminAuth } = useGlobalState()
|
||||
const { adminAuth, showAdminAuth } = useGlobalState()
|
||||
const message = useMessage()
|
||||
const cleanupModel = ref({
|
||||
enableMailsAutoCleanup: false,
|
||||
@@ -20,7 +20,6 @@ const cleanupModel = ref({
|
||||
})
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
tip: 'Please input the cleanup days',
|
||||
|
||||
@@ -5,10 +5,9 @@ import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import SendBox from '../../components/SendBox.vue';
|
||||
|
||||
const { localeCache, adminSendBoxTabAddress } = useGlobalState()
|
||||
const { adminSendBoxTabAddress } = useGlobalState()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
query: 'Query',
|
||||
|
||||
@@ -5,11 +5,10 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { localeCache, loading } = useGlobalState()
|
||||
const { loading } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
address: 'Address',
|
||||
|
||||
@@ -7,11 +7,10 @@ import { SendOutlined } from '@vicons/material'
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { localeCache, adminAuth } = useGlobalState()
|
||||
const { adminAuth } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
userCount: 'Account Count',
|
||||
|
||||
@@ -6,13 +6,10 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from '../../store'
|
||||
// @ts-ignore
|
||||
import { api } from '../../api'
|
||||
|
||||
const { localeCache } = useGlobalState()
|
||||
// @ts-ignore
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
init: 'Init',
|
||||
|
||||
@@ -8,11 +8,10 @@ import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import { hashPassword } from '../../utils';
|
||||
|
||||
const { localeCache, loading } = useGlobalState()
|
||||
const { loading } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
success: 'Success',
|
||||
|
||||
@@ -5,11 +5,10 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { localeCache, loading } = useGlobalState()
|
||||
const { loading } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
save: 'Save',
|
||||
|
||||
@@ -6,13 +6,10 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from '../../store'
|
||||
// @ts-ignore
|
||||
import { api } from '../../api'
|
||||
|
||||
const { localeCache } = useGlobalState()
|
||||
// @ts-ignore
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
successTip: 'Success',
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from '../../store'
|
||||
const { localeCache, openSettings } = useGlobalState()
|
||||
const { openSettings } = useGlobalState()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
adminContact: 'If you need help, please contact the administrator ({msg})',
|
||||
|
||||
@@ -5,13 +5,12 @@ import { useIsMobile } from '../../utils/composables'
|
||||
import { useGlobalState } from '../../store'
|
||||
|
||||
const {
|
||||
localeCache, mailboxSplitSize, useIframeShowMail, preferShowTextMail,
|
||||
mailboxSplitSize, useIframeShowMail, preferShowTextMail,
|
||||
globalTabplacement, useSideMargin
|
||||
} = useGlobalState()
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
mailboxSplitSize: 'Mailbox Split Size',
|
||||
|
||||
@@ -9,6 +9,7 @@ import Turnstile from '../../components/Turnstile.vue'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import { getRouterPathWithLang } from '../../utils'
|
||||
|
||||
const props = defineProps({
|
||||
bindUserAddress: {
|
||||
@@ -36,7 +37,7 @@ const message = useMessage()
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
jwt, localeCache, loading, openSettings,
|
||||
jwt, loading, openSettings,
|
||||
showAddressCredential, userSettings
|
||||
} = useGlobalState()
|
||||
|
||||
@@ -59,14 +60,13 @@ const login = async () => {
|
||||
} catch (error) {
|
||||
message.error(`${t('bindUserAddressError')}: ${error.message}`);
|
||||
}
|
||||
await router.push("/");
|
||||
await router.push(getRouterPathWithLang("/", locale.value));
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
const { locale, t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
login: 'Login',
|
||||
@@ -128,7 +128,7 @@ const newEmail = async () => {
|
||||
);
|
||||
jwt.value = res["jwt"];
|
||||
await api.getSettings();
|
||||
await router.push("/");
|
||||
await router.push(getRouterPathWithLang("/", locale.value));
|
||||
showAddressCredential.value = true;
|
||||
try {
|
||||
await props.bindUserAddress();
|
||||
@@ -189,7 +189,8 @@ onMounted(async () => {
|
||||
<n-input-group-label v-if="openSettings.prefix">
|
||||
{{ openSettings.prefix }}
|
||||
</n-input-group-label>
|
||||
<n-input v-model:value="emailName" />
|
||||
<n-input v-model:value="emailName" show-count :minlength="openSettings.minAddressLen"
|
||||
:maxlength="openSettings.maxAddressLen" />
|
||||
<n-input-group-label>@</n-input-group-label>
|
||||
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
|
||||
:options="openSettings.domains" />
|
||||
|
||||
@@ -6,17 +6,17 @@ import { useRouter } from 'vue-router'
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import Appearance from '../common/Appearance.vue'
|
||||
import { getRouterPathWithLang } from '../../utils'
|
||||
|
||||
const {
|
||||
jwt, localeCache, settings, showAddressCredential, loading
|
||||
jwt, settings, showAddressCredential, loading
|
||||
} = useGlobalState()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
const showLogout = ref(false)
|
||||
const showDelteAccount = ref(false)
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
const { locale, t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
logout: "Logout",
|
||||
@@ -39,7 +39,7 @@ const { t } = useI18n({
|
||||
|
||||
const logout = async () => {
|
||||
jwt.value = '';
|
||||
await router.push('/')
|
||||
await router.push(getRouterPathWithLang("/", locale.value))
|
||||
location.reload()
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ const deleteAccount = async () => {
|
||||
method: 'DELETE'
|
||||
});
|
||||
jwt.value = '';
|
||||
await router.push('/')
|
||||
await router.push(getRouterPathWithLang("/", locale.value))
|
||||
location.reload()
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
|
||||
@@ -10,18 +10,19 @@ import { api } from '../../api'
|
||||
import Login from '../common/Login.vue'
|
||||
import AddressManagement from '../user/AddressManagement.vue'
|
||||
import TelegramAddress from './TelegramAddress.vue'
|
||||
import LocalAddress from './LocalAddress.vue'
|
||||
import { getRouterPathWithLang } from '../../utils'
|
||||
|
||||
const { toClipboard } = useClipboard()
|
||||
const message = useMessage()
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
jwt, localeCache, settings, showAddressCredential, userJwt,
|
||||
jwt, settings, showAddressCredential, userJwt,
|
||||
isTelegram
|
||||
} = useGlobalState()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
const { locale, t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
addressManage: 'Address Manage',
|
||||
@@ -50,6 +51,7 @@ const { t } = useI18n({
|
||||
|
||||
const showChangeAddress = ref(false)
|
||||
const showTelegramChangeAddress = ref(false)
|
||||
const showLocalAddress = ref(false)
|
||||
|
||||
const copy = async () => {
|
||||
try {
|
||||
@@ -60,6 +62,10 @@ const copy = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const onUserLogin = async () => {
|
||||
await router.push(getRouterPathWithLang("/user", locale.value))
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await api.getSettings();
|
||||
});
|
||||
@@ -82,6 +88,10 @@ onMounted(async () => {
|
||||
size="small" tertiary type="primary">
|
||||
<n-icon :component="ExchangeAlt" /> {{ t('changeAddress') }}
|
||||
</n-button>
|
||||
<n-button v-else style="margin-left: 10px" @click="showLocalAddress = true" size="small" tertiary
|
||||
type="primary">
|
||||
<n-icon :component="ExchangeAlt" /> {{ t('addressManage') }}
|
||||
</n-button>
|
||||
<n-button style="margin-left: 10px" @click="copy" size="small" tertiary type="primary">
|
||||
<n-icon :component="Copy" /> {{ t('copy') }}
|
||||
</n-button>
|
||||
@@ -98,7 +108,7 @@ onMounted(async () => {
|
||||
</n-alert>
|
||||
<Login />
|
||||
<n-divider />
|
||||
<n-button @click="router.push('/user')" type="primary" block secondary strong>
|
||||
<n-button @click="onUserLogin" type="primary" block secondary strong>
|
||||
<template #icon>
|
||||
<n-icon :component="User" />
|
||||
</template>
|
||||
@@ -112,6 +122,9 @@ onMounted(async () => {
|
||||
<n-modal v-model:show="showChangeAddress" preset="card" :title="t('changeAddress')">
|
||||
<AddressManagement />
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showLocalAddress" preset="card" :title="t('changeAddress')">
|
||||
<LocalAddress />
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showAddressCredential" preset="dialog" :title="t('addressCredential')">
|
||||
<span>
|
||||
<p>{{ t("addressCredentialTip") }}</p>
|
||||
|
||||
91
frontend/src/views/index/Attachment.vue
Normal file
91
frontend/src/views/index/Attachment.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script setup>
|
||||
import { ref, h, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { api } from '../../api'
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
download: 'Download',
|
||||
action: 'Action',
|
||||
},
|
||||
zh: {
|
||||
download: '下载',
|
||||
action: '操作',
|
||||
}
|
||||
}
|
||||
});
|
||||
const data = ref([])
|
||||
const showDownload = ref(false)
|
||||
const curRow = ref({})
|
||||
const curDownloadUrl = ref('')
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const { results } = await api.fetch(
|
||||
`/api/attachment/list`
|
||||
);
|
||||
data.value = results;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "key",
|
||||
key: "key"
|
||||
},
|
||||
{
|
||||
title: t('action'),
|
||||
key: 'actions',
|
||||
render(row) {
|
||||
return h('div', [
|
||||
h(NButton,
|
||||
{
|
||||
type: 'success',
|
||||
tertiary: true,
|
||||
onClick: async () => {
|
||||
try {
|
||||
const { url } = await api.fetch(`/api/attachment/get_url`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ key: row.key })
|
||||
});
|
||||
curDownloadUrl.value = url;
|
||||
curRow.value = row;
|
||||
showDownload.value = true;
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
},
|
||||
{ default: () => t('download') }
|
||||
)
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-modal v-model:show="showDownload" preset="dialog" :title="t('download')">
|
||||
<n-tag type="info">{{ curRow.key }}</n-tag>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curRow.key.replace('/', '_')"
|
||||
:href="curDownloadUrl">
|
||||
{{ t('download') }}
|
||||
</n-button>
|
||||
</n-modal>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||
</div>
|
||||
</template>
|
||||
159
frontend/src/views/index/LocalAddress.vue
Normal file
159
frontend/src/views/index/LocalAddress.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, h, computed } from 'vue';
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { NPopconfirm, NButton } from 'naive-ui'
|
||||
|
||||
// @ts-ignore
|
||||
import { useGlobalState } from '../../store'
|
||||
// @ts-ignore
|
||||
import Login from '../common/Login.vue';
|
||||
|
||||
const { jwt } = useGlobalState()
|
||||
// @ts-ignore
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
tip: 'These addresses are stored in your browser, maybe loss if you clear the browser cache.',
|
||||
success: 'success',
|
||||
address: 'Address',
|
||||
actions: 'Actions',
|
||||
changeMailAddress: 'Change Mail Address',
|
||||
unbindMailAddress: 'Unbind Mail Address credential',
|
||||
bind: 'Bind',
|
||||
bindAddressSuccess: 'Bind Address Success',
|
||||
},
|
||||
zh: {
|
||||
tip: '这些地址存储在您的浏览器中,如果您清除浏览器缓存,可能会丢失。',
|
||||
success: '成功',
|
||||
address: '地址',
|
||||
actions: '操作',
|
||||
changeMailAddress: '切换邮箱地址',
|
||||
unbindMailAddress: '解绑邮箱地址',
|
||||
bind: '绑定',
|
||||
bindAddressSuccess: '绑定地址成功',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const tabValue = ref('address')
|
||||
const localAddressCache = useLocalStorage("LocalAddressCache", []);
|
||||
const data = computed(() => {
|
||||
// @ts-ignore
|
||||
if (!localAddressCache.value.includes(jwt.value)) {
|
||||
// @ts-ignore
|
||||
localAddressCache.value.push(jwt.value)
|
||||
}
|
||||
return localAddressCache.value.map((curJwt: string) => {
|
||||
try {
|
||||
var payload = JSON.parse(
|
||||
decodeURIComponent(
|
||||
atob(curJwt.split(".")[1]
|
||||
.replace(/-/g, "+").replace(/_/g, "/")
|
||||
)
|
||||
)
|
||||
);
|
||||
return {
|
||||
valid: true,
|
||||
address: payload.address,
|
||||
jwt: curJwt
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
valid: false,
|
||||
address: `invalid jwt [${curJwt}]`,
|
||||
jwt: curJwt
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
const bindAddress = async () => {
|
||||
try {
|
||||
// @ts-ignore
|
||||
if (!localAddressCache.value.includes(jwt.value)) {
|
||||
// @ts-ignore
|
||||
localAddressCache.value.push(jwt.value)
|
||||
}
|
||||
tabValue.value = 'address'
|
||||
message.success(t('bindAddressSuccess'));
|
||||
} catch (error) {
|
||||
message.error((error as Error).message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('address'),
|
||||
key: "address"
|
||||
},
|
||||
{
|
||||
title: t('actions'),
|
||||
key: 'actions',
|
||||
render(row: any) {
|
||||
return h('div', [
|
||||
h(NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => {
|
||||
jwt.value = row.jwt
|
||||
location.reload()
|
||||
}
|
||||
},
|
||||
{
|
||||
trigger: () => h(NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
type: "primary",
|
||||
},
|
||||
{ default: () => t('changeMailAddress') }
|
||||
),
|
||||
default: () => `${t('changeMailAddress')}?`
|
||||
}
|
||||
),
|
||||
h(NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => {
|
||||
if (jwt.value === row.jwt) {
|
||||
return;
|
||||
}
|
||||
localAddressCache.value = localAddressCache.value.filter(
|
||||
(curJwt: string) => curJwt !== row.jwt
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
trigger: () => h(NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
disabled: jwt.value === row.jwt,
|
||||
type: "warning",
|
||||
},
|
||||
{ default: () => t('unbindMailAddress') }
|
||||
),
|
||||
default: () => `${t('unbindMailAddress')}?`
|
||||
}
|
||||
)
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-alert type="warning" :show-icon="false">
|
||||
<span>{{ t('tip') }}</span>
|
||||
</n-alert>
|
||||
<n-tabs type="segment" v-model:value="tabValue">
|
||||
<n-tab-pane name="address" :tab="t('address')">
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="bind" :tab="t('bind')">
|
||||
<Login :bindUserAddress="bindAddress" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
</template>
|
||||
@@ -10,12 +10,11 @@ import { api } from '../../api'
|
||||
// @ts-ignore
|
||||
import Login from '../common/Login.vue';
|
||||
|
||||
const { localeCache, jwt, telegramApp } = useGlobalState()
|
||||
const { jwt, telegramApp } = useGlobalState()
|
||||
// @ts-ignore
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
success: 'success',
|
||||
|
||||
@@ -7,12 +7,11 @@ import { useGlobalState } from '../../store'
|
||||
// @ts-ignore
|
||||
import { api } from '../../api'
|
||||
|
||||
const { localeCache, settings } = useGlobalState()
|
||||
const { settings } = useGlobalState()
|
||||
// @ts-ignore
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
successTip: 'Success',
|
||||
|
||||
@@ -6,13 +6,13 @@ import { NBadge, NPopconfirm, NButton } from 'naive-ui'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import { getRouterPathWithLang } from '../../utils'
|
||||
|
||||
const { localeCache, jwt } = useGlobalState()
|
||||
const { jwt } = useGlobalState()
|
||||
const message = useMessage()
|
||||
const router = useRouter()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
const { locale, t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
success: 'success',
|
||||
@@ -48,7 +48,7 @@ const changeMailAddress = async (address_id) => {
|
||||
return;
|
||||
}
|
||||
jwt.value = res.jwt;
|
||||
await router.push('/');
|
||||
await router.push(getRouterPathWithLang("/", locale.value))
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
|
||||
@@ -6,10 +6,9 @@ import { useRouter } from 'vue-router'
|
||||
import { useGlobalState } from '../../store'
|
||||
import Login from '../common/Login.vue'
|
||||
|
||||
const { userJwt, localeCache, userSettings, } = useGlobalState()
|
||||
const { userJwt, userSettings, } = useGlobalState()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
logout: 'Logout',
|
||||
|
||||
@@ -11,11 +11,10 @@ const message = useMessage()
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
localeCache, userSettings, userJwt, userOpenSettings
|
||||
userSettings, userJwt, userOpenSettings
|
||||
} = useGlobalState()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
currentUser: 'Current Login User',
|
||||
|
||||
@@ -10,12 +10,11 @@ import { hashPassword } from '../../utils';
|
||||
|
||||
import Turnstile from '../../components/Turnstile.vue';
|
||||
|
||||
const { userJwt, localeCache, userTab, userOpenSettings } = useGlobalState()
|
||||
const { userJwt, userTab, userOpenSettings } = useGlobalState()
|
||||
const message = useMessage();
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
login: 'Login',
|
||||
|
||||
@@ -6,14 +6,13 @@ import { useRouter } from 'vue-router'
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { userJwt, localeCache, userSettings, } = useGlobalState()
|
||||
const { userJwt, userSettings, } = useGlobalState()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
const showLogout = ref(false)
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
logout: 'Logout',
|
||||
|
||||
1
mail-parser-wasm/.gitignore
vendored
1
mail-parser-wasm/.gitignore
vendored
@@ -12,3 +12,4 @@ Cargo.lock
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
web/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mail-parser-wasm"
|
||||
version = "0.1.6"
|
||||
version = "0.1.8"
|
||||
edition = "2021"
|
||||
description = "A simple mail parser for wasm"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,16 +1,45 @@
|
||||
# mail-parser-wasm
|
||||
# mail-parser-wasm web and cf worker
|
||||
|
||||
## usage
|
||||
## [mail-parser-wasm](https://www.npmjs.com/package/mail-parser-wasm)
|
||||
|
||||
### mail-parser-wasm usage
|
||||
|
||||
```bash
|
||||
pnpm add mail-parser-wasm
|
||||
```
|
||||
|
||||
```js
|
||||
import { parse_message } from 'mail-parser-wasm'
|
||||
|
||||
const parsedEmail = parse_message(item.raw);
|
||||
const parsedEmail = parse_message(rawEmail);
|
||||
```
|
||||
|
||||
## build
|
||||
### mail-parser-wasm build
|
||||
|
||||
```bash
|
||||
wasm-pack build --release
|
||||
wasm-pack publish
|
||||
```
|
||||
|
||||
## [mail-parser-wasm-worker](https://www.npmjs.com/package/mail-parser-wasm-worker)
|
||||
|
||||
### mail-parser-wasm-worker usage
|
||||
|
||||
```bash
|
||||
pnpm add mail-parser-wasm-worker
|
||||
```
|
||||
|
||||
```js
|
||||
import { parse_message_wrapper } from 'mail-parser-wasm-worker'
|
||||
|
||||
const parsedEmail = parse_message_wrapper(rawEmail);
|
||||
```
|
||||
|
||||
### mail-parser-wasm-worker build
|
||||
|
||||
```bash
|
||||
wasm-pack build --out-dir web --target web --release
|
||||
find web/ -type f ! -name '*.json' ! -name '.gitignore' -exec cp {} worker/ \;
|
||||
# modify worker/package.json version or whatever
|
||||
pnpm publish worker --no-git-checks
|
||||
```
|
||||
|
||||
9
mail-parser-wasm/worker/index.d.ts
vendored
Normal file
9
mail-parser-wasm/worker/index.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import initAsync, { MessageResult } from './mail_parser_wasm';
|
||||
import MODULE from './mail_parser_wasm_bg.wasm';
|
||||
export { initAsync, MODULE };
|
||||
export * from './mail_parser_wasm';
|
||||
/**
|
||||
* @param {string} raw_message
|
||||
* @returns {MessageResult}
|
||||
*/
|
||||
export function parse_message_wrapper(raw_message: string): MessageResult;
|
||||
12
mail-parser-wasm/worker/index.js
Normal file
12
mail-parser-wasm/worker/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import initAsync, { initSync, parse_message } from './mail_parser_wasm';
|
||||
import MODULE from './mail_parser_wasm_bg.wasm';
|
||||
|
||||
initSync(MODULE);
|
||||
|
||||
|
||||
export { initAsync, MODULE };
|
||||
export * from './mail_parser_wasm';
|
||||
export const parse_message_wrapper = (raw_message) => {
|
||||
initSync(MODULE);
|
||||
return parse_message(raw_message);
|
||||
}
|
||||
24
mail-parser-wasm/worker/package.json
Normal file
24
mail-parser-wasm/worker/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "mail-parser-wasm-worker",
|
||||
"description": "A simple mail parser for worker",
|
||||
"homepage": "https://github.com/dreamhunter2333/cloudflare_temp_email/tree/main/mail-parser-wasm",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dreamhunter2333/cloudflare_temp_email",
|
||||
"directory": "mail-parser-wasm"
|
||||
},
|
||||
"version": "0.1.8",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"mail_parser_wasm_bg.wasm",
|
||||
"mail_parser_wasm.js",
|
||||
"mail_parser_wasm.d.ts",
|
||||
"index.js",
|
||||
"index.d.ts"
|
||||
],
|
||||
"module": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"sideEffects": [
|
||||
"./snippets/*"
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
const API_PATHS = ["/api/", "/open_api/", "/user_api/", "/admin/"]
|
||||
const API_PATHS = [
|
||||
"/api/",
|
||||
"/open_api/",
|
||||
"/user_api/",
|
||||
"/admin/",
|
||||
"/telegram/"
|
||||
];
|
||||
|
||||
export async function onRequest(context) {
|
||||
const reqPath = new URL(context.request.url).pathname;
|
||||
|
||||
@@ -96,7 +96,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||
},
|
||||
{
|
||||
text: '通过命令行部署',
|
||||
collapsed: false,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: '命令行部署准备', link: 'cli/pre-requisite' },
|
||||
{ text: 'D1 数据库', link: 'cli/d1' },
|
||||
@@ -109,7 +109,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||
},
|
||||
{
|
||||
text: '通过用户界面部署',
|
||||
collapsed: false,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'D1 数据库', link: 'ui/d1' },
|
||||
{ text: '配置 DKIM', link: 'dkim' },
|
||||
@@ -121,25 +121,27 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||
},
|
||||
{
|
||||
text: '通过 Github Actions 部署',
|
||||
collapsed: false,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: '通过 Github Actions 部署', link: 'github-action' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '附加功能',
|
||||
collapsed: false,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: '配置 SMTP IMAP 代理服务', link: 'feature/config-smtp-proxy' },
|
||||
{ text: '发送邮件 API', link: 'feature/send-mail-api' },
|
||||
{ text: '查看邮件 API', link: 'feature/mail-api' },
|
||||
{ text: '配置子域名邮箱', link: 'feature/subdomain' },
|
||||
{ text: '配置 Telegram Bot', link: 'feature/telegram' },
|
||||
{ text: '配置 S3 附件', link: 'feature/s3-attachment' },
|
||||
{ text: '配置 worker 使用 wasm 解析邮件', link: 'feature/mail_parser_wasm_worker' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '功能简介',
|
||||
collapsed: false,
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Admin 控制台', link: 'feature/admin' },
|
||||
{ text: 'Admin 用户管理', link: 'feature/admin-user-management' },
|
||||
|
||||
@@ -74,7 +74,11 @@ node_compat = true
|
||||
# ]
|
||||
|
||||
[vars]
|
||||
# TITLE = "Custom Title" # The title of the site
|
||||
PREFIX = "tmp" # The mailbox name prefix to be processed
|
||||
# (min, max) length of the adderss, if not set, the default is (1, 30)
|
||||
# MIN_ADDRESS_LEN = 1
|
||||
# MAX_ADDRESS_LEN = 30
|
||||
# If you want your site to be private, uncomment below and change your password
|
||||
# PASSWORDS = ["123", "456"]
|
||||
# admin console password, if not configured, access to the console is not allowed
|
||||
@@ -104,6 +108,8 @@ ENABLE_AUTO_REPLY = false
|
||||
# DKIM_PRIVATE_KEY = "" # Refer to the contents of priv_key.txt in the DKIM section
|
||||
# telegram bot
|
||||
# TG_MAX_ACCOUNTS = 5
|
||||
# global forward address list, if set, all emails will be forwarded to these addresses
|
||||
# FORWARD_ADDRESS_LIST = ["xxx@xxx.com"]
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
|
||||
BIN
vitepress-docs/docs/public/feature/s3-download.png
Normal file
BIN
vitepress-docs/docs/public/feature/s3-download.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
BIN
vitepress-docs/docs/public/feature/s3-save.png
Normal file
BIN
vitepress-docs/docs/public/feature/s3-save.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@@ -42,7 +42,11 @@ node_compat = true
|
||||
# ]
|
||||
|
||||
[vars]
|
||||
# TITLE = "Custom Title" # 自定义网站标题
|
||||
PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空字符串
|
||||
# (min, max) adderss的长度,如果不设置,默认为(1, 30)
|
||||
# MIN_ADDRESS_LEN = 1
|
||||
# MAX_ADDRESS_LEN = 30
|
||||
# 如果你想要你的网站私有,取消下面的注释,并修改密码
|
||||
# PASSWORDS = ["123", "456"]
|
||||
# admin 控制台密码, 不配置则不允许访问控制台
|
||||
@@ -72,6 +76,8 @@ ENABLE_AUTO_REPLY = false
|
||||
# DKIM_PRIVATE_KEY = "" # 参考 DKIM 部分 priv_key.txt 的内容
|
||||
# telegram bot 最多绑定邮箱数量
|
||||
# TG_MAX_ACCOUNTS = 5
|
||||
# 全局转发地址列表,如果不配置则不启用,启用后所有邮件都会转发到列表中的地址
|
||||
# FORWARD_ADDRESS_LIST = ["xxx@xxx.com"]
|
||||
|
||||
# D1 数据库的名称和 ID 可以在 cloudflare 控制台查看
|
||||
[[d1_databases]]
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 搭建 SMTP IMAP 代理服务
|
||||
|
||||
::: warning
|
||||
如果你使用了 `resend`, 可直接使用 `resend` 的 `SMTP` 服务,不需要使用此服务
|
||||
:::
|
||||
|
||||
## 为什么需要 SMTP IMAP 代理服务
|
||||
|
||||
`SMTP` `IMAP` 的应用场景更加广泛
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
limit = 10
|
||||
offset = 0
|
||||
res = requests.get(
|
||||
f"http://localhost:8787/api/mails?limit={limit}&offset={offset}`;",
|
||||
f"http://localhost:8787/api/mails?limit={limit}&offset={offset}",
|
||||
headers={
|
||||
"Authorization": f"Bearer {你的JWT密码}",
|
||||
# "x-custom-auth": "<你的网站密码>", # 如果启用了自定义密码
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# mail-parser-wasm-worker
|
||||
|
||||
> [!NOTE]
|
||||
> 如果你使用了 webhook 转发,或者 telegram bot 接受邮件,但是邮件内容是乱码,或者无法解析,你对解析的需要更高的要求,可以使用这个功能。
|
||||
|
||||
## 修改代码
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
pnpm add mail-parser-wasm-worker
|
||||
```
|
||||
|
||||
编辑 `worker/src/common.ts`, 取消注释这段代码,使用 mail-parser-wasm-worker 来解析邮件
|
||||
|
||||
```ts
|
||||
export const commonParseMail = async (raw_mail: string | undefined | null): Promise<{
|
||||
sender: string,
|
||||
subject: string,
|
||||
text: string,
|
||||
html: string
|
||||
} | undefined> => {
|
||||
if (!raw_mail) {
|
||||
return undefined;
|
||||
}
|
||||
// 取消注释这段代码,使用 mail-parser-wasm-worker 来解析邮件 start
|
||||
// TODO: WASM parse email
|
||||
try {
|
||||
const { parse_message_wrapper } = await import('mail-parser-wasm-worker');
|
||||
|
||||
const parsedEmail = parse_message_wrapper(raw_mail);
|
||||
return {
|
||||
sender: parsedEmail.sender || "",
|
||||
subject: parsedEmail.subject || "",
|
||||
text: parsedEmail.text || "",
|
||||
html: parsedEmail.body_html || "",
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Failed use mail-parser-wasm-worker to parse email", e);
|
||||
}
|
||||
// 取消注释这段代码,使用 mail-parser-wasm-worker 来解析邮件 end
|
||||
try {
|
||||
const { default: PostalMime } = await import('postal-mime');
|
||||
const parsedEmail = await PostalMime.parse(raw_mail);
|
||||
return {
|
||||
sender: parsedEmail.from ? `${parsedEmail.from.name} <${parsedEmail.from.address}>` : "",
|
||||
subject: parsedEmail.subject || "",
|
||||
text: parsedEmail.text || "",
|
||||
html: parsedEmail.html || "",
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
console.error("Failed use PostalMime to parse email", e);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
```
|
||||
|
||||
## 部署
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
pnpm run deploy
|
||||
```
|
||||
34
vitepress-docs/docs/zh/guide/feature/s3-attachment.md
Normal file
34
vitepress-docs/docs/zh/guide/feature/s3-attachment.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# 配置 S3 附件
|
||||
|
||||
## 配置
|
||||
|
||||
> [!NOTE]
|
||||
> 如果不需要 S3 附件, 可跳过此步骤
|
||||
|
||||
在 Cloudflare 创建一个 R2 bucket, 你也可以使用其他的 S3 服务(如有 bug 请提 issue)
|
||||
|
||||
参考: [配置 Cloudflare R2 的 cors](https://developers.cloudflare.com/r2/buckets/cors/#add-cors-policies-from-the-dashboard)
|
||||
|
||||
参考 [Cloudflare R2 s3 toke](https://developers.cloudflare.com/r2/api/s3/tokens/) 创建 token, 拿到 `ENDPOINT`, `Access Key ID` 和 `Secret Access Key`,然后执行下面的命令添加到 secrets 中
|
||||
|
||||
> [!NOTE]
|
||||
> 你也可以在 Cloudflare worker 的 UI 界面中添加 `secrets`
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
pnpm wrangler secret put S3_ENDPOINT
|
||||
pnpm wrangler secret put S3_ACCESS_KEY_ID
|
||||
pnpm wrangler secret put S3_SECRET_ACCESS_KEY
|
||||
# 请注意这里的 bucket 是你的 bucket 名称
|
||||
pnpm wrangler secret put S3_BUCKET
|
||||
```
|
||||
|
||||
## 使用
|
||||
|
||||
保存附件
|
||||
|
||||

|
||||
|
||||
下载附件
|
||||
|
||||

|
||||
@@ -1,5 +1,18 @@
|
||||
# 配置 Telegram Bot
|
||||
|
||||
## Telegram Bot 配置
|
||||
|
||||
> [!NOTE]
|
||||
> 如果不需要 Telegram Bot, 可跳过此步骤
|
||||
|
||||
请先创建一个 Telegram Bot,然后获取 `token`,然后执行下面的命令,将 `token` 添加到 secrets 中
|
||||
|
||||
你也可以在 Cloudflare 的 UI 界面中添加 `secrets`
|
||||
|
||||
```bash
|
||||
pnpm wrangler secret put TELEGRAM_BOT_TOKEN
|
||||
```
|
||||
|
||||
## Bot
|
||||
|
||||
- 可设置白名单用户
|
||||
|
||||
@@ -11,15 +11,17 @@
|
||||
"build": "wrangler deploy --dry-run --outdir dist --minify"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240512.0",
|
||||
"@cloudflare/workers-types": "^4.20240603.0",
|
||||
"@eslint/js": "8.56.0",
|
||||
"eslint": "8.56.0",
|
||||
"globals": "^15.3.0",
|
||||
"typescript-eslint": "^7.10.0",
|
||||
"wrangler": "^3.57.1"
|
||||
"typescript-eslint": "^7.12.0",
|
||||
"wrangler": "^3.58.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"hono": "^4.3.9",
|
||||
"@aws-sdk/client-s3": "^3.588.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.588.0",
|
||||
"hono": "^4.4.3",
|
||||
"mimetext": "^3.0.24",
|
||||
"postal-mime": "^2.2.5",
|
||||
"resend": "^3.2.0",
|
||||
|
||||
1496
worker/pnpm-lock.yaml
generated
1496
worker/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -40,7 +40,7 @@ api.post('/admin/new_address', async (c) => {
|
||||
return c.text("Please provide a name", 400)
|
||||
}
|
||||
try {
|
||||
const res = await newAddress(c, name, domain, enablePrefix);
|
||||
const res = await newAddress(c, name, domain, enablePrefix, false);
|
||||
return c.json(res);
|
||||
} catch (e) {
|
||||
return c.text(`Failed create address: ${(e as Error).message}`, 400)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Hono } from 'hono'
|
||||
|
||||
import { getDomains, getPasswords, getBooleanValue } from './utils';
|
||||
import { getDomains, getPasswords, getBooleanValue, getIntValue } from './utils';
|
||||
import { CONSTANTS } from './constants';
|
||||
import { HonoCustomType } from './types';
|
||||
import { isS3Enabled } from './mails_api/s3_attachment';
|
||||
|
||||
const api = new Hono<HonoCustomType>
|
||||
|
||||
@@ -15,7 +16,10 @@ api.get('/open_api/settings', async (c) => {
|
||||
needAuth = !auth || !passwords.includes(auth);
|
||||
}
|
||||
return c.json({
|
||||
"title": c.env.TITLE,
|
||||
"prefix": c.env.PREFIX,
|
||||
"minAddressLen": getIntValue(c.env.MIN_ADDRESS_LEN, 1),
|
||||
"maxAddressLen": getIntValue(c.env.MAX_ADDRESS_LEN, 30),
|
||||
"domains": getDomains(c),
|
||||
"needAuth": needAuth,
|
||||
"adminContact": c.env.ADMIN_CONTACT,
|
||||
@@ -26,6 +30,7 @@ api.get('/open_api/settings', async (c) => {
|
||||
"copyright": c.env.COPYRIGHT,
|
||||
"cfTurnstileSiteKey": c.env.CF_TURNSTILE_SITE_KEY,
|
||||
"enableWebhook": getBooleanValue(c.env.ENABLE_WEBHOOK),
|
||||
"isS3Enabled": isS3Enabled(c),
|
||||
"version": CONSTANTS.VERSION,
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,28 +1,39 @@
|
||||
import { Context } from 'hono';
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { getBooleanValue, getDomains, getStringValue } from './utils';
|
||||
import { getBooleanValue, getDomains, getStringValue, getIntValue } from './utils';
|
||||
import { HonoCustomType } from './types';
|
||||
import { unbindTelegramByAddress } from './telegram_api/common';
|
||||
|
||||
export const newAddress = async (
|
||||
c: Context<HonoCustomType>,
|
||||
name: string, domain: string | undefined | null,
|
||||
enablePrefix: boolean
|
||||
enablePrefix: boolean,
|
||||
checkLengthByConfig: boolean = true
|
||||
): Promise<{ address: string, jwt: string }> => {
|
||||
// remove special characters
|
||||
name = name.replace(/[^a-zA-Z0-9.]/g, '')
|
||||
// name min length min 1
|
||||
const minAddressLength = Math.max(
|
||||
checkLengthByConfig ? getIntValue(c.env.MIN_ADDRESS_LEN, 1) : 1,
|
||||
1
|
||||
);
|
||||
// name max length min 1
|
||||
const maxAddressLength = Math.max(
|
||||
checkLengthByConfig ? getIntValue(c.env.MAX_ADDRESS_LEN, 30) : 30,
|
||||
1
|
||||
);
|
||||
// check name length
|
||||
if (name.length <= 0) {
|
||||
throw new Error("Name too short")
|
||||
if (name.length < minAddressLength) {
|
||||
throw new Error(`Name too short (min ${minAddressLength})`);
|
||||
}
|
||||
if (name.length > maxAddressLength) {
|
||||
throw new Error(`Name too long (max ${maxAddressLength})`);
|
||||
}
|
||||
// create address
|
||||
if (enablePrefix) {
|
||||
name = getStringValue(c.env.PREFIX) + name;
|
||||
}
|
||||
if (name.length >= 30) {
|
||||
throw new Error("Name too long (max 30)")
|
||||
}
|
||||
// check domain, generate random domain
|
||||
const domains = getDomains(c);
|
||||
if (!domain || !domains.includes(domain)) {
|
||||
@@ -166,3 +177,43 @@ export const handleListQuery = async (
|
||||
).bind(...params).first("count") : 0;
|
||||
return c.json({ results, count });
|
||||
}
|
||||
|
||||
|
||||
export const commonParseMail = async (raw_mail: string | undefined | null): Promise<{
|
||||
sender: string,
|
||||
subject: string,
|
||||
text: string,
|
||||
html: string
|
||||
} | undefined> => {
|
||||
if (!raw_mail) {
|
||||
return undefined;
|
||||
}
|
||||
// TODO: WASM parse email
|
||||
// try {
|
||||
// const { parse_message_wrapper } = await import('mail-parser-wasm-worker');
|
||||
|
||||
// const parsedEmail = parse_message_wrapper(raw_mail);
|
||||
// return {
|
||||
// sender: parsedEmail.sender || "",
|
||||
// subject: parsedEmail.subject || "",
|
||||
// text: parsedEmail.text || "",
|
||||
// html: parsedEmail.body_html || "",
|
||||
// };
|
||||
// } catch (e) {
|
||||
// console.error("Failed use mail-parser-wasm-worker to parse email", e);
|
||||
// }
|
||||
try {
|
||||
const { default: PostalMime } = await import('postal-mime');
|
||||
const parsedEmail = await PostalMime.parse(raw_mail);
|
||||
return {
|
||||
sender: parsedEmail.from ? `${parsedEmail.from.name} <${parsedEmail.from.address}>` : "",
|
||||
subject: parsedEmail.subject || "",
|
||||
text: parsedEmail.text || "",
|
||||
html: parsedEmail.html || "",
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
console.error("Failed use PostalMime to parse email", e);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const CONSTANTS = {
|
||||
VERSION: 'v0.4.5',
|
||||
VERSION: 'v0.5.1',
|
||||
|
||||
// DB settings
|
||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Context } from "hono";
|
||||
|
||||
import { getEnvStringList } from "../utils";
|
||||
import { sendMailToTelegram } from "../telegram_api";
|
||||
import { Bindings, HonoCustomType } from "../types";
|
||||
import { auto_reply } from "./auto_reply";
|
||||
@@ -25,6 +26,16 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
|
||||
console.log(`Failed save message from ${message.from} to ${message.to}`);
|
||||
}
|
||||
|
||||
// forward email
|
||||
try {
|
||||
const forwardAddressList = getEnvStringList(env.FORWARD_ADDRESS_LIST)
|
||||
for (const forwardAddress of forwardAddressList) {
|
||||
await message.forward(forwardAddress);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("forward email error", error);
|
||||
}
|
||||
|
||||
// send email to telegram
|
||||
try {
|
||||
await sendMailToTelegram(
|
||||
|
||||
@@ -6,6 +6,7 @@ import { newAddress, handleListQuery, deleteAddressWithData } from '../common'
|
||||
import { CONSTANTS } from '../constants'
|
||||
import auto_reply from './auto_reply'
|
||||
import webhook_settings from './webhook_settings';
|
||||
import s3_attachment from './s3_attachment';
|
||||
|
||||
export const api = new Hono<HonoCustomType>()
|
||||
|
||||
@@ -14,6 +15,9 @@ api.post('/api/auto_reply', auto_reply.saveAutoReply)
|
||||
api.get('/api/webhook/settings', webhook_settings.getWebhookSettings)
|
||||
api.post('/api/webhook/settings', webhook_settings.saveWebhookSettings)
|
||||
api.post('/api/webhook/test', webhook_settings.testWebhookSettings)
|
||||
api.get('/api/attachment/list', s3_attachment.list)
|
||||
api.post('/api/attachment/put_url', s3_attachment.getSignedPutUrl)
|
||||
api.post('/api/attachment/get_url', s3_attachment.getSignedGetUrl)
|
||||
|
||||
api.get('/api/mails', async (c) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
|
||||
84
worker/src/mails_api/s3_attachment.ts
Normal file
84
worker/src/mails_api/s3_attachment.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { HonoCustomType } from "../types";
|
||||
import { Context } from "hono";
|
||||
import {
|
||||
S3Client,
|
||||
ListObjectsV2Command,
|
||||
GetObjectCommand,
|
||||
PutObjectCommand
|
||||
} from "@aws-sdk/client-s3";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
|
||||
export const isS3Enabled = (c: Context<HonoCustomType>) => {
|
||||
return !(!c.env.S3_ENDPOINT ||
|
||||
!c.env.S3_ACCESS_KEY_ID ||
|
||||
!c.env.S3_SECRET_ACCESS_KEY ||
|
||||
!c.env.S3_BUCKET);
|
||||
}
|
||||
|
||||
const getS3Client = (c: Context<HonoCustomType>) => {
|
||||
if (
|
||||
!c.env.S3_ENDPOINT ||
|
||||
!c.env.S3_ACCESS_KEY_ID ||
|
||||
!c.env.S3_SECRET_ACCESS_KEY ||
|
||||
!c.env.S3_BUCKET
|
||||
) {
|
||||
throw new Error("S3 config is not set");
|
||||
}
|
||||
return new S3Client({
|
||||
region: "auto",
|
||||
endpoint: c.env.S3_ENDPOINT,
|
||||
credentials: {
|
||||
accessKeyId: c.env.S3_ACCESS_KEY_ID,
|
||||
secretAccessKey: c.env.S3_SECRET_ACCESS_KEY,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
getSignedGetUrl: async (c: Context<HonoCustomType>) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
const { key } = await c.req.json()
|
||||
const client = getS3Client(c);
|
||||
const url = await getSignedUrl(
|
||||
client,
|
||||
new GetObjectCommand({
|
||||
Bucket: c.env.S3_BUCKET,
|
||||
Key: `${address}/${key}`
|
||||
}),
|
||||
{ expiresIn: c.env.S3_URL_EXPIRES || 360 }
|
||||
);
|
||||
return c.json({ url });
|
||||
},
|
||||
getSignedPutUrl: async (c: Context<HonoCustomType>) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
const { key } = await c.req.json()
|
||||
const client = getS3Client(c);
|
||||
const url = await getSignedUrl(
|
||||
client,
|
||||
new PutObjectCommand({
|
||||
Bucket: c.env.S3_BUCKET,
|
||||
Key: `${address}/${key}`
|
||||
}),
|
||||
{ expiresIn: c.env.S3_URL_EXPIRES || 360 }
|
||||
);
|
||||
return c.json({ url });
|
||||
},
|
||||
list: async (c: Context<HonoCustomType>) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
const client = getS3Client(c);
|
||||
const data = await client.send(
|
||||
new ListObjectsV2Command({
|
||||
Bucket: c.env.S3_BUCKET,
|
||||
Prefix: `${address}/`
|
||||
})
|
||||
);
|
||||
return c.json(
|
||||
{
|
||||
results: data?.Contents
|
||||
?.map((v) => v.Key?.replace(`${address}/`, ""))
|
||||
?.filter(k => k)
|
||||
?.map((k) => ({ key: k }))
|
||||
}
|
||||
);
|
||||
},
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { HonoCustomType } from "../types";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { AdminWebhookSettings, WebhookMail } from "../models";
|
||||
import { getBooleanValue } from "../utils";
|
||||
import PostalMime from 'postal-mime';
|
||||
import { commonParseMail } from "../common";
|
||||
|
||||
|
||||
class WebhookSettings {
|
||||
@@ -15,10 +15,10 @@ class WebhookSettings {
|
||||
body: string = JSON.stringify({
|
||||
"from": "${from}",
|
||||
"to": "${to}",
|
||||
"headers": "${headers}",
|
||||
"subject": "${subject}",
|
||||
"raw": "${raw}",
|
||||
"parsedText": "${parsedText}",
|
||||
"parsedHtml": "${parsedHtml}",
|
||||
}, null, 2)
|
||||
}
|
||||
|
||||
@@ -98,14 +98,14 @@ export async function trigerWebhook(
|
||||
if (!settings) {
|
||||
return;
|
||||
}
|
||||
const parsedEmail = await PostalMime.parse(raw_mail);
|
||||
const parsedEmail = await commonParseMail(raw_mail);
|
||||
const res = await sendWebhook(settings, {
|
||||
from: parsedEmail.from.address || "",
|
||||
from: parsedEmail?.sender || "",
|
||||
to: address,
|
||||
headers: JSON.stringify(parsedEmail.headers),
|
||||
subject: parsedEmail.subject || "",
|
||||
subject: parsedEmail?.subject || "",
|
||||
raw: raw_mail,
|
||||
parsedText: parsedEmail.text || parsedEmail.html || ""
|
||||
parsedText: parsedEmail?.text || "",
|
||||
parsedHtml: parsedEmail?.html || ""
|
||||
});
|
||||
if (!res.success) {
|
||||
console.log(res.message);
|
||||
@@ -119,14 +119,15 @@ async function testWebhookSettings(c: Context<HonoCustomType>): Promise<Response
|
||||
const raw = await c.env.DB.prepare(
|
||||
`SELECT raw FROM raw_mails WHERE address = ? ORDER BY RANDOM() LIMIT 1`
|
||||
).bind(address).first<string>("raw");
|
||||
const parsedEmail = raw ? await PostalMime.parse(raw) : {} as any;
|
||||
|
||||
const parsedEmail = await commonParseMail(raw);
|
||||
const res = await sendWebhook(settings, {
|
||||
from: parsedEmail?.from?.address || "test@test.com",
|
||||
from: parsedEmail?.sender || "test@test.com",
|
||||
to: address,
|
||||
headers: JSON.stringify(parsedEmail?.headers || { "X-Test": "test" }),
|
||||
subject: parsedEmail?.subject || "test subject",
|
||||
raw: raw || "test raw email",
|
||||
parsedText: parsedEmail?.text || parsedEmail?.html || "test parsed text"
|
||||
parsedText: parsedEmail?.text || "test parsed text",
|
||||
parsedHtml: parsedEmail?.html || "test parsed html"
|
||||
});
|
||||
if (!res.success) {
|
||||
return c.text(res.message || "send webhook error", 400);
|
||||
|
||||
@@ -9,10 +9,10 @@ export class AdminWebhookSettings {
|
||||
export type WebhookMail = {
|
||||
from: string;
|
||||
to: string;
|
||||
headers: string;
|
||||
subject: string;
|
||||
raw: string;
|
||||
parsedText: string;
|
||||
parsedHtml: string;
|
||||
}
|
||||
|
||||
export class CleanupSettings {
|
||||
|
||||
@@ -102,6 +102,7 @@ export const unbindTelegramAddress = async (
|
||||
export const unbindTelegramByAddress = async (
|
||||
c: Context<HonoCustomType>, address: string
|
||||
): Promise<boolean> => {
|
||||
if (!c.env.KV) return true;
|
||||
const userId = await c.env.KV.get<string>(`${CONSTANTS.TG_KV_PREFIX}:${address}`)
|
||||
if (userId) {
|
||||
return await unbindTelegramAddress(c, userId, address);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
|
||||
import { Context } from "hono";
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
import { Telegraf, Context as TgContext, Markup } from "telegraf";
|
||||
import { callbackQuery } from "telegraf/filters";
|
||||
import PostalMime from 'postal-mime';
|
||||
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { getDomains, getStringValue } from '../utils';
|
||||
import { HonoCustomType } from "../types";
|
||||
import { TelegramSettings } from "./settings";
|
||||
import { bindTelegramAddress, deleteTelegramAddress, jwtListToAddressData, tgUserNewAddress, unbindTelegramAddress, unbindTelegramByAddress } from "./common";
|
||||
import { commonParseMail } from "../common";
|
||||
|
||||
|
||||
const COMMANDS = [
|
||||
{
|
||||
@@ -195,13 +195,13 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
if (!db_address_id) {
|
||||
return await ctx.reply("无效地址");
|
||||
}
|
||||
const { raw, id: mailId } = await c.env.DB.prepare(
|
||||
const { raw, id: mailId, created_at } = await c.env.DB.prepare(
|
||||
`SELECT * FROM raw_mails where address = ? `
|
||||
+ ` order by id desc limit 1 offset ?`
|
||||
).bind(
|
||||
queryAddress, mailIndex
|
||||
).first<{ raw: string, id: string }>() || {};
|
||||
const { mail } = raw ? await parseMail(raw) : { mail: "已经没有邮件了" };
|
||||
).first<{ raw: string, id: string, created_at: string }>() || {};
|
||||
const { mail } = raw ? await parseMail(raw, queryAddress, created_at) : { mail: "已经没有邮件了" };
|
||||
const settings = await c.env.KV.get<TelegramSettings>(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
|
||||
const miniAppButtons = []
|
||||
if (settings?.miniAppUrl && settings?.miniAppUrl?.length > 0 && mailId) {
|
||||
@@ -265,22 +265,26 @@ export async function initTelegramBotCommands(bot: Telegraf) {
|
||||
await bot.telegram.setMyCommands(COMMANDS);
|
||||
}
|
||||
|
||||
const parseMail = async (raw_mail: string | undefined | null) => {
|
||||
const parseMail = async (
|
||||
raw_mail: string | undefined | null,
|
||||
address: string, created_at: string | undefined | null
|
||||
) => {
|
||||
if (!raw_mail) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const parsedEmail = await PostalMime.parse(raw_mail);
|
||||
if (parsedEmail?.text?.length && parsedEmail?.text?.length > 1000) {
|
||||
parsedEmail.text = parsedEmail.text.substring(0, 1000) + "...消息过长请到miniapp查看";
|
||||
const parsedEmail = await commonParseMail(raw_mail);
|
||||
let parsedText = parsedEmail?.text || "";
|
||||
if (parsedText.length && parsedText.length > 1000) {
|
||||
parsedText = parsedEmail?.text.substring(0, 1000) + "\n\n...\n消息过长请到miniapp查看";
|
||||
}
|
||||
return {
|
||||
isHtml: false,
|
||||
mail: `From: ${parsedEmail.from ? `${parsedEmail.from.name}[${parsedEmail.from.address}]` : "无发件人"}\n`
|
||||
+ `To: ${parsedEmail.to?.map(t => `${t.name}[${t.address}]`).join(" ")}\n`
|
||||
+ `Subject: ${parsedEmail.subject}\n`
|
||||
+ `Date: ${parsedEmail.date}\n`
|
||||
+ `Content:\n${parsedEmail.text || "解析失败,请打开 mini app 查看"}`
|
||||
mail: `From: ${parsedEmail?.sender || "无发件人"}\n`
|
||||
+ `To: ${address}\n`
|
||||
+ (created_at ? `Date: ${created_at}\n` : "")
|
||||
+ `Subject: ${parsedEmail?.subject}\n`
|
||||
+ `Content:\n${parsedText || "解析失败,请打开 mini app 查看"}`
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
@@ -299,7 +303,7 @@ export async function sendMailToTelegram(
|
||||
return;
|
||||
}
|
||||
const userId = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${address}`);
|
||||
const { mail } = await parseMail(raw_mail);
|
||||
const { mail } = await parseMail(raw_mail, address, new Date().toUTCString());
|
||||
if (!mail) {
|
||||
return;
|
||||
}
|
||||
|
||||
11
worker/src/types.d.ts
vendored
11
worker/src/types.d.ts
vendored
@@ -6,7 +6,10 @@ export type Bindings = {
|
||||
SEND_MAIL: any
|
||||
|
||||
// config
|
||||
TITLE: string | undefined
|
||||
PREFIX: string | undefined
|
||||
MIN_ADDRESS_LEN: string | number | undefined
|
||||
MAX_ADDRESS_LEN: string | number | undefined
|
||||
DOMAINS: string | string[] | undefined
|
||||
PASSWORDS: string | string[] | undefined
|
||||
ADMIN_PASSWORDS: string | string[] | undefined
|
||||
@@ -20,6 +23,14 @@ export type Bindings = {
|
||||
DEFAULT_SEND_BALANCE: number | string | undefined
|
||||
ADMIN_CONTACT: string | undefined
|
||||
COPYRIGHT: string | undefined
|
||||
FORWARD_ADDRESS_LIST: string | string[] | undefined
|
||||
|
||||
// s3 config
|
||||
S3_ENDPOINT: string | undefined
|
||||
S3_ACCESS_KEY_ID: string | undefined
|
||||
S3_SECRET_ACCESS_KEY: string | undefined
|
||||
S3_BUCKET: string | undefined
|
||||
S3_URL_EXPIRES: number | undefined
|
||||
|
||||
// dkim
|
||||
DKIM_SELECTOR: string | undefined
|
||||
|
||||
@@ -129,6 +129,23 @@ export const getAdminPasswords = (c: Context<HonoCustomType>): string[] => {
|
||||
return c.env.ADMIN_PASSWORDS.filter((item) => item.length > 0);
|
||||
}
|
||||
|
||||
export const getEnvStringList = (value: string | string[] | undefined): string[] => {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
// check if is an array, if not use json.parse
|
||||
if (!Array.isArray(value)) {
|
||||
try {
|
||||
const res = JSON.parse(value) as string[];
|
||||
return res.filter((item) => item.length > 0);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse ADMIN_PASSWORDS", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return value.filter((item) => item.length > 0);
|
||||
}
|
||||
|
||||
export const sendAdminInternalMail = async (
|
||||
c: Context<HonoCustomType>, toMail: string, subject: string, text: string
|
||||
): Promise<boolean> => {
|
||||
|
||||
@@ -125,8 +125,14 @@ app.route('/', apiV1)
|
||||
app.route('/', apiSendMail)
|
||||
app.route('/', telegramApi)
|
||||
|
||||
app.get('/', async c => c.text("OK"))
|
||||
app.get('/health_check', async c => c.text("OK"))
|
||||
app.get('/', async c => {
|
||||
if (!c.env.DB) { return c.text("DB is not available", 400); }
|
||||
return c.text("OK");
|
||||
})
|
||||
app.get('/health_check', async c => {
|
||||
if (!c.env.DB) { return c.text("DB is not available", 400); }
|
||||
return c.text("OK");
|
||||
})
|
||||
app.all('/*', async c => c.text("Not Found", 404))
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,11 @@ node_compat = true
|
||||
# ]
|
||||
|
||||
[vars]
|
||||
# TITLE = "Custom Title" # custom title
|
||||
PREFIX = "tmp"
|
||||
# (min, max) length of the adderss, if not set, the default is (1, 30)
|
||||
# MIN_ADDRESS_LEN = 1
|
||||
# MAX_ADDRESS_LEN = 30
|
||||
# IF YOU WANT TO MAKE YOUR SITE PRIVATE, UNCOMMENT THE FOLLOWING LINES
|
||||
# PASSWORDS = ["123", "456"]
|
||||
# For admin panel
|
||||
@@ -46,6 +50,8 @@ ENABLE_AUTO_REPLY = false
|
||||
# DKIM_PRIVATE_KEY = ""
|
||||
# telegram bot
|
||||
# TG_MAX_ACCOUNTS = 5
|
||||
# global forward address list, if set, all emails will be forwarded to these addresses
|
||||
# FORWARD_ADDRESS_LIST = ["xxx@xxx.com"]
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
|
||||
Reference in New Issue
Block a user