diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1f8f35d1..e49908ad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,12 @@
# CHANGE LOG
+## main branch
+
+- UI lazy load
+- telegram bot 添加用户全局推送功能
+- 增加对 cloudflare verified 用户发送邮件
+
## v0.4.4
- 增加 telegram mini app
diff --git a/frontend/src/views/admin/AccountSettings.vue b/frontend/src/views/admin/AccountSettings.vue
index 85db40a4..892387d7 100644
--- a/frontend/src/views/admin/AccountSettings.vue
+++ b/frontend/src/views/admin/AccountSettings.vue
@@ -17,6 +17,7 @@ const { t } = useI18n({
address_block_list: 'Address Block Keywords for Users(Admin can skip)',
address_block_list_placeholder: 'Please enter the keywords you want to block',
send_address_block_list: 'Address Block Keywords for send email',
+ verified_address_list: 'Verified Address List(Can send email by cf internal api)',
},
zh: {
save: '保存',
@@ -24,18 +25,21 @@ const { t } = useI18n({
address_block_list: '邮件地址屏蔽关键词(管理员可跳过检查)',
address_block_list_placeholder: '请输入您想要屏蔽的关键词',
send_address_block_list: '发送邮件地址屏蔽关键词',
+ verified_address_list: '已验证地址列表(可通过 cf 内部 api 发送邮件)',
}
}
});
const addressBlockList = ref([])
const sendAddressBlockList = ref([])
+const verifiedAddressList = ref([])
const fetchData = async () => {
try {
const res = await api.fetch(`/admin/account_settings`)
addressBlockList.value = res.blockList || []
sendAddressBlockList.value = res.sendBlockList || []
+ verifiedAddressList.value = res.verifiedAddressList || []
} catch (error) {
message.error(error.message || "error");
}
@@ -47,7 +51,8 @@ const save = async () => {
method: 'POST',
body: JSON.stringify({
blockList: addressBlockList.value || [],
- sendBlockList: sendAddressBlockList.value || []
+ sendBlockList: sendAddressBlockList.value || [],
+ verifiedAddressList: verifiedAddressList.value || []
})
})
message.success(t('successTip'))
@@ -73,6 +78,10 @@ onMounted(async () => {
+
+
+
{{ t('save') }}
diff --git a/frontend/src/views/admin/Telegram.vue b/frontend/src/views/admin/Telegram.vue
index 6a8e0998..2a15516a 100644
--- a/frontend/src/views/admin/Telegram.vue
+++ b/frontend/src/views/admin/Telegram.vue
@@ -23,6 +23,8 @@ const { t } = useI18n({
telegramAllowList: 'Telegram Allow List',
save: 'Save',
miniAppUrl: 'Telegram Mini App URL',
+ enableGlobalMailPush: 'Enable Global Mail Push(Manually input telegram user ID)',
+ globalMailPushList: 'Global Mail Push List',
},
zh: {
init: '初始化',
@@ -33,6 +35,8 @@ const { t } = useI18n({
telegramAllowList: 'Telegram 白名单',
save: '保存',
miniAppUrl: '电报小程序 URL(请输入你部署的电报小程序网页地址)',
+ enableGlobalMailPush: '启用全局邮件推送(手动输入 telegram 用户 ID)',
+ globalMailPushList: '全局邮件推送用户列表',
}
}
});
@@ -66,15 +70,22 @@ class TelegramSettings {
enableAllowList: boolean;
allowList: string[];
miniAppUrl: string;
+ enableGlobalMailPush: boolean;
+ globalMailPushList: string[];
- constructor(enableAllowList: boolean, allowList: string[], miniAppUrl: string) {
+ constructor(
+ enableAllowList: boolean, allowList: string[], miniAppUrl: string,
+ enableGlobalMailPush: boolean, globalMailPushList: string[]
+ ) {
this.enableAllowList = enableAllowList;
this.allowList = allowList;
this.miniAppUrl = miniAppUrl;
+ this.enableGlobalMailPush = enableGlobalMailPush;
+ this.globalMailPushList = globalMailPushList;
}
}
-const settings = ref(new TelegramSettings(false, [], ''))
+const settings = ref(new TelegramSettings(false, [], '', false, []))
const getSettings = async () => {
try {
@@ -115,6 +126,15 @@ onMounted(async () => {
:placeholder="t('telegramAllowList')" />
+
+
+
+ {{ t('enable') }}
+
+
+
+
diff --git a/vitepress-docs/docs/en/cli.md b/vitepress-docs/docs/en/cli.md
index 2f929fc1..9f608ef8 100644
--- a/vitepress-docs/docs/en/cli.md
+++ b/vitepress-docs/docs/en/cli.md
@@ -68,6 +68,11 @@ node_compat = true
# [triggers]
# crons = [ "0 0 * * *" ]
+# send mail by cf mail
+# send_email = [
+# { name = "SEND_MAIL" },
+# ]
+
[vars]
PREFIX = "tmp" # The mailbox name prefix to be processed
# If you want your site to be private, uncomment below and change your password
diff --git a/vitepress-docs/docs/zh/guide/cli/worker.md b/vitepress-docs/docs/zh/guide/cli/worker.md
index 7623667b..6740faff 100644
--- a/vitepress-docs/docs/zh/guide/cli/worker.md
+++ b/vitepress-docs/docs/zh/guide/cli/worker.md
@@ -36,6 +36,11 @@ node_compat = true
# [triggers]
# crons = [ "0 0 * * *" ]
+# 通过 Cloudflare 发送邮件
+# send_email = [
+# { name = "SEND_MAIL" },
+# ]
+
[vars]
PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空字符串
# 如果你想要你的网站私有,取消下面的注释,并修改密码
diff --git a/worker/src/admin_api/admin_user_api.js b/worker/src/admin_api/admin_user_api.ts
similarity index 79%
rename from worker/src/admin_api/admin_user_api.js
rename to worker/src/admin_api/admin_user_api.ts
index 8a8ba644..bae73dc1 100644
--- a/worker/src/admin_api/admin_user_api.js
+++ b/worker/src/admin_api/admin_user_api.ts
@@ -1,21 +1,27 @@
+import { Context } from 'hono';
+
import { CONSTANTS } from '../constants';
import { getJsonSetting, saveSetting, checkUserPassword, getDomains } from '../utils';
import { UserSettings, GeoData, UserInfo } from "../models";
import { handleListQuery } from '../common'
+import { HonoCustomType } from '../types';
export default {
- getSetting: async (c) => {
+ getSetting: async (c: Context) => {
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
const settings = new UserSettings(value);
return c.json(settings)
},
- saveSetting: async (c) => {
+ saveSetting: async (c: Context) => {
const value = await c.req.json();
const settings = new UserSettings(value);
if (settings.enableMailVerify && !c.env.KV) {
return c.text("Please enable KV first if you want to enable mail verify", 403)
}
- if (settings.enableMailVerify) {
+ if (settings.enableMailVerify && !settings.verifyMailSender) {
+ return c.text("Please provide verifyMailSender", 400)
+ }
+ if (settings.enableMailVerify && settings.verifyMailSender) {
const mailDomain = settings.verifyMailSender.split("@")[1];
const domains = getDomains(c);
if (!domains.includes(mailDomain)) {
@@ -28,7 +34,7 @@ export default {
await saveSetting(c, CONSTANTS.USER_SETTINGS_KEY, JSON.stringify(settings));
return c.json({ success: true })
},
- getUsers: async (c) => {
+ getUsers: async (c: Context) => {
const { limit, offset, query } = c.req.query();
if (query) {
return await handleListQuery(c,
@@ -48,15 +54,15 @@ export default {
[], limit, offset
);
},
- createUser: async (c) => {
+ createUser: async (c: Context) => {
const { email, password } = await c.req.json();
if (!email || !password) {
return c.text("Invalid email or password", 400)
}
// geo data
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
- const geoData = new GeoData(reqIp, c.req.raw.cf);
- const userInfo = new UserInfo(geoData);
+ const geoData = new GeoData(reqIp, c.req.raw.cf as any);
+ const userInfo = new UserInfo(geoData, email);
try {
checkUserPassword(password);
const { success } = await c.env.DB.prepare(
@@ -69,14 +75,15 @@ export default {
return c.text("Failed to register", 500)
}
} catch (e) {
- if (e.message && e.message.includes("UNIQUE")) {
+ const errorMsg = (e as Error).message;
+ if (errorMsg && errorMsg.includes("UNIQUE")) {
return c.text("User already exists", 400)
}
- return c.text(`Failed to register: ${e.message}`, 500)
+ return c.text(`Failed to register: ${errorMsg}`, 500)
}
return c.json({ success: true })
},
- deleteUser: async (c) => {
+ deleteUser: async (c: Context) => {
const { user_id } = c.req.param();
if (!user_id) return c.text("Invalid user_id", 400);
const { success } = await c.env.DB.prepare(
@@ -90,7 +97,7 @@ export default {
}
return c.json({ success: true })
},
- resetPassword: async (c) => {
+ resetPassword: async (c: Context) => {
const { user_id } = c.req.param();
const { password } = await c.req.json();
if (!user_id) return c.text("Invalid user_id", 400);
@@ -103,7 +110,7 @@ export default {
return c.text("Failed to reset password", 500)
}
} catch (e) {
- return c.text(`Failed to reset password: ${e.message}`, 500)
+ return c.text(`Failed to reset password: ${(e as Error).message}`, 500)
}
return c.json({ success: true });
},
diff --git a/worker/src/admin_api/cleanup_api.js b/worker/src/admin_api/cleanup_api.ts
similarity index 72%
rename from worker/src/admin_api/cleanup_api.js
rename to worker/src/admin_api/cleanup_api.ts
index a2bdc7f3..65a89703 100644
--- a/worker/src/admin_api/cleanup_api.js
+++ b/worker/src/admin_api/cleanup_api.ts
@@ -1,25 +1,28 @@
+import { Context } from 'hono';
+
import { cleanup } from '../common';
import { CONSTANTS } from '../constants';
import { getJsonSetting, saveSetting } from '../utils';
import { CleanupSettings } from '../models';
+import { HonoCustomType } from '../types';
export default {
- cleanup: async (c) => {
+ cleanup: async (c: Context) => {
const { cleanType, cleanDays } = await c.req.json();
try {
await cleanup(c, cleanType, cleanDays);
} catch (error) {
console.error(error);
- return c.text(`Failed to cleanup ${error.message}`, 500)
+ return c.text(`Failed to cleanup ${(error as Error).message}`, 500)
}
return c.json({ success: true })
},
- getCleanup: async (c) => {
+ getCleanup: async (c: Context) => {
const value = await getJsonSetting(c, CONSTANTS.AUTO_CLEANUP_KEY);
const cleanupSetting = new CleanupSettings(value);
return c.json(cleanupSetting)
},
- saveCleanup: async (c) => {
+ saveCleanup: async (c: Context) => {
const value = await c.req.json();
const cleanupSetting = new CleanupSettings(value);
await saveSetting(c, CONSTANTS.AUTO_CLEANUP_KEY, JSON.stringify(cleanupSetting));
diff --git a/worker/src/admin_api/index.js b/worker/src/admin_api/index.ts
similarity index 89%
rename from worker/src/admin_api/index.js
rename to worker/src/admin_api/index.ts
index 3cca9deb..63e5bc81 100644
--- a/worker/src/admin_api/index.js
+++ b/worker/src/admin_api/index.ts
@@ -1,5 +1,7 @@
import { Hono } from 'hono'
import { Jwt } from 'hono/utils/jwt'
+
+import { HonoCustomType } from '../types'
import { sendAdminInternalMail, getJsonSetting, saveSetting } from '../utils'
import { newAddress, handleListQuery } from '../common'
import { CONSTANTS } from '../constants'
@@ -7,7 +9,7 @@ import cleanup_api from './cleanup_api'
import admin_user_api from './admin_user_api'
import webhook_settings from './webhook_settings'
-const api = new Hono()
+export const api = new Hono()
api.get('/admin/address', async (c) => {
const { limit, offset, query } = c.req.query();
@@ -41,7 +43,7 @@ api.post('/admin/new_address', async (c) => {
const res = await newAddress(c, name, domain, enablePrefix);
return c.json(res);
} catch (e) {
- return c.text(`Failed create address: ${e.message}`, 400)
+ return c.text(`Failed create address: ${(e as Error).message}`, 400)
}
})
@@ -181,16 +183,16 @@ api.get('/admin/sendbox', async (c) => {
api.get('/admin/statistics', async (c) => {
const { count: mailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM raw_mails`
- ).first();
+ ).first<{ count: number }>() || {};
const { count: addressCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM address`
- ).first();
+ ).first<{ count: number }>() || {};
const { count: activeUserCount7days } = await c.env.DB.prepare(
`SELECT count(*) as count FROM address where updated_at > datetime('now', '-7 day')`
- ).first();
+ ).first<{ count: number }>() || {};
const { count: sendMailCount } = await c.env.DB.prepare(
`SELECT count(*) as count FROM sendbox`
- ).first();
+ ).first<{ count: number }>() || {};
return c.json({
mailCount: mailCount,
userCount: addressCount,
@@ -201,13 +203,13 @@ api.get('/admin/statistics', async (c) => {
api.get('/admin/account_settings', async (c) => {
try {
- /** @type {Array|undefined|null} */
const blockList = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY);
- /** @type {Array|undefined|null} */
const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY);
+ const verifiedAddressList = await getJsonSetting(c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY);
return c.json({
blockList: blockList || [],
- sendBlockList: sendBlockList || []
+ sendBlockList: sendBlockList || [],
+ verifiedAddressList: verifiedAddressList || []
})
} catch (error) {
console.error(error);
@@ -217,10 +219,13 @@ api.get('/admin/account_settings', async (c) => {
api.post('/admin/account_settings', async (c) => {
/** @type {{ blockList: Array, sendBlockList: Array }} */
- const { blockList, sendBlockList } = await c.req.json();
- if (!blockList || !sendBlockList) {
+ const { blockList, sendBlockList, verifiedAddressList } = await c.req.json();
+ if (!blockList || !sendBlockList || !verifiedAddressList) {
return c.text("Invalid blockList or sendBlockList", 400)
}
+ if (!c.env.SEND_MAIL && verifiedAddressList.length > 0) {
+ return c.text("Please enable SEND_MAIL to use verifiedAddressList", 400)
+ }
await saveSetting(
c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY,
JSON.stringify(blockList)
@@ -229,6 +234,10 @@ api.post('/admin/account_settings', async (c) => {
c, CONSTANTS.SEND_BLOCK_LIST_KEY,
JSON.stringify(sendBlockList)
);
+ await saveSetting(
+ c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY,
+ JSON.stringify(verifiedAddressList)
+ )
return c.json({
success: true
})
@@ -245,5 +254,3 @@ api.post('/admin/users', admin_user_api.createUser)
api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword)
api.get("/admin/webhook/settings", webhook_settings.getWebhookSettings);
api.post("/admin/webhook/settings", webhook_settings.saveWebhookSettings);
-
-export { api }
diff --git a/worker/src/admin_api/webhook_settings.ts b/worker/src/admin_api/webhook_settings.ts
index 8cc7b1e0..0834a6ba 100644
--- a/worker/src/admin_api/webhook_settings.ts
+++ b/worker/src/admin_api/webhook_settings.ts
@@ -1,7 +1,7 @@
import { Context } from "hono";
import { HonoCustomType } from "../types";
import { CONSTANTS } from "../constants";
-import { AdminWebhookSettings } from "../models/models";
+import { AdminWebhookSettings } from "../models";
async function getWebhookSettings(c: Context): Promise {
const settings = await c.env.KV.get(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
diff --git a/worker/src/constants.ts b/worker/src/constants.ts
index b0edfe87..572d70d0 100644
--- a/worker/src/constants.ts
+++ b/worker/src/constants.ts
@@ -6,6 +6,7 @@ export const CONSTANTS = {
SEND_BLOCK_LIST_KEY: 'send_block_list',
AUTO_CLEANUP_KEY: 'auto_cleanup',
USER_SETTINGS_KEY: 'user_settings',
+ VERIFIED_ADDRESS_LIST_KEY: 'verified_address_list',
// KV
TG_KV_PREFIX: "temp-mail-telegram",
diff --git a/worker/src/mails_api/send_mail_api.ts b/worker/src/mails_api/send_mail_api.ts
index 5179cdcc..262fd15f 100644
--- a/worker/src/mails_api/send_mail_api.ts
+++ b/worker/src/mails_api/send_mail_api.ts
@@ -1,8 +1,10 @@
import { Context, Hono } from 'hono'
import { Jwt } from 'hono/utils/jwt'
+import { createMimeMessage } from 'mimetext';
+
import { CONSTANTS } from '../constants'
import { getJsonSetting, getDomains, getIntValue } from '../utils';
-import { GeoData } from '../models/models'
+import { GeoData } from '../models'
import { handleListQuery } from '../common'
import { HonoCustomType } from '../types';
@@ -34,6 +36,30 @@ api.post('/api/requset_send_mail_access', async (c) => {
return c.json({ status: "ok" })
})
+export const sendMailToVerifyAddress = async (
+ c: Context, address: string,
+ reqJson: {
+ from_name: string, to_mail: string, to_name: string,
+ subject: string, content: string, is_html: boolean
+ }
+) => {
+ const {
+ from_name, to_mail, to_name,
+ subject, content, is_html
+ } = reqJson;
+ const msg = createMimeMessage();
+ msg.setSender({ name: from_name, addr: address });
+ msg.setRecipient({ name: to_name, addr: to_mail });
+ msg.setSubject(subject);
+ msg.addMessage({
+ contentType: is_html ? 'text/html' : 'text/plain',
+ data: content
+ });
+ const { EmailMessage } = await import('cloudflare:email');
+ const message = new EmailMessage(address, to_mail, msg.asRaw());
+ await c.env.SEND_MAIL.send(message);
+}
+
export const sendMail = async (
c: Context, address: string,
reqJson: {
@@ -78,6 +104,15 @@ export const sendMail = async (
if (!content) {
throw new Error("Invalid content")
}
+ // send to verified address list, do not update balance
+ if (c.env.SEND_MAIL) {
+ const verifiedAddressList = await getJsonSetting(c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY) || [];
+ if (verifiedAddressList.includes(to_mail)) {
+ return sendMailToVerifyAddress(c, address, {
+ from_name, to_mail, to_name, subject, content, is_html
+ });
+ }
+ }
let dmikBody = {}
if (c.env.DKIM_SELECTOR && c.env.DKIM_PRIVATE_KEY && address.includes("@")) {
dmikBody = {
@@ -169,7 +204,7 @@ api.post('/external/api/send_mail', async (c) => {
return c.text("No address", 400)
}
const reqJson = await c.req.json();
- await sendMail(c, address, reqJson);
+ await sendMail(c, address as string, reqJson);
return c.json({ status: "ok" })
} catch (e) {
console.error("Failed to send mail", e);
diff --git a/worker/src/mails_api/webhook_settings.ts b/worker/src/mails_api/webhook_settings.ts
index 50626904..98e1900a 100644
--- a/worker/src/mails_api/webhook_settings.ts
+++ b/worker/src/mails_api/webhook_settings.ts
@@ -1,7 +1,7 @@
import { Context } from "hono";
import { HonoCustomType } from "../types";
import { CONSTANTS } from "../constants";
-import { AdminWebhookSettings, WebhookMail } from "../models/models";
+import { AdminWebhookSettings, WebhookMail } from "../models";
import { getBooleanValue } from "../utils";
import PostalMime from 'postal-mime';
diff --git a/worker/src/models/index.js b/worker/src/models/index.js
deleted file mode 100644
index 7ec5efde..00000000
--- a/worker/src/models/index.js
+++ /dev/null
@@ -1,87 +0,0 @@
-export class UserSettings {
- /** @param {UserSettings|undefined|null} data */
- constructor(data) {
- if (data === null) {
- return;
- }
- const {
- enable, enableMailVerify, verifyMailSender,
- enableMailAllowList, mailAllowList, maxAddressCount
- } = data || {};
- /** @type {boolean|undefined} */
- this.enable = enable;
- /** @type {boolean|undefined} */
- this.enableMailVerify = enableMailVerify;
- /** @type {string|undefined} */
- this.verifyMailSender = verifyMailSender;
- /** @type {boolean|undefined} */
- this.enableMailAllowList = enableMailAllowList;
- /** @type {Array|undefined} */
- this.mailAllowList = mailAllowList;
- /** @type {number|undefined} */
- this.maxAddressCount = maxAddressCount || 5;
- }
-}
-
-export class CleanupSettings {
- /** @param {CleanupSettings|undefined|null} data */
- constructor(data) {
- const {
- enableMailsAutoCleanup, cleanMailsDays,
- enableUnknowMailsAutoCleanup, cleanUnknowMailsDays,
- enableSendBoxAutoCleanup, cleanSendBoxDays
- } = data || {};
- /** @type {boolean|undefined} */
- this.enableMailsAutoCleanup = enableMailsAutoCleanup;
- /** @type {number|undefined} */
- this.cleanMailsDays = cleanMailsDays;
- /** @type {boolean|undefined} */
- this.enableUnknowMailsAutoCleanup = enableUnknowMailsAutoCleanup;
- /** @type {number|undefined} */
- this.cleanUnknowMailsDays = cleanUnknowMailsDays;
- /** @type {boolean|undefined} */
- this.enableSendBoxAutoCleanup = enableSendBoxAutoCleanup;
- /** @type {number|undefined} */
- this.cleanSendBoxDays = cleanSendBoxDays;
- }
-}
-
-export class GeoData {
- /** @param {string} ip @param {GeoData|undefined|null} data */
- constructor(ip, data) {
- const {
- country, city, timezone, postalCode, region,
- latitude, longitude, regionCode, asOrganization
- } = data || {};
- /** @type {string} */
- this.ip = ip;
- /** @type {string|undefined} */
- this.country = country;
- /** @type {string|undefined} */
- this.city = city;
- /** @type {string|undefined} */
- this.timezone = timezone;
- /** @type {string|undefined} */
- this.postalCode = postalCode;
- /** @type {string|undefined} */
- this.region = region;
- /** @type {number|undefined} */
- this.latitude = latitude;
- /** @type {number|undefined} */
- this.longitude = longitude;
- /** @type {string|undefined} */
- this.regionCode = regionCode;
- /** @type {string|undefined} */
- this.asOrganization = asOrganization;
- }
-}
-
-export class UserInfo {
- /** @param {GeoData} geoData @param {string} userEmail */
- constructor(geoData, userEmail) {
- /** @type {geoData} */
- this.geoData = geoData;
- /** @type {string} */
- this.userEmail = userEmail;
- }
-}
diff --git a/worker/src/models/models.ts b/worker/src/models/index.ts
similarity index 68%
rename from worker/src/models/models.ts
rename to worker/src/models/index.ts
index a0f9ef85..003afa7c 100644
--- a/worker/src/models/models.ts
+++ b/worker/src/models/index.ts
@@ -70,3 +70,37 @@ export class GeoData {
this.asOrganization = asOrganization;
}
}
+
+export class UserSettings {
+
+ enable: boolean | undefined;
+ enableMailVerify: boolean | undefined;
+ verifyMailSender: string | undefined;
+ enableMailAllowList: boolean | undefined;
+ mailAllowList: string[] | undefined;
+ maxAddressCount: number;
+
+ constructor(data: UserSettings | undefined | null) {
+ const {
+ enable, enableMailVerify, verifyMailSender,
+ enableMailAllowList, mailAllowList, maxAddressCount
+ } = data || {};
+ this.enable = enable;
+ this.enableMailVerify = enableMailVerify;
+ this.verifyMailSender = verifyMailSender;
+ this.enableMailAllowList = enableMailAllowList;
+ this.mailAllowList = mailAllowList;
+ this.maxAddressCount = maxAddressCount || 5;
+ }
+}
+
+export class UserInfo {
+
+ geoData: GeoData;
+ userEmail: string;
+
+ constructor(geoData: GeoData, userEmail: string) {
+ this.geoData = geoData;
+ this.userEmail = userEmail;
+ }
+}
diff --git a/worker/src/scheduled.ts b/worker/src/scheduled.ts
index 92bb6bf6..0bd9723d 100644
--- a/worker/src/scheduled.ts
+++ b/worker/src/scheduled.ts
@@ -2,7 +2,7 @@ import { Context } from 'hono';
import { cleanup } from './common'
import { CONSTANTS } from './constants'
import { getJsonSetting } from './utils';
-import { CleanupSettings } from './models/models';
+import { CleanupSettings } from './models';
import { Bindings, HonoCustomType } from './types';
export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any) {
diff --git a/worker/src/telegram_api/miniapp.ts b/worker/src/telegram_api/miniapp.ts
index 776a6f33..dc062df1 100644
--- a/worker/src/telegram_api/miniapp.ts
+++ b/worker/src/telegram_api/miniapp.ts
@@ -4,6 +4,7 @@ import { HonoCustomType } from "../types";
import { CONSTANTS } from "../constants";
import { bindTelegramAddress, jwtListToAddressData, tgUserNewAddress, unbindTelegramAddress } from "./common";
import { checkCfTurnstile } from "../utils";
+import { TelegramSettings } from "./settings";
const encoder = new TextEncoder();
const TG_AUTH_TIMEOUT = 300;
@@ -130,7 +131,12 @@ async function getMail(c: Context): Promise {
const result = await c.env.DB.prepare(
`SELECT * FROM raw_mails where id = ?`
).bind(mailId).first();
- if (result?.address && !(result.address as string in addressIdMap)) {
+ const settings = await c.env.KV.get(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
+ const superUser = settings?.enableGlobalMailPush && settings?.globalMailPushList.includes(userId);
+ if (
+ !superUser && result?.address &&
+ !(result.address as string in addressIdMap)
+ ) {
return c.text("无权查看此邮件", 403);
}
const address_id = addressIdMap[result?.address as string];
diff --git a/worker/src/telegram_api/settings.ts b/worker/src/telegram_api/settings.ts
index 7cd73154..667961ed 100644
--- a/worker/src/telegram_api/settings.ts
+++ b/worker/src/telegram_api/settings.ts
@@ -6,17 +6,24 @@ export class TelegramSettings {
enableAllowList: boolean;
allowList: string[];
miniAppUrl: string;
+ enableGlobalMailPush: boolean;
+ globalMailPushList: string[];
- constructor(enableAllowList: boolean, allowList: string[], miniAppUrl: string) {
+ constructor(
+ enableAllowList: boolean, allowList: string[], miniAppUrl: string,
+ enableGlobalMailPush: boolean, globalMailPushList: string[]
+ ) {
this.enableAllowList = enableAllowList;
this.allowList = allowList;
this.miniAppUrl = miniAppUrl;
+ this.enableGlobalMailPush = enableGlobalMailPush;
+ this.globalMailPushList = globalMailPushList;
}
}
async function getTelegramSettings(c: Context): Promise {
const settings = await c.env.KV.get(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
- return c.json(settings || new TelegramSettings(false, [], ""));
+ return c.json(settings || new TelegramSettings(false, [], "", false, []));
}
diff --git a/worker/src/telegram_api/telegram.ts b/worker/src/telegram_api/telegram.ts
index 95c22347..6081cccb 100644
--- a/worker/src/telegram_api/telegram.ts
+++ b/worker/src/telegram_api/telegram.ts
@@ -318,6 +318,15 @@ export async function sendMailToTelegram(
url.searchParams.set("mail_id", mailId);
miniAppButtons.push(Markup.button.webApp("查看邮件", url.toString()));
}
+ if (settings?.enableGlobalMailPush && settings?.globalMailPushList) {
+ for (const pushId of settings.globalMailPushList) {
+ await bot.telegram.sendMessage(pushId, mail, {
+ ...Markup.inlineKeyboard([
+ ...miniAppButtons,
+ ])
+ });
+ }
+ }
await bot.telegram.sendMessage(userId, mail, {
...Markup.inlineKeyboard([
...miniAppButtons,
diff --git a/worker/src/types.d.ts b/worker/src/types.d.ts
index d753ef9d..92f373fa 100644
--- a/worker/src/types.d.ts
+++ b/worker/src/types.d.ts
@@ -3,6 +3,7 @@ export type Bindings = {
DB: D1Database
KV: KVNamespace
RATE_LIMITER: any
+ SEND_MAIL: any
// config
PREFIX: string | undefined
diff --git a/worker/src/user_api/bind_address.js b/worker/src/user_api/bind_address.ts
similarity index 91%
rename from worker/src/user_api/bind_address.js
rename to worker/src/user_api/bind_address.ts
index 46585ccb..2859a7d8 100644
--- a/worker/src/user_api/bind_address.js
+++ b/worker/src/user_api/bind_address.ts
@@ -1,11 +1,13 @@
+import { Context } from 'hono';
import { Jwt } from 'hono/utils/jwt'
+import { HonoCustomType } from '../types';
import { UserSettings } from "../models";
import { getJsonSetting } from "../utils"
import { CONSTANTS } from "../constants";
export default {
- bind: async (c) => {
+ bind: async (c: Context) => {
const { user_id } = c.get("userPayload");
const { address_id } = c.get("jwtPayload");
if (!address_id || !user_id) {
@@ -36,7 +38,7 @@ export default {
if (settings.maxAddressCount > 0) {
const { count } = await c.env.DB.prepare(
`SELECT COUNT(*) as count FROM users_address where user_id = ?`
- ).bind(user_id).first();
+ ).bind(user_id).first<{ count: number }>() || { count: 0 };
if (count >= settings.maxAddressCount) {
return c.text("Max address count reached", 400)
}
@@ -50,14 +52,15 @@ export default {
return c.text("Failed to bind", 500)
}
} catch (e) {
- if (e.message && e.message.includes("UNIQUE")) {
+ const error = e as Error;
+ if (error.message && error.message.includes("UNIQUE")) {
return c.text("Address already binded, please unbind first", 400)
}
return c.text("Failed to bind", 500)
}
return c.json({ success: true })
},
- unbind: async (c) => {
+ unbind: async (c: Context) => {
const { user_id } = c.get("userPayload");
const { address_id } = await c.req.json();
if (!address_id || !user_id) {
@@ -90,7 +93,7 @@ export default {
}
return c.json({ success: true })
},
- getBindedAddresses: async (c) => {
+ getBindedAddresses: async (c: Context) => {
const { user_id } = c.get("userPayload");
if (!user_id) {
return c.text("No user token", 400)
@@ -110,7 +113,7 @@ export default {
results: results,
})
},
- getBindedAddressJwt: async (c) => {
+ getBindedAddressJwt: async (c: Context) => {
const { address_id } = c.req.param();
// check binded
const { user_id } = c.get("userPayload");
diff --git a/worker/src/user_api/index.ts b/worker/src/user_api/index.ts
index aa4a4191..78674635 100644
--- a/worker/src/user_api/index.ts
+++ b/worker/src/user_api/index.ts
@@ -1,11 +1,8 @@
import { Hono } from 'hono';
import { HonoCustomType } from '../types';
-// @ts-ignore
import settings from './settings';
-// @ts-ignore
import user from './user';
-// @ts-ignore
import bind_address from './bind_address';
export const api = new Hono();
diff --git a/worker/src/user_api/settings.js b/worker/src/user_api/settings.ts
similarity index 79%
rename from worker/src/user_api/settings.js
rename to worker/src/user_api/settings.ts
index 685aabb3..f575719e 100644
--- a/worker/src/user_api/settings.js
+++ b/worker/src/user_api/settings.ts
@@ -1,9 +1,12 @@
+import { Context } from "hono";
+
+import { HonoCustomType } from "../types";
import { UserSettings } from "../models";
import { getJsonSetting } from "../utils"
import { CONSTANTS } from "../constants";
export default {
- openSettings: async (c) => {
+ openSettings: async (c: Context) => {
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
const settings = new UserSettings(value);
return c.json({
@@ -11,7 +14,7 @@ export default {
enableMailVerify: settings.enableMailVerify,
})
},
- settings: async (c) => {
+ settings: async (c: Context) => {
const user = c.get("userPayload");
// check if user exists
const db_user_id = await c.env.DB.prepare(
diff --git a/worker/src/user_api/user.js b/worker/src/user_api/user.ts
similarity index 85%
rename from worker/src/user_api/user.js
rename to worker/src/user_api/user.ts
index 500d8f1c..ffe617a3 100644
--- a/worker/src/user_api/user.js
+++ b/worker/src/user_api/user.ts
@@ -1,12 +1,14 @@
+import { Context } from 'hono';
import { Jwt } from 'hono/utils/jwt'
+import { HonoCustomType } from '../types';
import { checkCfTurnstile, getJsonSetting, checkUserPassword } from "../utils"
import { CONSTANTS } from "../constants";
import { GeoData, UserInfo, UserSettings } from "../models";
import { sendMail } from "../mails_api/send_mail_api";
export default {
- verifyCode: async (c) => {
+ verifyCode: async (c: Context) => {
const { email, cf_token } = await c.req.json();
// check cf turnstile
try {
@@ -24,6 +26,9 @@ export default {
) {
return c.text(`Mail domain must in ${JSON.stringify(settings.mailAllowList, null, 2)}`, 400)
}
+ if (!settings.verifyMailSender) {
+ return c.text("Verify mail sender not set", 400)
+ }
// check if code exists in KV
const tmpcode = await c.env.KV.get(`temp-mail:${email}`)
if (tmpcode) {
@@ -34,12 +39,15 @@ export default {
// send code to email
try {
await sendMail(c, settings.verifyMailSender, {
- to_mail: email,
+ from_name: "Temp Mail Verify",
+ to_name: '',
+ to_mail: email as string,
subject: "Temp Mail Verify code",
content: `Your verify code is ${code}`,
+ is_html: false,
})
} catch (e) {
- return c.text(`Failed to send verify code: ${e.message}`, 500)
+ return c.text(`Failed to send verify code: ${(e as Error).message}`, 500)
}
// save to KV
await c.env.KV.put(`temp-mail:${email}`, code, { expirationTtl: 300 });
@@ -48,7 +56,7 @@ export default {
expirationTtl: 300
})
},
- register: async (c) => {
+ register: async (c: Context) => {
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
const settings = new UserSettings(value)
// check enable
@@ -67,6 +75,7 @@ export default {
// check mail domain allow list
const mailDomain = email.split("@")[1];
if (settings.enableMailAllowList
+ && settings.mailAllowList
&& !settings.mailAllowList.includes(mailDomain)
) {
return c.text(`Mail domain must in ${JSON.stringify(settings.mailAllowList, null, 2)}`, 400)
@@ -80,8 +89,8 @@ export default {
}
// geo data
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
- const geoData = new GeoData(reqIp, c.req.raw.cf);
- const userInfo = new UserInfo(geoData);
+ const geoData = new GeoData(reqIp, c.req.raw.cf as any);
+ const userInfo = new UserInfo(geoData, email);
// if not enable mail verify, do not on conflict update
if (!settings.enableMailVerify) {
try {
@@ -95,10 +104,11 @@ export default {
return c.text("Failed to register", 500)
}
} catch (e) {
- if (e.message && e.message.includes("UNIQUE")) {
+ const error = e as Error;
+ if (error.message && error.message.includes("UNIQUE")) {
return c.text("User already exists, please login", 400)
}
- return c.text(`Failed to register: ${e.message}`, 500)
+ return c.text(`Failed to register: ${error.message}`, 500)
}
return c.json({ success: true })
}
@@ -116,7 +126,7 @@ export default {
}
return c.json({ success: true })
},
- login: async (c) => {
+ login: async (c: Context) => {
const { email, password } = await c.req.json();
if (!email || !password) return c.text("Invalid email or password", 400);
const { id: user_id, password: dbPassword } = await c.env.DB.prepare(
diff --git a/worker/wrangler.toml.template b/worker/wrangler.toml.template
index a66cfd82..69e51d14 100644
--- a/worker/wrangler.toml.template
+++ b/worker/wrangler.toml.template
@@ -11,6 +11,10 @@ node_compat = true
# [triggers]
# crons = [ "0 0 * * *" ]
+# send_email = [
+# { name = "SEND_MAIL" },
+# ]
+
[vars]
PREFIX = "tmp"
# IF YOU WANT TO MAKE YOUR SITE PRIVATE, UNCOMMENT THE FOLLOWING LINES