Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76843a1aef | ||
|
|
59ad0264db | ||
|
|
da0b954a17 | ||
|
|
4e91689026 | ||
|
|
53d24b4f06 | ||
|
|
121c13704a | ||
|
|
43ed02392e | ||
|
|
4ea9527cb7 | ||
|
|
4bf5c97017 | ||
|
|
82ae8f07d5 | ||
|
|
679cb4c035 | ||
|
|
629a32c651 | ||
|
|
80a0cb65a5 | ||
|
|
a84b2ae469 | ||
|
|
22c35119c0 | ||
|
|
edba6b80d3 | ||
|
|
6ae3d43f9e | ||
|
|
40fc68e56c | ||
|
|
e68099edb3 | ||
|
|
fdf4259701 | ||
|
|
f6eaf6d0fd | ||
|
|
421a963a92 | ||
|
|
53c4db3bc9 | ||
|
|
f227bd75ca | ||
|
|
23b1d5e6be | ||
|
|
3a0ded3d01 | ||
|
|
db0e930bd1 | ||
|
|
1f898e8f65 | ||
|
|
421dc3b10a | ||
|
|
fbf889ab12 | ||
|
|
44bbf9c3da | ||
|
|
80242d5c24 | ||
|
|
09516c9cc8 | ||
|
|
84e7e2e9d8 | ||
|
|
65680c46fd | ||
|
|
ead8f9e2b8 | ||
|
|
7423a25fd6 | ||
|
|
5f45563c49 | ||
|
|
a45324a742 | ||
|
|
ac2c17e31c | ||
|
|
6189a91000 | ||
|
|
a46192b214 | ||
|
|
92e18ea683 | ||
|
|
baa70a863c | ||
|
|
d48b139aed | ||
|
|
c96e349fda | ||
|
|
984b734d4c | ||
|
|
a1c47d73e8 | ||
|
|
1b95707819 | ||
|
|
c14615d1ef | ||
|
|
8bf9adbd13 | ||
|
|
24f271a1ed | ||
|
|
f0bf6da963 | ||
|
|
522db4ee3a | ||
|
|
b60add170c | ||
|
|
154f536c61 | ||
|
|
e92a44b069 | ||
|
|
980b62f6ac | ||
|
|
af18ad3562 | ||
|
|
9ab766879e | ||
|
|
dafb8dda01 | ||
|
|
216f7272d8 | ||
|
|
24e37288e5 | ||
|
|
ee1b76cd1b | ||
|
|
5da6b44ad6 | ||
|
|
3ea917a572 | ||
|
|
5d2b49ff70 | ||
|
|
bfb896bae6 | ||
|
|
1d4979bc43 | ||
|
|
9f5076e3b5 | ||
|
|
d7c3cf27b0 | ||
|
|
31dbf1a9e6 | ||
|
|
8a47418c91 | ||
|
|
cbae3ffbdf | ||
|
|
d3235bdd17 | ||
|
|
ec3bd76666 | ||
|
|
5c11d16bdf | ||
|
|
7694278045 | ||
|
|
97b2be8d25 | ||
|
|
dc38535ea8 | ||
|
|
693e3aa9f1 | ||
|
|
6f83a440dc | ||
|
|
e723af4229 | ||
|
|
88a4b9dff5 | ||
|
|
69632c8cc6 | ||
|
|
9ef1edb341 | ||
|
|
dcd9e9c95f | ||
|
|
70923fee59 | ||
|
|
c61bda1828 | ||
|
|
0c4be2b723 | ||
|
|
9a089da3b7 | ||
|
|
0395992b5b | ||
|
|
025dc1fbdb | ||
|
|
56b42d29b3 | ||
|
|
a343a4a08f | ||
|
|
8c6cdd0279 | ||
|
|
3cf347ad7a | ||
|
|
6cd3dcb7ef | ||
|
|
4d00f0aeb7 | ||
|
|
37cad0f281 | ||
|
|
a4a1db22e8 | ||
|
|
7980e6a444 | ||
|
|
d5a5bf8c29 | ||
|
|
aded699314 | ||
|
|
57638aa8e1 | ||
|
|
fdf9e95d67 | ||
|
|
dad4a22356 | ||
|
|
083dc8daab | ||
|
|
1b441774a4 | ||
|
|
8ee140743a | ||
|
|
ad36be00a2 | ||
|
|
46cfd61b12 | ||
|
|
88da574dfb | ||
|
|
172f34dfe3 | ||
|
|
86fddf4a5d | ||
|
|
82bd308d56 | ||
|
|
457304eafb | ||
|
|
b21b63aea7 | ||
|
|
062c9f7dff | ||
|
|
8b74656c83 | ||
|
|
2c6803475b | ||
|
|
25a27ec038 | ||
|
|
b046f49ea2 | ||
|
|
2fd34b407e | ||
|
|
613582c13e | ||
|
|
4d31dbc9c4 | ||
|
|
c72230e812 | ||
|
|
2390143dc6 | ||
|
|
b4954e948b | ||
|
|
97a7950808 | ||
|
|
6f25991ff2 | ||
|
|
86a80900c1 | ||
|
|
562528e968 | ||
|
|
45994c24e3 | ||
|
|
ce09d4ac46 | ||
|
|
6219a1474e | ||
|
|
375613c219 | ||
|
|
344389cf0f | ||
|
|
fc0a5564f4 | ||
|
|
00fd14ad88 | ||
|
|
01cef2355d | ||
|
|
de0bb288ec | ||
|
|
799cc4277f | ||
|
|
0eae1bd9b8 | ||
|
|
8cd75868bc | ||
|
|
ff8ba81661 | ||
|
|
d3325f0fe3 | ||
|
|
ea8ad14049 | ||
|
|
6dc126d3b0 | ||
|
|
ab977ae64a | ||
|
|
181dcd87a3 | ||
|
|
6a11324ef9 | ||
|
|
46127a8d67 | ||
|
|
2437ff4599 | ||
|
|
658c1776d5 | ||
|
|
3a40cfc63b | ||
|
|
d03b7ca21d | ||
|
|
12833cd901 | ||
|
|
0e86b93e0c | ||
|
|
775d976f10 | ||
|
|
7267f03482 | ||
|
|
bbcff4908d | ||
|
|
249edb9e19 | ||
|
|
196fb4a6b5 | ||
|
|
bd28de5522 | ||
|
|
7762670c78 | ||
|
|
12e39c5f9c | ||
|
|
254325431b | ||
|
|
7e5a383bc6 | ||
|
|
f2c12216c8 | ||
|
|
1ecce5af21 | ||
|
|
ab6ea3847e | ||
|
|
5bc8dfdf00 | ||
|
|
b0daf8f5fd | ||
|
|
cfebeb1fda | ||
|
|
fc249dde6e | ||
|
|
3385635b26 | ||
|
|
e426ee7cbe | ||
|
|
e2aeaf3783 | ||
|
|
7a0711071d | ||
|
|
22b56781d5 | ||
|
|
52e5915d93 | ||
|
|
c72213b97d | ||
|
|
33141fe7dd | ||
|
|
bd244e48ec | ||
|
|
cd8767c1ce | ||
|
|
99bbdb3df1 | ||
|
|
2bef5ee0f6 | ||
|
|
136c0496b8 | ||
|
|
010e0451ab | ||
|
|
6af78c21ea | ||
|
|
9aa8d3a410 | ||
|
|
621859c454 | ||
|
|
f9956507fe | ||
|
|
7ec485907a | ||
|
|
bb6440b387 | ||
|
|
b6142483ed | ||
|
|
605829f180 | ||
|
|
ea309f9171 | ||
|
|
8054c0390c | ||
|
|
1c40dc8338 | ||
|
|
cdcd22962f | ||
|
|
9f9dc3d019 | ||
|
|
0692f8ec5e | ||
|
|
dac0383bab | ||
|
|
3190b47bb9 | ||
|
|
9b8598da57 | ||
|
|
462bb132e2 | ||
|
|
2341d14b2d | ||
|
|
de742d31e7 | ||
|
|
59c5be2623 | ||
|
|
aa02573217 | ||
|
|
ba2a56e1f0 | ||
|
|
741f839d09 | ||
|
|
ba3442ccb1 | ||
|
|
24ba4772e6 |
@@ -14,119 +14,258 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
D1_DATABASE_ID: ${{ secrets.D1_DATABASE_ID }}
|
||||
KV_NAMESPACE_ID: ${{ secrets.KV_NAMESPACE_ID }}
|
||||
R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }}
|
||||
DOMAIN: ${{ secrets.DOMAIN }}
|
||||
ADMIN: ${{ secrets.ADMIN }}
|
||||
JWT_SECRET: ${{ secrets.JWT_SECRET }}
|
||||
NAME: ${{ secrets.NAME || vars.NAME || 'cloud-mail' }}
|
||||
CUSTOM_DOMAIN: ${{ secrets.CUSTOM_DOMAIN || vars.CUSTOM_DOMAIN }}
|
||||
DOMAIN: ${{ secrets.DOMAIN || vars.DOMAIN }}
|
||||
ADMIN: ${{ secrets.ADMIN || vars.ADMIN }}
|
||||
JWT_SECRET: ${{ secrets.JWT_SECRET || vars.JWT_SECRET }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN || vars.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID || vars.CLOUDFLARE_ACCOUNT_ID }}
|
||||
D1_DATABASE_ID: ${{ secrets.D1_DATABASE_ID || vars.D1_DATABASE_ID }}
|
||||
KV_NAMESPACE_ID: ${{ secrets.KV_NAMESPACE_ID || vars.KV_NAMESPACE_ID }}
|
||||
R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME || vars.R2_BUCKET_NAME }}
|
||||
PROJECT_LINK: ${{ secrets.PROJECT_LINK || vars.PROJECT_LINK }}
|
||||
AI_MODEL: ${{ secrets.AI_MODEL || vars.AI_MODEL || '@cf/meta/llama-3.1-8b-instruct' }}
|
||||
CLOUDFLARE_EMAIL: ${{ secrets.CF_EMAIL || vars.CF_EMAIL || false }}
|
||||
ANALYSIS_CACHE: ${{ secrets.ANALYSIS_CACHE || vars.ANALYSIS_CACHE || false }}
|
||||
LINUXDO_CLIENT_ID: ${{ secrets.LINUXDO_CLIENT_ID || vars.LINUXDO_CLIENT_ID }}
|
||||
LINUXDO_CLIENT_SECRET: ${{ secrets.LINUXDO_CLIENT_SECRET || vars.LINUXDO_CLIENT_SECRET }}
|
||||
LINUXDO_CALLBACK_URL: ${{ secrets.LINUXDO_CALLBACK_URL || vars.LINUXDO_CALLBACK_URL }}
|
||||
LINUXDO_SWITCH: ${{ secrets.LINUXDO_SWITCH || vars.LINUXDO_SWITCH }}
|
||||
|
||||
|
||||
outputs:
|
||||
deployment_skipped: ${{ steps.deploy.outputs.deployment_skipped }}
|
||||
worker_url: ${{ steps.deploy.outputs.worker_url }}
|
||||
|
||||
|
||||
steps:
|
||||
- name: ➡️ 检出代码仓库 - Checkout repository
|
||||
- name: 🚚 检出代码仓库 / Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 📦 设置 pnpm - Setup pnpm
|
||||
- name: ⚙ 设置 pnpm / Set up pnpm
|
||||
uses: pnpm/[email protected]
|
||||
with:
|
||||
version: latest
|
||||
version: 9
|
||||
|
||||
- name: 📦 设置 Node.js - Setup Node.js
|
||||
- name: ⚙ 设置 Node.js / Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "./mail-worker/pnpm-lock.yaml"
|
||||
|
||||
- name: 📥 安装依赖 - Install dependencies
|
||||
- name: 📦 安装依赖 / Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
working-directory: ./mail-worker
|
||||
|
||||
- name: 📡 禁用 Wrangler 遥测 - Disable wrangler telemetry
|
||||
- name: 📡 禁用 Wrangler 遥测 / Disable wrangler telemetry
|
||||
working-directory: ./mail-worker
|
||||
run: npx wrangler telemetry disable
|
||||
run: pnpm wrangler telemetry disable
|
||||
|
||||
- name: 🛠️ 设置环境变量和部署 - Set Secrets and Deploy
|
||||
id: deploy
|
||||
- name: 🛠️ 设置环境 / Set up environment
|
||||
working-directory: ./mail-worker
|
||||
run: |
|
||||
if [ -z "$D1_DATABASE_ID" ] || [ -z "$KV_NAMESPACE_ID" ]; then
|
||||
echo "⚠️ Required secrets (D1_DATABASE_ID or KV_NAMESPACE_ID) are not set."
|
||||
echo "❌ Skipping deployment."
|
||||
echo "deployment_skipped=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
|
||||
echo "🔐 Starting environment setup..."
|
||||
|
||||
if [ -z "$JWT_SECRET" ] || grep -q '[?%#/\\]' <<< "$JWT_SECRET"; then
|
||||
echo "❌ JWT_SECRET variable is empty or contains invalid characters (?, %, #, /, \\)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "deployment_skipped=false" >> $GITHUB_OUTPUT
|
||||
if ! jq -e 'type == "array"' <<< "$DOMAIN" ; then
|
||||
echo "❌ DOMAIN variable must be a JSON array (e.g., [\"example.com\"])."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$ADMIN" ]; then
|
||||
echo "❌ ADMIN variable cannot be empty."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$CLOUDFLARE_ACCOUNT_ID" ]; then
|
||||
echo "❌ CLOUDFLARE_ACCOUNT_ID variable cannot be empty."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$CLOUDFLARE_API_TOKEN" ]; then
|
||||
echo "❌ CLOUDFLARE_API_TOKEN variable cannot be empty."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$CUSTOM_DOMAIN" ]; then
|
||||
echo "::warning:: CUSTOM_DOMAIN variable is not set."
|
||||
fi
|
||||
|
||||
CONFIG_FILE="wrangler-action.toml"
|
||||
|
||||
if [ -z "$R2_BUCKET_NAME" ]; then
|
||||
sed -i "18,20d" "$CONFIG_FILE"
|
||||
sed -i '/\[\[r2_buckets\]\]/,/^$/d' "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
echo "⚙️ Dynamically updating '$CONFIG_FILE' with binding IDs..."
|
||||
if [ -z "$PROJECT_LINK" ]; then
|
||||
sed -i '/^project_link = /d' "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
if [ -z "$LINUXDO_CLIENT_ID" ] || [ -z "$LINUXDO_CLIENT_SECRET" ]; then
|
||||
sed -i '/^linuxdo_client_id = /,/^linuxdo_switch = /d' "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
if [ -z "$CUSTOM_DOMAIN" ]; then
|
||||
sed -i '/\[\[routes\]\]/,/^$/d' "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
if [ "$(printf '%s' "$CLOUDFLARE_EMAIL" | tr '[:upper:]' '[:lower:]')" = "true" ]; then
|
||||
cat >> "$CONFIG_FILE" <<'EOF'
|
||||
|
||||
sed -i "s|\${D1_DATABASE_ID}|${D1_DATABASE_ID}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${KV_NAMESPACE_ID}|${KV_NAMESPACE_ID}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${R2_BUCKET_NAME}|${R2_BUCKET_NAME}|g" "$CONFIG_FILE"
|
||||
[[send_email]]
|
||||
name = "email"
|
||||
EOF
|
||||
fi
|
||||
|
||||
sed -i "s|\${NAME}|${NAME}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${CUSTOM_DOMAIN}|${CUSTOM_DOMAIN}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\"\${DOMAIN}\"|${DOMAIN}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${ADMIN}|${ADMIN}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${JWT_SECRET}|${JWT_SECRET}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${R2_BUCKET_NAME}|${R2_BUCKET_NAME}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${PROJECT_LINK}|${PROJECT_LINK}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${ANALYSIS_CACHE}|${ANALYSIS_CACHE}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${AI_MODEL}|${AI_MODEL}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${LINUXDO_CLIENT_ID}|${LINUXDO_CLIENT_ID}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${LINUXDO_CLIENT_SECRET}|${LINUXDO_CLIENT_SECRET}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${LINUXDO_CALLBACK_URL}|${LINUXDO_CALLBACK_URL}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${LINUXDO_SWITCH}|${LINUXDO_SWITCH}|g" "$CONFIG_FILE"
|
||||
|
||||
echo "✅ Environment setup completed."
|
||||
|
||||
echo "🔍 Debug: Checking configuration after replacement..."
|
||||
echo "R2_BUCKET_NAME value: '$R2_BUCKET_NAME'"
|
||||
echo "R2 bucket configuration in $CONFIG_FILE:"
|
||||
grep -A 2 -B 2 "bucket_name" "$CONFIG_FILE" || true
|
||||
|
||||
echo "🔒 Setting Worker secrets..."
|
||||
echo "🚀 Configuration updated. Starting deployment..."
|
||||
npx wrangler deploy -c "$CONFIG_FILE" | tee deploy.log | grep -v "https://.*\.workers\.dev"
|
||||
WORKER_URL=$(grep -o "https://.*\.workers\.dev" deploy.log)
|
||||
echo "::add-mask::$WORKER_URL"
|
||||
echo "worker_url=$WORKER_URL" >> $GITHUB_OUTPUT
|
||||
echo "✅ Deployment command executed."
|
||||
|
||||
- name: 🗄️ 初始化数据库 - Initialize Database
|
||||
if: ${{ steps.deploy.outputs.deployment_skipped == 'false' }}
|
||||
- name: ⚡ 设置KV数据库 / Set up KV database
|
||||
working-directory: ./mail-worker
|
||||
run: |
|
||||
|
||||
echo "⏳ Waiting 15s before checking initialization status..."
|
||||
CONFIG_FILE="wrangler-action.toml"
|
||||
|
||||
if [ -n "$KV_NAMESPACE_ID" ]; then
|
||||
sed -i "s|\${KV_NAMESPACE_ID}|${KV_NAMESPACE_ID}|g" "$CONFIG_FILE"
|
||||
echo "✅ Using the database from environment variables."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "🔍 Checking if the database exists..."
|
||||
|
||||
set +e
|
||||
KV_LIST=$(pnpm wrangler kv namespace list 2>&1)
|
||||
STATUS=$?
|
||||
set -e
|
||||
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
echo "$KV_LIST"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$KV_LIST" | jq -e ".[] | select(.title == \"$NAME\")" >/dev/null; then
|
||||
echo "✅ Database $NAME already exists."
|
||||
KV_ID=$(echo "$KV_LIST" | jq -r ".[] | select(.title == \"$NAME\") | .id")
|
||||
echo "KV_NAMESPACE_ID: $KV_ID"
|
||||
else
|
||||
echo "⚠️ Database $NAME does not exist. Starting creation..."
|
||||
pnpm wrangler kv namespace create $NAME
|
||||
KV_LIST=$(pnpm wrangler kv namespace list)
|
||||
KV_ID=$(echo "$KV_LIST" | jq -r ".[] | select(.title == \"$NAME\") | .id")
|
||||
fi
|
||||
|
||||
sed -i "s|\${KV_NAMESPACE_ID}|$KV_ID|g" "$CONFIG_FILE"
|
||||
echo "✅ Setup completed."
|
||||
|
||||
- name: 🐬 设置D1数据库 / Set up D1 database
|
||||
working-directory: ./mail-worker
|
||||
run: |
|
||||
|
||||
CONFIG_FILE="wrangler-action.toml"
|
||||
|
||||
if [ -n "$D1_DATABASE_ID" ]; then
|
||||
sed -i "s|\${D1_DATABASE_ID}|${D1_DATABASE_ID}|g" "$CONFIG_FILE"
|
||||
echo "✅ Using the database from environment variables."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "🔍 Checking if the database exists..."
|
||||
|
||||
set +e
|
||||
DB_LIST=$(pnpm wrangler d1 list --json 2>&1)
|
||||
STATUS=$?
|
||||
set -e
|
||||
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
echo "$DB_LIST"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$DB_LIST" | jq -e ".[] | select(.name == \"$NAME\")" >/dev/null; then
|
||||
echo "✅ Database $NAME already exists."
|
||||
D1_ID=$(echo "$DB_LIST" | jq -r ".[] | select(.name == \"$NAME\") | .uuid")
|
||||
echo "D1_DATABASE_ID: $D1_ID"
|
||||
else
|
||||
echo "⚠️ Database $NAME does not exist. Starting creation..."
|
||||
pnpm wrangler d1 create $NAME
|
||||
DB_LIST=$(pnpm wrangler d1 list --json)
|
||||
D1_ID=$(echo "$DB_LIST" | jq -r ".[] | select(.name == \"$NAME\") | .uuid")
|
||||
fi
|
||||
|
||||
sed -i "s|\${D1_DATABASE_ID}|$D1_ID|g" "$CONFIG_FILE"
|
||||
echo "✅ Setup completed."
|
||||
|
||||
- name: 🚀 开始部署 / Start deployment
|
||||
id: deploy
|
||||
working-directory: ./mail-worker
|
||||
run: |
|
||||
echo "🚀 Starting deployment..."
|
||||
pnpm wrangler deploy -c wrangler-action.toml 2>&1 \
|
||||
| tee deploy.log \
|
||||
| grep -v "https://.*\.workers\.dev" \
|
||||
| sed -E 's/env\.domain .*/env.domain (***)/' \
|
||||
|| true
|
||||
|
||||
DEPLOY_EXIT_CODE=${PIPESTATUS[0]}
|
||||
if [ $DEPLOY_EXIT_CODE -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORKER_URL=$(grep -o "https://.*\.workers\.dev" deploy.log || echo "")
|
||||
|
||||
if [ -n "$WORKER_URL" ]; then
|
||||
echo "::add-mask::$WORKER_URL"
|
||||
fi
|
||||
|
||||
echo "worker_url=$WORKER_URL" >> $GITHUB_OUTPUT
|
||||
echo "✅ Setup completed."
|
||||
|
||||
- name: ♻️ 初始化数据库 / Initialize database
|
||||
run: |
|
||||
echo "🛠️ Starting database initialization..."
|
||||
sleep 15
|
||||
|
||||
HTTP_CODE=$(curl -s -w "%{http_code}" -o response.txt "${{ steps.deploy.outputs.worker_url }}/api/init/${JWT_SECRET}")
|
||||
RESPONSE_BODY=$(cat response.txt)
|
||||
WORKER_URL="${CUSTOM_DOMAIN:+https://$CUSTOM_DOMAIN}"
|
||||
WORKER_URL="${WORKER_URL:-${{ steps.deploy.outputs.worker_url }}}"
|
||||
|
||||
echo "🔎 Checking response... (Status: $HTTP_CODE)"
|
||||
if [ -z "$WORKER_URL" ]; then
|
||||
echo "❌ Preview URL not available. Please set CUSTOM_DOMAIN."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HTTP_CODE=$(curl -sL -w "%{http_code}" -o response.txt "$WORKER_URL/api/init/${JWT_SECRET}")
|
||||
RESPONSE_BODY=$(cat response.txt)
|
||||
|
||||
if [ "$HTTP_CODE" = "200" ] && [ "$RESPONSE_BODY" = "初始化成功" -o "$RESPONSE_BODY" = "Successfully initialized" ]; then
|
||||
echo "✅ Database initialization successful!"
|
||||
elif [ "$HTTP_CODE" = "200" ]; then
|
||||
echo "❌ Database initialization error: $RESPONSE_BODY"
|
||||
if [ "$RESPONSE_BODY" = "success" ]; then
|
||||
echo "✅ Setup completed."
|
||||
else
|
||||
echo "❌ Database initialization check failed with HTTP status: $HTTP_CODE. response: $RESPONSE_BODY"
|
||||
echo "❌ Failed. HTTP: $HTTP_CODE, Response: $RESPONSE_BODY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 📣 部署状态 - Notify Final Status
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ job.status }}" == "success" ]; then
|
||||
if [ "${{ steps.deploy.outputs.deployment_skipped }}" == "true" ]; then
|
||||
echo "❌ Deployment was skipped due to missing configuration."
|
||||
else
|
||||
echo "🎉🎉🎉 Hooray! Deployment completed successfully! 🎉🎉🎉"
|
||||
fi
|
||||
else
|
||||
echo "❌❌❌ Oh no! The deployment failed. Please check the logs above for errors. ❌❌❌"
|
||||
fi
|
||||
|
||||
- name: Delete workflow runs
|
||||
- name: 🗑️ 删除运行记录 / Delete workflow runs
|
||||
uses: GitRML/delete-workflow-runs@main
|
||||
continue-on-error: true
|
||||
with:
|
||||
retain_days: '3'
|
||||
retain_days: '1'
|
||||
keep_minimum_runs: '0'
|
||||
|
||||
@@ -1,28 +1,45 @@
|
||||
|
||||
<p align="center">
|
||||
<img src="doc/demo/logo.png" width="80px" />
|
||||
<img src="doc/demo/logo.png" width="80px" />
|
||||
<h1 align="center">Cloud Mail</h1>
|
||||
<p align="center">A simple, responsive email service designed to run on Cloudflare Workers 🎉</p>
|
||||
<p align="center">
|
||||
<a href="/README.md" style="margin-left: 5px">简体中文</a> | English
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/maillab/cloud-mail/tree/main?tab=MIT-1-ov-file" target="_blank" >
|
||||
<img src="https://img.shields.io/badge/license-MIT-green" />
|
||||
</a>
|
||||
<a href="https://github.com/maillab/cloud-mail/releases" target="_blank" >
|
||||
<img src="https://img.shields.io/github/v/release/maillab/cloud-mail" alt="releases" />
|
||||
</a>
|
||||
<a href="https://github.com/maillab/cloud-mail/issues" >
|
||||
<img src="https://img.shields.io/github/issues/maillab/cloud-mail" alt="issues" />
|
||||
</a>
|
||||
<a href="https://github.com/maillab/cloud-mail/stargazers" target="_blank">
|
||||
<img src="https://img.shields.io/github/stars/maillab/cloud-mail" alt="stargazers" />
|
||||
</a>
|
||||
<a href="https://github.com/maillab/cloud-mail/forks" target="_blank" >
|
||||
<img src="https://img.shields.io/github/forks/maillab/cloud-mail" alt="forks" />
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/20459" target="_blank" >
|
||||
<img src="https://trendshift.io/api/badge/repositories/20459" alt="trendshift" >
|
||||
</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<h1>Cloud Mail</h1>
|
||||
</div>
|
||||
<div align="center">
|
||||
<h4>A responsive email service built with Vue 3 that supports email sending and can be deployed on Cloudflare. 🎉</h4>
|
||||
</div>
|
||||
|
||||
|
||||
## Description
|
||||
With only one domain, you can create multiple different email addresses, similar to major email platforms. This project can be deployed on Cloudflare Workers to reduce server costs and build your own email service.
|
||||
## Project Showcase
|
||||
|
||||
- [Online Demo](https://skymail.ink)<br>
|
||||
- [Live Demo](https://skymail.ink)<br>
|
||||
- [Deployment Guide](https://doc.skymail.ink/en/)<br>
|
||||
- [Beginner’s Guide – UI Deployment](https://doc.skymail.ink/en/guide/via-ui.html)
|
||||
|
||||
|
||||
|  |  |
|
||||
|--------------------------|---------------------|
|
||||
|--------------------------|--------------------------|
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
## Features
|
||||
|
||||
@@ -30,11 +47,9 @@
|
||||
|
||||
- **💻 Responsive Design**: Automatically adapts to both desktop and most mobile browsers.
|
||||
|
||||
- **📧 Email Sending**: Integrated with Resend for bulk email sending, embedded images, attachments, and status tracking.
|
||||
- **📧 Email Sending**: Integrated with Resend, supporting bulk email sending and attachments.
|
||||
|
||||
- **🛡️ Admin Features**: Admins can manage users and emails, with RBAC permission control to limit access to features and resources.
|
||||
|
||||
- **🔀 Multiple Accounts**: Users can add multiple email accounts.
|
||||
- **🛡️ Admin Features**: Admin controls for user and email management with RBAC-based access control.
|
||||
|
||||
- **📦 Attachment Support**: Send and receive attachments, stored and downloaded via R2 object storage.
|
||||
|
||||
@@ -42,50 +57,87 @@
|
||||
|
||||
- **📡 Open API**: Supports batch user creation via API and multi-condition email queries
|
||||
|
||||
- **📈 Data Visualization**: Use Echarts to visualize system data, including user email growth.
|
||||
- **🔢 Verification Code Recognition**: Auto-detect codes via Workers AI
|
||||
|
||||
- **⭐ Starred Emails**: Mark important emails for quick access.
|
||||
- **📈 Data Visualization**: Use ECharts to visualize system data, including user email growth.
|
||||
|
||||
- **🎨 Personalization**: Customize website title, login background, and transparency.
|
||||
|
||||
- **⚙️ Feature Settings**: Toggle on or off features like registration, email sending, and more, with the option to make the site private.
|
||||
|
||||
- **🤖 CAPTCHA**: Integrated with Turnstile CAPTCHA to prevent automated registration.
|
||||
|
||||
- **📜 More Features**: Under development...
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: [Vue3](https://vuejs.org/) + [Element Plus](https://element-plus.org/)
|
||||
- **Platform**: [Cloudflare Workers](https://developers.cloudflare.com/workers/)
|
||||
|
||||
- **Web Framework**: [Hono](https://hono.dev/)
|
||||
|
||||
- **ORM**: [Drizzle](https://orm.drizzle.team/)
|
||||
|
||||
- **Platform**: [Cloudflare Workers](https://developers.cloudflare.com/workers/)
|
||||
- **Frontend Framework**: [Vue3](https://vuejs.org/)
|
||||
|
||||
- **UI Framework**: [Element Plus](https://element-plus.org/)
|
||||
|
||||
- **Email Service**: [Resend](https://resend.com/)
|
||||
|
||||
- **Caching**: [Cloudflare KV](https://developers.cloudflare.com/kv/)
|
||||
- **Cache**: [Cloudflare KV](https://developers.cloudflare.com/kv/)
|
||||
|
||||
- **Database**: [Cloudflare D1](https://developers.cloudflare.com/d1/)
|
||||
|
||||
- **File Storage**: [Cloudflare R2](https://developers.cloudflare.com/r2/)
|
||||
|
||||
## Project Structure
|
||||
|
||||
## Support
|
||||
```
|
||||
cloud-mail
|
||||
├── mail-worker # Backend worker project
|
||||
│ ├── src
|
||||
│ │ ├── api # API layer
|
||||
│ │ ├── const # Project constants
|
||||
│ │ ├── dao # Data access layer
|
||||
│ │ ├── email # Email processing and handling
|
||||
│ │ ├── entity # Database entities
|
||||
│ │ ├── error # Custom exceptions
|
||||
│ │ ├── hono # Web framework, middleware, error handling
|
||||
│ │ ├── i18n # Internationalization
|
||||
│ │ ├── init # Database and cache initialization
|
||||
│ │ ├── model # Response data models
|
||||
│ │ ├── security # Authentication and authorization
|
||||
│ │ ├── service # Business logic layer
|
||||
│ │ ├── template # Message templates
|
||||
│ │ ├── utils # Utility functions
|
||||
│ │ └── index.js # Entry point
|
||||
│ ├── package.json # Project dependencies
|
||||
│ └── wrangler.toml # Project configuration
|
||||
│
|
||||
├─ mail-vue # Frontend Vue project
|
||||
│ ├── src
|
||||
│ │ ├── axios # Axios configuration
|
||||
│ │ ├── components # Custom components
|
||||
│ │ ├── echarts # ECharts integration
|
||||
│ │ ├── i18n # Internationalization
|
||||
│ │ ├── init # Startup initialization
|
||||
│ │ ├── layout # Main layout components
|
||||
│ │ ├── perm # Permissions and access control
|
||||
│ │ ├── request # API request layer
|
||||
│ │ ├── router # Router configuration
|
||||
│ │ ├── store # Global state management
|
||||
│ │ ├── utils # Utility functions
|
||||
│ │ ├── views # Page components
|
||||
│ │ ├── app.vue # Root component
|
||||
│ │ ├── main.js # Entry JS file
|
||||
│ │ └── style.css # Global styles
|
||||
│ ├── package.json # Project dependencies
|
||||
└── └── env.release # Environment configuration
|
||||
|
||||
```
|
||||
|
||||
## Sponsor
|
||||
|
||||
<a href="https://doc.skymail.ink/support.html">
|
||||
<img width="170px" src="./doc/images/support.png" alt="">
|
||||
</a><br><br>
|
||||
|
||||
|
||||
**Special Sponsors**
|
||||
|
||||
[DartNode](https://dartnode.com):Providing cloud computing service resource support.
|
||||
|
||||
[](https://dartnode.com "Powered by DartNode - Free VPS for Open Source")
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,62 +1,73 @@
|
||||
<p align="center">
|
||||
<img src="doc/demo/logo.png" width="80px" />
|
||||
<img src="doc/demo/logo.png" width="80px" />
|
||||
<h1 align="center">Cloud Mail</h1>
|
||||
<p align="center">基于 Cloudflare 的简约响应式邮箱服务,支持邮件发送、附件收发 🎉</p>
|
||||
<p align="center">
|
||||
简体中文 | <a href="/README-en.md" style="margin-left: 5px">English </a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/maillab/cloud-mail/tree/main?tab=MIT-1-ov-file" target="_blank" >
|
||||
<img src="https://img.shields.io/badge/license-MIT-green" />
|
||||
</a>
|
||||
<a href="https://github.com/maillab/cloud-mail/releases" target="_blank" >
|
||||
<img src="https://img.shields.io/github/v/release/maillab/cloud-mail" alt="releases" />
|
||||
</a>
|
||||
<a href="https://github.com/maillab/cloud-mail/issues" >
|
||||
<img src="https://img.shields.io/github/issues/maillab/cloud-mail" alt="issues" />
|
||||
</a>
|
||||
<a href="https://github.com/maillab/cloud-mail/stargazers" target="_blank">
|
||||
<img src="https://img.shields.io/github/stars/maillab/cloud-mail" alt="stargazers" />
|
||||
</a>
|
||||
<a href="https://github.com/maillab/cloud-mail/forks" target="_blank" >
|
||||
<img src="https://img.shields.io/github/forks/maillab/cloud-mail" alt="forks" />
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/20459" target="_blank" >
|
||||
<img src="https://trendshift.io/api/badge/repositories/20459" alt="trendshift" >
|
||||
</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<h1>Cloud Mail</h1>
|
||||
</div>
|
||||
<div align="center">
|
||||
<h4>使用Vue3开发的响应式邮箱服务,支持邮件发送,无需服务器可部署到Cloudflare平台 🎉</h4>
|
||||
</div>
|
||||
<div align="center">
|
||||
<span>简体中文 | <a href="/README-en.md" style="margin-left: 5px">English </a></span>
|
||||
</div>
|
||||
|
||||
## 项目简介
|
||||
|
||||
只需要一个域名,就可以创建多个不同的邮箱,类似各大邮箱平台 QQ邮箱,谷歌邮箱等,本项目使用Cloudflare部署,Resend推送邮件,无需服务器费用,搭建自己的邮箱服务
|
||||
只需要一个域名,就可以创建多个不同的邮箱,类似各大邮箱平台,本项目支持署到 Cloudflare Workers ,降低服务器成本,搭建自己的邮箱服务
|
||||
|
||||
## 项目展示
|
||||
|
||||
- [在线演示](https://skymail.ink)<br>
|
||||
- [部署文档](https://doc.skymail.ink)<br>
|
||||
- [小白保姆教程-界面部署](https://doc.skymail.ink/guide/via-ui.html)
|
||||
|
||||
|  |  |
|
||||
|--------------------------|---------------------|
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|-----------------------|-----------------------|
|
||||
|  |  |
|
||||
|
||||
|
||||
|
||||
|
||||
## 功能介绍
|
||||
|
||||
- **💰 低成本使用**:无需服务器,部署到 Cloudflare Workers 降低使用成本
|
||||
- **💰 低成本使用**: 可部署到 Cloudflare Workers 降低服务器成本
|
||||
|
||||
- **💻 响应式设计**:响应式布局自动适配PC和大部分手机端浏览器
|
||||
|
||||
- **📧 邮件发送**:集成resend发送邮件,支持群发,内嵌图片和附件发送,发送状态查看
|
||||
- **📧 邮件发送**:集成Resend发送邮件,支持群发,内嵌图片和附件发送,发送状态查看
|
||||
|
||||
- **🛡️ 管理员功能**:可以对用户,邮件进行管理,RABC权限控制对功能及使用资源限制
|
||||
|
||||
- **🔀 多号模式**:开启后一个用户可以添加多个邮箱,默认一用户一邮箱,类似各大邮箱平台
|
||||
|
||||
- **📦 附件收发**:支持收发附件,使用R2对象存储保存和下载文件
|
||||
|
||||
- **🔔 邮件推送**:接收邮件后可以转发到TG机器人或其他服务商邮箱
|
||||
|
||||
- **📡 开放API**:支持使用API批量生成用户,多条件查询邮件
|
||||
|
||||
- **📈 数据可视化**:使用echarts对系统数据详情,用户邮件增长可视化显示
|
||||
- **🔢 验证码识别**:使用Workers AI,自动识别邮件验证码
|
||||
|
||||
- **⭐ 星标邮件**:标记重要邮件,以便快速查阅
|
||||
- **📈 数据可视化**:使用ECharts对系统数据详情,用户邮件增长可视化显示
|
||||
|
||||
- **🎨 个性化设置**:可以自定义网站标题,登录背景,透明度
|
||||
|
||||
- **⚙️ 功能设置**:可以对注册,邮件发送,添加等功能关闭和开启,设为私人站点
|
||||
|
||||
- **🤖 人机验证**:集成Turnstile人机验证,防止人机批量注册
|
||||
|
||||
- **📜 更多功能**:正在开发中...
|
||||
@@ -65,13 +76,15 @@
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **框架**:[Vue3](https://vuejs.org/) + [Element Plus](https://element-plus.org/)
|
||||
- **平台**:[Cloudflare Workers](https://developers.cloudflare.com/workers/)
|
||||
|
||||
- **Web框架**:[Hono](https://hono.dev/)
|
||||
|
||||
- **ORM:**[Drizzle](https://orm.drizzle.team/)
|
||||
|
||||
- **平台:** [Cloudflare workers](https://developers.cloudflare.com/workers/)
|
||||
- **前端框架**:[Vue3](https://vuejs.org/)
|
||||
|
||||
- **UI框架**:[Element Plus](https://element-plus.org/)
|
||||
|
||||
- **邮件推送:** [Resend](https://resend.com/)
|
||||
|
||||
@@ -81,20 +94,56 @@
|
||||
|
||||
- **文件存储**:[Cloudflare R2](https://developers.cloudflare.com/r2/)
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
cloud-mail
|
||||
├── mail-worker # worker后端项目
|
||||
│ ├── src
|
||||
│ │ ├── api # api接口层
|
||||
│ │ ├── const # 项目常量
|
||||
│ │ ├── dao # 数据访问层
|
||||
│ │ ├── email # 邮件处理接收
|
||||
│ │ ├── entity # 数据库实体
|
||||
│ │ ├── error # 自定义异常
|
||||
│ │ ├── hono # web框架配置、拦截器、全局异常等
|
||||
│ │ ├── i18n # 语言国际化
|
||||
│ │ ├── init # 数据库缓存初始化
|
||||
│ │ ├── model # 响应体数据封装
|
||||
│ │ ├── security # 身份权限认证
|
||||
│ │ ├── service # 业务服务层
|
||||
│ │ ├── template # 消息模板
|
||||
│ │ ├── utils # 工具类
|
||||
│ │ └── index.js # 入口文件
|
||||
│ ├── pageckge.json # 项目依赖
|
||||
│ └── wrangler.toml # 项目配置
|
||||
│
|
||||
├── mail-vue # vue前端项目
|
||||
│ ├── src
|
||||
│ │ ├── axios # axios配置
|
||||
│ │ ├── components # 自定义组件
|
||||
│ │ ├── echarts # echarts组件导入
|
||||
│ │ ├── i18n # 语言国际化
|
||||
│ │ ├── init # 入站初始化
|
||||
│ │ ├── layout # 主体布局组件
|
||||
│ │ ├── perm # 权限认证
|
||||
│ │ ├── request # api接口
|
||||
│ │ ├── router # 路由配置
|
||||
│ │ ├── store # 全局状态管理
|
||||
│ │ ├── utils # 工具类
|
||||
│ │ ├── views # 页面组件
|
||||
│ │ ├── app.vue # 入口组件
|
||||
│ │ ├── main.js # 入口js
|
||||
│ │ └── style.css # 全局css
|
||||
│ ├── package.json # 项目依赖
|
||||
└── └── env.release # 项目配置
|
||||
```
|
||||
|
||||
## 赞助
|
||||
|
||||
|
||||
<a href="https://doc.skymail.ink/support.html" >
|
||||
<img width="170px" src="./doc/images/support.png" alt="">
|
||||
</a><br><br>
|
||||
|
||||
|
||||
**特别赞助商**
|
||||
|
||||
[DartNode](https://dartnode.com):提供云计算服务资源支持
|
||||
|
||||
[](https://dartnode.com "Powered by DartNode - Free VPS for Open Source")
|
||||
</a>
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 450 KiB |
|
Before Width: | Height: | Size: 461 KiB After Width: | Height: | Size: 525 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 252 KiB After Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 4.4 KiB |
@@ -1,4 +0,0 @@
|
||||
NODE_ENV = 'eo'
|
||||
VITE_APP_TITLE = 'eo环境'
|
||||
VITE_BASE_URL = ''
|
||||
VITE_PWA_NAME = 'Cloud Mail'
|
||||
@@ -1,4 +1,4 @@
|
||||
NODE_ENV = 'remote'
|
||||
VITE_APP_TITLE = '远程环境'
|
||||
VITE_BASE_URL = ''
|
||||
VITE_BASE_URL = 'https://skymail.ink/api'
|
||||
VITE_PWA_NAME = 'Cloud Mail'
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta name="theme-color" content="#F1F1F1" id="theme-color-meta">
|
||||
<title></title>
|
||||
<link rel="icon" href="./public/mail.png" type="image/png">
|
||||
<meta name="description" content="Cloud Mail 是一个 Serverless 响应式邮箱服务,支持邮件发送,可部署到 Cloudflare 平台,降低服务器成本。Cloud Mail is a serverless responsive email service that supports email sending and can be deployed on Cloudflare to reduce server costs.">
|
||||
<title>Cloud Mail</title>
|
||||
<link rel="icon" href="/public/mail.png" type="image/png">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
<script>
|
||||
@@ -42,77 +43,94 @@
|
||||
|
||||
.loading-hide {
|
||||
pointer-events: none;
|
||||
transition: all 200ms;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 30px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.circular {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
animation: loading-rotate 1s linear infinite;
|
||||
.loading-image {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.loading-path .dot1 {
|
||||
transform: translate(3.75px, 3.75px);
|
||||
fill: #409EFF;
|
||||
animation: custom-spin-move 1s infinite linear alternate;
|
||||
opacity: 0.3;
|
||||
.loading-progress {
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 4px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: rgba(64, 158, 255, 0.18);
|
||||
}
|
||||
|
||||
.loading-path .dot2 {
|
||||
transform: translate(calc(100% - 3.75px), 3.75px);
|
||||
fill: #409EFF;
|
||||
animation: custom-spin-move 1s infinite linear alternate;
|
||||
opacity: 0.3;
|
||||
animation-delay: 0.4s;
|
||||
.loading-progress::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: #409EFF;
|
||||
transform: scaleX(0);
|
||||
transform-origin: left center;
|
||||
animation: loading-progress-grow 2.4s linear forwards;
|
||||
}
|
||||
|
||||
.loading-path .dot3 {
|
||||
transform: translate(3.75px, calc(100% - 3.75px));
|
||||
fill: #409EFF;
|
||||
animation: custom-spin-move 1s infinite linear alternate;
|
||||
opacity: 0.3;
|
||||
animation-delay: 1.2s;
|
||||
.loading-progress::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 24px;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: rgba(64, 158, 255, 0.45);
|
||||
filter: blur(4px);
|
||||
transform: translateX(-24px);
|
||||
animation: loading-progress-peg 2.4s linear forwards;
|
||||
}
|
||||
|
||||
.loading-path .dot4 {
|
||||
transform: translate(calc(100% - 3.75px), calc(100% - 3.75px));
|
||||
fill: #409EFF;
|
||||
animation: custom-spin-move 1s infinite linear alternate;
|
||||
opacity: 0.3;
|
||||
animation-delay: 0.8s;
|
||||
.loading-complete .loading-progress::before {
|
||||
animation: none;
|
||||
transform: scaleX(1);
|
||||
transition: transform 200ms linear;
|
||||
}
|
||||
|
||||
@keyframes loading-rotate {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
.loading-complete .loading-progress::after {
|
||||
animation: none;
|
||||
transform: translateX(60px);
|
||||
transition: transform 200ms linear;
|
||||
}
|
||||
|
||||
@keyframes loading-progress-grow {
|
||||
0% {
|
||||
transform: scaleX(0);
|
||||
}
|
||||
100% {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes custom-spin-move {
|
||||
to {
|
||||
opacity: 1;
|
||||
@keyframes loading-progress-peg {
|
||||
0% {
|
||||
transform: translateX(-24px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(60px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<div id="loading-first" >
|
||||
<div class="loading-icon">
|
||||
<svg class="circular" viewBox="0 0 20 20">
|
||||
<g class="loading-path">
|
||||
<circle r="3.375" class="dot1" rx="0" ry="0"/>
|
||||
<circle r="3.375" class="dot2" rx="0" ry="0"/>
|
||||
<circle r="3.375" class="dot4" rx="0" ry="0"/>
|
||||
<circle r="3.375" class="dot3" rx="0" ry="0"/>
|
||||
</g>
|
||||
</svg>
|
||||
<img class="loading-image" src="/mail-pwa.png" alt="Cloud Mail">
|
||||
<div class="loading-progress" aria-hidden="true"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -12,14 +12,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/vue": "^4.3.0",
|
||||
"@simplewebauthn/browser": "^13.3.0",
|
||||
"@vueuse/components": "^14.1.0",
|
||||
"@vueuse/core": "^12.0.0",
|
||||
"axios": "^1.7.8",
|
||||
"axios": "1.15.2",
|
||||
"compressorjs": "^1.2.1",
|
||||
"date-time-format-timezone": "^1.0.22",
|
||||
"dayjs": "^1.11.13",
|
||||
"dexie": "^4.0.11",
|
||||
"echarts": "^5.6.0",
|
||||
"element-plus": "^2.9.11",
|
||||
"element-plus": "^2.13.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nprogress": "^0.2.0",
|
||||
"path": "^0.12.7",
|
||||
|
||||
@@ -11,12 +11,18 @@ importers:
|
||||
'@iconify/vue':
|
||||
specifier: ^4.3.0
|
||||
version: 4.3.0([email protected])
|
||||
'@simplewebauthn/browser':
|
||||
specifier: ^13.3.0
|
||||
version: 13.3.0
|
||||
'@vueuse/components':
|
||||
specifier: ^14.1.0
|
||||
version: 14.1.0([email protected])
|
||||
'@vueuse/core':
|
||||
specifier: ^12.0.0
|
||||
version: 12.8.2
|
||||
axios:
|
||||
specifier: ^1.7.8
|
||||
version: 1.11.0
|
||||
specifier: 1.15.2
|
||||
version: 1.15.2([email protected])
|
||||
compressorjs:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
@@ -33,8 +39,8 @@ importers:
|
||||
specifier: ^5.6.0
|
||||
version: 5.6.0
|
||||
element-plus:
|
||||
specifier: ^2.9.11
|
||||
version: 2.11.1([email protected])
|
||||
specifier: ^2.13.1
|
||||
version: 2.13.1([email protected])
|
||||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
@@ -1058,6 +1064,9 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@simplewebauthn/[email protected]':
|
||||
resolution: {integrity: sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==}
|
||||
|
||||
'@surma/[email protected]':
|
||||
resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==}
|
||||
|
||||
@@ -1082,8 +1091,8 @@ packages:
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||
|
||||
'@types/[email protected].16':
|
||||
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
|
||||
'@types/[email protected].20':
|
||||
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
|
||||
|
||||
'@types/[email protected]':
|
||||
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
|
||||
@@ -1136,23 +1145,41 @@ packages:
|
||||
'@vue/[email protected]':
|
||||
resolution: {integrity: sha512-SoRGP596KU/ig6TfgkCMbXkr4YJ91n/QSdMuqeP5r3hVIYA3CPHUBCc7Skak0EAKV+5lL4KyIh61VA/pK1CIAA==}
|
||||
|
||||
'@vueuse/[email protected]':
|
||||
resolution: {integrity: sha512-SDRJUAv3H7/PMh+KkYpq0d5KMzpKOfqx4qcV4xyN4mZOLPw8NkiWu+yDcfXwI8h1uCqhRNz2cdeaLa+IuaehFw==}
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
'@vueuse/[email protected]':
|
||||
resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==}
|
||||
|
||||
'@vueuse/[email protected]':
|
||||
resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==}
|
||||
|
||||
'@vueuse/core@9.13.0':
|
||||
resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
|
||||
'@vueuse/core@14.1.0':
|
||||
resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==}
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
'@vueuse/[email protected]':
|
||||
resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==}
|
||||
|
||||
'@vueuse/[email protected]':
|
||||
resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==}
|
||||
|
||||
'@vueuse/metadata@9.13.0':
|
||||
resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
|
||||
'@vueuse/metadata@14.1.0':
|
||||
resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==}
|
||||
|
||||
'@vueuse/[email protected]':
|
||||
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
|
||||
|
||||
'@vueuse/[email protected]':
|
||||
resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==}
|
||||
|
||||
'@vueuse/shared@9.13.0':
|
||||
resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
|
||||
'@vueuse/shared@14.1.0':
|
||||
resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==}
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
|
||||
@@ -1195,8 +1222,8 @@ packages:
|
||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
[email protected]1.0:
|
||||
resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==}
|
||||
[email protected]5.2:
|
||||
resolution: {integrity: sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==}
|
||||
@@ -1328,6 +1355,9 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
@@ -1385,10 +1415,10 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==}
|
||||
|
||||
[email protected]1.1:
|
||||
resolution: {integrity: sha512-weYFIniyNXTAe9vJZnmZpYzurh4TDbdKhBsJwhbzuo0SDZ8PLwHVll0qycJUxc6SLtH+7A9F7dvdDh5CnqeIVA==}
|
||||
[email protected]3.1:
|
||||
resolution: {integrity: sha512-eG4BDBGdAsUGN6URH1PixzZb0ngdapLivIk1meghS1uEueLvQ3aljSKrCt5x6sYb6mUk8eGtzTQFgsPmLavQcA==}
|
||||
peerDependencies:
|
||||
vue: ^3.2.0
|
||||
vue: ^3.3.0
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
@@ -1431,9 +1461,6 @@ packages:
|
||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1492,8 +1519,8 @@ packages:
|
||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
[email protected].4:
|
||||
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
|
||||
[email protected].5:
|
||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
[email protected]:
|
||||
@@ -1543,7 +1570,7 @@ packages:
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting [email protected]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
|
||||
@@ -1994,8 +2021,9 @@ packages:
|
||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
proxy-from-env@2.1.0:
|
||||
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
|
||||
@@ -2401,6 +2429,7 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-LvyteQoXeQiuILbzqv13LbyBna/TEv2Ha+4ZWK2AwGHUzZ8+IBaZS0TJkCgn5izSPLcgZwXy9yyTrewCb2u/MA==}
|
||||
engines: {node: '>= 16'}
|
||||
deprecated: This version is NOT deprecated. Previous deprecation was a mistake.
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
|
||||
@@ -3478,6 +3507,8 @@ snapshots:
|
||||
'@rollup/[email protected]':
|
||||
optional: true
|
||||
|
||||
'@simplewebauthn/[email protected]': {}
|
||||
|
||||
'@surma/[email protected]':
|
||||
dependencies:
|
||||
ejs: 3.1.10
|
||||
@@ -3501,7 +3532,7 @@ snapshots:
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
'@types/[email protected].16': {}
|
||||
'@types/[email protected].20': {}
|
||||
|
||||
'@types/[email protected]': {}
|
||||
|
||||
@@ -3584,6 +3615,22 @@ snapshots:
|
||||
|
||||
'@vue/[email protected]': {}
|
||||
|
||||
'@vueuse/[email protected]([email protected])':
|
||||
dependencies:
|
||||
'@vueuse/core': 14.1.0([email protected])
|
||||
'@vueuse/shared': 14.1.0([email protected])
|
||||
vue: 3.5.20
|
||||
|
||||
'@vueuse/[email protected]([email protected])':
|
||||
dependencies:
|
||||
'@types/web-bluetooth': 0.0.20
|
||||
'@vueuse/metadata': 10.11.1
|
||||
'@vueuse/shared': 10.11.1([email protected])
|
||||
vue-demi: 0.14.10([email protected])
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
'@vueuse/[email protected]':
|
||||
dependencies:
|
||||
'@types/web-bluetooth': 0.0.21
|
||||
@@ -3593,32 +3640,35 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vueuse/core@9.13.0([email protected])':
|
||||
'@vueuse/core@14.1.0([email protected])':
|
||||
dependencies:
|
||||
'@types/web-bluetooth': 0.0.21
|
||||
'@vueuse/metadata': 14.1.0
|
||||
'@vueuse/shared': 14.1.0([email protected])
|
||||
vue: 3.5.20
|
||||
|
||||
'@vueuse/[email protected]': {}
|
||||
|
||||
'@vueuse/[email protected]': {}
|
||||
|
||||
'@vueuse/[email protected]': {}
|
||||
|
||||
'@vueuse/[email protected]([email protected])':
|
||||
dependencies:
|
||||
'@types/web-bluetooth': 0.0.16
|
||||
'@vueuse/metadata': 9.13.0
|
||||
'@vueuse/shared': 9.13.0([email protected])
|
||||
vue-demi: 0.14.10([email protected])
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
'@vueuse/[email protected]': {}
|
||||
|
||||
'@vueuse/[email protected]': {}
|
||||
|
||||
'@vueuse/[email protected]':
|
||||
dependencies:
|
||||
vue: 3.5.20
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vueuse/shared@9.13.0([email protected])':
|
||||
'@vueuse/shared@14.1.0([email protected])':
|
||||
dependencies:
|
||||
vue-demi: 0.14.10([email protected])
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
vue: 3.5.20
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -3663,11 +3713,11 @@ snapshots:
|
||||
dependencies:
|
||||
possible-typed-array-names: 1.1.0
|
||||
|
||||
[email protected]1.0:
|
||||
[email protected]5.2([email protected]):
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11
|
||||
form-data: 4.0.4
|
||||
proxy-from-env: 1.1.0
|
||||
follow-redirects: 1.15.11([email protected])
|
||||
form-data: 4.0.5
|
||||
proxy-from-env: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
@@ -3819,6 +3869,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@@ -3867,7 +3919,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]1.1([email protected]):
|
||||
[email protected]3.1([email protected]):
|
||||
dependencies:
|
||||
'@ctrl/tinycolor': 3.6.1
|
||||
'@element-plus/icons-vue': 2.3.2([email protected])
|
||||
@@ -3875,10 +3927,9 @@ snapshots:
|
||||
'@popperjs/core': '@sxzz/[email protected]'
|
||||
'@types/lodash': 4.17.20
|
||||
'@types/lodash-es': 4.17.12
|
||||
'@vueuse/core': 9.13.0([email protected])
|
||||
'@vueuse/core': 10.11.1([email protected])
|
||||
async-validator: 4.2.5
|
||||
dayjs: 1.11.18
|
||||
escape-html: 1.0.3
|
||||
dayjs: 1.11.19
|
||||
lodash: 4.17.21
|
||||
lodash-es: 4.17.21
|
||||
lodash-unified: 1.0.3(@types/[email protected])([email protected])([email protected])
|
||||
@@ -4004,8 +4055,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -4038,13 +4087,15 @@ snapshots:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
|
||||
[email protected]: {}
|
||||
[email protected]([email protected]):
|
||||
optionalDependencies:
|
||||
debug: 4.4.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
|
||||
[email protected].4:
|
||||
[email protected].5:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
@@ -4526,7 +4577,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
proxy-from-env@2.1.0: {}
|
||||
|
||||
[email protected]:
|
||||
optional: true
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/assets/*
|
||||
Cache-Control: public, max-age=31556952, immutable
|
||||
/tinymce/*
|
||||
Cache-Control: public, max-age=31556952, immutable
|
||||
Cache-Control: public, max-age=604800, immutable
|
||||
/image/*
|
||||
Cache-Control: public, max-age=604800, immutable
|
||||
|
||||
|
After Width: | Height: | Size: 622 B |
@@ -7,6 +7,15 @@ img {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #0E70DF;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 10px 8px 0 5px !important;
|
||||
font-size: 14px;
|
||||
|
||||
@@ -18,10 +18,10 @@ http.interceptors.response.use((res) => {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const showMsg = res.config.noMsg;
|
||||
const noMsg = res.config.noMsg;
|
||||
const data = res.data
|
||||
|
||||
if (showMsg) {
|
||||
if (noMsg) {
|
||||
|
||||
data.code === 200 ? resolve(data.data) : reject(data)
|
||||
|
||||
@@ -34,7 +34,7 @@ http.interceptors.response.use((res) => {
|
||||
repeatNum: -4,
|
||||
})
|
||||
localStorage.removeItem('token')
|
||||
router.push('/login')
|
||||
router.replace('/login')
|
||||
reject(data)
|
||||
} else if (data.code === 403) {
|
||||
ElMessage({
|
||||
@@ -76,9 +76,9 @@ http.interceptors.response.use((res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const showMsg = error.config.noMsg;
|
||||
const noMsg = error.config.noMsg;
|
||||
|
||||
if (showMsg) {
|
||||
if (noMsg) {
|
||||
return Promise.reject(error)
|
||||
} else if (error.message.includes('Network Error')) {
|
||||
ElMessage({
|
||||
|
||||
@@ -12,9 +12,12 @@
|
||||
|
||||
<slot name="first"></slot>
|
||||
<Icon class="icon reload" icon="ion:reload" width="18" height="18" @click="refresh"/>
|
||||
<Icon v-perm="'email:delete'" class="icon" icon="uiw:delete" width="16" height="16"
|
||||
<Icon v-perm="'email:delete'" class="icon delete" icon="uiw:delete" width="16" height="16"
|
||||
v-if="getSelectedMailsIds().length > 0"
|
||||
@click="handleDelete"/>
|
||||
<Icon v-perm="'email:delete'" class="icon delete" icon="fluent:mail-read-20-regular" width="21" height="21"
|
||||
v-if="getSelectedMailsIds().length > 0 && showUnread"
|
||||
@click="handleRead"/>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
@@ -25,13 +28,23 @@
|
||||
</div>
|
||||
|
||||
<div ref="scroll" class="scroll">
|
||||
<el-scrollbar ref="scrollbarRef" style="height: 100%">
|
||||
<div class="scroll-box" :infinite-scroll-immediate="false" v-infinite-scroll="loadData"
|
||||
infinite-scroll-distance="600">
|
||||
<div v-if="(skeleton && !loading)" v-for="item in emailList" :key="item.emailId">
|
||||
<div class="email-row"
|
||||
<UseVirtualList ref="scrollbarRef"
|
||||
@scroll="onScroll"
|
||||
:list="list"
|
||||
:options="{ itemHeight: itemHeight, overscan: 15 }"
|
||||
class="virtual"
|
||||
style="height: 100%"
|
||||
v-if="!loading && emailList.length > 0"
|
||||
:key="keyCount"
|
||||
>
|
||||
<template #default="{ data: item, index }" >
|
||||
<div :class="'email-row ' + props.type"
|
||||
:data-checked="item.checked"
|
||||
@click="jumpDetails(item)"
|
||||
v-if="!item.expand"
|
||||
:key="item.emailId"
|
||||
@contextmenu="handleContextmenu($event, item)"
|
||||
:style="item.rightChecked ? 'background: #FDF6EC' : ''"
|
||||
>
|
||||
<el-checkbox :class=" props.type === 'all-email' ? 'all-email-checkbox' : 'checkbox'"
|
||||
v-model="item.checked" @click.stop></el-checkbox>
|
||||
@@ -42,32 +55,13 @@
|
||||
<div v-if="!showStar"></div>
|
||||
<div class="title" :class="accountShow ? 'title-column' : 'title-column'">
|
||||
|
||||
<div class="email-sender" :style=" showStatus ? 'gap: 10px;' : ''">
|
||||
<div class="email-sender" :style=" (showStatus ? 'gap: 10px;' : '') + ((item.unread === EmailUnreadEnum.UNREAD && showUnread) ? 'font-weight: bold' : '')">
|
||||
<div class="email-status" v-if="showStatus">
|
||||
<el-tooltip v-if="item.status === 0" effect="dark" :content="$t('received')">
|
||||
<Icon icon="ic:round-mark-email-read" style="color: #51C76B" width="20" height="20"/>
|
||||
/>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="item.status === 1" effect="dark" :content="$t('sent')">
|
||||
<Icon icon="bi:send-arrow-up-fill" style="color: #51C76B" width="20" height="20"/>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="item.status === 2" effect="dark" :content="$t('delivered')">
|
||||
<Icon icon="bi:send-check-fill" style="color: #51C76B" width="20" height="20"/>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="item.status === 3" effect="dark" :content="$t('bounced')">
|
||||
<Icon icon="bi:send-x-fill" style="color: #F56C6C" width="20" height="20"/>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="item.status === 4" effect="dark" :content="$t('complained')">
|
||||
<Icon icon="bi:send-exclamation-fill" style="color:#FBBD08" width="20" height="20"/>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="item.status === 5" effect="dark" :content="$t('delayed')">
|
||||
<Icon icon="bi:send-arrow-up-fill" style="color:#FBBD08" width="20" height="20"/>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="item.status === 7" effect="dark" :content="$t('noRecipient')">
|
||||
<Icon icon="ic:round-mark-email-read" style="color:#FBBD08" width="20" height="20"/>
|
||||
<el-tooltip effect="dark" :content="item.statusIcon.content">
|
||||
<Icon :icon="item.statusIcon.icon" :style="`color: ${item.statusIcon.color}`" width="20" height="20"/>
|
||||
</el-tooltip>
|
||||
<div class="del-status" v-if="item.isDel">
|
||||
<el-tooltip effect="dark" :content="$t('selectDeleted')">
|
||||
<el-tooltip effect="dark" :content="item.isDelContent">
|
||||
<Icon class="icon" icon="mdi:email-remove" width="20" height="20"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
@@ -75,22 +69,27 @@
|
||||
<div v-else></div>
|
||||
<span class="name">
|
||||
<span>
|
||||
<div class="unread" v-if="isMobile && (item.unread === EmailUnreadEnum.UNREAD && showUnread) "/>
|
||||
<slot name="name" :email="item"> {{ item.name }}</slot>
|
||||
</span>
|
||||
<span>
|
||||
<Icon v-if="item.isStar" icon="fluent-color:star-16" width="18" height="18"/>
|
||||
</span>
|
||||
</span>
|
||||
<span class="phone-time">{{ fromNow(item.createTime) }}</span>
|
||||
<span class="phone-time">{{ item.formatCreateTime }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="email-text">
|
||||
<span class="email-subject">
|
||||
<slot name="subject" :email="item">
|
||||
{{ item.subject || '\u200B' }}
|
||||
</slot>
|
||||
<span class="email-subject" :style="(item.unread === EmailUnreadEnum.UNREAD && showUnread) ? 'font-weight: bold' : ''">
|
||||
<div class="unread" v-if="!isMobile && (item.unread === EmailUnreadEnum.UNREAD && showUnread) "/>
|
||||
<span v-if="item.code" class="code-tag" @click.stop="copyCode(item.code)">[{{ t('codeLabel') }}{{ item.code }}]</span>
|
||||
<span class="subject-text">
|
||||
<slot name="subject" :email="item" >
|
||||
{{ item.subject || '\u200B' }}
|
||||
</slot>
|
||||
</span>
|
||||
</span>
|
||||
<span class="email-content">{{ htmlToText(item) || '\u200B' }}</span>
|
||||
<span class="email-content">{{ item.formatText || '\u200B' }}</span>
|
||||
</div>
|
||||
<div class="user-info" v-if="showUserInfo">
|
||||
<div class="user">
|
||||
@@ -109,71 +108,149 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="email-right" :style="showUserInfo ? 'align-self: start;':''">
|
||||
<span class="email-time">{{ fromNow(item.createTime) }}</span>
|
||||
<span class="email-time" :style="(item.unread === EmailUnreadEnum.UNREAD && showUnread) ? 'font-weight: bold' : ''">{{ item.formatCreateTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="skeleton">
|
||||
<skeletonBlock v-if="firstLoad && showFirstLoading"
|
||||
:rows="20"
|
||||
<skeletonBlock v-else-if="item.expand === 'loading'"
|
||||
:rows="1"
|
||||
:showStar="showStar"
|
||||
:accountShow="accountShow"
|
||||
:showStatus="showStatus"
|
||||
:showUserInfo="showUserInfo"
|
||||
:type="type"/>
|
||||
<skeletonBlock v-if="loading"
|
||||
:rows="skeletonRows"
|
||||
:showStar="showStar"
|
||||
:accountShow="accountShow"
|
||||
:showStatus="showStatus"
|
||||
:showUserInfo="showUserInfo"
|
||||
:type="type"/>
|
||||
<skeletonBlock v-if="followLoading"
|
||||
:rows="isMobile ? 1 : 2"
|
||||
:showStar="showStar"
|
||||
:accountShow="accountShow"
|
||||
:showStatus="showStatus"
|
||||
:showUserInfo="showUserInfo"
|
||||
:type="type"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div></div>
|
||||
<div class="loading" :class="loading ? 'loading-show' : 'loading-hide'"
|
||||
:style="firstLoad ? 'background: transparent' : ''">
|
||||
<Loading/>
|
||||
</div>
|
||||
<div class="follow-loading" v-if="followLoading">
|
||||
<Loading/>
|
||||
<div class="noLoading" v-else-if="item.expand === 'noMoreData'">
|
||||
<div>{{ $t('noMoreData') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="noLoading" v-if="noLoading && emailList.length > 0 && !(skeleton && loading)">
|
||||
<div>{{ $t('noMoreData') }}</div>
|
||||
</div>
|
||||
<div class="empty" v-if="noLoading && emailList.length === 0 && !(skeleton && loading)">
|
||||
<el-empty :image-size="isMobile ? 120 : 0" :description="$t('noMessagesFound')"/>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</UseVirtualList>
|
||||
<skeletonBlock v-if="firstLoad && showFirstLoading"
|
||||
:rows="20"
|
||||
:showStar="showStar"
|
||||
:accountShow="accountShow"
|
||||
:showStatus="showStatus"
|
||||
:showUserInfo="showUserInfo"
|
||||
:type="type"/>
|
||||
<skeletonBlock v-if="loading"
|
||||
:rows="skeletonRows"
|
||||
:showStar="showStar"
|
||||
:accountShow="accountShow"
|
||||
:showStatus="showStatus"
|
||||
:showUserInfo="showUserInfo"
|
||||
:type="type"/>
|
||||
<div class="empty" v-if="noLoading && emailList.length === 0 && !loading">
|
||||
<el-empty :image-size="isMobile ? 120 : null" :description="$t('noMessagesFound')"/>
|
||||
</div>
|
||||
</div>
|
||||
<el-dropdown
|
||||
ref="dropdownRef"
|
||||
@visible-change="visibleChange"
|
||||
:virtual-ref="triggerRef"
|
||||
:show-arrow="false"
|
||||
:popper-options="{
|
||||
modifiers: [{ name: 'offset', options: { offset: [0, 0] } }],
|
||||
}"
|
||||
virtual-triggering
|
||||
trigger="contextmenu"
|
||||
placement="bottom-start"
|
||||
>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-if="rightClickEmail.code" @click="copyCode(rightClickEmail.code)" >
|
||||
<template #default>
|
||||
<div class="right-dropdown-item">
|
||||
<Icon icon="fluent-color:clipboard-24" width="20" height="20" />
|
||||
<span>{{t('copyCode')}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="['email'].includes(props.type)" @click="emailRead(rightClickEmail.emailId)" >
|
||||
<template #default>
|
||||
<div class="right-dropdown-item">
|
||||
<Icon icon="fluent:mail-read-20-regular" width="20" height="20" />
|
||||
<span>{{t('markAsRead')}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="['email','star'].includes(props.type)" @click="openReply(rightClickEmail)">
|
||||
<template #default>
|
||||
<div class="right-dropdown-item">
|
||||
<Icon icon="la:reply" width="20" height="20" />
|
||||
<span>{{t('reply')}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="['email','send', 'star'].includes(props.type)" @click="openForward(rightClickEmail)">
|
||||
<template #default>
|
||||
<div class="right-dropdown-item">
|
||||
<Icon icon="iconoir:arrow-up-right" width="19" height="19" />
|
||||
<span>{{t('forward')}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="['email','send', 'star'].includes(props.type)" @click="starChange(rightClickEmail)">
|
||||
<template #default>
|
||||
<div class="right-dropdown-item">
|
||||
<Icon icon="solar:star-line-duotone" width="19" height="19"/>
|
||||
<span>{{t('star')}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="props.type === 'all-email'" @click="handleSearch('user', rightClickEmail.userEmail)">
|
||||
<template #default>
|
||||
<div class="right-dropdown-item">
|
||||
<Icon icon="iconoir:search" width="20" height="20" />
|
||||
<span>{{t('searchUser')}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="props.type === 'all-email' " @click="handleSearch('account', rightClickEmail.toEmail)">
|
||||
<template #default>
|
||||
<div class="right-dropdown-item">
|
||||
<Icon icon="iconoir:search" width="20" height="20" />
|
||||
<span>{{t('searchEmail')}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="props.type === 'all-email' " @click="handleSearch('name', rightClickEmail.name)">
|
||||
<template #default>
|
||||
<div class="right-dropdown-item">
|
||||
<Icon icon="iconoir:search" width="20" height="20" />
|
||||
<span>{{t('searchSender')}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="rightDelete(rightClickEmail.emailId)">
|
||||
<template #default>
|
||||
<div class="right-dropdown-item">
|
||||
<Icon icon="uiw:delete" width="16" height="20" style="margin-left: 1px;margin-right: 3px" />
|
||||
<span>{{t('delete')}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Loading from "@/components/loading/index.vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import skeletonBlock from "@/components/email-scroll/skeleton/index.vue"
|
||||
import {computed, onActivated, reactive, ref, watch} from "vue";
|
||||
import {onBeforeRouteLeave} from "vue-router";
|
||||
import {computed, onActivated, reactive, ref, watch, nextTick, onMounted, onUnmounted } from "vue";
|
||||
import {useEmailStore} from "@/store/email.js";
|
||||
import {useUiStore} from "@/store/ui.js";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import {sleep} from "@/utils/time-utils.js"
|
||||
import {fromNow} from "@/utils/day.js";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {EmailUnreadEnum} from "@/enums/email-enum.js";
|
||||
import { UseVirtualList } from '@vueuse/components'
|
||||
import { useScroll } from '@vueuse/core'
|
||||
|
||||
const props = defineProps({
|
||||
getEmailList: Function,
|
||||
emailDelete: Function,
|
||||
emailRead: Function,
|
||||
starAdd: Function,
|
||||
starCancel: Function,
|
||||
cancelSuccess: Function,
|
||||
@@ -208,19 +285,19 @@ const props = defineProps({
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
skeleton: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
default: 'email'
|
||||
},
|
||||
showFirstLoading: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showUnread: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['jump', 'refresh-before', 'delete-draft'])
|
||||
const emit = defineEmits(['jump', 'refresh-before', 'delete-draft', 'right-search'])
|
||||
const {t} = useI18n()
|
||||
const settingStore = useSettingStore()
|
||||
const uiStore = useUiStore();
|
||||
@@ -229,6 +306,7 @@ const loading = ref(false);
|
||||
const followLoading = ref(false);
|
||||
const noLoading = ref(false);
|
||||
const emailList = reactive([])
|
||||
const expandList = reactive([])
|
||||
const total = ref(0);
|
||||
const checkAll = ref(false);
|
||||
const isIndeterminate = ref(false);
|
||||
@@ -238,17 +316,38 @@ let scrollTop = 0
|
||||
const latestEmail = ref(null)
|
||||
const scrollbarRef = ref(null)
|
||||
let reqLock = false
|
||||
let isMobile = innerWidth < 1367
|
||||
let isMobile = ref(innerWidth < 1367)
|
||||
let skeletonRows = 0
|
||||
const timePaddingRight = ref('');
|
||||
const keyCount = ref(0);
|
||||
const dropdownRef = ref(null);
|
||||
const dropdownCloseLock = ref(false);
|
||||
const dropdownShow = ref(false);
|
||||
const rightClickEmail = ref({});
|
||||
const checkedEmailCount = ref(0);
|
||||
let timer = null
|
||||
const position = ref(
|
||||
DOMRect.fromRect({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
)
|
||||
|
||||
const triggerRef = ref({
|
||||
getBoundingClientRect() {
|
||||
return position.value;
|
||||
}
|
||||
})
|
||||
|
||||
const queryParam = reactive({
|
||||
emailId: 0,
|
||||
size: 30,
|
||||
size: 50
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
refreshList,
|
||||
deleteEmail,
|
||||
addItem,
|
||||
handleList,
|
||||
emailList,
|
||||
firstLoad,
|
||||
latestEmail,
|
||||
@@ -257,18 +356,100 @@ defineExpose({
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
scroll.value.scrollTop = scrollTop
|
||||
requestAnimationFrame(() => {
|
||||
const index = scrollTop / itemHeight.value
|
||||
scrollbarRef.value?.scrollTo(index);
|
||||
})
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
timer = setInterval(() => {
|
||||
emailList.forEach(email => {
|
||||
email.formatCreateTime = fromNow(email.createTime);
|
||||
})
|
||||
}, 1000 * 60);
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer)
|
||||
})
|
||||
|
||||
getEmailList()
|
||||
|
||||
onBeforeRouteLeave(() => {
|
||||
scrollTop = scroll.value.scrollTop
|
||||
window.onresize = () => {
|
||||
isMobile.value = innerWidth < 1367
|
||||
}
|
||||
|
||||
function onScroll(e) {
|
||||
scrollTop = e.target.scrollTop;
|
||||
}
|
||||
|
||||
const { arrivedState } = useScroll(scrollbarRef, {
|
||||
offset: { bottom: 1200 }
|
||||
})
|
||||
|
||||
|
||||
const list = computed(() => {
|
||||
return [...emailList, ...expandList]
|
||||
})
|
||||
|
||||
const itemHeight = computed(() => {
|
||||
if (props.type === 'all-email') {
|
||||
return isMobile.value ? 132 : 65;
|
||||
} else {
|
||||
return isMobile.value ? 83 : 48;
|
||||
}
|
||||
})
|
||||
|
||||
watch(emailList, () => {
|
||||
updateHasScrollbar();
|
||||
})
|
||||
|
||||
watch(scrollbarRef, () => {
|
||||
updateHasScrollbar();
|
||||
})
|
||||
|
||||
// 强制刷新 (itemHeight 更改后虚拟滚动列表不会自己更新)
|
||||
watch(itemHeight, () => {
|
||||
keyCount.value ++
|
||||
})
|
||||
|
||||
watch(followLoading, (isFollowLoading) => {
|
||||
if (isFollowLoading) {
|
||||
expandList.push({
|
||||
emailId: 0,
|
||||
expand: 'loading'
|
||||
})
|
||||
} else {
|
||||
const index = expandList.findIndex(item => item.expand === 'loading')
|
||||
expandList.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
watch(noLoading, (isNoLoading) => {
|
||||
if (isNoLoading) {
|
||||
expandList.push({
|
||||
emailId: 0,
|
||||
expand: 'noMoreData'
|
||||
})
|
||||
} else {
|
||||
const index = expandList.findIndex(item => item.expand === 'noMoreData')
|
||||
expandList.splice(index, 1);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// 监听是否到达底部
|
||||
watch(() => arrivedState.bottom, (isBottom) => {
|
||||
if (isBottom && !loading.value) {
|
||||
loadData();
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => emailList.map(item => item.checked),
|
||||
() => {
|
||||
checkedEmailCount.value = emailList.length
|
||||
if (emailList.length > 0) {
|
||||
updateCheckStatus();
|
||||
}
|
||||
@@ -299,6 +480,67 @@ watch(() => emailStore.addStarEmailId, () => {
|
||||
})
|
||||
})
|
||||
|
||||
window.addEventListener('wheel', (event) => {
|
||||
if (dropdownShow.value) {
|
||||
dropdownRef.value.handleClose();
|
||||
}
|
||||
})
|
||||
|
||||
function openReply(email) {
|
||||
uiStore.writerRef.openReply(email)
|
||||
}
|
||||
|
||||
function openForward(email) {
|
||||
uiStore.writerRef.openForward(email)
|
||||
}
|
||||
|
||||
function visibleChange(e) {
|
||||
dropdownShow.value = e;
|
||||
dropdownCloseLock.value = true;
|
||||
setTimeout(() => {
|
||||
dropdownCloseLock.value = false;
|
||||
},1500)
|
||||
|
||||
if (!e && rightClickEmail.value.rightChecked) {
|
||||
rightClickEmail.value.rightChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleContextmenu = (event, email) => {
|
||||
|
||||
if (props.type === 'draft') {
|
||||
return
|
||||
}
|
||||
|
||||
if (rightClickEmail.value.rightChecked) {
|
||||
rightClickEmail.value.rightChecked = false
|
||||
}
|
||||
|
||||
const { clientX, clientY } = event
|
||||
position.value = DOMRect.fromRect({
|
||||
x: clientX,
|
||||
y: clientY,
|
||||
})
|
||||
event.preventDefault();
|
||||
dropdownRef.value?.handleOpen();
|
||||
|
||||
rightClickEmail.value = email;
|
||||
rightClickEmail.value.rightChecked = true
|
||||
}
|
||||
|
||||
function updateHasScrollbar() {
|
||||
nextTick(() => {
|
||||
const doc = document.querySelector('.virtual');
|
||||
if (doc) {
|
||||
if (doc.scrollHeight > doc.clientHeight) {
|
||||
timePaddingRight.value = '5px';
|
||||
} else {
|
||||
timePaddingRight.value = '15px'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getSkeletonRows() {
|
||||
if (emailList.length > 20) return skeletonRows = 20
|
||||
if (emailList.length === 0) return skeletonRows = 1
|
||||
@@ -340,7 +582,6 @@ function cleanSpace(text) {
|
||||
.trim();
|
||||
}
|
||||
|
||||
|
||||
function starChange(email) {
|
||||
|
||||
if (!email.isStar) {
|
||||
@@ -372,7 +613,79 @@ function changeAccountShow() {
|
||||
uiStore.accountShow = !uiStore.accountShow;
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
const handleRead = () => {
|
||||
const emailIds = getSelectedMailsIds();
|
||||
props.emailRead(emailIds);
|
||||
localRead(emailIds);
|
||||
}
|
||||
|
||||
function emailRead(emailId) {
|
||||
props.emailRead([emailId])
|
||||
localRead([emailId]);
|
||||
}
|
||||
|
||||
function localRead(emailIds) {
|
||||
emailIds.forEach(emailId => {
|
||||
const index = emailList.findIndex(email => email.emailId === emailId);
|
||||
if (index > -1) {
|
||||
emailList[index].unread = EmailUnreadEnum.READ;
|
||||
emailList[index].checked = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function rightDelete(emailId) {
|
||||
|
||||
if (props.type === 'all-email') {
|
||||
ElMessageBox.confirm(t('delOneEmailConfirm'), {
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
props.emailDelete([emailId]).then(() => {
|
||||
ElMessage({
|
||||
message: t('delSuccessMsg'),
|
||||
type: 'success',
|
||||
plain: true
|
||||
})
|
||||
emailStore.deleteIds = [emailId];
|
||||
})
|
||||
})
|
||||
return;
|
||||
}
|
||||
props.emailDelete([emailId]).then(() => {
|
||||
ElMessage({
|
||||
message: t('delSuccessMsg'),
|
||||
type: 'success',
|
||||
plain: true
|
||||
})
|
||||
emailStore.deleteIds = [emailId];
|
||||
})
|
||||
}
|
||||
|
||||
function handleSearch(type, value) {
|
||||
emit('right-search', type, value);
|
||||
}
|
||||
|
||||
async function copyCode(code) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
ElMessage({
|
||||
message: t('copySuccessMsg'),
|
||||
type: 'success',
|
||||
plain: true
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(`${t('copyFailMsg')}:`, err);
|
||||
ElMessage({
|
||||
message: t('copyFailMsg'),
|
||||
type: 'error',
|
||||
plain: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
ElMessageBox.confirm(t('delEmailsConfirm'), {
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
@@ -415,31 +728,45 @@ function addItem(email) {
|
||||
const existIndex = emailList.findIndex(item => item.emailId === email.emailId)
|
||||
|
||||
if (existIndex > -1) {
|
||||
return
|
||||
return false;
|
||||
}
|
||||
|
||||
email.formatText = htmlToText(email);
|
||||
email.formatCreateTime = fromNow(email.formatCreateTime);
|
||||
|
||||
if (props.timeSort) {
|
||||
if (noLoading.value) {
|
||||
emailList.push(email)
|
||||
latestEmail.value = email
|
||||
total.value++
|
||||
} else {
|
||||
total.value++
|
||||
handleList([email]);
|
||||
emailList.push(email);
|
||||
}
|
||||
return;
|
||||
|
||||
if (email.emailId > latestEmail.value?.emailId) {
|
||||
latestEmail.value = email
|
||||
}
|
||||
|
||||
total.value++
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
const index = emailList.findIndex(item => item.emailId < email.emailId)
|
||||
|
||||
if (index !== -1) {
|
||||
handleList([email]);
|
||||
emailList.splice(index, 0, email);
|
||||
} else {
|
||||
if (noLoading.value) {
|
||||
emailList.push(email)
|
||||
handleList([email]);
|
||||
emailList.push(email);
|
||||
}
|
||||
}
|
||||
|
||||
if (email.emailId > latestEmail.value?.emailId) {
|
||||
latestEmail.value = email
|
||||
}
|
||||
|
||||
total.value++
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleCheckAllChange(val) {
|
||||
@@ -458,11 +785,24 @@ function getSelectedDraftsIds() {
|
||||
|
||||
function updateCheckStatus() {
|
||||
const checkedCount = emailList.filter(item => item.checked).length;
|
||||
checkedEmailCount.value = checkedCount;
|
||||
checkAll.value = checkedCount === emailList.length;
|
||||
isIndeterminate.value = checkedCount > 0 && checkedCount < emailList.length;
|
||||
}
|
||||
|
||||
function jumpDetails(email) {
|
||||
|
||||
if (dropdownShow.value) {
|
||||
dropdownRef.value.handleClose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dropdownCloseLock.value) {
|
||||
const sel = window.getSelection();
|
||||
if (sel.toString().trim()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
emit('jump', email)
|
||||
}
|
||||
|
||||
@@ -471,6 +811,8 @@ function getEmailList(refresh = false) {
|
||||
|
||||
if (reqLock) return;
|
||||
|
||||
let emailId = emailList.length > 0 ? emailList.at(-1).emailId : 0;
|
||||
|
||||
reqLock = true
|
||||
|
||||
if (!refresh) {
|
||||
@@ -482,7 +824,9 @@ function getEmailList(refresh = false) {
|
||||
|
||||
} else {
|
||||
getSkeletonRows()
|
||||
emailId = 0
|
||||
loading.value = true
|
||||
scrollTop = 0
|
||||
}
|
||||
|
||||
if (emailList.length === 0) {
|
||||
@@ -491,11 +835,12 @@ function getEmailList(refresh = false) {
|
||||
followLoading.value = !refresh;
|
||||
}
|
||||
let start = Date.now();
|
||||
props.getEmailList(queryParam.emailId, queryParam.size).then(async data => {
|
||||
|
||||
props.getEmailList(emailId, queryParam.size).then(async data => {
|
||||
let end = Date.now();
|
||||
let duration = end - start;
|
||||
if (duration < 500 && !queryParam.emailId) {
|
||||
await sleep(500 - duration)
|
||||
if (duration < 300 && !emailId) {
|
||||
await sleep(300 - duration)
|
||||
}
|
||||
firstLoad.value = false
|
||||
|
||||
@@ -510,21 +855,44 @@ function getEmailList(refresh = false) {
|
||||
}
|
||||
|
||||
latestEmail.value = data.latestEmail
|
||||
emailList.push(...list);
|
||||
|
||||
handleList(list);
|
||||
emailList.push(...list);
|
||||
if (refresh) scrollbarRef.value?.setScrollTop(0);
|
||||
|
||||
noLoading.value = data.list.length < queryParam.size;
|
||||
followLoading.value = data.list.length >= queryParam.size;
|
||||
|
||||
total.value = data.total;
|
||||
queryParam.emailId = data.list.length > 0 ? data.list.at(-1).emailId : 0
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
reqLock = false
|
||||
})
|
||||
}
|
||||
|
||||
function handleList(list) {
|
||||
list.forEach(email => {
|
||||
email.formatText = htmlToText(email)
|
||||
email.formatCreateTime = fromNow(email.createTime);
|
||||
email.test = t('received')
|
||||
const statusIconMap = {
|
||||
0: { icon: 'ic:round-mark-email-read', color: '#51C76B', content: t('received') },
|
||||
1: { icon: 'bi:send-arrow-up-fill', color: '#51C76B', content: t('sent') },
|
||||
2: { icon: 'bi:send-check-fill', color: '#51C76B', content: t('delivered') },
|
||||
3: { icon: 'bi:send-x-fill', color: '#F56C6C', content: t('bounced') },
|
||||
8: { icon: 'bi:send-x-fill', color: '#F56C6C', content: t('bounced') },
|
||||
4: { icon: 'bi:send-exclamation-fill', color: '#FBBD08', content: t('complained') },
|
||||
5: { icon: 'bi:send-arrow-up-fill', color: '#FBBD08', content: t('delayed') },
|
||||
7: { icon: 'ic:round-mark-email-read', color: '#FBBD08', content: t('noRecipient') },
|
||||
};
|
||||
|
||||
if (email.isDel) {
|
||||
email.isDelContent = t('selectDeleted');
|
||||
}
|
||||
email.statusIcon = statusIconMap[email.status];
|
||||
})
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
emit('refresh-before')
|
||||
if (props.skeleton) {
|
||||
@@ -536,7 +904,6 @@ function refresh() {
|
||||
function refreshList() {
|
||||
checkAll.value = false;
|
||||
isIndeterminate.value = false;
|
||||
queryParam.emailId = 0;
|
||||
getEmailList(true);
|
||||
}
|
||||
|
||||
@@ -559,12 +926,11 @@ function loadData() {
|
||||
|
||||
.scroll {
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.scroll-box {
|
||||
height: 100%;
|
||||
.virtual {
|
||||
will-change: scroll-position;
|
||||
}
|
||||
|
||||
.empty {
|
||||
@@ -579,7 +945,7 @@ function loadData() {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 15px 0;
|
||||
padding: 15px 0 0 0;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
@@ -624,7 +990,21 @@ function loadData() {
|
||||
align-items: center;
|
||||
position: relative;
|
||||
transition: background 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
height: 48px;
|
||||
@media (max-width: 1366px) {
|
||||
height: 83px;
|
||||
}
|
||||
|
||||
@media (pointer: coarse) {
|
||||
/* 触屏 */
|
||||
user-select: none;
|
||||
}
|
||||
&.all-email {
|
||||
height: 65px;
|
||||
@media (max-width: 1366px) {
|
||||
height: 132px;
|
||||
}
|
||||
}
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -677,7 +1057,7 @@ function loadData() {
|
||||
justify-content: start;
|
||||
height: 100%;
|
||||
align-self: start;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -701,7 +1081,6 @@ function loadData() {
|
||||
}
|
||||
|
||||
.email-sender {
|
||||
font-weight: bold;;
|
||||
color: var(--el-text-color-primary);
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
@@ -789,14 +1168,37 @@ function loadData() {
|
||||
}
|
||||
|
||||
.email-subject {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
@media (min-width: 1367px) {
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.code-tag {
|
||||
flex: 0 0 auto;
|
||||
max-width: 170px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-primary);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.subject-text {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.email-content {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
@@ -866,7 +1268,7 @@ function loadData() {
|
||||
}
|
||||
|
||||
.email-time {
|
||||
padding-right: 16px !important;
|
||||
padding-right: v-bind(timePaddingRight);
|
||||
}
|
||||
|
||||
:deep(.el-scrollbar__view) {
|
||||
@@ -886,7 +1288,7 @@ function loadData() {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
column-gap: 18px;
|
||||
column-gap: 20px;
|
||||
row-gap: 8px;
|
||||
padding-left: 2px;
|
||||
color: var(--el-text-color-primary);;
|
||||
@@ -925,6 +1327,37 @@ function loadData() {
|
||||
bottom: 1px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.right-dropdown-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item:last-child) {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item:first-child) {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item) {
|
||||
padding-right: 14px;
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.unread {
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
background: var(--el-color-primary);
|
||||
margin-bottom: 2px;
|
||||
margin-right: 5px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div v-for="item in rows" style="background: var(--el-bg-color)">
|
||||
<div class="email-row">
|
||||
<div :class="'email-row ' + type ">
|
||||
<el-checkbox disabled :class=" props.type === 'all-email' ? 'all-email-checkbox' : 'checkbox'"
|
||||
></el-checkbox>
|
||||
<div class="pc-star" v-if="showStar">
|
||||
@@ -46,7 +46,7 @@
|
||||
<el-skeleton animated>
|
||||
<template #template>
|
||||
<el-skeleton-item variant="text"
|
||||
style="width: 180px;margin-right: 15px;height: 1rem;margin-bottom: 4px;"/>
|
||||
style="width: 180px;margin-right: 5px;height: 1rem;margin-bottom: 4px;"/>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</div>
|
||||
@@ -54,7 +54,7 @@
|
||||
<el-skeleton animated>
|
||||
<template #template>
|
||||
<el-skeleton-item variant="text"
|
||||
style="width: 180px;margin-right: 15px;height: 1rem;margin-bottom: 4px;"/>
|
||||
style="width: 180px;margin-right: 5px;height: 1rem;margin-bottom: 4px;"/>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</div>
|
||||
@@ -67,7 +67,7 @@
|
||||
<div class="email-right-skeleton" :style="showUserInfo ? 'align-self: start;':''">
|
||||
<el-skeleton animated>
|
||||
<template #template>
|
||||
<el-skeleton-item variant="text" style="width: 50px;margin-right: 15px;height: 1rem;"/>
|
||||
<el-skeleton-item variant="text" style="width: 60px;margin-right: 15px;height: 1rem;"/>
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</div>
|
||||
@@ -115,6 +115,11 @@ import {Icon} from "@iconify/vue";
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
:deep(.el-skeleton__item) {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 1366px) {
|
||||
.pc-star {
|
||||
display: none;
|
||||
|
||||
@@ -36,13 +36,28 @@ function updateContent() {
|
||||
all: initial;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: 'HarmonyOS', -apple-system, BlinkMacSystemFont,
|
||||
font-family: -apple-system, Inter, BlinkMacSystemFont,
|
||||
'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #13181D;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #0E70DF;
|
||||
}
|
||||
|
||||
.shadow-content {
|
||||
background: #FFFFFF;
|
||||
width: fit-content;
|
||||
@@ -52,7 +67,7 @@ function updateContent() {
|
||||
}
|
||||
|
||||
img:not(table img) {
|
||||
max-width: 100% !important;
|
||||
max-width: 100%;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
@@ -72,16 +87,11 @@ function autoScale() {
|
||||
if (!shadowContent) return
|
||||
|
||||
const parentWidth = parent.offsetWidth
|
||||
const parentHeight = parent.offsetHeight
|
||||
|
||||
const childWidth = shadowContent.scrollWidth
|
||||
const childHeight = shadowContent.scrollHeight
|
||||
|
||||
if (childWidth === 0 || childHeight === 0) return
|
||||
if (childWidth === 0) return
|
||||
|
||||
const scaleX = parentWidth / childWidth
|
||||
const scaleY = parentHeight / childHeight
|
||||
const scale = Math.min(scaleX, scaleY)
|
||||
const scale = parentWidth / childWidth
|
||||
|
||||
const hostElement = shadowRoot.host
|
||||
hostElement.style.zoom = scale
|
||||
@@ -104,7 +114,7 @@ watch(() => props.html, () => {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
font-family: -apple-system, Inter, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
}
|
||||
|
||||
.content-html {
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<script setup>
|
||||
import {ref, onMounted, onBeforeUnmount, watch, nextTick, shallowRef, defineEmits, computed} from 'vue';
|
||||
import loading from "@/components/loading/index.vue";
|
||||
import {compressImage} from "@/utils/file-utils.js";
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useUiStore} from '@/store/ui.js'
|
||||
import {useSettingStore} from '@/store/setting.js'
|
||||
@@ -32,7 +31,7 @@ const props = defineProps({
|
||||
|
||||
|
||||
const {locale} = useI18n()
|
||||
const emit = defineEmits(['change']);
|
||||
const emit = defineEmits(['change','focus']);
|
||||
const editor = shallowRef(null);
|
||||
const isInitialized = ref(false);
|
||||
const editorRef = ref(null);
|
||||
@@ -64,10 +63,6 @@ const language = computed(() => {
|
||||
return 'zh_CN'
|
||||
}
|
||||
|
||||
if (locale.value === 'zhTW') {
|
||||
return 'zh_TW'
|
||||
}
|
||||
|
||||
return 'en'
|
||||
})
|
||||
|
||||
@@ -96,6 +91,8 @@ function initEditor() {
|
||||
statusbar: false,
|
||||
height: "100%",
|
||||
auto_focus: true,
|
||||
//relative_urls: false, //阻止 img标签域名和网站域名相同 自动把链接转换相对路径
|
||||
//remove_script_host: false, // 阻止删除 URL 中的域名
|
||||
forced_root_block: 'div',
|
||||
skin: `${uiStore.dark ? 'oxide-dark' : 'oxide'}`,
|
||||
content_css: `/tinymce/css/index.css,${uiStore.dark ? 'dark' : 'default'}`,
|
||||
@@ -124,6 +121,9 @@ function initEditor() {
|
||||
const text = ed.getContent({format: 'text'});
|
||||
emit('change', content, text);
|
||||
});
|
||||
ed.on('focus', () => {
|
||||
emit('focus', focus);
|
||||
})
|
||||
},
|
||||
autofocus: true,
|
||||
branding: false,
|
||||
@@ -139,7 +139,6 @@ function initEditor() {
|
||||
|
||||
input.addEventListener('change', async (e) => {
|
||||
let file = e.target.files[0];
|
||||
file = await compressImage(file);
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const id = 'blobid' + (new Date()).getTime();
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export const AccountAllReceiveEnum = {
|
||||
DISABLED: 0,
|
||||
ENABLED: 1
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export const EmailUnreadEnum = {
|
||||
UNREAD: 0,
|
||||
READ: 1
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export const AutoRefreshEnum = {
|
||||
DISABLED: 0,
|
||||
ENABLED: 1
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
const en = {
|
||||
'收件箱': 'Inbox',
|
||||
inbox: 'Inbox',
|
||||
drafts: 'Drafts',
|
||||
sent: 'Sent',
|
||||
starred: 'Starred',
|
||||
@@ -12,10 +12,10 @@ const en = {
|
||||
SystemSettings: 'System Settings',
|
||||
noMoreData: 'No more data',
|
||||
noMessagesFound: 'No messages found',
|
||||
addAccount: 'Add Account',
|
||||
addAccount: 'Add Email Address',
|
||||
emailAccount: 'Email',
|
||||
account: 'Account',
|
||||
userAccount: 'User Accounts',
|
||||
account: 'Address',
|
||||
userAccount: 'User Email Address',
|
||||
deleteUser: 'Delete Account',
|
||||
deleteUserBtn: 'Delete',
|
||||
changePassword: 'Change Password',
|
||||
@@ -32,10 +32,10 @@ const en = {
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
delAccount: 'Delete Account',
|
||||
delAccountMsg: 'This will permanently delete your account and data. It cannot be reactivated.',
|
||||
delAccountMsg: 'This will permanently delete your account and data. It cannot be reactivated',
|
||||
totalReceived: 'Total Received',
|
||||
totalSent: 'Total Sent',
|
||||
totalMailboxes: 'Total Accounts',
|
||||
totalMailboxes: 'Total Email Addresses',
|
||||
totalUsers: 'Total Users',
|
||||
deleted: 'Deleted',
|
||||
selectDeleted: 'Deleted',
|
||||
@@ -52,7 +52,7 @@ const en = {
|
||||
tabEmailAddress: 'Email',
|
||||
tabReceived: 'Received',
|
||||
tabSent: 'Sent',
|
||||
tabMailboxes: 'Accounts',
|
||||
tabMailboxes: 'Addresses',
|
||||
tabRegisteredAt: 'Registered at',
|
||||
tabStatus: 'Status',
|
||||
tabRole: 'Role',
|
||||
@@ -73,9 +73,9 @@ const en = {
|
||||
unauthorized: 'Unauthorized',
|
||||
unlimited: 'Unlimited',
|
||||
sendCount: 'Send email : ',
|
||||
accountCount: 'Add account : ',
|
||||
accountCount: 'Add address : ',
|
||||
action: 'Action',
|
||||
chgPwd: 'Pwd',
|
||||
chgPwd: 'Password',
|
||||
perm: 'Role',
|
||||
btnBan: 'Ban',
|
||||
admin: 'Admin',
|
||||
@@ -94,17 +94,18 @@ const en = {
|
||||
order: 'Order',
|
||||
default: 'Default',
|
||||
description: 'Description',
|
||||
removeContent: 'Remove content',
|
||||
removeAll: 'Remove all',
|
||||
expand: 'Expand',
|
||||
collapse: 'Collapse',
|
||||
daily: 'Daily',
|
||||
internal: 'Internal',
|
||||
searchRegKeyDesc: 'Enter invite code to search',
|
||||
remainingUses: 'Remaining Uses',
|
||||
exhausted: 'Exhausted',
|
||||
validUntil: 'Valid Until',
|
||||
expired: 'Expired',
|
||||
copy: 'Copy',
|
||||
copyCode: 'Copy Code',
|
||||
codeLabel: 'Code: ',
|
||||
history: 'History',
|
||||
addRegKey: 'Add Invite Code',
|
||||
regKey: 'Invite Code',
|
||||
@@ -135,9 +136,23 @@ const en = {
|
||||
loginSwitch: 'Sign in',
|
||||
websiteSetting: 'Website',
|
||||
websiteReg: 'Sign Up',
|
||||
loginDomain: 'Sign-In Box Domain',
|
||||
multipleEmail: 'Multiple Accounts',
|
||||
multipleEmailDesc: 'Enable this feature to allow users to add multiple accounts.',
|
||||
loginDomain: 'Hide Sign-In Domain',
|
||||
passkeyLogin: 'Passkey',
|
||||
passkeySetting: 'Passkey',
|
||||
passkeyNotConfigured: 'No Passkey configured',
|
||||
passkeyConfigured: 'Passkey configured',
|
||||
passkeyCreatedAt: 'Created: {time}',
|
||||
passkeyCreate: 'Create Passkey',
|
||||
passkeyReplace: 'Replace',
|
||||
passkeyDelete: 'Delete Passkey',
|
||||
passkeyCurrentPassword: 'Enter your current password',
|
||||
passkeyUnsupported: 'Passkeys are not supported by this browser or device',
|
||||
passkeyLoginFailed: 'Passkey verification failed. Please try again',
|
||||
passkeyCreateSuccess: 'Passkey created successfully',
|
||||
passkeyDeleteSuccess: 'Passkey deleted successfully',
|
||||
passkeyDeleteConfirm: 'Delete the current Passkey?',
|
||||
multipleEmail: 'Multiple Email Address',
|
||||
multipleEmailDesc: 'Enable this feature to allow users to add multiple email',
|
||||
customization: 'Customization',
|
||||
websiteTitle: 'Title',
|
||||
loginBoxOpacity: 'Login Box Opacity',
|
||||
@@ -145,11 +160,17 @@ const en = {
|
||||
emailSetting: 'Email',
|
||||
receiveEmail: 'Receive Email',
|
||||
autoRefresh: 'Auto Refresh',
|
||||
autoRefreshDesc: 'Automatically fetch the latest emails from the server.',
|
||||
autoRefreshDesc: 'Automatically fetch the latest emails from the server',
|
||||
sendEmail: 'Send Email',
|
||||
resendToken: 'Resend Token',
|
||||
cloudflareEmailSending: 'Cloudflare Email Sending',
|
||||
blackList: 'Blocklist',
|
||||
codeRecognition: 'Verification Code Recognition',
|
||||
codeRecognitionRules: 'Recognition Rules',
|
||||
codeRecognitionRulesDesc: 'Only recognize emails containing the following info. Empty means all.',
|
||||
oss: 'Object Storage',
|
||||
osDomain: 'Domain',
|
||||
ossDomainDesc: 'Leave empty if using KV storage.',
|
||||
emailPush: 'Email Push',
|
||||
tgBot: 'Telegram Bot',
|
||||
disable: 'Disable',
|
||||
@@ -160,12 +181,12 @@ const en = {
|
||||
rules: 'Rules',
|
||||
turnstileSetting: 'Turnstile',
|
||||
signUpVerification: 'Sign Up Verification',
|
||||
addEmailVerification: 'Add Account Verification',
|
||||
addEmailVerification: 'Add Email Verification',
|
||||
about: 'About',
|
||||
version: 'Version',
|
||||
community: 'Community',
|
||||
changeTitle: 'Change Title',
|
||||
addResendTokenDesc: 'Input to add; leave empty to delete.',
|
||||
addResendTokenDesc: 'Input to add; leave empty to delete',
|
||||
addOsDomain: 'Add Domain',
|
||||
domainDesc: 'Domain',
|
||||
addTurnstileSecret: 'Add turnstile secret',
|
||||
@@ -173,24 +194,25 @@ const en = {
|
||||
tgBotDesc: 'Forward received emails to a Telegram bot',
|
||||
tgBotToken: 'Bot token',
|
||||
toBotTokenDesc: 'Multiple user chat_ids, separated by commas',
|
||||
otherEmailDesc: 'emails can be forwarded to external email, but must be verified via cloudflare.',
|
||||
otherEmailInputDesc: 'Separate multiple email addresses with commas.',
|
||||
forwardingRulesDesc: 'Rule-based forwarding only forwards emails received by the specified address.',
|
||||
ruleEmailsInputDesc: 'Separate multiple email addresses with commas.',
|
||||
otherEmailDesc: 'emails can be forwarded to external email, but must be verified via cloudflare',
|
||||
otherEmailInputDesc: 'Separate multiple email addresses with commas',
|
||||
forwardingRulesDesc: 'Rule-based forwarding only forwards emails received by the specified address',
|
||||
ruleEmailsInputDesc: 'Separate multiple email addresses with commas',
|
||||
resendTokenList: 'Token List',
|
||||
domain: 'Domain',
|
||||
optional: 'Optional',
|
||||
subjectInputDesc: 'Please enter the email subject.',
|
||||
subjectInputDesc: 'Please enter the email subject',
|
||||
changeUserName: 'Change Username',
|
||||
sendSeparately: 'Separately',
|
||||
send: 'Send',
|
||||
reply: 'Reply',
|
||||
forward: 'Forward',
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
delEmailConfirm: 'Confirm deleting this email?',
|
||||
delSuccessMsg: 'Deleted successfully',
|
||||
emptyEmailMsg: 'Email cannot be empty',
|
||||
notEmailMsg: 'Invalid email',
|
||||
minEmailPrefix: 'Email must be at least {msg} characters',
|
||||
emptyPwdMsg: 'Password cannot be empty',
|
||||
pwdLengthMsg: 'Password must be at least 6 characters',
|
||||
confirmPwdFailMsg: 'The two passwords do not match',
|
||||
@@ -205,6 +227,7 @@ const en = {
|
||||
emptyCountMsg: 'Available count cannot be empty',
|
||||
addSuccessMsg: 'Addition successful',
|
||||
delConfirm: 'Confirm deleting {msg}?',
|
||||
delUsersConfirm: 'Confirm deletion of selected users?',
|
||||
emptyRoleNameMsg: 'Role name cannot be empty',
|
||||
saveSuccessMsg: 'Saved successfully',
|
||||
changeRoleTitle: 'Change Role',
|
||||
@@ -227,7 +250,6 @@ const en = {
|
||||
banRestore: 'Confirm banning {msg}?',
|
||||
logOut: 'Sign out',
|
||||
clearContentConfirm: 'Are you sure to clear all content?',
|
||||
attLimitMsg: 'Attachment size limit: 28MB',
|
||||
emptyRecipientMsg: 'Recipient email cannot be empty',
|
||||
emptySubjectMsg: 'Subject cannot be empty',
|
||||
emptyContentMsg: 'Content cannot be empty',
|
||||
@@ -235,7 +257,8 @@ const en = {
|
||||
sendSuccessMsg: 'Send successful',
|
||||
sendFailMsg: 'Send failed',
|
||||
saveDraftConfirm: 'Save draft?',
|
||||
delEmailsConfirm: 'Confirm batch delete these emails?',
|
||||
delEmailsConfirm: 'Confirm deletion of selected emails?',
|
||||
delOneEmailConfirm: 'Confirm deletion of this email?',
|
||||
sending: 'Sending email...',
|
||||
sendingErrorMsg: 'Sending in progress',
|
||||
networkErrorMsg: 'Network error. Check your internet',
|
||||
@@ -246,19 +269,20 @@ const en = {
|
||||
language: 'Language',
|
||||
totalUserAccount: '{msg}',
|
||||
sendBanned: 'Banned',
|
||||
sendInternal: 'Only internal',
|
||||
wrote: 'wrote',
|
||||
support: 'Support',
|
||||
supportDesc: 'Buy me tea',
|
||||
featDesc: 'Feature Description',
|
||||
emailInterception: 'Email Interception',
|
||||
emailInterceptionDesc: 'Enter a domain or email address to prevent users from receiving emails from certain websites (Enter * to block all).',
|
||||
emailInterceptionDesc: 'Enter a domain or email address to prevent users from receiving emails from certain websites (Enter * to block all)',
|
||||
availableDomains: 'Available Domains',
|
||||
availableDomainsDesc: 'Restrict users to email domains specified. Domains not on the approved list will be blocked from registration, adding email addresses, and sending/receiving emails. If left blank, all domains will be allowed by default.',
|
||||
availableDomainsDesc: 'Restrict users to email domains specified. Domains not on the approved list will be blocked from registration, adding email addresses, and sending/receiving emails. If left blank, all domains will be allowed by default',
|
||||
backgroundUrlDesc: 'Image URL',
|
||||
localUpload: ' Local upload',
|
||||
imageLink: 'Image URL',
|
||||
imageLinkErrorMsg: 'Invalid image URL',
|
||||
backgroundWarning: 'Image file size affects website load speed.',
|
||||
backgroundWarning: 'Image file size affects website load speed',
|
||||
rulesVerify: 'Rules',
|
||||
rulesVerifyTitle: 'Trigger After {count} Daily Uses per IP',
|
||||
botVerifyMsg: 'Please verify that you are human',
|
||||
@@ -278,7 +302,7 @@ const en = {
|
||||
verifyModuleFailed: 'Verification module failed to load. Please refresh the page',
|
||||
popUp: 'Pop Up',
|
||||
noRecipientTitle: 'No Recipient',
|
||||
noRecipientDesc: 'Emails can be received even without a registered email address.',
|
||||
noRecipientDesc: 'Emails can be received even without a registered email address',
|
||||
preview: 'Preview',
|
||||
help: 'Help',
|
||||
document: 'Document',
|
||||
@@ -290,8 +314,43 @@ const en = {
|
||||
to: 'To',
|
||||
clear: 'Clear',
|
||||
include: 'Include',
|
||||
delAllEmailConfirm: 'Do you really want to delete it?',
|
||||
s3Configuration: 'S3 Configuration'
|
||||
delAllConfirm: 'Do you really want to delete it?',
|
||||
pin: 'Pin',
|
||||
s3Configuration: 'S3 Configuration',
|
||||
confirmDeletionOfContacts: 'Confirm clearing contacts?',
|
||||
recentContacts: 'Recent contacts',
|
||||
selectContacts: 'Select',
|
||||
forcePathStyleDesc: 'Some self-hosted object storages require path-style access to be enabled',
|
||||
storageType: 'Storage Location',
|
||||
customDomainDesc: 'Worker custom domain',
|
||||
show: 'Show',
|
||||
hide: 'Hide',
|
||||
onlyName: 'Only name',
|
||||
emailText: 'Email Text',
|
||||
emailPrefix: 'Email Prefix',
|
||||
atLeast: 'At Least',
|
||||
character: '',
|
||||
mustNotContain: 'Must Not Contain',
|
||||
mustNotContainDesc: 'Separate with commas',
|
||||
blackSubjectDesc: 'Subject',
|
||||
blackContentDesc: 'Content',
|
||||
blackFromDesc: 'Sender or domain',
|
||||
senderRules: 'Sender address or domain',
|
||||
blackListDesc: 'Reject emails containing the following information',
|
||||
setSuccess: 'Settings saved successfully',
|
||||
details: 'Details',
|
||||
userDetails: 'User Details',
|
||||
markAsRead: 'Mark as Read',
|
||||
star: 'Star',
|
||||
setRole: 'Set Role',
|
||||
adminDeleteUser: 'Delete User',
|
||||
banUser: 'Ban User',
|
||||
enableUser: 'Enable User',
|
||||
restoreUser: 'Restore User',
|
||||
searchUser: 'Search by user',
|
||||
searchEmail: 'Search by Email',
|
||||
searchSender: 'Search by Sender',
|
||||
userEmail: 'Email Address'
|
||||
}
|
||||
|
||||
export default en
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const zh = {
|
||||
'收件箱': '收件箱',
|
||||
inbox: '收件箱',
|
||||
drafts: '草稿箱',
|
||||
sent: '已发送',
|
||||
starred: '星标邮件',
|
||||
@@ -94,17 +94,18 @@ const zh = {
|
||||
order: '排序',
|
||||
default: '默认',
|
||||
description: '描述',
|
||||
removeContent: '移除正文',
|
||||
removeAll: '丢弃邮件',
|
||||
expand: '展开',
|
||||
collapse: '收起',
|
||||
daily: '每天',
|
||||
internal: '站内',
|
||||
searchRegKeyDesc: '输入注册码搜索',
|
||||
remainingUses: '剩余次数',
|
||||
exhausted: '已用尽',
|
||||
validUntil: '有效至期',
|
||||
expired: '已过期',
|
||||
copy: '复制',
|
||||
copyCode: '复制验证码',
|
||||
codeLabel: '验证码:',
|
||||
history: '记录',
|
||||
addRegKey: '添加注册码',
|
||||
regKey: '注册码',
|
||||
@@ -135,7 +136,21 @@ const zh = {
|
||||
loginSwitch: '去登录',
|
||||
websiteSetting: '网站设置',
|
||||
websiteReg: '用户注册',
|
||||
loginDomain: '登录框域名',
|
||||
loginDomain: '隐藏登录域名',
|
||||
passkeyLogin: 'Passkey 登录',
|
||||
passkeySetting: 'Passkey',
|
||||
passkeyNotConfigured: '尚未创建 Passkey',
|
||||
passkeyConfigured: '已创建 Passkey',
|
||||
passkeyCreatedAt: '创建时间:{time}',
|
||||
passkeyCreate: '创建 Passkey',
|
||||
passkeyReplace: '重新创建',
|
||||
passkeyDelete: '删除 Passkey',
|
||||
passkeyCurrentPassword: '请输入当前密码',
|
||||
passkeyUnsupported: '当前浏览器或设备不支持 Passkey',
|
||||
passkeyLoginFailed: 'Passkey 验证失败,请重试',
|
||||
passkeyCreateSuccess: 'Passkey 创建成功',
|
||||
passkeyDeleteSuccess: 'Passkey 删除成功',
|
||||
passkeyDeleteConfirm: '确认删除当前 Passkey?',
|
||||
multipleEmail: '多号模式',
|
||||
multipleEmailDesc: '开启后账号栏出现一个用户可以添加多个邮箱',
|
||||
customization: '个性化设置',
|
||||
@@ -148,8 +163,14 @@ const zh = {
|
||||
autoRefreshDesc: '轮询请求服务器获取最新邮件',
|
||||
sendEmail: '邮件发送',
|
||||
resendToken: 'Resend Token',
|
||||
cloudflareEmailSending: 'Cloudflare 邮件发送',
|
||||
blackList: '邮件黑名单',
|
||||
codeRecognition: '识别验证码',
|
||||
codeRecognitionRules: '识别规则',
|
||||
codeRecognitionRulesDesc: '只识别包含以下信息的邮件,为空识别全部',
|
||||
oss: '对象存储',
|
||||
osDomain: '访问域名',
|
||||
ossDomainDesc: '如果是KV存储不要填',
|
||||
emailPush: '邮件推送',
|
||||
tgBot: 'Telegram 机器人',
|
||||
disable: '关闭',
|
||||
@@ -182,9 +203,9 @@ const zh = {
|
||||
optional: '可选',
|
||||
subjectInputDesc: '请输入邮件主题',
|
||||
changeUserName: '修改用户名',
|
||||
sendSeparately: '分别发送',
|
||||
send: '发送',
|
||||
reply: '回复',
|
||||
forward: '转发',
|
||||
confirm: '确定',
|
||||
cancel: '取消',
|
||||
delEmailConfirm: '确认删除该邮件吗?',
|
||||
@@ -192,7 +213,8 @@ const zh = {
|
||||
emptyEmailMsg: '邮箱不能为空',
|
||||
notEmailMsg: '输入的邮箱不合法',
|
||||
emptyPwdMsg: '密码不能为空',
|
||||
pwdLengthMsg: '密码最少六位',
|
||||
pwdLengthMsg: '密码至少六位',
|
||||
minEmailPrefix: '邮箱名至少{msg}位',
|
||||
confirmPwdFailMsg: '两次密码输入不一致',
|
||||
emptyRegKeyMsg: '注册码不能为空',
|
||||
regSuccessMsg: '注册成功',
|
||||
@@ -205,6 +227,7 @@ const zh = {
|
||||
emptyCountMsg: '可用次数不能为空',
|
||||
addSuccessMsg: '添加成功',
|
||||
delConfirm: '确认删除{msg}吗?',
|
||||
delUsersConfirm: '确定删除选中的用户吗?',
|
||||
emptyRoleNameMsg: '身份名不能为空',
|
||||
saveSuccessMsg: '保存成功',
|
||||
changeRoleTitle: '修改身份',
|
||||
@@ -227,7 +250,6 @@ const zh = {
|
||||
banRestore: '确认禁用 {msg} 吗?',
|
||||
logOut: '退出',
|
||||
clearContentConfirm: '确定要清空所有内容吗?',
|
||||
attLimitMsg: '附件大小限制28mb',
|
||||
emptyRecipientMsg: '收件人邮箱地址不能为空',
|
||||
emptySubjectMsg: '主题不能为空',
|
||||
emptyContentMsg: '邮件正文不能为空',
|
||||
@@ -235,7 +257,8 @@ const zh = {
|
||||
sendSuccessMsg: '发送成功',
|
||||
sendFailMsg: '发送失败',
|
||||
saveDraftConfirm: '是否保存草稿?',
|
||||
delEmailsConfirm: '确认批量删除这些邮件吗?',
|
||||
delEmailsConfirm: '确认删除选中的邮件吗?',
|
||||
delOneEmailConfirm: '确认删除这个邮件吗?',
|
||||
sending: '邮件正在发送中',
|
||||
sendingErrorMsg: '邮件正在发送中',
|
||||
networkErrorMsg: '网络错误,请检查网络连接',
|
||||
@@ -243,9 +266,10 @@ const zh = {
|
||||
serverBusyErrorMsg: '服务器繁忙,请稍后重试',
|
||||
reqFailErrorMsg: '请求失败,请稍后再试',
|
||||
message: '邮件详情',
|
||||
language: '网站语言',
|
||||
language: '系统语言',
|
||||
totalUserAccount: '{msg} 个',
|
||||
sendBanned: '已禁用',
|
||||
sendInternal: '仅站内',
|
||||
wrote: '来信',
|
||||
support: '捐助',
|
||||
supportDesc: '请我喝杯奶茶',
|
||||
@@ -290,8 +314,42 @@ const zh = {
|
||||
to: '至',
|
||||
clear: '清除',
|
||||
include: '包含',
|
||||
delAllEmailConfirm: '确定要删除吗?',
|
||||
s3Configuration: 'S3 配置'
|
||||
|
||||
delAllConfirm: '确定要删除吗?',
|
||||
pin: '置顶',
|
||||
s3Configuration: 'S3 配置',
|
||||
confirmDeletionOfContacts: '确认清除这些联系人吗?',
|
||||
recentContacts: '最近联系人',
|
||||
selectContacts: '选中',
|
||||
forcePathStyleDesc: '路径样式访问,一些自建的对象存储需要打开',
|
||||
storageType: '存储类型',
|
||||
customDomainDesc: 'Worker 自定义域',
|
||||
show: '显示',
|
||||
hide: '隐藏',
|
||||
onlyName: '仅名字',
|
||||
emailText: '邮件文本',
|
||||
emailPrefix: '邮箱前缀',
|
||||
atLeast: '至少',
|
||||
character: '位',
|
||||
mustNotContain: '禁止包含',
|
||||
mustNotContainDesc: '输入多个值用,分开',
|
||||
blackSubjectDesc: '邮件标题',
|
||||
blackContentDesc: '邮件内容',
|
||||
blackFromDesc: '发件人邮箱或域名',
|
||||
senderRules: '发件人邮箱或域名',
|
||||
blackListDesc: '拒收包含以下信息的邮件',
|
||||
setSuccess: '设置成功',
|
||||
details: '详情',
|
||||
userDetails: '用户详情',
|
||||
markAsRead: '标为已读',
|
||||
star: '星标',
|
||||
setRole: '设置权限',
|
||||
adminDeleteUser: '删除用户',
|
||||
banUser: '封禁用户',
|
||||
enableUser: '启动用户',
|
||||
restoreUser: '恢复用户',
|
||||
searchUser: '搜索用户',
|
||||
searchEmail: '搜索邮箱',
|
||||
searchSender: '搜索发件人',
|
||||
userEmail: '用户邮箱'
|
||||
}
|
||||
export default zh
|
||||
|
||||
@@ -609,4 +609,101 @@ addCollection({
|
||||
}
|
||||
}
|
||||
})
|
||||
addCollection({
|
||||
"prefix": "mage",
|
||||
"lastModified": 1754899875,
|
||||
"aliases": {},
|
||||
"width": 24,
|
||||
"height": 24,
|
||||
"icons": {
|
||||
"user": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M19.618 21.25c0-3.602-4.016-6.53-7.618-6.53s-7.618 2.928-7.618 6.53M12 11.456a4.353 4.353 0 1 0 0-8.706a4.353 4.353 0 0 0 0 8.706\"/>"
|
||||
}
|
||||
}
|
||||
})
|
||||
addCollection({
|
||||
"prefix": "fa7-solid",
|
||||
"lastModified": 1759381039,
|
||||
"aliases": {},
|
||||
"width": 512,
|
||||
"height": 512,
|
||||
"icons": {
|
||||
"user-plus": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M136 128a120 120 0 1 1 240 0a120 120 0 1 1-240 0M48 482.3C48 383.8 127.8 304 226.3 304h59.4c98.5 0 178.3 79.8 178.3 178.3c0 16.4-13.3 29.7-29.7 29.7H77.7C61.3 512 48 498.7 48 482.3M544 96c13.3 0 24 10.7 24 24v48h48c13.3 0 24 10.7 24 24s-10.7 24-24 24h-48v48c0 13.3-10.7 24-24 24s-24-10.7-24-24v-48h-48c-13.3 0-24-10.7-24-24s10.7-24 24-24h48v-48c0-13.3 10.7-24 24-24\"/>",
|
||||
"width": 640
|
||||
}
|
||||
}
|
||||
})
|
||||
addCollection({
|
||||
"prefix": "line-md",
|
||||
"lastModified": 1754899771,
|
||||
"aliases": {},
|
||||
"width": 24,
|
||||
"height": 24,
|
||||
"icons": {
|
||||
"loading-loop": {
|
||||
"body": "<path fill=\"none\" stroke=\"currentColor\" stroke-dasharray=\"16\" stroke-dashoffset=\"16\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 3c4.97 0 9 4.03 9 9\"><animate fill=\"freeze\" attributeName=\"stroke-dashoffset\" dur=\"0.2s\" values=\"16;0\"/><animateTransform attributeName=\"transform\" dur=\"1.5s\" repeatCount=\"indefinite\" type=\"rotate\" values=\"0 12 12;360 12 12\"/></path>"
|
||||
}
|
||||
}
|
||||
})
|
||||
addCollection({
|
||||
"prefix": "mingcute",
|
||||
"lastModified": 1754900188,
|
||||
"aliases": {},
|
||||
"width": 24,
|
||||
"height": 24,
|
||||
"icons": {
|
||||
"github-line": {
|
||||
"body": "<g fill=\"none\"><path d=\"m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z\"/><path fill=\"currentColor\" d=\"M6.315 6.176c-.25-.638-.24-1.367-.129-2.034a6.8 6.8 0 0 1 2.12 1.07c.28.214.647.283.989.18A9.3 9.3 0 0 1 12 5c.961 0 1.874.14 2.703.391c.342.104.709.034.988-.18a6.8 6.8 0 0 1 2.119-1.07c.111.667.12 1.396-.128 2.033c-.15.384-.075.826.208 1.14C18.614 8.117 19 9.04 19 10c0 2.114-1.97 4.187-5.134 4.818c-.792.158-1.101 1.155-.495 1.726c.389.366.629.882.629 1.456v3a1 1 0 0 0 2 0v-3c0-.57-.12-1.112-.334-1.603C18.683 15.35 21 12.993 21 10c0-1.347-.484-2.585-1.287-3.622c.21-.82.191-1.646.111-2.28c-.071-.568-.17-1.312-.57-1.756c-.595-.659-1.58-.271-2.28-.032a9 9 0 0 0-2.125 1.045A11.4 11.4 0 0 0 12 3c-.994 0-1.953.125-2.851.356a9 9 0 0 0-2.125-1.045c-.7-.24-1.686-.628-2.281.031c-.408.452-.493 1.137-.566 1.719l-.005.038c-.08.635-.098 1.462.112 2.283C3.484 7.418 3 8.654 3 10c0 2.992 2.317 5.35 5.334 6.397A4 4 0 0 0 8 17.98l-.168.034c-.717.099-1.176.01-1.488-.122c-.76-.322-1.152-1.133-1.63-1.753c-.298-.385-.732-.866-1.398-1.088a1 1 0 0 0-.632 1.898c.558.186.944 1.142 1.298 1.566c.373.448.869.916 1.58 1.218c.682.29 1.483.393 2.438.276V21a1 1 0 0 0 2 0v-3c0-.574.24-1.09.629-1.456c.607-.572.297-1.568-.495-1.726C6.969 14.187 5 12.114 5 10c0-.958.385-1.881 1.108-2.684c.283-.314.357-.756.207-1.14\"/></g>"
|
||||
}
|
||||
}
|
||||
})
|
||||
addCollection({
|
||||
"prefix": "hugeicons",
|
||||
"lastModified": 1759033396,
|
||||
"aliases": {},
|
||||
"width": 24,
|
||||
"height": 24,
|
||||
"icons": {
|
||||
"view": {
|
||||
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M21.544 11.045c.304.426.456.64.456.955c0 .316-.152.529-.456.955C20.178 14.871 16.689 19 12 19c-4.69 0-8.178-4.13-9.544-6.045C2.152 12.529 2 12.315 2 12c0-.316.152-.529.456-.955C3.822 9.129 7.311 5 12 5c4.69 0 8.178 4.13 9.544 6.045Z\"/><path d=\"M15 12a3 3 0 1 0-6 0a3 3 0 0 0 6 0Z\"/></g>"
|
||||
}
|
||||
}
|
||||
})
|
||||
addCollection({
|
||||
"prefix": "bitcoin-icons",
|
||||
"lastModified": 1754898649,
|
||||
"aliases": {},
|
||||
"width": 24,
|
||||
"height": 24,
|
||||
"icons": {
|
||||
"refresh-filled": {
|
||||
"body": "<g fill=\"currentColor\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"><path d=\"M6.64 9.788a.75.75 0 0 1 .53.918a5 5 0 0 0 7.33 5.624a.75.75 0 1 1 .75 1.3a6.501 6.501 0 0 1-9.529-7.312a.75.75 0 0 1 .919-.53M8.75 6.37a6.5 6.5 0 0 1 9.529 7.312a.75.75 0 1 1-1.45-.388A5.001 5.001 0 0 0 9.5 7.67a.75.75 0 1 1-.75-1.3\"/><path d=\"M5.72 9.47a.75.75 0 0 1 1.06 0l2.5 2.5a.75.75 0 1 1-1.06 1.06l-1.97-1.97l-1.97 1.97a.75.75 0 0 1-1.06-1.06zm9 1.5a.75.75 0 0 1 1.06 0l1.97 1.97l1.97-1.97a.75.75 0 1 1 1.06 1.06l-2.5 2.5a.75.75 0 0 1-1.06 0l-2.5-2.5a.75.75 0 0 1 0-1.06\"/></g>"
|
||||
}
|
||||
}
|
||||
})
|
||||
addCollection({
|
||||
"prefix": "fluent",
|
||||
"lastModified": 1761371481,
|
||||
"aliases": {},
|
||||
"width": 20,
|
||||
"height": 20,
|
||||
"icons": {
|
||||
"mail-read-20-regular": {
|
||||
"body": "<path fill=\"currentColor\" d=\"M9.743 2.071a.5.5 0 0 1 .514 0l6.772 4.063A2 2 0 0 1 18 7.85v6.65a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 2 14.5V7.85a2 2 0 0 1 .971-1.716zM10 3.083L3.486 6.992L3.472 7L10 10.917L16.528 7l-.013-.008zm7 4.8l-6.743 4.046a.5.5 0 0 1-.514 0L3 7.883V14.5A1.5 1.5 0 0 0 4.5 16h11a1.5 1.5 0 0 0 1.5-1.5z\"/>"
|
||||
}
|
||||
}
|
||||
})
|
||||
addCollection({
|
||||
"prefix": "flat-color-icons",
|
||||
"lastModified": 1754899066,
|
||||
"aliases": {},
|
||||
"width": 48,
|
||||
"height": 48,
|
||||
"icons": {
|
||||
"folder": {
|
||||
"body": "<path fill=\"rgb(51, 126, 204)\" d=\"M40 12H22l-4-4H8c-2.2 0-4 1.8-4 4v8h40v-4c0-2.2-1.8-4-4-4\"/><path fill=\"#23C5F0\" d=\"M40 12H8c-2.2 0-4 1.8-4 4v20c0 2.2 1.8 4 4 4h32c2.2 0 4-1.8 4-4V16c0-2.2-1.8-4-4-4\"/>"
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@ export async function init() {
|
||||
document.title = setting.title;
|
||||
|
||||
if (user) {
|
||||
accountStore.currentAccountId = user.accountId;
|
||||
accountStore.currentAccountId = user.account.accountId;
|
||||
accountStore.currentAccount = user.account;
|
||||
userStore.user = user;
|
||||
|
||||
const routers = permsToRouter(user.permKeys);
|
||||
@@ -53,18 +54,4 @@ export async function init() {
|
||||
settingStore.domainList = setting.domainList;
|
||||
document.title = setting.title;
|
||||
}
|
||||
|
||||
removeLoading();
|
||||
}
|
||||
|
||||
function removeLoading() {
|
||||
if (window.innerWidth < 1025) {
|
||||
document.documentElement.style.setProperty('--loading-hide-transition', 'none')
|
||||
}
|
||||
const doc = document.getElementById('loading-first');
|
||||
doc.classList.add('loading-hide')
|
||||
setTimeout(() => {
|
||||
doc.remove()
|
||||
},1000)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,15 @@
|
||||
</div>
|
||||
<el-scrollbar class="scrollbar" ref="scrollbarRef">
|
||||
<div v-infinite-scroll="getAccountList" :infinite-scroll-distance="600" :infinite-scroll-immediate="false">
|
||||
<el-card class="item" :class="itemBg(item.accountId)" v-for="item in accounts" :key="item.accountId"
|
||||
<el-card class="item" :class="itemBg(item.accountId)" v-for="(item, index) in accounts" :key="item.accountId"
|
||||
@click="changeAccount(item)">
|
||||
<div class="account">
|
||||
{{ item.email }}
|
||||
</div>
|
||||
<div class="opt">
|
||||
<div class="send-email" @click.stop>
|
||||
<Icon icon="eva:email-fill" width="22" height="22" color="#fccb1a"/>
|
||||
<Icon @click="setAllReceive(item)" v-if="!item.allReceive" icon="eva:email-fill" width="22" height="22" color="#fccb1a"/>
|
||||
<Icon @click="setAllReceive(item)" v-else icon="flat-color-icons:folder" width="22" height="22" color="#23c4f1" />
|
||||
</div>
|
||||
<div class="settings" @click.stop>
|
||||
<Icon icon="fluent-color:clipboard-24" width="22" height="22" @click.stop="copyAccount(item.email)"/>
|
||||
@@ -23,9 +24,9 @@
|
||||
<Icon icon="fluent:settings-24-filled" width="21" height="21" color="#909399"/>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-if="hasPerm('email:send')" @click="openSetName(item)">{{ $t('rename') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="item.accountId !== userStore.user.accountId && hasPerm('account:delete')"
|
||||
<el-dropdown-item v-if="hasPerm('email:send')" @click="openSetName(item)">{{ $t('rename') }}</el-dropdown-item>
|
||||
<el-dropdown-item v-if="item.accountId !== userStore.user.account.accountId" @click="setAsTop(item, index)">{{ $t('pin') }}</el-dropdown-item>
|
||||
<el-dropdown-item v-if="item.accountId !== userStore.user.account.accountId && hasPerm('account:delete')"
|
||||
@click="remove(item)">{{ $t('delete') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -126,23 +127,33 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {nextTick, reactive, ref, watch} from "vue";
|
||||
import {accountList, accountAdd, accountDelete, accountSetName} from "@/request/account.js";
|
||||
import {computed, nextTick, reactive, ref, watch} from "vue";
|
||||
import {
|
||||
accountList,
|
||||
accountAdd,
|
||||
accountDelete,
|
||||
accountSetName,
|
||||
accountSetAllReceive,
|
||||
accountSetAsTop
|
||||
} from "@/request/account.js";
|
||||
import {sleep} from "@/utils/time-utils.js"
|
||||
import {isEmail} from "@/utils/verify-utils.js";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import {useAccountStore} from "@/store/account.js";
|
||||
import {useEmailStore} from "@/store/email.js";
|
||||
import {useUserStore} from "@/store/user.js";
|
||||
import {hasPerm} from "@/perm/perm.js"
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {AccountAllReceiveEnum} from "@/enums/account-enum.js";
|
||||
|
||||
const {t} = useI18n();
|
||||
const userStore = useUserStore();
|
||||
const accountStore = useAccountStore();
|
||||
const settingStore = useSettingStore();
|
||||
const emailStore = useEmailStore();
|
||||
const showAdd = ref(false)
|
||||
const addLoading = ref(false);
|
||||
const domainList = settingStore.domainList
|
||||
const domainList = computed(() => settingStore.domainList)
|
||||
const accounts = reactive([])
|
||||
const noLoading = ref(false)
|
||||
const loading = ref(false)
|
||||
@@ -165,8 +176,7 @@ const addForm = reactive({
|
||||
})
|
||||
let skeletonRows = 10
|
||||
const queryParams = {
|
||||
accountId: 0,
|
||||
size: 20
|
||||
size: 30
|
||||
}
|
||||
|
||||
const mySelect = ref()
|
||||
@@ -179,6 +189,12 @@ watch(() => accountStore.changeUserAccountName, () => {
|
||||
accounts[0].name = accountStore.changeUserAccountName
|
||||
})
|
||||
|
||||
watch(() => settingStore.domainList, (list) => {
|
||||
if (!addForm.suffix && list.length > 0) {
|
||||
addForm.suffix = list[0]
|
||||
}
|
||||
}, {immediate: true})
|
||||
|
||||
|
||||
const openSelect = () => {
|
||||
mySelect.value.toggleMenu()
|
||||
@@ -234,7 +250,7 @@ function setName() {
|
||||
account.name = name
|
||||
setNameShow.value = false
|
||||
|
||||
if (account.accountId === userStore.user.accountId) {
|
||||
if (account.accountId === userStore.user.account.accountId) {
|
||||
userStore.user.name = name
|
||||
}
|
||||
|
||||
@@ -254,14 +270,38 @@ function openSetName(accountItem) {
|
||||
setNameShow.value = true
|
||||
}
|
||||
|
||||
function setAllReceive(account) {
|
||||
let allReceiveAccount = accounts.find(account => account.allReceive === AccountAllReceiveEnum.ENABLED);
|
||||
if (allReceiveAccount && allReceiveAccount.accountId !== account.accountId) allReceiveAccount.allReceive = AccountAllReceiveEnum.DISABLED;
|
||||
account.allReceive = account.allReceive === AccountAllReceiveEnum.DISABLED ? AccountAllReceiveEnum.ENABLED : AccountAllReceiveEnum.DISABLED;
|
||||
accountSetAllReceive(account.accountId).catch(() => {
|
||||
account.allReceive = account.allReceive === AccountAllReceiveEnum.DISABLED ? AccountAllReceiveEnum.ENABLED : AccountAllReceiveEnum.DISABLED;
|
||||
if (allReceiveAccount) allReceiveAccount.allReceive = AccountAllReceiveEnum.ENABLED;
|
||||
}).then(() => {
|
||||
if (account.allReceive === AccountAllReceiveEnum.ENABLED) {
|
||||
ElMessage({
|
||||
message: t('setSuccess'),
|
||||
type: 'success',
|
||||
plain: true,
|
||||
})
|
||||
}
|
||||
changeAccount(account);
|
||||
emailStore.emailScroll?.refreshList();
|
||||
emailStore.sendScroll?.refreshList();
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function showNullSetting(item) {
|
||||
return !hasPerm('email:send') && !(item.accountId !== userStore.user.accountId && hasPerm('account:delete'))
|
||||
return !hasPerm('email:send') && !(item.accountId !== userStore.user.account.accountId && hasPerm('account:delete'))
|
||||
}
|
||||
|
||||
function itemBg(accountId) {
|
||||
return accountStore.currentAccountId === accountId ? 'item-choose' : ''
|
||||
}
|
||||
|
||||
|
||||
|
||||
function remove(account) {
|
||||
ElMessageBox.confirm(t('delConfirm', {msg: account.email}), {
|
||||
confirmButtonText: t('confirm'),
|
||||
@@ -291,6 +331,7 @@ function refresh() {
|
||||
followLoading.value = false
|
||||
noLoading.value = false
|
||||
queryParams.accountId = 0
|
||||
queryParams.lastSort = null
|
||||
getSkeletonRows();
|
||||
scrollbarRef.value.setScrollTop(0)
|
||||
accounts.splice(0, accounts.length)
|
||||
@@ -303,12 +344,27 @@ function changeAccount(account) {
|
||||
}
|
||||
|
||||
function add() {
|
||||
addForm.suffix = addForm.suffix || settingStore.domainList[0]
|
||||
showAdd.value = true
|
||||
setTimeout(() => {
|
||||
addRef.value.focus()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
function setAsTop(account, index) {
|
||||
accountSetAsTop(account.accountId).then(() => {
|
||||
ElMessage({
|
||||
message: t('setSuccess'),
|
||||
type: 'success',
|
||||
plain: true,
|
||||
})
|
||||
|
||||
const [item] = accounts.splice(index, 1);
|
||||
accounts.splice(1, 0, item);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
async function copyAccount(account) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(account);
|
||||
@@ -339,22 +395,24 @@ function getAccountList() {
|
||||
|
||||
let start = Date.now();
|
||||
|
||||
accountList(queryParams.accountId, queryParams.size).then(async list => {
|
||||
const accountId = accounts.length > 0 ? accounts.at(-1).accountId : 0;
|
||||
const lastSort = accounts.length > 0 ? accounts.at(-1).sort : null;
|
||||
|
||||
accountList(accountId, queryParams.size, lastSort).then(async list => {
|
||||
|
||||
let end = Date.now();
|
||||
let duration = end - start;
|
||||
if (duration < 500) {
|
||||
await sleep(500 - duration)
|
||||
|
||||
if (duration < 300) {
|
||||
await sleep(300 - duration)
|
||||
}
|
||||
|
||||
if (list.length < queryParams.size) {
|
||||
noLoading.value = true
|
||||
}
|
||||
if (accounts.length === 0) {
|
||||
accountStore.currentAccount = list[0].accountId
|
||||
accountStore.currentAccount = list[0]
|
||||
}
|
||||
queryParams.accountId = list.at(-1).accountId
|
||||
|
||||
accounts.push(...list)
|
||||
|
||||
loading.value = false
|
||||
@@ -378,6 +436,15 @@ function submit() {
|
||||
return
|
||||
}
|
||||
|
||||
if (addForm.email.length < settingStore.settings.minEmailPrefix) {
|
||||
ElMessage({
|
||||
message: t('minEmailPrefix', {msg: settingStore.settings.minEmailPrefix}),
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!isEmail(addForm.email + addForm.suffix)) {
|
||||
ElMessage({
|
||||
message: t('notEmailMsg'),
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<el-menu-item @click="router.push({name: 'email'})" index="email"
|
||||
:class="route.meta.name === 'email' ? 'choose-item' : ''">
|
||||
<Icon icon="hugeicons:mailbox-01" width="20" height="20" />
|
||||
<span class="menu-name" style="margin-left: 21px">{{$t('收件箱')}}</span>
|
||||
<span class="menu-name" style="margin-left: 21px">{{$t('inbox')}}</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item @click="router.push({name: 'send'})" index="send" v-perm="'email:send'"
|
||||
:class="route.meta.name === 'send' ? 'choose-item' : ''">
|
||||
@@ -96,8 +96,17 @@ const route = useRoute();
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #1890ff, #3a80dd);
|
||||
transition: all 0.3s ease;
|
||||
max-width: 240px;
|
||||
padding: 0 10px;
|
||||
> div {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: calc(240px - 20px - 30px);
|
||||
}
|
||||
|
||||
:deep(.el-icon) {
|
||||
flex-shrink: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
{{ userStore.user.email }}
|
||||
</div>
|
||||
<div class="detail-user-type">
|
||||
<el-tag>{{ $t(userStore.user.role.name) }}</el-tag>
|
||||
<el-tag>{{ userStore.user.role.name }}</el-tag>
|
||||
</div>
|
||||
<div class="action-info">
|
||||
<div>
|
||||
@@ -113,6 +113,10 @@ const sendType = computed(() => {
|
||||
return t('sendBanned')
|
||||
}
|
||||
|
||||
if (userStore.user.role.sendType === 'internal') {
|
||||
return t('sendInternal')
|
||||
}
|
||||
|
||||
if (!userStore.user.role.sendCount) {
|
||||
return t('unlimited')
|
||||
}
|
||||
@@ -137,6 +141,10 @@ const sendCount = computed(() => {
|
||||
return null
|
||||
}
|
||||
|
||||
if (userStore.user.role.sendType === 'internal') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!userStore.user.role.sendCount) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="send" v-show="show">
|
||||
<div class="write-box" @click.stop>
|
||||
<div class="write-box">
|
||||
<div class="title">
|
||||
<div class="title-left">
|
||||
<span class="title-text">
|
||||
@@ -15,22 +15,36 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<el-input-tag @add-tag="addTagChange" tag-type="primary" size="default" v-model="form.receiveEmail"
|
||||
:placeholder="$t('ruleEmailsInputDesc')">
|
||||
<el-input-tag @add-tag="addTagChange" tag-type="primary" @input="inputChange" size="default" v-model="form.receiveEmail" >
|
||||
<template #prefix>
|
||||
<div class="item-title">{{ $t('recipient') }}</div>
|
||||
<div class="item-title" >{{ $t('recipient') }}</div>
|
||||
<el-select
|
||||
ref="mySelect"
|
||||
class="write-select"
|
||||
popper-class="write-select"
|
||||
:show-arrow="false"
|
||||
:no-match-text="' '"
|
||||
:no-data-text="' '"
|
||||
@visible-change="selectStatusChange"
|
||||
@change="selectChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in selectRecipientList"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
style="color: #999896;"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<span class="distribute" :class="form.manyType ? 'checked' : ''"
|
||||
@click.stop="checkDistribute">{{ $t('sendSeparately') }}</span>
|
||||
<div style="display: flex;margin-right: 3px;">
|
||||
<Icon icon="fa7-solid:user-plus" width="20" height="20" class="add-contact" @click.stop="openContacts" />
|
||||
</div>
|
||||
</template>
|
||||
</el-input-tag>
|
||||
<el-input v-model="form.subject" :placeholder="$t('subjectInputDesc')">
|
||||
<template #prefix>
|
||||
<div class="item-title">{{ $t('subject') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
<tinyEditor :def-value="defValue" ref="editor" @change="change"/>
|
||||
<el-input v-model="form.subject" :placeholder="t('subject')" />
|
||||
<tinyEditor :def-value="defValue" ref="editor" @change="change" @focus="focusChange" />
|
||||
<div class="button-item">
|
||||
<div class="att-add" @click="chooseFile">
|
||||
<Icon icon="iconamoon:attachment-fill" width="24" height="24"/>
|
||||
@@ -49,16 +63,38 @@
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="primary" @click="sendEmail" v-if="form.sendType === 'reply'">{{ $t('reply') }}</el-button>
|
||||
<el-button type="primary" @click="sendEmail" v-else-if="form.sendType === 'forward'">{{ $t('forward') }}</el-button>
|
||||
<el-button type="primary" @click="sendEmail" v-else>{{ $t('send') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog top="10vh" v-model="showContacts" @closed="clearSelectContact" :title="t('recentContacts')">
|
||||
<el-table ref="contactsTabRef" row-key="email" :data="contacts" style="height: 445px">
|
||||
<el-table-column type="selection" width="32" />
|
||||
<el-table-column property="email" :label="t('emailAccount')" >
|
||||
<template #default="props">
|
||||
<div class="email-row">{{ props.row.email }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="55" label="" >
|
||||
<template #default>
|
||||
<div style="display: flex;">
|
||||
<Icon icon="mage:user" style="color: var(--el-text-color-primary)" width="22" height="22" color="#606266" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="contacts-bottom">
|
||||
<el-button type="default" @click="deleteContact">{{t('clear')}}</el-button>
|
||||
<el-button type="primary" @click="chooseContact">{{t('selectContacts')}}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import tinyEditor from '@/components/tiny-editor/index.vue'
|
||||
import {h, nextTick, onMounted, onUnmounted, reactive, ref, toRaw} from "vue";
|
||||
import {h, nextTick, onMounted, onUnmounted, reactive, ref, toRaw, computed} from "vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {useUserStore} from "@/store/user.js";
|
||||
import {emailSend} from "@/request/email.js";
|
||||
@@ -72,17 +108,22 @@ import {toOssDomain} from "@/utils/convert.js";
|
||||
import {formatDetailDate} from "@/utils/day.js";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import {userDraftStore} from "@/store/draft.js";
|
||||
import {useWriterStore} from "@/store/writer.js";
|
||||
import db from "@/db/db.js";
|
||||
import dayjs from "dayjs";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import router from "@/router/index.js";
|
||||
import {ElMessageBox} from "element-plus";
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
openReply,
|
||||
openForward,
|
||||
openDraft
|
||||
})
|
||||
|
||||
const {t} = useI18n()
|
||||
const writerStore = useWriterStore();
|
||||
const draftStore = userDraftStore()
|
||||
const settingStore = useSettingStore()
|
||||
const emailStore = useEmailStore();
|
||||
@@ -94,6 +135,10 @@ const percent = ref(0)
|
||||
let percentMessage = null
|
||||
let sending = false
|
||||
const defValue = ref('')
|
||||
const contactsTabRef = ref({})
|
||||
const showContacts = ref(false)
|
||||
const mySelect = ref()
|
||||
let selectStatus = false
|
||||
const backReply = reactive({
|
||||
receiveEmail: [],
|
||||
subject: '',
|
||||
@@ -104,7 +149,6 @@ const form = reactive({
|
||||
sendEmail: '',
|
||||
receiveEmail: [],
|
||||
accountId: -1,
|
||||
manyType: null,
|
||||
name: '',
|
||||
subject: '',
|
||||
content: '',
|
||||
@@ -115,6 +159,79 @@ const form = reactive({
|
||||
draftId: null,
|
||||
})
|
||||
|
||||
const selectRecipientList = ref([])
|
||||
|
||||
const contacts = computed(() => writerStore.sendRecipientRecord.map(item => ({email: item})))
|
||||
|
||||
function openContacts() {
|
||||
showContacts.value = true
|
||||
nextTick(() => {
|
||||
form.receiveEmail.forEach(item => {
|
||||
if (writerStore.sendRecipientRecord.includes(item)) {
|
||||
contactsTabRef.value.toggleRowSelection({email: item});
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function deleteContact() {
|
||||
ElMessageBox.confirm(t('confirmDeletionOfContacts'), {
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const contactList = contactsTabRef.value.getSelectionRows().map(item => item.email);
|
||||
form.receiveEmail = form.receiveEmail.filter(item => !contactList.includes(item));
|
||||
writerStore.sendRecipientRecord = writerStore.sendRecipientRecord.filter(item => !contactList.includes(item));
|
||||
})
|
||||
}
|
||||
|
||||
function chooseContact() {
|
||||
|
||||
const contactList = contactsTabRef.value.getSelectionRows().map(item => item.email);
|
||||
contactList.forEach(item => {
|
||||
if (!form.receiveEmail.includes(item)) {
|
||||
form.receiveEmail.push(item);
|
||||
}
|
||||
})
|
||||
|
||||
form.receiveEmail = form.receiveEmail.filter(item => {
|
||||
return contactList.includes(item) || !writerStore.sendRecipientRecord.includes(item);
|
||||
});
|
||||
|
||||
showContacts.value = false
|
||||
}
|
||||
|
||||
function clearSelectContact() {
|
||||
contactsTabRef.value.clearSelection();
|
||||
}
|
||||
|
||||
function selectChange(value) {
|
||||
form.receiveEmail.push(value)
|
||||
}
|
||||
|
||||
function selectStatusChange(status) {
|
||||
selectStatus = status
|
||||
}
|
||||
|
||||
const openSelect = () => {
|
||||
mySelect.value.toggleMenu()
|
||||
}
|
||||
|
||||
function inputChange(value) {
|
||||
|
||||
selectRecipientList.value = writerStore.sendRecipientRecord.filter(item => value && !form.receiveEmail.includes(item) && item.startsWith(value)).slice(0, 10);
|
||||
|
||||
if (!selectStatus && selectRecipientList.value.length > 0) {
|
||||
openSelect()
|
||||
}
|
||||
|
||||
if (selectStatus && selectRecipientList.value.length === 0) {
|
||||
openSelect()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function addTagChange(val) {
|
||||
|
||||
const emails = Array.from(new Set(
|
||||
@@ -123,16 +240,14 @@ function addTagChange(val) {
|
||||
|
||||
form.receiveEmail.splice(form.receiveEmail.length - 1, 1)
|
||||
|
||||
let has = false
|
||||
emails.forEach(email => {
|
||||
if (isEmail(email) && !form.receiveEmail.includes(email)) {
|
||||
form.receiveEmail.push(email)
|
||||
has = true
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function checkDistribute() {
|
||||
form.manyType = form.manyType ? null : 'divide'
|
||||
if (selectStatus && has) openSelect()
|
||||
}
|
||||
|
||||
function clearContent() {
|
||||
@@ -153,25 +268,23 @@ function delAtt(index) {
|
||||
function chooseFile() {
|
||||
const doc = document.createElement("input")
|
||||
doc.setAttribute("type", "file")
|
||||
doc.multiple = true;
|
||||
doc.click()
|
||||
doc.onchange = async (e) => {
|
||||
|
||||
const file = e.target.files[0]
|
||||
const size = file.size
|
||||
const filename = file.name
|
||||
const contentType = file.type
|
||||
const fileList = e.target.files;
|
||||
|
||||
for (const file of fileList) {
|
||||
|
||||
const size = file.size
|
||||
const filename = file.name
|
||||
const contentType = file.type
|
||||
|
||||
const content = await fileToBase64(file)
|
||||
form.attachments.push({content, filename, size, contentType})
|
||||
|
||||
const TotalSize = form.attachments.reduce((acc, item) => acc + item.size, 0);
|
||||
if ((TotalSize + size) > 29360128) {
|
||||
ElMessage({
|
||||
message: t('attLimitMsg'),
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
const content = await fileToBase64(file)
|
||||
form.attachments.push({content, filename, size, contentType})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +308,10 @@ async function sendEmail() {
|
||||
return
|
||||
}
|
||||
|
||||
if (!form.content) {
|
||||
form.content = editor.value.getContent();
|
||||
}
|
||||
|
||||
if (!form.content) {
|
||||
ElMessage({
|
||||
message: t('emptyContentMsg'),
|
||||
@@ -251,6 +368,8 @@ async function sendEmail() {
|
||||
|
||||
userStore.refreshUserInfo();
|
||||
|
||||
addRecipientRecord();
|
||||
|
||||
if (form.draftId) {
|
||||
form.subject = ''
|
||||
form.content = ''
|
||||
@@ -258,8 +377,8 @@ async function sendEmail() {
|
||||
draftStore.setDraft = {...toRaw(form)}
|
||||
}
|
||||
|
||||
resetForm()
|
||||
show.value = false
|
||||
resetForm();
|
||||
}).catch((e) => {
|
||||
ElNotification({
|
||||
title: t('sendFailMsg'),
|
||||
@@ -267,7 +386,12 @@ async function sendEmail() {
|
||||
message: h('span', {style: 'color: teal'}, e.message),
|
||||
position: 'bottom-right'
|
||||
})
|
||||
if (e.code === 401) {
|
||||
localStorage.removeItem('token');
|
||||
router.replace('/login');
|
||||
}
|
||||
show.value = true
|
||||
addRecipientRecord();
|
||||
}).finally(() => {
|
||||
percentMessage.close()
|
||||
percent.value = 0
|
||||
@@ -275,6 +399,14 @@ async function sendEmail() {
|
||||
})
|
||||
}
|
||||
|
||||
function addRecipientRecord() {
|
||||
writerStore.sendRecipientRecord = writerStore.sendRecipientRecord.filter(
|
||||
email => !form.receiveEmail.includes(email)
|
||||
);
|
||||
|
||||
writerStore.sendRecipientRecord.unshift(...form.receiveEmail);
|
||||
writerStore.sendRecipientRecord = writerStore.sendRecipientRecord.slice(0, 500);
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.receiveEmail = []
|
||||
@@ -297,6 +429,36 @@ function change(content, text) {
|
||||
form.text = text
|
||||
}
|
||||
|
||||
function focusChange() {
|
||||
if (selectStatus) openSelect()
|
||||
}
|
||||
|
||||
function openForward(email) {
|
||||
resetForm();
|
||||
|
||||
email.subject = email.subject || ''
|
||||
|
||||
form.subject = email.subject
|
||||
form.sendType = 'forward'
|
||||
|
||||
defValue.value = ''
|
||||
|
||||
setTimeout(() => {
|
||||
defValue.value = `
|
||||
${formatImage(email.content) || `<pre style="font-family: inherit;word-break: break-word;white-space: pre-wrap;margin: 0">${email.text}</pre>`}
|
||||
`
|
||||
open()
|
||||
|
||||
nextTick(() => {
|
||||
backReply.content = editor.value.getContent()
|
||||
backReply.subject = form.subject
|
||||
backReply.receiveEmail = form.receiveEmail
|
||||
backReply.sendType = form.sendType
|
||||
})
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function openReply(email) {
|
||||
|
||||
resetForm();
|
||||
@@ -304,7 +466,11 @@ function openReply(email) {
|
||||
email.subject = email.subject || ''
|
||||
|
||||
form.receiveEmail.push(email.sendEmail)
|
||||
form.subject = (email.subject.startsWith('Re:') || email.subject.startsWith('回复:')) ? email.subject : 'Re: ' + email.subject
|
||||
form.subject = (
|
||||
email.subject.startsWith('Re:') ||
|
||||
email.subject.startsWith('Re:') ||
|
||||
email.subject.startsWith('回复:') ||
|
||||
email.subject.startsWith('回复:')) ? email.subject : 'Re: ' + email.subject
|
||||
form.sendType = 'reply'
|
||||
form.emailId = email.emailId
|
||||
|
||||
@@ -343,7 +509,7 @@ function formatImage(content) {
|
||||
function open() {
|
||||
if (!accountStore.currentAccount.email) {
|
||||
form.sendEmail = userStore.user.email;
|
||||
form.accountId = userStore.user.accountId;
|
||||
form.accountId = userStore.user.account.accountId;
|
||||
form.name = userStore.user.name;
|
||||
} else {
|
||||
form.sendEmail = accountStore.currentAccount.email;
|
||||
@@ -378,6 +544,12 @@ onUnmounted(() => {
|
||||
|
||||
function close() {
|
||||
|
||||
if (selectStatus) openSelect();
|
||||
|
||||
if (!form.content) {
|
||||
form.content = editor.value.getContent();
|
||||
}
|
||||
|
||||
if (form.draftId) {
|
||||
draftStore.setDraft = {...toRaw(form)}
|
||||
show.value = false
|
||||
@@ -391,10 +563,13 @@ function close() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (backReply.sendType === 'reply') {
|
||||
if (backReply.sendType === 'reply' || backReply.sendType === 'forward') {
|
||||
let subjectFlag = form.subject === backReply.subject
|
||||
let contentFlag = editor.value.getContent() === backReply.content
|
||||
let receiveFlag = form.receiveEmail.length === 1 && form.receiveEmail[0] === backReply.receiveEmail[0]
|
||||
if (backReply.sendType === 'forward' && form.receiveEmail.length === 0) {
|
||||
receiveFlag = true;
|
||||
}
|
||||
if (subjectFlag && contentFlag && receiveFlag) {
|
||||
resetForm();
|
||||
close()
|
||||
@@ -408,7 +583,7 @@ function close() {
|
||||
type: 'warning',
|
||||
distinguishCancelAndClose: true
|
||||
}).then(async () => {
|
||||
const formData = {...toRaw(form)}
|
||||
const formData = {...toRaw(form)};
|
||||
delete formData.draftId
|
||||
delete formData.attachments
|
||||
formData.createTime = dayjs().utc().format('YYYY-MM-DD HH:mm:ss');
|
||||
@@ -416,7 +591,9 @@ function close() {
|
||||
db.value.att.add({draftId, attachments: toRaw(form.attachments)})
|
||||
draftStore.refreshList++
|
||||
show.value = false
|
||||
resetForm()
|
||||
await nextTick(() => {
|
||||
resetForm()
|
||||
})
|
||||
}).catch((action) => {
|
||||
if (action === 'cancel') {
|
||||
show.value = false
|
||||
@@ -427,7 +604,18 @@ function close() {
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.write-select .el-select-dropdown__list {
|
||||
padding: 4px 4px !important;
|
||||
}
|
||||
.write-select .el-select-dropdown__item {
|
||||
padding: 0 10px 0 10px;
|
||||
}
|
||||
|
||||
.write-select .el-select-dropdown {
|
||||
min-width: 0 !important;
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
.send {
|
||||
position: fixed;
|
||||
@@ -506,28 +694,6 @@ function close() {
|
||||
grid-template-rows: auto auto 1fr auto;
|
||||
gap: 15px;
|
||||
|
||||
.distribute {
|
||||
color: var(--el-color-info);
|
||||
background: var(--el-color-info-light-9);
|
||||
border: var(--el-color-info-light-8);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.distribute.checked {
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary) !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
||||
.distribute:hover {
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary) !important;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
}
|
||||
|
||||
@@ -578,6 +744,40 @@ function close() {
|
||||
|
||||
}
|
||||
|
||||
.email-row {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
width: 420px !important;
|
||||
@media (max-width: 460px) {
|
||||
width: calc(100% - 40px) !important;
|
||||
margin-right: 20px !important;
|
||||
margin-left: 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.contacts-bottom {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.add-contact {
|
||||
color: var(--regular-text-color)
|
||||
}
|
||||
|
||||
.write-select {
|
||||
position: absolute;
|
||||
width: 300px;
|
||||
left: 60px;
|
||||
z-index: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.el-input-tag__suffix) {
|
||||
padding-right: 4px;
|
||||
}
|
||||
@@ -585,4 +785,4 @@ function close() {
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import http from '@/axios/index.js'
|
||||
|
||||
export function accountList(accountId, size) {
|
||||
return http.get('/account/list', {params: {accountId, size}});
|
||||
export function accountList(accountId, size, lastSort) {
|
||||
return http.get('/account/list', {params: {accountId, size, lastSort}});
|
||||
}
|
||||
|
||||
export function accountAdd(email,token) {
|
||||
@@ -16,3 +16,10 @@ export function accountDelete(accountId) {
|
||||
return http.delete('/account/delete', {params: {accountId}})
|
||||
}
|
||||
|
||||
export function accountSetAllReceive(accountId) {
|
||||
return http.put('/account/setAllReceive', {accountId})
|
||||
}
|
||||
|
||||
export function accountSetAsTop(accountId) {
|
||||
return http.put('/account/setAsTop', {accountId})
|
||||
}
|
||||
@@ -11,3 +11,7 @@ export function allEmailDelete(emailIds) {
|
||||
export function allEmailBatchDelete(params) {
|
||||
return http.delete('/allEmail/batchDelete', {params: params} )
|
||||
}
|
||||
|
||||
export function allEmailLatest(emailId) {
|
||||
return http.get('/allEmail/latest', {params: {emailId}, noMsg: true, timeout: 35 * 1000})
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import http from '@/axios/index.js';
|
||||
|
||||
export function emailList(accountId, emailId, timeSort, size, type) {
|
||||
return http.get('/email/list', {params: {accountId, emailId, timeSort, size, type}})
|
||||
export function emailList(accountId, allReceive, emailId, timeSort, size, type) {
|
||||
return http.get('/email/list', {params: {accountId, allReceive, emailId, timeSort, size, type}})
|
||||
}
|
||||
|
||||
export function emailDelete(emailIds) {
|
||||
return http.delete('/email/delete?emailIds=' + emailIds)
|
||||
}
|
||||
|
||||
export function emailLatest(emailId, accountId) {
|
||||
return http.get('/email/latest', {params: {emailId, accountId}, noMsg: true })
|
||||
export function emailLatest(emailId, accountId, allReceive) {
|
||||
return http.get('/email/latest', {params: {emailId, accountId, allReceive}, noMsg: true, timeout: 35 * 1000})
|
||||
}
|
||||
|
||||
export function emailRead(emailIds) {
|
||||
return http.put('/email/read', {emailIds})
|
||||
}
|
||||
|
||||
export function emailSend(form,progress) {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import http from '@/axios/index.js';
|
||||
|
||||
export function oauthLinuxDoLogin(code) {
|
||||
return http.post('/oauth/linuxDo/login',{code})
|
||||
}
|
||||
|
||||
export function oauthBindUser(form) {
|
||||
return http.put('/oauth/bindUser', form)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import http from '@/axios/index.js';
|
||||
|
||||
export function passkeyAuthOptions() {
|
||||
return http.post('/passkey/auth/options')
|
||||
}
|
||||
|
||||
export function passkeyAuthVerify(transactionId, response) {
|
||||
return http.post('/passkey/auth/verify', {transactionId, response})
|
||||
}
|
||||
|
||||
export function passkeyStatus() {
|
||||
return http.get('/my/passkey')
|
||||
}
|
||||
|
||||
export function passkeyRegOptions(password) {
|
||||
return http.post('/my/passkey/reg/options', {password})
|
||||
}
|
||||
|
||||
export function passkeyRegVerify(transactionId, response) {
|
||||
return http.post('/my/passkey/reg/verify', {transactionId, response})
|
||||
}
|
||||
|
||||
export function passkeyDelete(password) {
|
||||
return http.delete('/my/passkey', {data: {password}})
|
||||
}
|
||||
@@ -5,7 +5,7 @@ export function roleAdd(params) {
|
||||
}
|
||||
|
||||
export function rolePermTree() {
|
||||
return http.get('/role/permTree')
|
||||
return http.get('/role/tree')
|
||||
}
|
||||
|
||||
export function roleRoleList() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import http from '@/axios/index.js';
|
||||
|
||||
export function settingSet(setting) {
|
||||
return http.put('/setting/set',setting)
|
||||
return http.put('/setting/set', setting)
|
||||
}
|
||||
|
||||
export function settingQuery() {
|
||||
@@ -16,6 +16,10 @@ export function setBackground(background) {
|
||||
return http.put('/setting/setBackground',{background})
|
||||
}
|
||||
|
||||
export function physicsDeleteAll() {
|
||||
return http.delete('/setting/physicsDeleteAll')
|
||||
export function deleteBackground() {
|
||||
return http.delete('/setting/deleteBackground')
|
||||
}
|
||||
|
||||
export function setBlackList(params) {
|
||||
return http.put('/setting/setBlacklist', params)
|
||||
}
|
||||
@@ -18,8 +18,8 @@ export function userSetType(params) {
|
||||
}
|
||||
|
||||
|
||||
export function userDelete(userId) {
|
||||
return http.delete('/user/delete', {params:{userId}})
|
||||
export function userDelete(userIds) {
|
||||
return http.delete('/user/delete', {params:{userIds: userIds + ''}})
|
||||
}
|
||||
|
||||
export function userAdd(form) {
|
||||
|
||||
@@ -16,7 +16,7 @@ const routes = [
|
||||
name: 'email',
|
||||
component: () => import('@/views/email/index.vue'),
|
||||
meta: {
|
||||
title: '收件箱',
|
||||
title: 'inbox',
|
||||
name: 'email',
|
||||
menu: true
|
||||
}
|
||||
@@ -92,9 +92,11 @@ router.beforeEach((to, from, next) => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
|
||||
timer = setTimeout(() => {
|
||||
NProgress.start()
|
||||
}, first ? 200 : 100)
|
||||
if (!first) {
|
||||
timer = setTimeout(() => {
|
||||
NProgress.start()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
@@ -135,17 +137,25 @@ function loadBackground(next) {
|
||||
next()
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
console.warn("背景加载超时,已放行");
|
||||
next()
|
||||
}, 3000)
|
||||
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
router.afterEach((to) => {
|
||||
|
||||
clearTimeout(timer)
|
||||
NProgress.done();
|
||||
if (first) {
|
||||
removeLoading()
|
||||
} else {
|
||||
NProgress.done();
|
||||
}
|
||||
|
||||
const uiStore = useUiStore()
|
||||
if (to.meta.menu) {
|
||||
@@ -163,4 +173,13 @@ router.afterEach((to) => {
|
||||
first = false
|
||||
})
|
||||
|
||||
function removeLoading() {
|
||||
const doc = document.getElementById('loading-first');
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
doc.remove()
|
||||
}
|
||||
|
||||
export default router
|
||||
|
||||
@@ -12,6 +12,7 @@ export const useEmailStore = defineStore('email', {
|
||||
delType: null,
|
||||
showStar: true,
|
||||
showReply: true,
|
||||
showUnread: false
|
||||
},
|
||||
sendScroll: null,
|
||||
}),
|
||||
|
||||
@@ -11,5 +11,8 @@ export const useSettingStore = defineStore('setting', {
|
||||
}),
|
||||
actions: {
|
||||
|
||||
}
|
||||
},
|
||||
persist: {
|
||||
pick: ['lang'],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
export const useWriterStore = defineStore('writer', {
|
||||
state: () => ({
|
||||
|
||||
})
|
||||
})
|
||||
sendRecipientRecord: []
|
||||
}),
|
||||
persist: {
|
||||
pick: ['sendRecipientRecord'],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--dark-border) transparent;
|
||||
}
|
||||
|
||||
html, body {
|
||||
@@ -24,7 +27,7 @@ html, body {
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
font-family: -apple-system, Inter, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 14px;
|
||||
@@ -34,10 +37,6 @@ html, body {
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
list-style: none;
|
||||
}
|
||||
@@ -172,28 +171,6 @@ button, input, select, textarea {
|
||||
--el-input-text-color: var(--el-text-color-primary) !important;
|
||||
}
|
||||
|
||||
|
||||
@media (pointer: fine) and (hover: hover) {
|
||||
/* 整个滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px; /* 垂直滚动条宽度 */
|
||||
height: 6px; /* 水平滚动条高度 */
|
||||
}
|
||||
|
||||
/* 滚动条轨道 */
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* 滚动条滑块 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-track-color);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
/* 视图过渡,基于径向剪裁 */
|
||||
html.dark,
|
||||
html:not(.dark) {
|
||||
|
||||
@@ -13,6 +13,10 @@ export function cvtR2Url(key) {
|
||||
|
||||
let domain = settings.r2Domain
|
||||
|
||||
if (!domain) {
|
||||
return key;
|
||||
}
|
||||
|
||||
if (!domain.startsWith('http')) {
|
||||
return 'https://' + domain + '/' + key
|
||||
}
|
||||
@@ -26,7 +30,7 @@ export function cvtR2Url(key) {
|
||||
export function toOssDomain(domain) {
|
||||
|
||||
if (!domain) {
|
||||
return null
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!domain.startsWith('http')) {
|
||||
|
||||
@@ -56,6 +56,14 @@ export function fromNow(date) {
|
||||
|
||||
}
|
||||
|
||||
export function updateNow(date) {
|
||||
if (isToday) {
|
||||
if (diffSeconds < 60) return `Just now`;
|
||||
if (diffMinutes < 60) return `${diffMinutes} min ago`;
|
||||
if (diffHours < 2) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
||||
return d.format('hh:mm A');
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDetailDate(time) {
|
||||
const d = dayjs.utc(time).tz(timeZone);
|
||||
|
||||
@@ -10,8 +10,11 @@
|
||||
show-status
|
||||
actionLeft="4px"
|
||||
:show-account-icon="false"
|
||||
:time-sort="params.timeSort"
|
||||
:item-height="65"
|
||||
@jump="jumpContent"
|
||||
@refresh-before="refreshBefore"
|
||||
@right-search="rightSearch"
|
||||
:type="'all-email'"
|
||||
|
||||
>
|
||||
@@ -87,24 +90,30 @@
|
||||
<script setup>
|
||||
import {starAdd, starCancel} from "@/request/star.js";
|
||||
import emailScroll from "@/components/email-scroll/index.vue"
|
||||
import {computed, defineOptions, reactive, ref, watch} from "vue";
|
||||
import {computed, defineOptions, reactive, ref, watch, onMounted} from "vue";
|
||||
import {useEmailStore} from "@/store/email.js";
|
||||
import {
|
||||
allEmailList,
|
||||
allEmailDelete,
|
||||
allEmailBatchDelete
|
||||
allEmailBatchDelete,
|
||||
allEmailLatest
|
||||
} from "@/request/all-email.js";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import router from "@/router/index.js";
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {toUtc} from "@/utils/day.js";
|
||||
import {sleep} from "@/utils/time-utils.js";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
defineOptions({
|
||||
name: 'all-email'
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const {t} = useI18n();
|
||||
const emailStore = useEmailStore();
|
||||
const settingStore = useSettingStore();
|
||||
const clearTime = ref('')
|
||||
const sysEmailScroll = ref({})
|
||||
const searchValue = ref('')
|
||||
@@ -112,6 +121,10 @@ const mySelect = ref()
|
||||
const showBathDelete = ref(false)
|
||||
const clearLoading = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
latest();
|
||||
})
|
||||
|
||||
const openSelect = () => {
|
||||
mySelect.value.toggleMenu()
|
||||
}
|
||||
@@ -191,7 +204,7 @@ function batchDelete() {
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(
|
||||
t('delAllEmailConfirm'),
|
||||
t('delAllConfirm'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
@@ -214,6 +227,12 @@ function batchDelete() {
|
||||
})
|
||||
}
|
||||
|
||||
function rightSearch(type, value) {
|
||||
params.searchType = type;
|
||||
searchValue.value = value;
|
||||
search();
|
||||
}
|
||||
|
||||
function refreshBefore() {
|
||||
searchValue.value = null
|
||||
params.timeSort = 0
|
||||
@@ -272,6 +291,69 @@ function jumpContent(email) {
|
||||
function getEmailList(emailId, size) {
|
||||
return allEmailList({emailId, size, ...params})
|
||||
}
|
||||
|
||||
async function latest() {
|
||||
|
||||
while (true) {
|
||||
|
||||
let autoRefresh = settingStore.settings.autoRefresh;
|
||||
|
||||
await sleep(autoRefresh > 1 ? autoRefresh * 1000 : 3000);
|
||||
|
||||
const latestId = sysEmailScroll.value.latestEmail?.emailId
|
||||
|
||||
if (autoRefresh < 2) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!latestId && latestId !== 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (route.name !== 'all-email') {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
if (params.type !== 'receive') {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const curTimeSort = params.timeSort
|
||||
let list = await allEmailLatest(latestId)
|
||||
|
||||
if (list.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (params.type !== 'receive') {
|
||||
continue
|
||||
}
|
||||
|
||||
// 确保回来之后条件没变
|
||||
if (params.timeSort !== curTimeSort) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (let email of list) {
|
||||
|
||||
sysEmailScroll.value.addItem(email)
|
||||
await sleep(50)
|
||||
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
if (e.code === 401 || e.code === 403) {
|
||||
settingStore.settings.autoRefresh = 0;
|
||||
}
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
|
||||
@@ -397,7 +479,7 @@ function getEmailList(emailId, size) {
|
||||
}
|
||||
|
||||
.clear {
|
||||
@media (max-width: 407px) {
|
||||
@media (max-width: 419px) {
|
||||
position: absolute;
|
||||
top: 41px;
|
||||
left: 242px;
|
||||
@@ -405,10 +487,24 @@ function getEmailList(emailId, size) {
|
||||
}
|
||||
|
||||
:deep(.reload) {
|
||||
@media (max-width: 407px) {
|
||||
@media (max-width: 419px) {
|
||||
position: absolute;
|
||||
top: 42px;
|
||||
left: 208px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.delete) {
|
||||
@media (max-width: 456px) {
|
||||
position: absolute;
|
||||
top: 43px;
|
||||
left: 294px;
|
||||
}
|
||||
|
||||
@media (max-width: 419px) {
|
||||
position: absolute;
|
||||
top: 43px;
|
||||
left: 282px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
<Icon class="icon" @click="changeStar" v-if="email.isStar" icon="fluent-color:star-16" width="20" height="20"/>
|
||||
<Icon class="icon" @click="changeStar" v-else icon="solar:star-line-duotone" width="18" height="18"/>
|
||||
</span>
|
||||
<Icon class="icon" v-if="emailStore.contentData.showReply" v-perm="'email:send'" @click="openReply" icon="la:reply" width="20" height="20" />
|
||||
<Icon class="icon" v-if="emailStore.contentData.showReply" v-perm="'email:send'" @click="openReply" icon="la:reply" width="21" height="21" />
|
||||
<Icon class="icon" v-if="emailStore.contentData.showReply" v-perm="'email:send'" @click="openForward" icon="iconoir:arrow-up-right" width="20" height="20" />
|
||||
</div>
|
||||
<div></div>
|
||||
<el-scrollbar class="scrollbar">
|
||||
@@ -29,7 +30,7 @@
|
||||
<div>{{ formatDetailDate(email.createTime) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-alert v-if="email.status === 3" :closable="false" :title="`${$t('bounced')} ` + toMessage(email.message)" class="email-msg" type="error" show-icon />
|
||||
<el-alert v-if="email.status === 3" :closable="false" :title="toMessage(email.message)" class="email-msg" type="error" show-icon />
|
||||
<el-alert v-if="email.status === 4" :closable="false" :title="$t('complained')" class="email-msg" type="warning" show-icon />
|
||||
<el-alert v-if="email.status === 5" :closable="false" :title="$t('delayed')" class="email-msg" type="warning" show-icon />
|
||||
</div>
|
||||
@@ -74,10 +75,10 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import ShadowHtml from '@/components/shadow-html/index.vue'
|
||||
import {reactive, ref, watch} from "vue";
|
||||
import {reactive, ref, watch, onMounted, onUnmounted} from "vue";
|
||||
import {useRouter} from 'vue-router'
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import {emailDelete} from "@/request/email.js";
|
||||
import {emailDelete, emailRead} from "@/request/email.js";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {useEmailStore} from "@/store/email.js";
|
||||
import {useAccountStore} from "@/store/account.js";
|
||||
@@ -90,6 +91,7 @@ import {useSettingStore} from "@/store/setting.js";
|
||||
import {allEmailDelete} from "@/request/all-email.js";
|
||||
import {useUiStore} from "@/store/ui.js";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {EmailUnreadEnum} from "@/enums/email-enum.js";
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const settingStore = useSettingStore();
|
||||
@@ -105,10 +107,25 @@ watch(() => accountStore.currentAccountId, () => {
|
||||
handleBack()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (emailStore.contentData.showUnread && email.unread === EmailUnreadEnum.UNREAD) {
|
||||
email.unread = EmailUnreadEnum.READ;
|
||||
emailRead([email.emailId]);
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
emailStore.contentData.showUnread = false;
|
||||
})
|
||||
|
||||
function openReply() {
|
||||
uiStore.writerRef.openReply(email)
|
||||
}
|
||||
|
||||
function openForward() {
|
||||
uiStore.writerRef.openForward(email)
|
||||
}
|
||||
|
||||
function toMessage(message) {
|
||||
return message ? JSON.parse(message).message : '';
|
||||
}
|
||||
@@ -116,7 +133,6 @@ function toMessage(message) {
|
||||
function formatImage(content) {
|
||||
content = content || '';
|
||||
const domain = settingStore.settings.r2Domain;
|
||||
console.log(domain)
|
||||
return content.replace(/{{domain}}/g, toOssDomain(domain) + '/');
|
||||
}
|
||||
|
||||
@@ -205,7 +221,7 @@ const handleDelete = () => {
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
padding: 9px 15px;
|
||||
padding: 9px 15px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
@@ -409,4 +425,4 @@ const handleDelete = () => {
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<emailScroll ref="scroll"
|
||||
:allow-star="false"
|
||||
:cancel-success="cancelStar"
|
||||
:getEmailList="getEmailList"
|
||||
:emailDelete="emailDelete"
|
||||
:star-add="starAdd"
|
||||
@@ -15,7 +14,7 @@
|
||||
:type="'draft'"
|
||||
>
|
||||
<template #name="props">
|
||||
<span class="send-email">{{ props.email.receiveEmail.join(',') || '(' + $t('noRecipient') + ')' }}</span>
|
||||
<span class="send-email">{{ props.email.receiveEmail?.join(',') || '(' + $t('noRecipient') + ')' }}</span>
|
||||
</template>
|
||||
<template #subject="props">
|
||||
{{ props.email.subject || '(' + $t('noSubject') + ')' }}
|
||||
@@ -27,8 +26,7 @@
|
||||
import emailScroll from "@/components/email-scroll/index.vue"
|
||||
import {emailDelete} from "@/request/email.js";
|
||||
import {starAdd, starCancel} from "@/request/star.js";
|
||||
import {useEmailStore} from "@/store/email.js";
|
||||
import {defineOptions, onMounted, ref, watch, toRaw} from "vue";
|
||||
import {defineOptions, ref, watch, toRaw} from "vue";
|
||||
import {useUiStore} from "@/store/ui.js";
|
||||
import {userDraftStore} from "@/store/draft.js";
|
||||
import db from "@/db/db.js"
|
||||
@@ -40,7 +38,6 @@ defineOptions({
|
||||
const draftStore = userDraftStore();
|
||||
const uiStore = useUiStore();
|
||||
const scroll = ref({})
|
||||
const emailStore = useEmailStore();
|
||||
|
||||
watch(() => draftStore.setDraft, async () => {
|
||||
|
||||
@@ -68,6 +65,7 @@ watch(() => draftStore.setDraft, async () => {
|
||||
watch(() => draftStore.refreshList, async () => {
|
||||
const {list} = await getEmailList();
|
||||
scroll.value.emailList.length = 0
|
||||
scroll.value.handleList(list);
|
||||
scroll.value.emailList.push(...list)
|
||||
})
|
||||
|
||||
@@ -90,15 +88,6 @@ async function jumpContent(email) {
|
||||
uiStore.writerRef.openDraft(email);
|
||||
}
|
||||
|
||||
function cancelStar(email) {
|
||||
emailStore.cancelStarEmailId = email.emailId
|
||||
scroll.value.deleteEmail([email.emailId])
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
emailStore.starScroll = scroll
|
||||
})
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.send-email {
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
:star-add="starAdd"
|
||||
:star-cancel="starCancel"
|
||||
:time-sort="params.timeSort"
|
||||
:email-read="emailRead"
|
||||
:show-unread="true"
|
||||
actionLeft="4px"
|
||||
@jump="jumpContent"
|
||||
>
|
||||
@@ -25,17 +27,19 @@ import {useAccountStore} from "@/store/account.js";
|
||||
import {useEmailStore} from "@/store/email.js";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import emailScroll from "@/components/email-scroll/index.vue"
|
||||
import {emailList, emailDelete, emailLatest} from "@/request/email.js";
|
||||
import {emailList, emailDelete, emailLatest, emailRead} from "@/request/email.js";
|
||||
import {starAdd, starCancel} from "@/request/star.js";
|
||||
import {defineOptions, onMounted, reactive, ref, watch} from "vue";
|
||||
import {defineOptions, h, onMounted, reactive, ref, watch} from "vue";
|
||||
import {sleep} from "@/utils/time-utils.js";
|
||||
import router from "@/router/index.js";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
defineOptions({
|
||||
name: 'email'
|
||||
})
|
||||
|
||||
const route = useRoute();
|
||||
const emailStore = useEmailStore();
|
||||
const accountStore = useAccountStore();
|
||||
const settingStore = useSettingStore();
|
||||
@@ -62,6 +66,7 @@ function changeTimeSort() {
|
||||
function jumpContent(email) {
|
||||
emailStore.contentData.email = email
|
||||
emailStore.contentData.delType = 'logic'
|
||||
emailStore.contentData.showUnread = true
|
||||
emailStore.contentData.showStar = true
|
||||
emailStore.contentData.showReply = true
|
||||
router.push('/message')
|
||||
@@ -71,29 +76,57 @@ const existIds = new Set();
|
||||
|
||||
async function latest() {
|
||||
while (true) {
|
||||
const latestId = scroll.value.latestEmail?.emailId || 0
|
||||
|
||||
if (!scroll.value.firstLoad && settingStore.settings.autoRefreshTime) {
|
||||
let autoRefresh = settingStore.settings.autoRefresh;
|
||||
await sleep(autoRefresh > 1 ? autoRefresh * 1000 : 3000);
|
||||
|
||||
if (route.name !== 'email') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const latestId = scroll.value.latestEmail?.emailId
|
||||
|
||||
if (!scroll.value.firstLoad && autoRefresh > 1) {
|
||||
try {
|
||||
const accountId = accountStore.currentAccountId
|
||||
const allReceive = scroll.value.latestEmail?.allReceive
|
||||
const curTimeSort = params.timeSort
|
||||
const list = await emailLatest(latestId, accountId)
|
||||
if (accountId === accountStore.currentAccountId && params.timeSort === curTimeSort) {
|
||||
let list = []
|
||||
|
||||
//确保发起请求时最后一个邮件是当前账号的,或者
|
||||
if (accountId === scroll.value.latestEmail?.reqAccountId) {
|
||||
list = await emailLatest(latestId, accountId, allReceive);
|
||||
}
|
||||
|
||||
//确保请求回来后,账号没有切换,时间排序没有改变,全部邮件类型没变
|
||||
if (accountId === accountStore.currentAccountId && params.timeSort === curTimeSort && allReceive === accountStore.currentAccount.allReceive) {
|
||||
if (list.length > 0) {
|
||||
|
||||
list.forEach(email => {
|
||||
existIds.add(email.emailId)
|
||||
scroll.value.addItem(email)
|
||||
})
|
||||
for (let email of list) {
|
||||
|
||||
email.reqAccountId = accountId;
|
||||
email.allReceive = allReceive;
|
||||
|
||||
if (!existIds.has(email.emailId)) {
|
||||
|
||||
existIds.add(email.emailId)
|
||||
scroll.value.addItem(email)
|
||||
|
||||
await sleep(50)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.code === 401 || e.code === 403) {
|
||||
settingStore.settings.autoRefresh = 0;
|
||||
}
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
await sleep(settingStore.settings.autoRefreshTime * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +139,13 @@ function cancelStar(email) {
|
||||
}
|
||||
|
||||
function getEmailList(emailId, size) {
|
||||
return emailList(accountStore.currentAccountId, emailId, params.timeSort, size, 0)
|
||||
const accountId = accountStore.currentAccountId;
|
||||
const allReceive = accountStore.currentAccount.allReceive;
|
||||
return emailList(accountId, allReceive, emailId, params.timeSort, size, 0).then(data => {
|
||||
data.latestEmail.reqAccountId = accountId;
|
||||
data.latestEmail.allReceive = allReceive;
|
||||
return data;
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div id="login-box">
|
||||
<div id="login-box" :style=" background ? 'background: var(--el-bg-color)' : ''" v-loading="oauthLoading" element-loading-text="登录中...">
|
||||
<div id="background-wrap" v-if="!settingStore.settings.background">
|
||||
<div class="x1 cloud"></div>
|
||||
<div class="x2 cloud"></div>
|
||||
@@ -14,9 +14,9 @@
|
||||
<span class="form-desc" v-if="show === 'login'">{{ $t('loginTitle') }}</span>
|
||||
<span class="form-desc" v-else>{{ $t('regTitle') }}</span>
|
||||
<div v-show="show === 'login'">
|
||||
<el-input :class="settingStore.settings.loginDomain === 0 ? 'email-input' : ''" v-model="form.email"
|
||||
type="text" :placeholder="$t('emailAccount')" autocomplete="off">
|
||||
<template #append v-if="settingStore.settings.loginDomain === 0">
|
||||
<el-input ref="loginEmailInput" :class="!hideLoginDomain ? 'email-input' : ''" v-model="form.email"
|
||||
type="text" :placeholder="$t('emailAccount')" autocomplete="off" @keydown.tab="handleEmailTab">
|
||||
<template #append v-if="!hideLoginDomain">
|
||||
<div @click.stop="openSelect">
|
||||
<el-select
|
||||
v-if="show === 'login'"
|
||||
@@ -39,16 +39,26 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input v-model="form.password" :placeholder="$t('password')" type="password" autocomplete="off">
|
||||
<el-input ref="loginPasswordInput" v-model="form.password" :placeholder="$t('password')" type="password"
|
||||
autocomplete="off" @keydown.tab="handlePasswordTab" @keyup.enter="submit">
|
||||
</el-input>
|
||||
<el-button class="btn" type="primary" @click="submit" :loading="loginLoading"
|
||||
>{{ $t('loginBtn') }}
|
||||
<div class="login-actions">
|
||||
<el-button class="btn" type="primary" @click="submit" :loading="loginLoading"
|
||||
>{{ $t('loginBtn') }}
|
||||
</el-button>
|
||||
<el-button v-if="settingStore.settings.passkey === 1" class="btn" @click="submitPasskey" :loading="passkeyLoading">
|
||||
<Icon icon="material-symbols:passkey-outline-rounded" width="18" height="18"/>
|
||||
{{ $t('passkeyLogin') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button class="btn" v-if="settingStore.settings.linuxdoSwitch" style="margin-top: 10px" @click="linuxDoLogin">
|
||||
<el-avatar src="/image/linuxdo.webp" :size="18" style="margin-right: 10px" />LinuxDo
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-show="show !== 'login'">
|
||||
<el-input class="email-input" v-model="registerForm.email" type="text" :placeholder="$t('emailAccount')"
|
||||
<el-input :class="!hideLoginDomain ? 'email-input' : ''" v-model="registerForm.email" type="text" :placeholder="$t('emailAccount')"
|
||||
autocomplete="off">
|
||||
<template #append>
|
||||
<template #append v-if="!hideLoginDomain">
|
||||
<div @click.stop="openSelect">
|
||||
<el-select
|
||||
v-if="show !== 'login'"
|
||||
@@ -88,9 +98,12 @@
|
||||
>
|
||||
<span style="font-size: 12px;color: #F56C6C" v-if="botJsError">{{ $t('verifyModuleFailed') }}</span>
|
||||
</div>
|
||||
<el-button class="btn" type="primary" @click="submitRegister" :loading="registerLoading"
|
||||
<el-button class="btn" style="margin: 0" type="primary" @click="submitRegister" :loading="registerLoading"
|
||||
>{{ $t('regBtn') }}
|
||||
</el-button>
|
||||
<el-button v-if="settingStore.settings.linuxdoSwitch" class="btn" style="margin-top: 10px" @click="linuxDoLogin">
|
||||
<el-avatar src="/image/linuxdo.webp" :size="18" style="margin-right: 10px" />LinuxDo
|
||||
</el-button>
|
||||
</div>
|
||||
<template v-if="settingStore.settings.register === 0">
|
||||
<div class="switch" @click="show = 'register'" v-if="show === 'login'">{{ $t('noAccount') }}
|
||||
@@ -100,6 +113,43 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog class="bind-dialog" v-model="showBindForm" title="注册邮箱" >
|
||||
<div class="bind-container">
|
||||
<el-input :class="!hideLoginDomain ? 'email-input' : ''" v-model="bindForm.email" type="text" :placeholder="$t('emailAccount')" autocomplete="off">
|
||||
<template #append v-if="!hideLoginDomain">
|
||||
<div @click.stop="openSelect">
|
||||
<el-select
|
||||
ref="mySelect"
|
||||
v-model="suffix"
|
||||
:placeholder="$t('select')"
|
||||
class="select"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in domainList"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
<div>
|
||||
<span>{{ suffix }}</span>
|
||||
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input v-if="settingStore.settings.regKey === 0" v-model="bindForm.code" :placeholder="$t('regKey')"
|
||||
type="text" autocomplete="off"/>
|
||||
<el-input v-if="settingStore.settings.regKey === 2" v-model="bindForm.code"
|
||||
:placeholder="$t('regKeyOptional')" type="text" autocomplete="off"/>
|
||||
<el-button class="btn" type="primary" @click="bind" :loading="bindLoading"
|
||||
>绑定
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<a v-show="settingStore.settings.projectLink" class="github" href="https://github.com/maillab/cloud-mail">
|
||||
<Icon icon="mingcute:github-line" color="#1890ff" width="20" height="20" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -108,6 +158,7 @@ import router from "@/router";
|
||||
import {computed, nextTick, reactive, ref} from "vue";
|
||||
import {login} from "@/request/login.js";
|
||||
import {register} from "@/request/login.js";
|
||||
import {websiteConfig} from "@/request/setting.js";
|
||||
import {isEmail} from "@/utils/verify-utils.js";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import {useAccountStore} from "@/store/account.js";
|
||||
@@ -118,6 +169,9 @@ import {cvtR2Url} from "@/utils/convert.js";
|
||||
import {loginUserInfo} from "@/request/my.js";
|
||||
import {permsToRouter} from "@/perm/perm.js";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {oauthBindUser, oauthLinuxDoLogin} from "@/request/ouath.js";
|
||||
import {browserSupportsWebAuthn, startAuthentication} from "@simplewebauthn/browser";
|
||||
import {passkeyAuthOptions, passkeyAuthVerify} from "@/request/passkey.js";
|
||||
|
||||
const {t} = useI18n();
|
||||
const accountStore = useAccountStore();
|
||||
@@ -125,7 +179,20 @@ const userStore = useUserStore();
|
||||
const uiStore = useUiStore();
|
||||
const settingStore = useSettingStore();
|
||||
const loginLoading = ref(false)
|
||||
const passkeyLoading = ref(false)
|
||||
const bindLoading = ref(false)
|
||||
const oauthLoading = ref(false);
|
||||
const showBindForm = ref(false);
|
||||
const show = ref('login')
|
||||
const loginEmailInput = ref()
|
||||
const loginPasswordInput = ref()
|
||||
|
||||
const bindForm = reactive({
|
||||
email: '',
|
||||
oauthUserId: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
email: '',
|
||||
password: '',
|
||||
@@ -144,6 +211,18 @@ const registerLoading = ref(false)
|
||||
suffix.value = domainList[0]
|
||||
const verifyShow = ref(false)
|
||||
let verifyToken = ''
|
||||
|
||||
function handleEmailTab(event) {
|
||||
if (event.shiftKey) return
|
||||
event.preventDefault()
|
||||
loginPasswordInput.value?.focus()
|
||||
}
|
||||
|
||||
function handlePasswordTab(event) {
|
||||
if (!event.shiftKey) return
|
||||
event.preventDefault()
|
||||
loginEmailInput.value?.focus()
|
||||
}
|
||||
let turnstileId = null
|
||||
let botJsError = ref(false)
|
||||
let verifyErrorCount = 0
|
||||
@@ -182,6 +261,8 @@ const loginOpacity = computed(() => {
|
||||
return uiStore.dark ? `rgba(0, 0, 0, ${opacity})` : `rgba(255, 255, 255, ${opacity})`
|
||||
})
|
||||
|
||||
const hideLoginDomain = computed(() => settingStore.settings.loginDomain === 1)
|
||||
|
||||
const background = computed(() => {
|
||||
|
||||
return settingStore.settings.background ? {
|
||||
@@ -192,11 +273,118 @@ const background = computed(() => {
|
||||
} : ''
|
||||
})
|
||||
|
||||
|
||||
const openSelect = () => {
|
||||
mySelect.value.toggleMenu()
|
||||
}
|
||||
|
||||
const getFullEmail = (email) => {
|
||||
return hideLoginDomain.value ? email : email + suffix.value
|
||||
}
|
||||
|
||||
const getEmailName = (email) => {
|
||||
return email.split('@')[0]
|
||||
}
|
||||
|
||||
function linuxDoLogin() {
|
||||
const clientId = settingStore.settings.linuxdoClientId
|
||||
const redirectUri = encodeURIComponent(settingStore.settings.linuxdoCallbackUrl)
|
||||
window.location.href =
|
||||
`https://connect.linux.do/oauth2/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&scope=openid+profile+email`
|
||||
}
|
||||
|
||||
linuxDoGetUser();
|
||||
|
||||
async function linuxDoGetUser() {
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const code = params.get('code')
|
||||
|
||||
if (code) {
|
||||
|
||||
oauthLoading.value = true
|
||||
oauthLinuxDoLogin(code).then(data => {
|
||||
|
||||
bindForm.oauthUserId = data.userInfo.oauthUserId;
|
||||
|
||||
if (!data.token) {
|
||||
showBindForm.value = true
|
||||
oauthLoading.value = false
|
||||
ElMessage({
|
||||
message: '请注册绑定一个邮箱',
|
||||
type: 'warning',
|
||||
duration: 4000,
|
||||
plain: true,
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
saveToken(data.token);
|
||||
}).catch(() => {
|
||||
oauthLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const cleanUrl = window.location.origin + window.location.pathname
|
||||
window.history.replaceState({}, '', cleanUrl)
|
||||
}
|
||||
|
||||
function bind() {
|
||||
|
||||
if (!bindForm.email) {
|
||||
ElMessage({
|
||||
message: t('emptyEmailMsg'),
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (getEmailName(bindForm.email).length < settingStore.settings.minEmailPrefix) {
|
||||
ElMessage({
|
||||
message: t('minEmailPrefix', {msg: settingStore.settings.minEmailPrefix}),
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let email = getFullEmail(bindForm.email);
|
||||
|
||||
|
||||
if (!isEmail(email)) {
|
||||
ElMessage({
|
||||
message: t('notEmailMsg'),
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (settingStore.settings.regKey === 0) {
|
||||
|
||||
if (!bindForm.code) {
|
||||
|
||||
ElMessage({
|
||||
message: t('emptyRegKeyMsg'),
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const form = {email, oauthUserId: bindForm.oauthUserId, code: bindForm.code}
|
||||
|
||||
bindLoading.value = true
|
||||
oauthBindUser(form).then(data => {
|
||||
saveToken(data.token)
|
||||
}).catch(() => {
|
||||
bindLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
|
||||
if (!form.email) {
|
||||
@@ -208,7 +396,7 @@ const submit = () => {
|
||||
return
|
||||
}
|
||||
|
||||
let email = form.email + (settingStore.settings.loginDomain === 0 ? suffix.value : '');
|
||||
let email = getFullEmail(form.email);
|
||||
|
||||
if (!isEmail(email)) {
|
||||
ElMessage({
|
||||
@@ -230,21 +418,71 @@ const submit = () => {
|
||||
|
||||
loginLoading.value = true
|
||||
login(email, form.password).then(async data => {
|
||||
localStorage.setItem('token', data.token)
|
||||
const user = await loginUserInfo();
|
||||
accountStore.currentAccountId = user.accountId;
|
||||
userStore.user = user;
|
||||
const routers = permsToRouter(user.permKeys);
|
||||
routers.forEach(routerData => {
|
||||
router.addRoute('layout', routerData);
|
||||
});
|
||||
await router.replace({name: 'layout'})
|
||||
uiStore.showNotice()
|
||||
await saveToken(data.token)
|
||||
}).finally(() => {
|
||||
loginLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
async function submitPasskey() {
|
||||
if (!browserSupportsWebAuthn()) {
|
||||
ElMessage({
|
||||
message: t('passkeyUnsupported'),
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
passkeyLoading.value = true
|
||||
try {
|
||||
const {transactionId, options} = await passkeyAuthOptions()
|
||||
const response = await startAuthentication({optionsJSON: options})
|
||||
const data = await passkeyAuthVerify(transactionId, response)
|
||||
await saveToken(data.token)
|
||||
} catch (error) {
|
||||
if (!error?.code) {
|
||||
ElMessage({
|
||||
message: t('passkeyLoginFailed'),
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
passkeyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveToken(token) {
|
||||
localStorage.setItem('token', token)
|
||||
refreshWebsiteConfig()
|
||||
const user = await loginUserInfo();
|
||||
accountStore.currentAccountId = user.account.accountId;
|
||||
accountStore.currentAccount = user.account;
|
||||
userStore.user = user;
|
||||
const routers = permsToRouter(user.permKeys);
|
||||
routers.forEach(routerData => {
|
||||
router.addRoute('layout', routerData);
|
||||
});
|
||||
await router.replace({name: 'layout'})
|
||||
uiStore.showNotice()
|
||||
oauthLoading.value = false;
|
||||
bindLoading.value = false;
|
||||
}
|
||||
|
||||
function refreshWebsiteConfig() {
|
||||
websiteConfig().then(setting => {
|
||||
settingStore.settings = setting
|
||||
settingStore.domainList = setting.domainList
|
||||
if (!suffix.value && setting.domainList.length > 0) {
|
||||
suffix.value = setting.domainList[0]
|
||||
}
|
||||
document.title = setting.title
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function submitRegister() {
|
||||
|
||||
@@ -257,7 +495,20 @@ function submitRegister() {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isEmail(registerForm.email + suffix.value)) {
|
||||
console.log(registerForm.email)
|
||||
|
||||
if (getEmailName(registerForm.email).length < settingStore.settings.minEmailPrefix) {
|
||||
ElMessage({
|
||||
message: t('minEmailPrefix', {msg: settingStore.settings.minEmailPrefix}),
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const email = getFullEmail(registerForm.email);
|
||||
|
||||
if (!isEmail(email)) {
|
||||
ElMessage({
|
||||
message: t('notEmailMsg'),
|
||||
type: 'error',
|
||||
@@ -336,7 +587,7 @@ function submitRegister() {
|
||||
registerLoading.value = true
|
||||
|
||||
const form = {
|
||||
email: registerForm.email + suffix.value,
|
||||
email,
|
||||
password: registerForm.password,
|
||||
token: verifyToken,
|
||||
code: registerForm.code
|
||||
@@ -439,6 +690,15 @@ function submitRegister() {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.login-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-desc {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 18px;
|
||||
@@ -485,11 +745,43 @@ function submitRegister() {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
:deep(.bind-dialog) {
|
||||
width: 400px !important;
|
||||
@media (max-width: 440px) {
|
||||
width: calc(100% - 40px) !important;
|
||||
margin-right: 20px !important;
|
||||
margin-left: 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.bind-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.setting-icon {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
.github {
|
||||
position: fixed;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
background: var(--el-bg-color);
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
z-index: 1000;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-input-group__append) {
|
||||
padding: 0 !important;
|
||||
padding-left: 8px !important;
|
||||
@@ -498,6 +790,10 @@ function submitRegister() {
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
:deep(.el-button+.el-button) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.register-turnstile {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty" v-if="regKeyData.length === 0">
|
||||
<el-empty v-if="!regKeyFirst" :image-size="isMobile ? 120 : 0" :description="$t('noCodeFound')"/>
|
||||
<el-empty v-if="!regKeyFirst" :image-size="isMobile ? 120 : null" :description="$t('noCodeFound')"/>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<el-dialog v-model="showAdd" :title="$t('addRegKey')">
|
||||
|
||||
@@ -71,13 +71,8 @@
|
||||
autocomplete="off"/>
|
||||
<el-input class="dialog-input" v-model="form.description" :maxlength="30" type="text"
|
||||
:placeholder="$t('description')" autocomplete="off"/>
|
||||
<el-input-tag class="dialog-input-tag" tag-type="warning"
|
||||
:class="form.banEmail.length === 0 ? 'dialog-input' : '' " v-model="form.banEmail"
|
||||
<el-input-tag class="dialog-input" tag-type="warning" v-model="form.banEmail"
|
||||
@add-tag="banEmailAddTag" type="text" :placeholder="$t('emailInterception')" autocomplete="off"/>
|
||||
<el-radio-group class="dialog-radio" v-model="form.banEmailType" v-if="form.banEmail.length > 0">
|
||||
<el-radio :label="$t('removeAll')" :value="0"/>
|
||||
<el-radio :label="$t('removeContent')" :value="1"/>
|
||||
</el-radio-group>
|
||||
<el-select
|
||||
class="dialog-input"
|
||||
v-model="form.availDomain"
|
||||
@@ -121,13 +116,14 @@
|
||||
<div>
|
||||
<span>{{ node.label }}</span>
|
||||
<span class="send-num" v-if="data.permKey === 'email:send'" @click.stop>
|
||||
<el-input-number v-model="form.sendCount" controls-position="right" :min="0" :max="99999" size="small"
|
||||
<el-input-number v-if="form.sendType === 'day' || form.sendType === 'count'" v-model="form.sendCount" controls-position="right" :min="0" :max="99999" size="small"
|
||||
:placeholder="$t('total')">
|
||||
</el-input-number>
|
||||
<el-select v-model="form.sendType" placeholder="Select" size="small"
|
||||
style="width: 65px;margin-left: 5px;">
|
||||
:style="`width: ${ locale === 'zh' ? 65 : 85 }px;margin-left: 5px;`">
|
||||
<el-option :label="$t('total')" value="count"/>
|
||||
<el-option :label="$t('daily')" value="day"/>
|
||||
<el-option :label="$t('internal')" value="internal"/>
|
||||
<el-option :label="$t('btnBan')" value="ban"/>
|
||||
</el-select>
|
||||
</span>
|
||||
@@ -186,7 +182,6 @@ const form = reactive({
|
||||
name: null,
|
||||
description: null,
|
||||
banEmail: [],
|
||||
banEmailType: 0,
|
||||
sendType: 'count',
|
||||
sendCount: 0,
|
||||
accountCount: 0,
|
||||
@@ -335,7 +330,6 @@ function resetForm() {
|
||||
form.sendCount = 0
|
||||
form.accountCount = 0
|
||||
form.banEmail = []
|
||||
form.banEmailType = 0
|
||||
form.availDomain = []
|
||||
tree.value.setCheckedKeys([])
|
||||
}
|
||||
@@ -352,7 +346,6 @@ function openRoleSet(role) {
|
||||
form.sendCount = role.sendCount
|
||||
form.accountCount = role.accountCount
|
||||
form.banEmail = role.banEmail
|
||||
form.banEmailType = role.banEmailType
|
||||
form.availDomain = role.availDomain
|
||||
nextTick(() => {
|
||||
tree.value.setCheckedKeys(role.permIds)
|
||||
@@ -526,14 +519,6 @@ window.onresize = () => {
|
||||
.dialog-input {
|
||||
margin-bottom: 15px !important;
|
||||
}
|
||||
|
||||
.dialog-radio {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.dialog-input-tag {
|
||||
}
|
||||
}
|
||||
|
||||
.perm-expand {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
:star-cancel="starCancel"
|
||||
@jump="jumpContent"
|
||||
:time-sort="params.timeSort"
|
||||
:type="'send'"
|
||||
>
|
||||
<template #first>
|
||||
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-down-outline"
|
||||
@@ -71,7 +72,13 @@ function cancelStar(email) {
|
||||
}
|
||||
|
||||
function getEmailList(emailId, size) {
|
||||
return emailList(accountStore.currentAccountId, emailId, params.timeSort, size, 1)
|
||||
const accountId = accountStore.currentAccountId;
|
||||
const allReceive = accountStore.currentAccount.allReceive;
|
||||
return emailList(accountId, allReceive, emailId, params.timeSort, size, 1).then(data => {
|
||||
data.latestEmail.reqAccountId = accountId;
|
||||
data.latestEmail.allReceive = allReceive;
|
||||
return data;
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -30,13 +30,44 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="passkey" v-if="settingStore.settings.passkey === 1">
|
||||
<div class="title">{{$t('passkeySetting')}}</div>
|
||||
<div class="passkey-status">
|
||||
<div>
|
||||
<div>{{ passkey.registered ? $t('passkeyConfigured') : $t('passkeyNotConfigured') }}</div>
|
||||
<div class="passkey-time" v-if="passkey.registered && passkey.createTime">
|
||||
{{$t('passkeyCreatedAt', {time: passkey.createTime})}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="passkey-actions">
|
||||
<el-button type="primary" :loading="passkeyLoading" @click="openPasskeyAction('register')">
|
||||
{{ passkey.registered ? $t('passkeyReplace') : $t('passkeyCreate') }}
|
||||
</el-button>
|
||||
<el-button v-if="passkey.registered" :loading="passkeyLoading" @click="openPasskeyAction('delete')">
|
||||
{{$t('passkeyDelete')}}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="language">
|
||||
<div class="title">{{$t('language')}}</div>
|
||||
<el-select
|
||||
:model-value="langSelect"
|
||||
class="language-select"
|
||||
placeholder="Select"
|
||||
@change="changeLang"
|
||||
>
|
||||
<el-option label="中文" value="zh" @pointerdown.prevent.stop="changeLang('zh')"/>
|
||||
<el-option label="English" value="en" @pointerdown.prevent.stop="changeLang('en')"/>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="del-email" v-perm="'my:delete'">
|
||||
<div class="title">{{$t('deleteUser')}}</div>
|
||||
<div style="color: var(--regular-text-color);">
|
||||
{{$t('delAccountMsg')}}
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="primary" @click="deleteConfirm">{{$t('deleteUserBtn')}}</el-button>
|
||||
<el-button type="danger" @click="deleteConfirm">{{$t('deleteUserBtn')}}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="pwdShow" :title="$t('changePassword')" width="340">
|
||||
@@ -46,6 +77,15 @@
|
||||
<el-button type="primary" :loading="setPwdLoading" @click="submitPwd">{{$t('save')}}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="passkeyPwdShow"
|
||||
:title="passkeyAction === 'delete' ? $t('passkeyDelete') : $t('passkeySetting')" width="340">
|
||||
<div class="update-pwd">
|
||||
<div v-if="passkeyAction === 'delete'">{{$t('passkeyDeleteConfirm')}}</div>
|
||||
<el-input type="password" :placeholder="$t('passkeyCurrentPassword')" v-model="passkeyPassword"
|
||||
autocomplete="current-password" @keyup.enter="submitPasskeyAction"/>
|
||||
<el-button type="primary" :loading="passkeyLoading" @click="submitPasskeyAction">{{$t('confirm')}}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
@@ -56,18 +96,97 @@ import router from "@/router/index.js";
|
||||
import {accountSetName} from "@/request/account.js";
|
||||
import {useAccountStore} from "@/store/account.js";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import {browserSupportsWebAuthn, startRegistration} from "@simplewebauthn/browser";
|
||||
import {
|
||||
passkeyDelete,
|
||||
passkeyRegOptions,
|
||||
passkeyRegVerify,
|
||||
passkeyStatus
|
||||
} from "@/request/passkey.js";
|
||||
|
||||
const { t } = useI18n()
|
||||
const accountStore = useAccountStore()
|
||||
const settingStore = useSettingStore()
|
||||
const userStore = useUserStore();
|
||||
const setPwdLoading = ref(false)
|
||||
const setNameShow = ref(false)
|
||||
const accountName = ref(null)
|
||||
const langSelect = ref(settingStore.lang)
|
||||
const passkey = reactive({registered: false, createTime: null})
|
||||
const passkeyLoading = ref(false)
|
||||
const passkeyPwdShow = ref(false)
|
||||
const passkeyPassword = ref('')
|
||||
const passkeyAction = ref('register')
|
||||
|
||||
defineOptions({
|
||||
name: 'setting'
|
||||
})
|
||||
|
||||
if (settingStore.settings.passkey === 1) {
|
||||
loadPasskeyStatus()
|
||||
}
|
||||
|
||||
async function loadPasskeyStatus() {
|
||||
try {
|
||||
const status = await passkeyStatus()
|
||||
passkey.registered = status.registered
|
||||
passkey.createTime = status.createTime
|
||||
} catch (_) {
|
||||
// The shared HTTP interceptor already displays the server error.
|
||||
}
|
||||
}
|
||||
|
||||
function openPasskeyAction(action) {
|
||||
if (action === 'register' && !browserSupportsWebAuthn()) {
|
||||
ElMessage({
|
||||
message: t('passkeyUnsupported'),
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
passkeyAction.value = action
|
||||
passkeyPassword.value = ''
|
||||
passkeyPwdShow.value = true
|
||||
}
|
||||
|
||||
async function submitPasskeyAction() {
|
||||
if (!passkeyPassword.value) {
|
||||
ElMessage({
|
||||
message: t('emptyPwdMsg'),
|
||||
type: 'error',
|
||||
plain: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
passkeyLoading.value = true
|
||||
try {
|
||||
if (passkeyAction.value === 'delete') {
|
||||
await passkeyDelete(passkeyPassword.value)
|
||||
passkey.registered = false
|
||||
passkey.createTime = null
|
||||
ElMessage({message: t('passkeyDeleteSuccess'), type: 'success', plain: true})
|
||||
} else {
|
||||
const {transactionId, options} = await passkeyRegOptions(passkeyPassword.value)
|
||||
const response = await startRegistration({optionsJSON: options})
|
||||
const status = await passkeyRegVerify(transactionId, response)
|
||||
passkey.registered = status.registered
|
||||
passkey.createTime = status.createTime
|
||||
ElMessage({message: t('passkeyCreateSuccess'), type: 'success', plain: true})
|
||||
}
|
||||
passkeyPwdShow.value = false
|
||||
passkeyPassword.value = ''
|
||||
} catch (error) {
|
||||
if (!error?.code) {
|
||||
ElMessage({message: t('passkeyLoginFailed'), type: 'error', plain: true})
|
||||
}
|
||||
} finally {
|
||||
passkeyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function showSetName() {
|
||||
accountName.value = userStore.user.name
|
||||
setNameShow.value = true
|
||||
@@ -93,7 +212,7 @@ function setName() {
|
||||
|
||||
userStore.user.name = accountName.value
|
||||
|
||||
accountSetName(userStore.user.accountId,name).then(() => {
|
||||
accountSetName(userStore.user.account.accountId,name).then(() => {
|
||||
ElMessage({
|
||||
message: t('saveSuccessMsg'),
|
||||
type: 'success',
|
||||
@@ -107,6 +226,17 @@ function setName() {
|
||||
})
|
||||
}
|
||||
|
||||
function changeLang(lang) {
|
||||
let setting = {}
|
||||
try {
|
||||
setting = JSON.parse(localStorage.getItem('setting') || '{}')
|
||||
} catch (e) {
|
||||
setting = {}
|
||||
}
|
||||
localStorage.setItem('setting', JSON.stringify({...setting, lang}))
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
const pwdShow = ref(false)
|
||||
const form = reactive({
|
||||
password: '',
|
||||
@@ -249,6 +379,45 @@ function submitPwd() {
|
||||
}
|
||||
}
|
||||
|
||||
.language {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
|
||||
.language-select {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.passkey {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
|
||||
.passkey-status {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.passkey-time {
|
||||
color: var(--regular-text-color);
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.passkey-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
|
||||
.el-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.del-email {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
|
||||
@@ -20,10 +20,17 @@
|
||||
<div class="setting-item">
|
||||
<div><span>{{ $t('loginDomain') }}</span></div>
|
||||
<div>
|
||||
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
|
||||
<el-switch @change="change" :before-change="beforeChange" :active-value="1" :inactive-value="0"
|
||||
v-model="setting.loginDomain"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>{{ $t('passkeyLogin') }}</span></div>
|
||||
<div>
|
||||
<el-switch @change="change" :before-change="beforeChange" :active-value="1" :inactive-value="0"
|
||||
v-model="setting.passkey"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>{{ $t('regKey') }}</span></div>
|
||||
<div>
|
||||
@@ -61,7 +68,16 @@
|
||||
v-model="setting.manyEmail"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<span>{{ $t('emailPrefix') }}</span>
|
||||
</div>
|
||||
<div class="forward">
|
||||
<el-button class="opt-button" size="small" type="primary" @click="openEmailPrefix">
|
||||
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -96,7 +112,7 @@
|
||||
fit="cover"
|
||||
>
|
||||
<template #error>
|
||||
<div class="error-image" @click="openSetBackground">
|
||||
<div class="error-image">
|
||||
<Icon icon="ph:image" width="24" height="24"/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -136,11 +152,11 @@
|
||||
<el-select
|
||||
@change="change"
|
||||
:style="`width: ${ locale === 'en' ? 100 : 80 }px;`"
|
||||
v-model="setting.autoRefreshTime"
|
||||
v-model="setting.autoRefresh"
|
||||
placeholder="Select"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
v-for="item in authRefreshOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
@@ -168,8 +184,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>{{ $t('resendToken') }}</span></div>
|
||||
<div>
|
||||
<div><span>{{ setting.hasCfEmail ? $t('cloudflareEmailSending') : $t('resendToken') }}</span></div>
|
||||
<div v-if="setting.hasCfEmail">
|
||||
<span>{{ $t('enabled') }}</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-button class="opt-button" style="margin-top: 0" @click="openResendList" size="small"
|
||||
type="primary">
|
||||
<Icon icon="ic:round-list" width="18" height="18"/>
|
||||
@@ -180,6 +199,15 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>{{ $t('blackList') }}</span></div>
|
||||
<div>
|
||||
<el-button class="opt-button" style="margin-top: 0" @click="openBlackListForm" size="small"
|
||||
type="primary">
|
||||
<Icon icon="fluent:settings-48-regular" width="16" height="16"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -188,7 +216,12 @@
|
||||
<div class="card-title">{{ $t('oss') }}</div>
|
||||
<div class="card-content">
|
||||
<div class="r2domain-item">
|
||||
<div><span>{{ $t('osDomain') }}</span></div>
|
||||
<div>
|
||||
<span>{{ $t('osDomain') }}</span>
|
||||
<el-tooltip effect="dark" :content="$t('ossDomainDesc')">
|
||||
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="r2domain">
|
||||
<span>{{ setting.r2Domain || '' }}</span>
|
||||
<el-button class="opt-button" size="small" type="primary" @click="r2DomainShow = true">
|
||||
@@ -197,13 +230,25 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>{{ $t('s3Configuration') }}</span></div>
|
||||
<div>
|
||||
<span>{{ $t('s3Configuration') }}</span>
|
||||
</div>
|
||||
<div class="r2domain">
|
||||
<el-button class="opt-button" size="small" type="primary" @click="addS3Show = true">
|
||||
<Icon icon="fluent:settings-48-regular" width="16" height="16"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div>
|
||||
<span>{{ $t('storageType') }}</span>
|
||||
</div>
|
||||
<div class="r2domain">
|
||||
<div class="storage-type">
|
||||
<el-tag>{{ setting.storageType }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -326,13 +371,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="card-title">Workers AI</div>
|
||||
<div class="card-content">
|
||||
<div class="setting-item">
|
||||
<div><span>{{ $t('codeRecognition') }}</span></div>
|
||||
<div>
|
||||
<el-switch @change="changeField('aiCode', $event)" :before-change="beforeChange" :active-value="0" :inactive-value="1"
|
||||
v-model="setting.aiCode"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div><span>{{ $t('codeRecognitionRules') }}</span></div>
|
||||
<div class="forward">
|
||||
<el-button class="opt-button" size="small" type="primary" @click="openAiCodeFilter">
|
||||
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card about">
|
||||
<div class="card-title">{{ $t('about') }}</div>
|
||||
<div class="card-content">
|
||||
<div class="concerning-item">
|
||||
<span>{{ $t('version') }} :</span>
|
||||
<el-badge is-dot :hidden="!hasUpdate">
|
||||
<el-button @click="jump('https://github.com/eoao/cloud-mail/releases')">
|
||||
<el-button @click="jump('https://github.com/maillab/cloud-mail/releases')">
|
||||
{{ currentVersion }}
|
||||
<template #icon>
|
||||
<Icon icon="qlementine-icons:version-control-16" style="font-size: 20px" color="#1890FF"/>
|
||||
@@ -343,7 +409,7 @@
|
||||
<div class="concerning-item">
|
||||
<span>{{ $t('community') }} : </span>
|
||||
<div class="community">
|
||||
<el-button @click="jump('https://github.com/eoao/cloud-mail')">
|
||||
<el-button @click="jump('https://github.com/maillab/cloud-mail')">
|
||||
Github
|
||||
<template #icon>
|
||||
<Icon icon="codicon:github-inverted" width="22" height="22"/>
|
||||
@@ -465,9 +531,43 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="forward-set-body">
|
||||
<el-input :placeholder="$t('tgBotToken')" v-model="tgBotToken"></el-input>
|
||||
<el-input :placeholder="setting.tgBotToken || $t('tgBotToken')" v-model="tgBotToken"></el-input>
|
||||
<el-input-tag tag-type="warning" :placeholder="$t('toBotTokenDesc')" v-model="tgChatId"
|
||||
@add-tag="addChatTag"></el-input-tag>
|
||||
<el-input tag-type="warning" :placeholder="$t('customDomainDesc')" v-model="customDomain" ></el-input>
|
||||
<div class="tg-msg-label">
|
||||
<span>{{t('from')}}</span>
|
||||
<el-select v-model="tgMsgFrom" >
|
||||
<el-option
|
||||
v-for="item in tgMsgFromOption"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="tg-msg-label">
|
||||
<span>{{t('recipient')}}</span>
|
||||
<el-select v-model="tgMsgTo" >
|
||||
<el-option
|
||||
v-for="item in tgMsgToOption"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="tg-msg-label">
|
||||
<span>{{t('emailText')}}</span>
|
||||
<el-select v-model="tgMsgText" >
|
||||
<el-option
|
||||
v-for="item in tgMsgTextOption"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
@@ -636,20 +736,83 @@
|
||||
<el-input class="dialog-input" type="text" placeholder="Region" v-model="s3.region"/>
|
||||
<el-input class="dialog-input" type="text" :placeholder="setting.s3AccessKey || 'Access Key'"
|
||||
v-model="s3.s3AccessKey"/>
|
||||
<el-input type="text" :placeholder="setting.s3SecretKey || 'Secret Key'" v-model="s3.s3SecretKey"/>
|
||||
<el-input style="margin-bottom: 10px" type="text" :placeholder="setting.s3SecretKey || 'Secret Key'" v-model="s3.s3SecretKey"/>
|
||||
<div class="force-path-style">
|
||||
<div class="force-path-style-left">
|
||||
<span>ForcePathStyle</span>
|
||||
<el-tooltip effect="dark" :content="$t('forcePathStyleDesc')">
|
||||
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-switch :before-change="beforeChange" :active-value="0" :inactive-value="1"
|
||||
v-model="s3.forcePathStyle"/>
|
||||
</div>
|
||||
<div class="s3-button">
|
||||
<el-button :loading="clearS3Loading" @click="clearS3">{{ t('clear') }}</el-button>
|
||||
<el-button type="primary" :loading="settingLoading && !clearS3Loading" @click="saveS3">{{ t('save') }}</el-button>
|
||||
</div>
|
||||
</form>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="emailPrefixShow" :title="t('emailPrefix')" @closed="resetEmailPrefix" >
|
||||
<div class="email-prefix">
|
||||
<div>{{ t('atLeast') }}</div>
|
||||
<el-input-number v-model="minEmailPrefix" :min="1" :max="20" style="width: 150px" >
|
||||
<template #suffix>
|
||||
<span>{{ t('character') }}</span>
|
||||
</template>
|
||||
</el-input-number>
|
||||
</div>
|
||||
<div class="prefix-filter">
|
||||
<div style="margin-bottom: 10px;">{{ t('mustNotContain') }}</div>
|
||||
<el-input-tag style="margin-bottom: 10px;" v-model="emailPrefixFilter" />
|
||||
</div>
|
||||
<el-button type="primary" style="width: 100%;" :loading="settingLoading" @click="saveEmailPrefix">{{ $t('save') }}</el-button>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="blackFormShow" class="forward-dialog" @closed="resetBlackList">
|
||||
<template #header>
|
||||
<div class="forward-head">
|
||||
<span class="forward-set-title">{{ $t('blackList') }}</span>
|
||||
<el-tooltip effect="dark" :content="$t('blackListDesc')">
|
||||
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-form>
|
||||
<el-form-item :label="t('blackFromDesc')" label-position="top">
|
||||
<el-input-tag v-model="blackListForm.blackFrom" @add-tag="banEmailAddTag" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('blackSubjectDesc')" label-position="top">
|
||||
<el-input-tag v-model="blackListForm.blackSubject"/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('blackContentDesc')" label-position="top">
|
||||
<el-input-tag v-model="blackListForm.blackContent"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button type="primary" style="width: 100%;" :loading="settingLoading" @click="saveBlackList">{{ $t('save') }}</el-button>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="aiCodeFilterShow" class="forward-dialog" @closed="resetAiCodeFilter">
|
||||
<template #header>
|
||||
<div class="forward-head">
|
||||
<span class="forward-set-title">{{ $t('codeRecognitionRules') }}</span>
|
||||
<el-tooltip effect="dark" :content="$t('codeRecognitionRulesDesc')">
|
||||
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-form>
|
||||
<el-form-item :label="t('senderRules')" label-position="top">
|
||||
<el-input-tag v-model="aiCodeFilter" @add-tag="aiCodeFilterAddTag"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button type="primary" style="width: 100%;" :loading="settingLoading" @click="saveAiCodeFilter">{{ $t('save') }}</el-button>
|
||||
</el-dialog>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, defineOptions, reactive, ref} from "vue";
|
||||
import {physicsDeleteAll, setBackground, settingQuery, settingSet} from "@/request/setting.js";
|
||||
import {computed, defineOptions, nextTick, reactive, ref} from "vue";
|
||||
import {deleteBackground, setBackground, setBlackList, settingQuery, settingSet} from "@/request/setting.js";
|
||||
import {useSettingStore} from "@/store/setting.js";
|
||||
import {useUiStore} from "@/store/ui.js";
|
||||
import {useUserStore} from "@/store/user.js";
|
||||
@@ -658,7 +821,7 @@ import {Icon} from "@iconify/vue";
|
||||
import {cvtR2Url} from "@/utils/convert.js";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {debounce} from 'lodash-es'
|
||||
import {isEmail} from "@/utils/verify-utils.js";
|
||||
import {isDomain, isEmail} from "@/utils/verify-utils.js";
|
||||
import loading from "@/components/loading/index.vue";
|
||||
import {getTextWidth} from "@/utils/text.js";
|
||||
import {fileToBase64} from "@/utils/file-utils.js"
|
||||
@@ -669,23 +832,27 @@ defineOptions({
|
||||
name: 'sys-setting'
|
||||
})
|
||||
|
||||
const currentVersion = 'v2.1.0'
|
||||
const currentVersion = 'v3.0.0'
|
||||
const hasUpdate = ref(false)
|
||||
let getUpdateErrorCount = 1;
|
||||
const {t, locale} = useI18n();
|
||||
const firstLoading = ref(true)
|
||||
const settingReady = ref(false)
|
||||
const backgroundImage = ref('')
|
||||
const localUpShow = ref(false)
|
||||
const accountStore = useAccountStore();
|
||||
const userStore = useUserStore();
|
||||
const editTitleShow = ref(false)
|
||||
const resendTokenFormShow = ref(false)
|
||||
const blackFormShow = ref(false)
|
||||
const aiCodeFilterShow = ref(false)
|
||||
const r2DomainShow = ref(false)
|
||||
const turnstileShow = ref(false)
|
||||
const tgSettingShow = ref(false)
|
||||
const noticePopupShow = ref(false)
|
||||
const thirdEmailShow = ref(false)
|
||||
const forwardRulesShow = ref(false)
|
||||
const emailPrefixShow = ref(false)
|
||||
const showResendList = ref(false)
|
||||
const settingStore = useSettingStore();
|
||||
const uiStore = useUiStore();
|
||||
@@ -695,6 +862,8 @@ const settingLoading = ref(false)
|
||||
const clearS3Loading = ref(false)
|
||||
const r2DomainInput = ref('')
|
||||
const loginOpacity = ref(0)
|
||||
const minEmailPrefix = ref(0)
|
||||
const emailPrefixFilter = ref([])
|
||||
const backgroundUrl = ref('')
|
||||
let backgroundFile = {}
|
||||
const showSetBackground = ref(false)
|
||||
@@ -718,7 +887,8 @@ const s3 = reactive({
|
||||
endpoint: '',
|
||||
region: '',
|
||||
s3AccessKey: '',
|
||||
s3SecretKey: ''
|
||||
s3SecretKey: '',
|
||||
forcePathStyle: 1
|
||||
})
|
||||
|
||||
const noticeForm = reactive({
|
||||
@@ -738,17 +908,24 @@ const regKeyOptions = computed(() => [
|
||||
{label: t('optional'), value: 2},
|
||||
])
|
||||
|
||||
const options = computed(() => [
|
||||
const blackListForm = ref({
|
||||
blackSubject: [],
|
||||
blackContent: [],
|
||||
blackFrom: []
|
||||
})
|
||||
const aiCodeFilter = ref([])
|
||||
|
||||
const authRefreshOptions = computed(() => [
|
||||
{label: t('disable'), value: 0},
|
||||
{label: '3s', value: 3},
|
||||
{label: '5s', value: 5},
|
||||
{label: '7s', value: 7},
|
||||
{label: '10s', value: 10},
|
||||
{label: '15s', value: 15},
|
||||
{label: '20s', value: 20}
|
||||
{label: '20s', value: 20},
|
||||
])
|
||||
|
||||
const tgChatId = ref([])
|
||||
const customDomain = ref('')
|
||||
const tgBotStatus = ref(0)
|
||||
const tgBotToken = ref('')
|
||||
const forwardEmail = ref([])
|
||||
@@ -757,16 +934,27 @@ const emailColumnWidth = ref(0)
|
||||
const tokenColumnWidth = ref(0)
|
||||
const ruleType = ref(0)
|
||||
const ruleEmail = ref([])
|
||||
const tgMsgFrom = ref('')
|
||||
const tgMsgTo = ref('')
|
||||
const tgMsgText = ref('')
|
||||
|
||||
const tgMsgFromOption = [{label: t('show'), value: 'show'}, {label: t('hide'), value: 'hide'}, {label: t('onlyName'), value:'only-name'}]
|
||||
const tgMsgToOption = [{label: t('show'), value: 'show'}, {label: t('hide'), value: 'hide'}]
|
||||
const tgMsgTextOption = [{label: t('show'), value: 'show'}, {label: t('hide'), value: 'hide'}]
|
||||
const tgMsgLabelWidth = computed(() => locale.value === 'en' ? '120px' : '100px');
|
||||
|
||||
getSettings()
|
||||
getUpdate()
|
||||
|
||||
function getSettings() {
|
||||
settingReady.value = false
|
||||
settingQuery().then(settingData => {
|
||||
setting.value = settingData
|
||||
settingStore.settings.passkey = settingData.passkey
|
||||
settingStore.domainList = settingData.domainList;
|
||||
resendTokenForm.domain = setting.value.domainList[0]
|
||||
loginOpacity.value = setting.value.loginOpacity
|
||||
minEmailPrefix.value = setting.value.minEmailPrefix
|
||||
firstLoading.value = false
|
||||
backgroundUrl.value = setting.value.background?.startsWith('http') ? setting.value.background : ''
|
||||
editTitle.value = setting.value.title
|
||||
@@ -775,6 +963,12 @@ function getSettings() {
|
||||
regVerifyCount.value = setting.value.regVerifyCount
|
||||
resetNoticeForm()
|
||||
resetAddS3Form()
|
||||
resetEmailPrefix()
|
||||
resetBlackList()
|
||||
resetAiCodeFilter()
|
||||
nextTick(() => {
|
||||
settingReady.value = true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -799,6 +993,7 @@ function resetAddS3Form() {
|
||||
s3.region = setting.value.region
|
||||
s3.s3AccessKey = ''
|
||||
s3.s3SecretKey = ''
|
||||
s3.forcePathStyle = setting.value.forcePathStyle
|
||||
}
|
||||
|
||||
const resendList = computed(() => {
|
||||
@@ -825,7 +1020,7 @@ const resendList = computed(() => {
|
||||
|
||||
function getUpdate() {
|
||||
if (getUpdateErrorCount > 5 || !getUpdateErrorCount) return
|
||||
axios.get('https://api.github.com/repos/eoao/cloud-mail/releases/latest').then(({data}) => {
|
||||
axios.get('https://api.github.com/repos/maillab/cloud-mail/releases/latest').then(({data}) => {
|
||||
hasUpdate.value = data.name !== currentVersion
|
||||
getUpdateErrorCount = 0
|
||||
}).catch(e => {
|
||||
@@ -868,7 +1063,11 @@ function closedSetBackground() {
|
||||
|
||||
function openTgSetting() {
|
||||
tgBotStatus.value = setting.value.tgBotStatus
|
||||
tgBotToken.value = setting.value.tgBotToken
|
||||
tgBotToken.value = ''
|
||||
customDomain.value = setting.value.customDomain
|
||||
tgMsgFrom.value = setting.value.tgMsgFrom
|
||||
tgMsgText.value = setting.value.tgMsgText
|
||||
tgMsgTo.value = setting.value.tgMsgTo
|
||||
tgChatId.value = []
|
||||
if (setting.value.tgChatId) {
|
||||
const list = setting.value.tgChatId.split(',')
|
||||
@@ -917,6 +1116,10 @@ function openThirdEmailSetting() {
|
||||
thirdEmailShow.value = true
|
||||
}
|
||||
|
||||
function openEmailPrefix() {
|
||||
emailPrefixShow.value = true
|
||||
}
|
||||
|
||||
function openForwardRules() {
|
||||
ruleType.value = setting.value.ruleType
|
||||
ruleEmail.value = []
|
||||
@@ -977,7 +1180,8 @@ function clearS3() {
|
||||
endpoint: '',
|
||||
region: '',
|
||||
s3AccessKey: '',
|
||||
s3SecretKey: ''
|
||||
s3SecretKey: '',
|
||||
forcePathStyle: 1
|
||||
}
|
||||
clearS3Loading.value = true
|
||||
editSetting(form)
|
||||
@@ -988,7 +1192,8 @@ function saveS3() {
|
||||
const form = {
|
||||
bucket: s3.bucket,
|
||||
endpoint: s3.endpoint,
|
||||
region: s3.region
|
||||
region: s3.region,
|
||||
forcePathStyle: s3.forcePathStyle
|
||||
}
|
||||
|
||||
if (s3.s3AccessKey) form.s3AccessKey = s3.s3AccessKey
|
||||
@@ -999,10 +1204,14 @@ function saveS3() {
|
||||
|
||||
function tgBotSave() {
|
||||
const form = {
|
||||
tgBotToken: tgBotToken.value,
|
||||
customDomain: customDomain.value,
|
||||
tgBotStatus: tgBotStatus.value,
|
||||
tgChatId: tgChatId.value + ''
|
||||
tgChatId: tgChatId.value + '',
|
||||
tgMsgFrom: tgMsgFrom.value,
|
||||
tgMsgText: tgMsgText.value,
|
||||
tgMsgTo: tgMsgTo.value
|
||||
}
|
||||
if (tgBotToken.value) form.tgBotToken = tgBotToken.value
|
||||
editSetting(form)
|
||||
}
|
||||
|
||||
@@ -1024,45 +1233,110 @@ function ruleEmailSave() {
|
||||
}
|
||||
|
||||
function doOpacityChange() {
|
||||
if (!settingReady.value) return
|
||||
const form = {}
|
||||
form.loginOpacity = loginOpacity.value
|
||||
editSetting(form, true)
|
||||
}
|
||||
|
||||
function resetEmailPrefix() {
|
||||
minEmailPrefix.value = setting.value.minEmailPrefix
|
||||
emailPrefixFilter.value = setting.value.emailPrefixFilter
|
||||
}
|
||||
|
||||
function resetBlackList() {
|
||||
blackListForm.value.blackFrom = setting.value.blackFrom ? setting.value.blackFrom.split(',') : []
|
||||
blackListForm.value.blackContent = setting.value.blackContent ? setting.value.blackContent.split(',') : []
|
||||
blackListForm.value.blackSubject = setting.value.blackSubject ? setting.value.blackSubject.split(',') : []
|
||||
}
|
||||
|
||||
function resetAiCodeFilter() {
|
||||
aiCodeFilter.value = setting.value.aiCodeFilter ? setting.value.aiCodeFilter.split(',') : []
|
||||
}
|
||||
|
||||
function saveEmailPrefix() {
|
||||
const form = {}
|
||||
form.minEmailPrefix = minEmailPrefix.value
|
||||
form.emailPrefixFilter = emailPrefixFilter.value
|
||||
editSetting(form, true)
|
||||
}
|
||||
|
||||
function saveAiCodeFilter() {
|
||||
editSetting({aiCodeFilter: aiCodeFilter.value + ''})
|
||||
}
|
||||
|
||||
const opacityChange = debounce(doOpacityChange, 1000, {
|
||||
leading: false,
|
||||
trailing: true
|
||||
})
|
||||
|
||||
function physicsDeleteAllData() {
|
||||
ElMessageBox.prompt(t('clearAllDelConfirm'), {
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
dangerouslyUseHTMLString: true,
|
||||
title: t('warning'),
|
||||
type: 'warning',
|
||||
inputPattern: new RegExp(`^${t('delInputPattern')}$`),
|
||||
inputErrorMessage: t('inputErrorMessage'),
|
||||
}).then(() => {
|
||||
physicsDeleteAll().then(() => {
|
||||
ElMessage({
|
||||
message: t('delSuccessMsg'),
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
function saveBlackList() {
|
||||
|
||||
let form = {
|
||||
blackContent: blackListForm.value.blackContent + '',
|
||||
blackSubject: blackListForm.value.blackSubject + '',
|
||||
blackFrom: blackListForm.value.blackFrom + ''
|
||||
}
|
||||
|
||||
settingLoading.value = true
|
||||
|
||||
setBlackList(form).then(() => {
|
||||
getSettings()
|
||||
ElMessage({
|
||||
message: t('setSuccess'),
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
blackFormShow.value = false;
|
||||
}).finally(() => {
|
||||
settingLoading.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
function banEmailAddTag(val) {
|
||||
const emails = Array.from(new Set(
|
||||
val.split(/[,,]/).map(item => item.trim()).filter(item => item)
|
||||
));
|
||||
|
||||
blackListForm.value.blackFrom.splice(blackListForm.value.blackFrom.length - 1, 1)
|
||||
|
||||
emails.forEach(email => {
|
||||
if ((isEmail(email) || isDomain(email)) && !blackListForm.value.blackFrom.includes(email)) {
|
||||
blackListForm.value.blackFrom.push(email)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function aiCodeFilterAddTag(val) {
|
||||
const emails = Array.from(new Set(
|
||||
val.split(/[,,]/).map(item => item.trim()).filter(item => item)
|
||||
));
|
||||
|
||||
aiCodeFilter.value.splice(aiCodeFilter.value.length - 1, 1)
|
||||
|
||||
emails.forEach(email => {
|
||||
if ((isEmail(email) || isDomain(email)) && !aiCodeFilter.value.includes(email)) {
|
||||
aiCodeFilter.value.push(email)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function delBackground() {
|
||||
ElMessageBox.confirm(t('delBackgroundConfirm'), {
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
backgroundUrl.value = ''
|
||||
setting.value.background = null
|
||||
editSetting({background: null})
|
||||
deleteBackground().then(() => {
|
||||
backgroundUrl.value = ''
|
||||
setting.value.background = null
|
||||
ElMessage({
|
||||
message: t('delSuccessMsg'),
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1133,6 +1407,14 @@ function openResendForm() {
|
||||
resendTokenFormShow.value = true
|
||||
}
|
||||
|
||||
function openBlackListForm() {
|
||||
blackFormShow.value = true
|
||||
}
|
||||
|
||||
function openAiCodeFilter() {
|
||||
aiCodeFilterShow.value = true
|
||||
}
|
||||
|
||||
function saveResendToken() {
|
||||
const settingForm = {
|
||||
resendTokens: {}
|
||||
@@ -1155,21 +1437,29 @@ function cleanResendTokenForm() {
|
||||
}
|
||||
|
||||
function beforeChange() {
|
||||
if (settingLoading.value) return false
|
||||
if (!settingReady.value || settingLoading.value) return false
|
||||
backupSetting()
|
||||
return true
|
||||
}
|
||||
|
||||
function change(e) {
|
||||
if (!settingReady.value) return
|
||||
const settingForm = {...setting.value}
|
||||
delete settingForm.siteKey
|
||||
delete settingForm.secretKey
|
||||
delete settingForm.s3AccessKey
|
||||
delete settingForm.s3SecretKey
|
||||
delete settingForm.tgBotToken
|
||||
delete settingForm.resendTokens
|
||||
editSetting(settingForm, false)
|
||||
}
|
||||
|
||||
function changeField(key, value) {
|
||||
if (!settingReady.value) return
|
||||
setting.value[key] = value
|
||||
editSetting({[key]: value}, false)
|
||||
}
|
||||
|
||||
function saveTitle() {
|
||||
editSetting({title: editTitle.value})
|
||||
}
|
||||
@@ -1193,7 +1483,7 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
plain: true
|
||||
})
|
||||
if (setting.value.manyEmail === 1) {
|
||||
accountStore.currentAccountId = userStore.user.accountId;
|
||||
accountStore.currentAccountId = userStore.user.account.accountId;
|
||||
}
|
||||
if (refreshStatus) {
|
||||
getSettings()
|
||||
@@ -1209,6 +1499,8 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
regVerifyCountShow.value = false
|
||||
noticePopupShow.value = false
|
||||
addS3Show.value = false
|
||||
emailPrefixShow.value = false
|
||||
aiCodeFilterShow.value = false
|
||||
}).catch((e) => {
|
||||
loginOpacity.value = setting.value.loginOpacity
|
||||
setting.value = {...setting.value, ...JSON.parse(backup)}
|
||||
@@ -1269,7 +1561,7 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
.card-grid {
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(440px, 1fr));
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
@media (max-width: 500px) {
|
||||
@@ -1282,13 +1574,13 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
}
|
||||
|
||||
.background {
|
||||
width: 230px;
|
||||
height: 120px;
|
||||
width: 249px;
|
||||
height: 140px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--light-border);
|
||||
@media (max-width: 500px) {
|
||||
width: 150px;
|
||||
height: 83px;
|
||||
width: 160px;
|
||||
height: 90px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1368,7 +1660,7 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
}
|
||||
|
||||
.warning {
|
||||
margin-left: 4px;
|
||||
margin-left: 2px;
|
||||
color: grey;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1475,6 +1767,7 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
|
||||
.forward-set-title {
|
||||
top: 1px;
|
||||
padding-right: 5px;
|
||||
position: relative;
|
||||
font-size: 16px;
|
||||
font-weight: bold;;
|
||||
@@ -1527,11 +1820,24 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
.forward-set-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
|
||||
.el-switch {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
> *:nth-child(-n+2) {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tg-msg-label {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.el-select {
|
||||
width: v-bind(tgMsgLabelWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.forward {
|
||||
@@ -1550,6 +1856,16 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
width: fit-content !important;
|
||||
}
|
||||
|
||||
.email-prefix {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.prefix-filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.s3-button {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 1fr;
|
||||
@@ -1565,6 +1881,10 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
|
||||
.storage-type {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
@@ -1595,6 +1915,20 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.force-path-style {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
.force-path-style-left {
|
||||
padding-left: 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.concerning-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1648,7 +1982,7 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
}
|
||||
|
||||
form .el-button {
|
||||
margin-top: 15px;
|
||||
margin-top: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +1,11 @@
|
||||
<template>
|
||||
<el-scrollbar>
|
||||
<div class="scrollbar-flex-content">
|
||||
<p v-for="item in 1000" :key="item" class="scrollbar-demo-item">
|
||||
{{ item }}
|
||||
</p>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.scrollbar-flex-content {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 200px 200px 200px 200px 200px 200px 200px 200px 200px 200px;
|
||||
width: 40px;
|
||||
}
|
||||
.scrollbar-demo-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
marngin-bottom: 10px;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
background: var(--el-color-danger-light-9);
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -23,7 +23,7 @@
|
||||
<Icon class="icon" @click="changeTimeSort" icon="material-symbols-light:timer-arrow-up-outline" v-else width="28"
|
||||
height="28"/>
|
||||
<Icon class="icon" icon="ion:reload" width="18" height="18" @click="refresh"/>
|
||||
<Icon class="icon" icon="pepicons-pencil:expand" width="26" height="26" @click="changeExpand"/>
|
||||
<Icon class="icon" icon="uiw:delete" width="16" height="16" @click="delUser"/>
|
||||
</div>
|
||||
<el-scrollbar ref="scrollbarRef" class="scrollbar">
|
||||
<div>
|
||||
@@ -34,75 +34,21 @@
|
||||
<el-table
|
||||
@filter-change="tableFilter"
|
||||
:empty-text="first ? '' : null"
|
||||
:default-expand-all="expandStatus"
|
||||
:data="users"
|
||||
:preserve-expanded-content="preserveExpanded"
|
||||
style="width: 100%;"
|
||||
:key="key"
|
||||
ref="tableRef"
|
||||
@cell-contextmenu="handleContextmenu"
|
||||
:cell-class-name="cellClassName"
|
||||
>
|
||||
<el-table-column :width="expandWidth" type="expand">
|
||||
<template #default="props">
|
||||
<div class="details">
|
||||
<div v-if="!sendNumShow"><span
|
||||
class="details-item-title">{{ $t('tabSent') }}:</span>{{ props.row.sendEmailCount }}
|
||||
</div>
|
||||
<div v-if="!accountNumShow"><span class="details-item-title">{{ $t('tabMailboxes') }}:</span>{{
|
||||
props.row.accountCount
|
||||
}}
|
||||
</div>
|
||||
<div v-if="!createTimeShow"><span class="details-item-title">{{ $t('tabRegisteredAt') }}:</span>{{
|
||||
tzDayjs(props.row.createTime).format('YYYY-MM-DD HH:mm')
|
||||
}}
|
||||
</div>
|
||||
<div v-if="!typeShow"><span class="details-item-title">{{ $t('perm') }}:</span>
|
||||
{{ toRoleName(props.row.type) }}
|
||||
</div>
|
||||
<div v-if="!statusShow">
|
||||
<span class="details-item-title">{{ $t('tabStatus') }}:</span>
|
||||
<el-tag disable-transitions v-if="props.row.isDel === 1" type="info">{{ $t('deleted') }}</el-tag>
|
||||
<el-tag disable-transitions v-else-if="props.row.status === 0" type="primary">{{ $t('active') }}
|
||||
</el-tag>
|
||||
<el-tag disable-transitions v-else-if="props.row.status === 1" type="danger">{{ $t('banned') }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div><span class="details-item-title">{{ $t('registrationIp') }}:</span>{{
|
||||
props.row.createIp || $t('unknown')
|
||||
}}
|
||||
</div>
|
||||
<div><span class="details-item-title">{{ $t('recentIP') }}:</span>{{
|
||||
props.row.activeIp || $t('unknown')
|
||||
}}
|
||||
</div>
|
||||
<div><span class="details-item-title">{{ $t('recentActivity') }}:</span>{{
|
||||
props.row.activeTime ? tzDayjs(props.row.activeTime).format('YYYY-MM-DD') : $t('unknown')
|
||||
}}
|
||||
</div>
|
||||
<div><span
|
||||
class="details-item-title">{{ $t('loginDevice') }}:</span>{{ props.row.device || $t('unknown') }}
|
||||
</div>
|
||||
<div><span class="details-item-title">{{ $t('loginSystem') }}:</span>{{ props.row.os || $t('unknown') }}
|
||||
</div>
|
||||
<div><span
|
||||
class="details-item-title">{{ $t('browserLogin') }}:</span>{{ props.row.browser || $t('unknown') }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="details-item-title">{{ $t('sendEmail') }}:</span>
|
||||
<span>{{ formatSendCount(props.row) }}</span>
|
||||
<el-tag style="margin-left: 10px" v-if="props.row.sendAction.hasPerm">
|
||||
{{ formatSendType(props.row) }}
|
||||
</el-tag>
|
||||
<el-button size="small" style="margin-left: 10px"
|
||||
v-if="props.row.sendAction.hasPerm && props.row.sendAction.sendCount"
|
||||
@click="resetSendCount(props.row)" type="primary">{{ $t('reset') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :width="expandWidth" type="selection" :selectable="row => row.type !== 0" />
|
||||
<el-table-column show-overflow-tooltip :tooltip-formatter="tableRowFormatter" :label="$t('tabEmailAddress')"
|
||||
:min-width="emailWidth">
|
||||
<template #default="props">
|
||||
<div class="email-row">{{ props.row.email }}</div>
|
||||
<div style="display: flex;gap: 5px">
|
||||
<div class="email-row">{{ props.row.email }}</div>
|
||||
<el-tag type="warning" v-if="props.row.username">L</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :formatter="formatterReceive" label-class-name="receive" column-key="receive"
|
||||
@@ -137,20 +83,21 @@
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('tabSetting')" :width="settingWidth">
|
||||
<template #default="props">
|
||||
<el-dropdown trigger="click">
|
||||
<el-button size="small" type="primary" v-if="(props.row.type === 0 && userStore.user.type !== 0)" >{{ $t('action') }}</el-button>
|
||||
<el-dropdown v-else >
|
||||
<el-button size="small" type="primary">{{ $t('action') }}</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="openSetPwd(props.row)">{{ $t('chgPwd') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="openSetType(props.row)">{{ $t('perm') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="openSetPwd(props.row)" >{{ $t('chgPwd') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="openSetType(props.row)" >{{ $t('perm') }}</el-dropdown-item>
|
||||
<template v-if="props.row.type !== 0">
|
||||
<el-dropdown-item v-if="props.row.isDel !== 1" @click="setStatus(props.row)">
|
||||
{{ setStatusName(props.row) }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-else @click="restore(props.row)">{{ $t('restore') }}</el-dropdown-item>
|
||||
</template>
|
||||
<el-dropdown-item @click="openAccountList(props.row.userId)">{{ $t('account') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="delUser(props.row)" v-if="props.row.type !== 0">{{ $t('delete') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="openAccountList(props.row.userId)" >{{ $t('account') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click="openDetails(props.row)" >{{ $t('details') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -206,7 +153,7 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="showAdd" :title="$t('addUser')">
|
||||
<el-dialog v-model="showAdd" :title="$t('addUser')" @closed="resetAddForm">
|
||||
<div class="container">
|
||||
<el-input v-model="addForm.email" type="text" :placeholder="$t('emailAccount')" autocomplete="off">
|
||||
<template #append>
|
||||
@@ -278,6 +225,142 @@
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-dialog class="account-dialog" v-model="detailsShow" :title="t('userDetails')" >
|
||||
<div class="details">
|
||||
<div v-if="userDetails.username"><span class="details-item-title">LinuxDo:</span>
|
||||
<el-avatar :src="userDetails.avatar" :size="30" class="linuxdo-avatar" />
|
||||
<span style="margin: 0 10px">用户名:{{userDetails.username}}</span>
|
||||
<span>
|
||||
等级:<el-tag type="success">{{userDetails.trustLevel}}</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="!sendNumShow"><span
|
||||
class="details-item-title">{{ $t('tabSent') }}:</span>{{ userDetails.sendEmailCount }}
|
||||
</div>
|
||||
<div v-if="!accountNumShow"><span class="details-item-title">{{ $t('tabMailboxes') }}:</span>{{
|
||||
userDetails.accountCount
|
||||
}}
|
||||
</div>
|
||||
<div v-if="!createTimeShow"><span class="details-item-title">{{ $t('tabRegisteredAt') }}:</span>{{
|
||||
tzDayjs(userDetails.createTime).format('YYYY-MM-DD HH:mm')
|
||||
}}
|
||||
</div>
|
||||
<div v-if="!typeShow"><span class="details-item-title">{{ $t('perm') }}:</span>
|
||||
{{ toRoleName(userDetails.type) }}
|
||||
</div>
|
||||
<div v-if="!statusShow">
|
||||
<span class="details-item-title">{{ $t('tabStatus') }}:</span>
|
||||
<el-tag disable-transitions v-if="userDetails.isDel === 1" type="info">{{ $t('deleted') }}</el-tag>
|
||||
<el-tag disable-transitions v-else-if="userDetails.status === 0" type="primary">{{ $t('active') }}
|
||||
</el-tag>
|
||||
<el-tag disable-transitions v-else-if="userDetails.status === 1" type="danger">{{ $t('banned') }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div><span class="details-item-title">{{ $t('registrationIp') }}:</span>{{
|
||||
userDetails.createIp || $t('unknown')
|
||||
}}
|
||||
</div>
|
||||
<div><span class="details-item-title">{{ $t('recentIP') }}:</span>{{
|
||||
userDetails.activeIp || $t('unknown')
|
||||
}}
|
||||
</div>
|
||||
<div><span class="details-item-title">{{ $t('recentActivity') }}:</span>{{
|
||||
userDetails.activeTime ? tzDayjs(userDetails.activeTime).format('YYYY-MM-DD') : $t('unknown')
|
||||
}}
|
||||
</div>
|
||||
<div><span
|
||||
class="details-item-title">{{ $t('loginDevice') }}:</span>{{ userDetails.device || $t('unknown') }}
|
||||
</div>
|
||||
<div><span class="details-item-title">{{ $t('loginSystem') }}:</span>{{ userDetails.os || $t('unknown') }}
|
||||
</div>
|
||||
<div><span
|
||||
class="details-item-title">{{ $t('browserLogin') }}:</span>{{ userDetails.browser || $t('unknown') }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="details-item-title">{{ $t('sendEmail') }}:</span>
|
||||
<span>{{ formatSendCount(userDetails) }}</span>
|
||||
<el-tag style="margin-left: 10px" v-if="userDetails.sendAction.hasPerm">
|
||||
{{ formatSendType(userDetails) }}
|
||||
</el-tag>
|
||||
<el-button size="small" style="margin-left: 10px"
|
||||
v-if="userDetails.sendAction.hasPerm && userDetails.sendAction.sendCount"
|
||||
@click="resetSendCount(userDetails)" type="primary">{{ $t('reset') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-dropdown
|
||||
:show-timeout="0"
|
||||
:hide-timeout="0"
|
||||
ref="dropdownRef"
|
||||
@visible-change="visibleChange"
|
||||
:virtual-ref="triggerRef"
|
||||
:show-arrow="false"
|
||||
:popper-options="{
|
||||
modifiers: [{ name: 'offset', options: { offset: [0, 0] } }],
|
||||
}"
|
||||
virtual-triggering
|
||||
trigger="contextmenu"
|
||||
placement="bottom-start"
|
||||
>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="openSetPwd(rightClickUser)">
|
||||
<template #default>
|
||||
<div class="right-dropdown-item">
|
||||
<icon icon="fluent:fingerprint-20-filled" width="22" height="22" />
|
||||
<span>{{t('changePassword')}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="openSetType(rightClickUser)">
|
||||
<template #default>
|
||||
<div class="right-dropdown-item">
|
||||
<icon icon="fluent:lock-closed-16-regular" width="21" height="21" />
|
||||
<span>{{ t('setRole') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="rightClickUser.type !== 0">
|
||||
<template #default>
|
||||
<div class="right-dropdown-item" v-if="rightClickUser.isDel !== 1" @click="setStatus(rightClickUser)" >
|
||||
<Icon icon="ion:reload" v-if="rightClickUser.status" style="margin-left: 1px;margin-right: 1px" width="19" height="19" />
|
||||
<Icon icon="ion:ban-outline" v-else style="margin-left: 1px;margin-right: 1px" width="19" height="19" />
|
||||
<span>{{ setRightStatusName(rightClickUser) }}</span>
|
||||
</div>
|
||||
<div class="right-dropdown-item" v-else @click="restore(rightClickUser)">
|
||||
<Icon icon="ion:reload" style="margin-left: 1px;margin-right: 1px" width="19" height="19" />
|
||||
<span>{{ t('restoreUser') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="openAccountList(rightClickUser.userId)" >
|
||||
<template #default>
|
||||
<div class="right-dropdown-item" >
|
||||
<Icon icon="hugeicons:mailbox-01" width="20" height="20" />
|
||||
<span>{{ t('userEmail') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="openDetails(rightClickUser)" >
|
||||
<template #default>
|
||||
<div class="right-dropdown-item" >
|
||||
<Icon icon="si:user-alt-2-line" width="20" height="20" />
|
||||
<span>{{ t('userDetails') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="rightClickUser.type !== 0" @click="delOneUser(rightClickUser)" >
|
||||
<template #default>
|
||||
<div class="right-dropdown-item" >
|
||||
<Icon icon="uiw:delete" width="18" height="18" style="margin-left: 1px;margin-right: 1px" />
|
||||
<span>{{ t('adminDeleteUser') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -326,15 +409,31 @@ const statusShow = ref(true)
|
||||
const typeShow = ref(true)
|
||||
const receiveWidth = ref(null)
|
||||
const phonePageShow = ref(false)
|
||||
const detailsShow = ref(false);
|
||||
const layout = ref('prev, pager, next, sizes, total')
|
||||
const pageSize = ref('')
|
||||
const expandStatus = ref(false)
|
||||
const users = ref([])
|
||||
const tableRef = ref({})
|
||||
const userDetails = ref({})
|
||||
const total = ref(0)
|
||||
const first = ref(true)
|
||||
const scrollbarRef = ref(null)
|
||||
const accountLoading = ref(false)
|
||||
const dropdownRef = ref(null);
|
||||
const dropdownShow = ref(false);
|
||||
const rightClickUser = ref({});
|
||||
const position = ref(
|
||||
DOMRect.fromRect({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
)
|
||||
|
||||
const triggerRef = ref({
|
||||
getBoundingClientRect() {
|
||||
return position.value;
|
||||
}
|
||||
})
|
||||
const domainList = settingStore.domainList
|
||||
|
||||
const addForm = reactive({
|
||||
@@ -368,7 +467,6 @@ const settingLoading = ref(false)
|
||||
const tableLoading = ref(true)
|
||||
const roleList = reactive([])
|
||||
const mySelect = ref({})
|
||||
const key = ref(0)
|
||||
const accountList = reactive([])
|
||||
const accountParams = reactive({
|
||||
size: 10,
|
||||
@@ -416,6 +514,43 @@ const filterItem = reactive({
|
||||
receive: ['normal', 'del']
|
||||
})
|
||||
|
||||
window.addEventListener('wheel', (event) => {
|
||||
if (dropdownShow.value) {
|
||||
dropdownRef.value.handleClose();
|
||||
}
|
||||
})
|
||||
|
||||
function visibleChange(e) {
|
||||
dropdownShow.value = e;
|
||||
if (!e) {
|
||||
rightClickUser.value.checkedClass = '';
|
||||
}
|
||||
}
|
||||
|
||||
function cellClassName({ row }) {
|
||||
return row.checkedClass;
|
||||
}
|
||||
|
||||
const handleContextmenu = (row, column, cell, event) => {
|
||||
|
||||
if (row.type === 0 && userStore.user.type !== 0) {
|
||||
return
|
||||
}
|
||||
|
||||
rightClickUser.value.checkedClass = '';
|
||||
|
||||
const { clientX, clientY } = event
|
||||
position.value = DOMRect.fromRect({
|
||||
x: clientX,
|
||||
y: clientY,
|
||||
})
|
||||
event.preventDefault()
|
||||
dropdownRef.value?.handleOpen()
|
||||
|
||||
row.checkedClass = 'checked-row';
|
||||
rightClickUser.value = row;
|
||||
}
|
||||
|
||||
function deleteAccount(account) {
|
||||
ElMessageBox.confirm(t('delConfirm', {msg: account.email}), {
|
||||
confirmButtonText: t('confirm'),
|
||||
@@ -425,7 +560,7 @@ function deleteAccount(account) {
|
||||
userDeleteAccount(account.accountId).then(() => {
|
||||
getAccountList()
|
||||
ElMessage({
|
||||
message: t('删除成功'),
|
||||
message: t('delSuccessMsg'),
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
@@ -450,6 +585,11 @@ function openAccountList(userId) {
|
||||
accountShow.value = true
|
||||
}
|
||||
|
||||
function openDetails(user) {
|
||||
userDetails.value = user;
|
||||
detailsShow.value = true;
|
||||
}
|
||||
|
||||
function getAccountList(loading = false) {
|
||||
accountLoading.value = loading
|
||||
userAllAccount(accountParams.userId,accountParams.num, accountParams.size).then(({list,total}) => {
|
||||
@@ -526,13 +666,14 @@ function setStatusName(user) {
|
||||
if (user.status === 1) return t('enable')
|
||||
}
|
||||
|
||||
const tableRowFormatter = (data) => {
|
||||
return data.row.email
|
||||
function setRightStatusName(user) {
|
||||
if (user.isDel === 1) return t('adminDeleteUser')
|
||||
if (user.status === 0) return t('banUser')
|
||||
if (user.status === 1) return t('enableUser')
|
||||
}
|
||||
|
||||
function changeExpand() {
|
||||
expandStatus.value = !expandStatus.value
|
||||
key.value++
|
||||
const tableRowFormatter = (data) => {
|
||||
return data.row.email
|
||||
}
|
||||
|
||||
const openSelect = () => {
|
||||
@@ -602,13 +743,12 @@ function submit() {
|
||||
form.email = form.email + form.suffix
|
||||
userAdd(form).then(() => {
|
||||
addLoading.value = false
|
||||
showAdd.value = false
|
||||
addForm.email = ''
|
||||
ElMessage({
|
||||
message: t('addSuccessMsg'),
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
resetAddForm()
|
||||
getUserList(false)
|
||||
}).finally(res => {
|
||||
addLoading.value = false
|
||||
@@ -620,6 +760,7 @@ function formatSendType(user) {
|
||||
if (user.sendAction.sendType === 'day') return t('daily')
|
||||
if (user.sendAction.sendType === 'count') return t('total')
|
||||
if (user.sendAction.sendType === 'ban') return t('sendBanned')
|
||||
if (user.sendAction.sendType === 'internal') return t('sendInternal')
|
||||
}
|
||||
|
||||
function formatSendCount(user) {
|
||||
@@ -669,18 +810,40 @@ function resetSendCount(user) {
|
||||
}
|
||||
|
||||
function delUser(user) {
|
||||
ElMessageBox.confirm(t('delConfirm', {msg: user.email}), {
|
||||
const rows = tableRef.value.getSelectionRows();
|
||||
const userIds = rows.map(row => row.userId);
|
||||
if (userIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
ElMessageBox.confirm(t('delUsersConfirm'), {
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
userDelete(user.userId).then(() => {
|
||||
userDelete(userIds).then(() => {
|
||||
ElMessage({
|
||||
message: t('delSuccessMsg'),
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
getUserList(false)
|
||||
getUserList(true)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function delOneUser(user) {
|
||||
ElMessageBox.confirm(t('delConfirm', {msg: user.email}), {
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
userDelete([user.userId]).then(() => {
|
||||
ElMessage({
|
||||
message: t('delSuccessMsg'),
|
||||
type: "success",
|
||||
plain: true
|
||||
})
|
||||
getUserList(true)
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -693,14 +856,14 @@ function restore(user) {
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
message: () => h('div', [
|
||||
h('div', {class: 'mb-2'}, t('restoreConfirm', {msg: user.email})),
|
||||
h(ElRadioGroup, {
|
||||
modelValue: type.value,
|
||||
'onUpdate:modelValue': (val) => (type.value = val),
|
||||
}, [
|
||||
h(ElRadio, {label: 'option1', value: 0}, t('normalRestore')),
|
||||
h(ElRadio, {label: 'option2', value: 1}, t('allRestore')),
|
||||
])
|
||||
h('div', {class: 'mb-2'}, t('restoreConfirm', {msg: user.email}))
|
||||
// h(ElRadioGroup, {
|
||||
// modelValue: type.value,
|
||||
// 'onUpdate:modelValue': (val) => (type.value = val),
|
||||
// }, [
|
||||
// h(ElRadio, {label: 'option1', value: 0}, t('normalRestore')),
|
||||
// h(ElRadio, {label: 'option2', value: 1}, t('allRestore')),
|
||||
// ])
|
||||
]),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
@@ -716,18 +879,7 @@ function restore(user) {
|
||||
}
|
||||
|
||||
function setStatus(user) {
|
||||
|
||||
if (user.status === 0) {
|
||||
ElMessageBox.confirm(t('banRestore', {msg: user.email}), {
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
httpSetStatus(user)
|
||||
});
|
||||
} else {
|
||||
httpSetStatus(user)
|
||||
}
|
||||
httpSetStatus(user);
|
||||
}
|
||||
|
||||
function httpSetStatus(user) {
|
||||
@@ -852,7 +1004,7 @@ function getUserList(loading = true) {
|
||||
newParams.isDel = 1
|
||||
}
|
||||
userList(newParams).then(data => {
|
||||
users.value = data.list
|
||||
users.value = data.list.map(item => ({...item, checkedClass: ''}))
|
||||
total.value = data.total
|
||||
scrollbarRef.value?.setScrollTop(0);
|
||||
}).finally(() => {
|
||||
@@ -878,7 +1030,7 @@ function adjustWidth() {
|
||||
typeShow.value = width > 767
|
||||
emailWidth.value = width > 480 ? 230 : null
|
||||
settingWidth.value = width < 480 ? (locale.value === 'en' ? 85 : 75) : null
|
||||
expandWidth.value = width < 480 ? 25 : 40
|
||||
expandWidth.value = width < 480 ? 30 : 35
|
||||
pagerCount.value = width < 768 ? 7 : 11
|
||||
receiveWidth.value = width < 480 ? 90 : null
|
||||
layout.value = width < 768 ? 'pager' : 'prev, pager, next,sizes, total'
|
||||
@@ -903,6 +1055,10 @@ function adjustWidth() {
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
:deep(.el-table .checked-row) {
|
||||
background: var(--el-color-warning-light-9);
|
||||
}
|
||||
|
||||
.user-box {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
@@ -980,13 +1136,9 @@ function adjustWidth() {
|
||||
}
|
||||
|
||||
.details {
|
||||
padding: 15px 15px 15px 52px;
|
||||
padding: 0 10px 10px 10px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
@media (max-width: 767px) {
|
||||
padding-left: 35px;
|
||||
}
|
||||
|
||||
.details-item-title {
|
||||
white-space: pre;
|
||||
color: #909399;
|
||||
@@ -995,6 +1147,11 @@ function adjustWidth() {
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.linuxdo-avatar) {
|
||||
position: relative !important;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.account-pagination {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
@@ -1078,6 +1235,10 @@ function adjustWidth() {
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
.right-dropdown-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
@@ -1112,6 +1273,13 @@ function adjustWidth() {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
@media (pointer: coarse) {
|
||||
/* 触屏 */
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table th.el-table__cell>.cell.highlight) {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
@@ -11,18 +11,19 @@
|
||||
"devDependencies": {
|
||||
"@cloudflare/vitest-pool-workers": "^0.7.5",
|
||||
"vitest": "~3.0.7",
|
||||
"wrangler": "^4.7.0"
|
||||
"wrangler": "^4.90.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.882.0",
|
||||
"@cloudflare/vite-plugin": "1.6.0",
|
||||
"@simplewebauthn/server": "^13.3.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"drizzle-orm": "^0.42.0",
|
||||
"hono": "^4.7.5",
|
||||
"hono": "4.12.16",
|
||||
"i18next": "^25.3.2",
|
||||
"linkedom": "^0.18.10",
|
||||
"postal-mime": "^2.4.3",
|
||||
"resend": "^4.5.1",
|
||||
"resend": "^6.4.1",
|
||||
"ua-parser-js": "^2.0.3",
|
||||
"uuid": "^11.1.0"
|
||||
}
|
||||
|
||||
@@ -22,3 +22,13 @@ app.put('/account/setName', async (c) => {
|
||||
await accountService.setName(c, await c.req.json(), userContext.getUserId(c));
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
app.put('/account/setAllReceive', async (c) => {
|
||||
await accountService.setAllReceive(c, await c.req.json(), userContext.getUserId(c));
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
app.put('/account/setAsTop', async (c) => {
|
||||
await accountService.setAsTop(c, await c.req.json(), userContext.getUserId(c));
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
@@ -2,17 +2,22 @@ import app from '../hono/hono';
|
||||
import emailService from '../service/email-service';
|
||||
import result from '../model/result';
|
||||
|
||||
app.get('/allEmail/list',async (c) => {
|
||||
app.get('/allEmail/list', async (c) => {
|
||||
const data = await emailService.allList(c, c.req.query());
|
||||
return c.json(result.ok(data));
|
||||
})
|
||||
|
||||
app.delete('/allEmail/delete',async (c) => {
|
||||
app.delete('/allEmail/delete', async (c) => {
|
||||
const list = await emailService.physicsDelete(c, c.req.query());
|
||||
return c.json(result.ok(list));
|
||||
})
|
||||
|
||||
app.delete('/allEmail/batchDelete',async (c) => {
|
||||
app.delete('/allEmail/batchDelete', async (c) => {
|
||||
await emailService.batchDelete(c, c.req.query());
|
||||
return c.json(result.ok());
|
||||
})
|
||||
|
||||
app.get('/allEmail/latest', async (c) => {
|
||||
const list = await emailService.allEmailLatest(c, c.req.query());
|
||||
return c.json(result.ok(list));
|
||||
})
|
||||
|
||||
@@ -29,3 +29,8 @@ app.post('/email/send', async (c) => {
|
||||
return c.json(result.ok(email));
|
||||
});
|
||||
|
||||
app.put('/email/read', async (c) => {
|
||||
await emailService.read(c, await c.req.json(), userContext.getUserId(c));
|
||||
return c.json(result.ok());
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import app from '../hono/hono';
|
||||
import initService from '../init/init';
|
||||
import { dbInit } from '../init/init';
|
||||
|
||||
app.get('/init/:secret', (c) => {
|
||||
return initService.init(c);
|
||||
return dbInit.init(c);
|
||||
})
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import app from '../hono/hono';
|
||||
import result from "../model/result";
|
||||
import oauthService from "../service/oauth-service";
|
||||
|
||||
app.post('/oauth/linuxDo/login', async (c) => {
|
||||
const loginInfo = await oauthService.linuxDoLogin(c, await c.req.json());
|
||||
return c.json(result.ok(loginInfo))
|
||||
});
|
||||
|
||||
app.put('/oauth/bindUser', async (c) => {
|
||||
const loginInfo = await oauthService.bindUser(c, await c.req.json());
|
||||
return c.json(result.ok(loginInfo))
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
import app from '../hono/hono';
|
||||
import result from '../model/result';
|
||||
import userContext from '../security/user-context';
|
||||
import passkeyService from '../service/passkey-service';
|
||||
|
||||
app.post('/passkey/auth/options', async (c) => {
|
||||
return c.json(result.ok(await passkeyService.authenticationOptions(c)));
|
||||
});
|
||||
|
||||
app.post('/passkey/auth/verify', async (c) => {
|
||||
const token = await passkeyService.verifyAuthentication(c, await c.req.json());
|
||||
return c.json(result.ok({ token }));
|
||||
});
|
||||
|
||||
app.get('/my/passkey', async (c) => {
|
||||
return c.json(result.ok(await passkeyService.status(c, userContext.getUserId(c))));
|
||||
});
|
||||
|
||||
app.post('/my/passkey/reg/options', async (c) => {
|
||||
const data = await passkeyService.registrationOptions(c, userContext.getUserId(c), await c.req.json());
|
||||
return c.json(result.ok(data));
|
||||
});
|
||||
|
||||
app.post('/my/passkey/reg/verify', async (c) => {
|
||||
const data = await passkeyService.verifyRegistration(c, userContext.getUserId(c), await c.req.json());
|
||||
return c.json(result.ok(data));
|
||||
});
|
||||
|
||||
app.delete('/my/passkey', async (c) => {
|
||||
await passkeyService.delete(c, userContext.getUserId(c), await c.req.json());
|
||||
return c.json(result.ok());
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import r2Service from '../service/r2-service';
|
||||
import app from '../hono/hono';
|
||||
|
||||
app.get('/file/*', async (c) => {
|
||||
const key = c.req.path.split('/file/')[1];
|
||||
app.get('/oss/*', async (c) => {
|
||||
const key = c.req.path.split('/oss/')[1];
|
||||
const obj = await r2Service.getObj(c, key);
|
||||
return new Response(obj.body, {
|
||||
headers: {
|
||||
|
||||
@@ -14,13 +14,12 @@ app.put('/role/setDefault', async (c) => {
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
|
||||
app.put('/role/set', async (c) => {
|
||||
await roleService.setRole(c, await c.req.json());
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
app.get('/role/permTree', async (c) => {
|
||||
app.get('/role/tree', async (c) => {
|
||||
const tree = await permService.tree(c);
|
||||
return c.json(result.ok(tree));
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import app from '../hono/hono';
|
||||
import result from '../model/result';
|
||||
import settingService from '../service/setting-service';
|
||||
import userContext from "../security/user-context";
|
||||
|
||||
app.put('/setting/set', async (c) => {
|
||||
await settingService.set(c, await c.req.json());
|
||||
@@ -21,3 +22,14 @@ app.put('/setting/setBackground', async (c) => {
|
||||
const key = await settingService.setBackground(c, await c.req.json());
|
||||
return c.json(result.ok(key));
|
||||
});
|
||||
|
||||
app.delete('/setting/deleteBackground', async (c) => {
|
||||
await settingService.deleteBackground(c);
|
||||
return c.json(result.ok());
|
||||
});
|
||||
|
||||
app.put('/setting/setBlacklist', async (c) => {
|
||||
const setting = await settingService.setBlacklist(c, await c.req.json());
|
||||
return c.json(result.ok(setting));
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import app from '../hono/hono';
|
||||
import telegramService from '../service/telegram-service';
|
||||
|
||||
app.get('/telegram/getEmail/:token', async (c) => {
|
||||
const content = await telegramService.getEmailContent(c, c.req.param());
|
||||
c.header('Cache-Control', 'public, max-age=604800, immutable');
|
||||
return c.html(content)
|
||||
});
|
||||
|
||||
@@ -7,6 +7,13 @@ export const userConst = {
|
||||
}
|
||||
}
|
||||
|
||||
export const accountConst = {
|
||||
allReceive: {
|
||||
CLOSE: 0,
|
||||
OPEN: 1
|
||||
}
|
||||
}
|
||||
|
||||
export const roleConst = {
|
||||
isDefault: {
|
||||
CLOSE: 0,
|
||||
@@ -41,7 +48,12 @@ export const emailConst = {
|
||||
COMPLAINED: 4,
|
||||
DELAYED: 5,
|
||||
SAVING: 6,
|
||||
NOONE: 7
|
||||
NOONE: 7,
|
||||
FAILED: 8
|
||||
},
|
||||
unread: {
|
||||
UNREAD: 0,
|
||||
READ: 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +119,22 @@ export const settingConst = {
|
||||
noRecipient: {
|
||||
OPEN: 0,
|
||||
CLOSE: 1,
|
||||
},
|
||||
kvStorage: {
|
||||
OPEN: 0,
|
||||
CLOSE: 1
|
||||
},
|
||||
forcePathStyle: {
|
||||
OPEN: 0,
|
||||
CLOSE: 1
|
||||
},
|
||||
aiCode: {
|
||||
OPEN: 0,
|
||||
CLOSE: 1
|
||||
},
|
||||
authRefresh: {
|
||||
OPEN: 1,
|
||||
CLOSE: 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ const KvConst = {
|
||||
AUTH_INFO: 'auth-uid:',
|
||||
SETTING: 'setting:',
|
||||
SEND_DAY_COUNT: 'send_day_count:',
|
||||
ANALYSIS_ECHARTS: 'analysis_echarts:',
|
||||
PUBLIC_KEY: "public_key:"
|
||||
}
|
||||
|
||||
|
||||
@@ -5,17 +5,12 @@ import settingService from '../service/setting-service';
|
||||
import attService from '../service/att-service';
|
||||
import constant from '../const/constant';
|
||||
import fileUtils from '../utils/file-utils';
|
||||
import { emailConst, isDel, roleConst, settingConst } from '../const/entity-const';
|
||||
import { emailConst, isDel, settingConst } from '../const/entity-const';
|
||||
import emailUtils from '../utils/email-utils';
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import roleService from '../service/role-service';
|
||||
import verifyUtils from '../utils/verify-utils';
|
||||
import r2Service from '../service/r2-service';
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
import userService from '../service/user-service';
|
||||
import telegramService from '../service/telegram-service';
|
||||
import aiService from '../service/ai-service';
|
||||
|
||||
export async function email(message, env, ctx) {
|
||||
|
||||
@@ -23,7 +18,6 @@ export async function email(message, env, ctx) {
|
||||
|
||||
const {
|
||||
receive,
|
||||
tgBotToken,
|
||||
tgChatId,
|
||||
tgBotStatus,
|
||||
forwardStatus,
|
||||
@@ -31,7 +25,12 @@ export async function email(message, env, ctx) {
|
||||
ruleEmail,
|
||||
ruleType,
|
||||
r2Domain,
|
||||
noRecipient
|
||||
noRecipient,
|
||||
blackSubject,
|
||||
blackContent,
|
||||
blackFrom,
|
||||
aiCode,
|
||||
aiCodeFilter
|
||||
} = await settingService.query({ env });
|
||||
|
||||
if (receive === settingConst.receive.CLOSE) {
|
||||
@@ -39,7 +38,6 @@ export async function email(message, env, ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const reader = message.raw.getReader();
|
||||
let content = '';
|
||||
|
||||
@@ -51,6 +49,14 @@ export async function email(message, env, ctx) {
|
||||
|
||||
const email = await PostalMime.parse(content);
|
||||
|
||||
|
||||
const blockFlag = checkBlock(blackSubject, blackContent, blackFrom, email);
|
||||
|
||||
if (blockFlag) {
|
||||
message.setReject('Message rejected');
|
||||
return;
|
||||
}
|
||||
|
||||
const account = await accountService.selectByEmailIncludeDel({ env: env }, message.to);
|
||||
|
||||
if (!account && noRecipient === settingConst.noRecipient.CLOSE) {
|
||||
@@ -58,63 +64,43 @@ export async function email(message, env, ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (account && account.email !== env.admin) {
|
||||
let userRow = {}
|
||||
|
||||
let { banEmail, banEmailType, availDomain } = await roleService.selectByUserId({ env: env }, account.userId);
|
||||
if (account) {
|
||||
userRow = await userService.selectByIdIncludeDel({ env: env }, account.userId);
|
||||
}
|
||||
|
||||
if (account && userRow.email !== env.admin) {
|
||||
|
||||
let { banEmail, availDomain } = await roleService.selectByUserId({ env: env }, account.userId);
|
||||
|
||||
if (!roleService.hasAvailDomainPerm(availDomain, message.to)) {
|
||||
message.setReject('Mailbox disabled');
|
||||
message.setReject('The recipient is not authorized to use this domain.');
|
||||
return;
|
||||
}
|
||||
|
||||
banEmail = banEmail.split(',').filter(item => item !== '');
|
||||
|
||||
|
||||
if (banEmail.includes('*')) {
|
||||
|
||||
if (!banEmailHandler(banEmailType, message, email)) return;
|
||||
|
||||
}
|
||||
|
||||
for (const item of banEmail) {
|
||||
|
||||
if (verifyUtils.isDomain(item)) {
|
||||
|
||||
const banDomain = item.toLowerCase();
|
||||
const receiveDomain = emailUtils.getDomain(email.from.address.toLowerCase());
|
||||
|
||||
if (banDomain === receiveDomain) {
|
||||
|
||||
if (!banEmailHandler(banEmailType, message, email)) return;
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if (item.toLowerCase() === email.from.address.toLowerCase()) {
|
||||
|
||||
if (!banEmailHandler(banEmailType, message, email)) return;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(roleService.isBanEmail(banEmail, email.from.address)) {
|
||||
message.setReject('The recipient is disabled from receiving emails.');
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let toName = ''
|
||||
|
||||
if (Array.isArray(email.to)) {
|
||||
toName = email.to.find(item => item.address === message.to)?.name || '';
|
||||
if (!email.to) {
|
||||
email.to = [{ address: message.to, name: emailUtils.getName(message.to)}]
|
||||
}
|
||||
|
||||
const toName = email.to.find(item => item.address === message.to)?.name || '';
|
||||
const code = await aiService.extractCode({ env }, email, { aiCode, aiCodeFilter });
|
||||
|
||||
const params = {
|
||||
toEmail: message.to,
|
||||
toName: toName,
|
||||
sendEmail: email.from.address,
|
||||
name: email.from.name || emailUtils.getName(email.from.address),
|
||||
subject: email.subject,
|
||||
code,
|
||||
content: email.html,
|
||||
text: email.text,
|
||||
cc: email.cc ? JSON.stringify(email.cc) : '[]',
|
||||
@@ -151,7 +137,7 @@ export async function email(message, env, ctx) {
|
||||
});
|
||||
|
||||
try {
|
||||
if (attachments.length > 0 && await r2Service.hasOSS({ env })) {
|
||||
if (attachments.length > 0) {
|
||||
await attService.addAtt({ env }, attachments);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -171,42 +157,12 @@ export async function email(message, env, ctx) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
//转发到TG
|
||||
if (tgBotStatus === settingConst.tgBotStatus.OPEN && tgChatId) {
|
||||
|
||||
const tgMessage = `<b>${params.subject}</b>
|
||||
|
||||
<b>发件人:</b>${params.name} <${params.sendEmail}>
|
||||
<b>收件人:\u200B</b>${message.to}
|
||||
<b>时间:</b>${dayjs.utc(emailRow.createTime).tz('Asia/Shanghai').format('YYYY-MM-DD HH:mm')}
|
||||
|
||||
${params.text || emailUtils.htmlToText(params.content) || ''}
|
||||
`;
|
||||
|
||||
const tgChatIds = tgChatId.split(',');
|
||||
|
||||
await Promise.all(tgChatIds.map(async chatId => {
|
||||
try {
|
||||
const res = await fetch(`https://api.telegram.org/bot${tgBotToken}/sendMessage`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat_id: chatId,
|
||||
parse_mode: 'HTML',
|
||||
text: tgMessage
|
||||
})
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(`转发 Telegram 失败: chatId=${chatId}, 状态码=${res.status}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`转发 Telegram 失败: chatId=${chatId}`, e);
|
||||
}
|
||||
}));
|
||||
await telegramService.sendEmailToBot({ env }, emailRow)
|
||||
}
|
||||
|
||||
//转发到其他邮箱
|
||||
if (forwardStatus === settingConst.forwardStatus.OPEN && forwardEmail) {
|
||||
|
||||
const emails = forwardEmail.split(',');
|
||||
@@ -224,24 +180,35 @@ ${params.text || emailUtils.htmlToText(params.content) || ''}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
|
||||
console.error('邮件接收异常: ', e);
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
function banEmailHandler(banEmailType, message, email) {
|
||||
function checkBlock(blackSubjectStr, blackContentStr, blackFromStr, email) {
|
||||
|
||||
if (banEmailType === roleConst.banEmailType.ALL) {
|
||||
message.setReject('Mailbox disabled');
|
||||
return false;
|
||||
const blackFromList = blackFromStr ? blackFromStr.split(',') : []
|
||||
const blackContentList = blackContentStr ? blackContentStr.split(',') : []
|
||||
const blackSubjectList = blackSubjectStr ? blackSubjectStr.split(',') : []
|
||||
|
||||
for (const blackSubject of blackSubjectList) {
|
||||
if (email.subject?.includes(blackSubject)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (banEmailType === roleConst.banEmailType.CONTENT) {
|
||||
email.html = 'The content has been deleted';
|
||||
email.text = 'The content has been deleted';
|
||||
email.attachments = [];
|
||||
for (const blackContent of blackContentList) {
|
||||
if (email.html?.includes(blackContent) || email.text?.includes(blackContent)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
for (const blackFrom of blackFromList) {
|
||||
if (email.from.address === blackFrom || emailUtils.getDomain(email.from.address) === blackFrom) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ export const account = sqliteTable('account', {
|
||||
latestEmailTime: text('latest_email_time'),
|
||||
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`),
|
||||
userId: integer('user_id').notNull(),
|
||||
allReceive: integer('all_receive').default(0).notNull(),
|
||||
sort: integer('sort').default(0).notNull(),
|
||||
isDel: integer('is_del').default(0).notNull(),
|
||||
});
|
||||
export default account
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const att = sqliteTable('attachments', {
|
||||
export const att = sqliteTable('attachments', {
|
||||
attId: integer('att_id').primaryKey({ autoIncrement: true }),
|
||||
userId: integer('user_id').notNull(),
|
||||
emailId: integer('email_id').notNull(),
|
||||
|
||||
@@ -7,6 +7,7 @@ export const email = sqliteTable('email', {
|
||||
accountId: integer('account_id').notNull(),
|
||||
userId: integer('user_id').notNull(),
|
||||
subject: text('subject'),
|
||||
code: text('code').default('').notNull(),
|
||||
text: text('text'),
|
||||
content: text('content'),
|
||||
cc: text('cc').default('[]'),
|
||||
@@ -21,6 +22,7 @@ export const email = sqliteTable('email', {
|
||||
status: integer('status').default(0).notNull(),
|
||||
resendEmailId: text('resend_email_id'),
|
||||
message: text('message'),
|
||||
unread: integer('unread').default(0).notNull(),
|
||||
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
isDel: integer('is_del').default(0).notNull()
|
||||
});
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const oauth = sqliteTable('oauth', {
|
||||
oauthId: integer('oauth_id').primaryKey({ autoIncrement: true }),
|
||||
oauthUserId: text('oauth_user_id'),
|
||||
username: text('username'),
|
||||
name: text('name'),
|
||||
avatar: text('avatar'),
|
||||
active: integer('active'),
|
||||
trustLevel: integer('trust_level'),
|
||||
silenced: integer('silenced'),
|
||||
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
userId: integer('user_id').default(0).notNull()
|
||||
});
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const passkeyCredential = sqliteTable('passkey_credential', {
|
||||
passkeyId: integer('passkey_id').primaryKey({ autoIncrement: true }),
|
||||
userId: integer('user_id').notNull().unique(),
|
||||
userHandle: text('user_handle').notNull().unique(),
|
||||
credentialId: text('credential_id').notNull().unique(),
|
||||
publicKey: text('public_key').notNull(),
|
||||
counter: integer('counter').default(0).notNull(),
|
||||
transports: text('transports').default('[]').notNull(),
|
||||
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
});
|
||||
|
||||
export default passkeyCredential;
|
||||
@@ -3,9 +3,9 @@ export const setting = sqliteTable('setting', {
|
||||
register: integer('register').default(0).notNull(),
|
||||
receive: integer('receive').default(0).notNull(),
|
||||
title: text('title').default('').notNull(),
|
||||
manyEmail: integer('many_email').default(1).notNull(),
|
||||
manyEmail: integer('many_email').default(0).notNull(),
|
||||
addEmail: integer('add_email').default(0).notNull(),
|
||||
autoRefreshTime: integer('auto_refresh_time').default(0).notNull(),
|
||||
autoRefresh: integer('auto_refresh').default(0).notNull(),
|
||||
addEmailVerify: integer('add_email_verify').default(1).notNull(),
|
||||
registerVerify: integer('register_verify').default(1).notNull(),
|
||||
regVerifyCount: integer('reg_verify_count').default(1).notNull(),
|
||||
@@ -35,10 +35,23 @@ export const setting = sqliteTable('setting', {
|
||||
notice: integer('notice').default(0).notNull(),
|
||||
noRecipient: integer('no_recipient').default(1).notNull(),
|
||||
loginDomain: integer('login_domain').default(0).notNull(),
|
||||
passkey: integer('passkey').default(0).notNull(),
|
||||
bucket: text('bucket').default('').notNull(),
|
||||
region: text('region').default('').notNull(),
|
||||
endpoint: text('endpoint').default('').notNull(),
|
||||
s3AccessKey: text('s3_access_key').default('').notNull(),
|
||||
s3SecretKey: text('s3_secret_key').default('').notNull()
|
||||
s3SecretKey: text('s3_secret_key').default('').notNull(),
|
||||
forcePathStyle: integer('force_path_style').default(1).notNull(),
|
||||
customDomain: text('custom_domain').default('').notNull(),
|
||||
tgMsgFrom: text('tg_msg_from').default('only-name').notNull(),
|
||||
tgMsgTo: text('tg_msg_to').default('show').notNull(),
|
||||
tgMsgText: text('tg_msg_text').default('hide').notNull(),
|
||||
minEmailPrefix: integer('min_email_prefix').default(0).notNull(),
|
||||
emailPrefixFilter: text('email_prefix_filter').default('').notNull(),
|
||||
blackSubject: text('black_subject').default('').notNull(),
|
||||
blackContent: text('black_content').default('').notNull(),
|
||||
blackFrom: text('black_from').default('').notNull(),
|
||||
aiCode: integer('ai_code').default(1).notNull(),
|
||||
aiCodeFilter: text('ai_code_filter').default('').notNull()
|
||||
});
|
||||
export default setting
|
||||
|
||||
@@ -9,20 +9,20 @@ app.use('*', cors());
|
||||
app.onError((err, c) => {
|
||||
if (err.name === 'BizError') {
|
||||
console.log(err.message);
|
||||
}else {
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
if (err.message === `Cannot read properties of undefined (reading 'get')`) {
|
||||
return c.json(result.fail('KV数据库未绑定<br>KV database not bound',502));
|
||||
return c.json(result.fail('KV数据库未绑定 KV database not bound',502));
|
||||
}
|
||||
|
||||
if (err.message === `Cannot read properties of undefined (reading 'put')`) {
|
||||
return c.json(result.fail('KV数据库未绑定<br>KV database not bound',502));
|
||||
return c.json(result.fail('KV数据库未绑定 KV database not bound',502));
|
||||
}
|
||||
|
||||
if (err.message === `Cannot read properties of undefined (reading 'prepare')`) {
|
||||
return c.json(result.fail('D1数据库未绑定<br>D1 database not bound',502));
|
||||
return c.json(result.fail('D1数据库未绑定 D1 database not bound',502));
|
||||
}
|
||||
|
||||
return c.json(result.fail(err.message, err.code));
|
||||
|
||||
@@ -18,4 +18,7 @@ import '../api/init-api'
|
||||
import '../api/analysis-api'
|
||||
import '../api/reg-key-api'
|
||||
import '../api/public-api'
|
||||
import '../api/telegram-api'
|
||||
import '../api/oauth-api'
|
||||
import '../api/passkey-api'
|
||||
export default app;
|
||||
|
||||
@@ -1,33 +1,41 @@
|
||||
const en = {
|
||||
passkeyDisabled: 'Passkey sign-in is disabled',
|
||||
passkeyChallengeInvalid: 'The Passkey request expired. Please try again',
|
||||
passkeyVerifyFailed: 'Passkey verification failed',
|
||||
passkeyNotFound: 'No matching Passkey was found',
|
||||
passkeyOriginInvalid: 'Passkeys are unavailable on this site',
|
||||
IncorrectPwd: 'Incorrect password',
|
||||
addAccountDisabled: 'Add account feature is disabled',
|
||||
addAccountDisabled: 'Add Email Address feature is disabled',
|
||||
regDisabled: 'Sign up is disabled',
|
||||
emptyEmail: 'Email cannot be empty',
|
||||
notEmail: 'Invalid email',
|
||||
notExistDomain: 'Email domain does not exist',
|
||||
isDelAccount: 'This Email has been deleted',
|
||||
isRegAccount: 'This Email is already registered',
|
||||
accountLimit: 'Account limit reached',
|
||||
delMyAccount: 'Cannot delete your own account',
|
||||
accountLimit: 'Email address limit reached',
|
||||
delMyAccount: 'Cannot delete your own email',
|
||||
noUserAccount: 'This email does not belong to the current user',
|
||||
usernameLengthLimit: 'Username length exceeds the limit',
|
||||
noOsDomainSendPic: 'Cannot send body images: object storage domain not configured',
|
||||
noOsSendPic: 'Cannot send body images: object storage not configured',
|
||||
noOsDomainSendAtt: 'Cannot send attachments: object storage domain not configured',
|
||||
noOsSendAtt: 'Cannot send attachments: object storage not configured',
|
||||
disabledSend: 'Email sending feature is disabled',
|
||||
noSeparateSend: 'Attachments are not supported in separate sending',
|
||||
daySendLimit: 'Daily send limit reached',
|
||||
totalSendLimit: 'Total send limit reached',
|
||||
daySendLack: 'Not enough remaining sends today',
|
||||
totalSendLack: 'Not enough total remaining sends',
|
||||
senderAccountNotExist: 'Sender email does not exist',
|
||||
noResendToken: 'Resend API token not configured',
|
||||
noSendProvider: 'Email sending service is not configured',
|
||||
sendEmailNotCurUser: 'Sender email does not belong to current user',
|
||||
notExistEmailReply: 'Mail does not exist and cannot be replied to',
|
||||
imageAttLimit: 'The maximum number of image attachments is 10',
|
||||
attLimit: 'The maximum number of attachments is 10.',
|
||||
pwdLengthLimit: 'Password length exceeds the limit',
|
||||
emailLengthLimit: 'Email length exceeds the limit',
|
||||
pwdMinLengthLimit: 'Password must be at least 6 characters',
|
||||
minEmailPrefix: 'Email must be at least {{msg}} characters',
|
||||
banEmailPrefix: 'Invalid characters in email address',
|
||||
pwdMinLength: 'Password must be at least 6 characters',
|
||||
notEmailDomain: 'Invalid email domain',
|
||||
emptyRegKey: 'Invite code cannot be empty',
|
||||
notExistRegKey: 'Invite code does not exist',
|
||||
@@ -53,24 +61,24 @@ const en = {
|
||||
botVerifyFail: 'Bot verification failed, please try again',
|
||||
authExpired: 'Authentication has expired. Please sign in again',
|
||||
unauthorized: 'Unauthorized',
|
||||
bannedSend: 'You are banned from sending emails',
|
||||
initSuccess: 'Successfully initialized',
|
||||
bannedSend: 'You can only send emails to internal mailboxes',
|
||||
onlyInternalSend: 'Your current role allows sending internal emails only',
|
||||
noDomainPermAdd: "No permission to add this domain email",
|
||||
noDomainPermReg: "No permission to register this domain email",
|
||||
noDomainPermRegKey: "Registration code not valid for this domain",
|
||||
noDomainPermSend: "No permission to send from this domain email",
|
||||
JWTMismatch: 'JWT secret mismatch',
|
||||
publicTokenFail: 'Token validation failed',
|
||||
notAdmin: 'The entered email is not an administrator email',
|
||||
emailExistDatabase: 'Email already exists in the database',
|
||||
notConfigOss: 'Object storage not configured',
|
||||
perms: {
|
||||
"邮件": "Email",
|
||||
"邮件": "Emails",
|
||||
"邮件发送": "Send Email",
|
||||
"邮件删除": "Delete Email",
|
||||
"邮箱侧栏": "Account",
|
||||
"邮箱查看": "View Account",
|
||||
"邮箱添加": "Add Account",
|
||||
"邮箱删除": "Delete Account",
|
||||
"邮箱侧栏": "Email Address",
|
||||
"邮箱查看": "View Email",
|
||||
"邮箱添加": "Add Email",
|
||||
"邮箱删除": "Delete Email",
|
||||
"个人设置": "Settings",
|
||||
"用户注销": "Delete User",
|
||||
"分析页": "Analytics",
|
||||
|
||||
@@ -25,6 +25,6 @@ i18next.init({
|
||||
resources,
|
||||
});
|
||||
|
||||
export const t = (key) => i18next.t(key)
|
||||
export const t = (key, values) => i18next.t(key, values)
|
||||
|
||||
export default i18next;
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
const zh = {
|
||||
passkeyDisabled: 'Passkey 登录未启用',
|
||||
passkeyChallengeInvalid: 'Passkey 请求已过期,请重试',
|
||||
passkeyVerifyFailed: 'Passkey 验证失败',
|
||||
passkeyNotFound: '未找到可用的 Passkey',
|
||||
passkeyOriginInvalid: '当前站点无法使用 Passkey',
|
||||
IncorrectPwd: '密码输入错误',
|
||||
addAccountDisabled: '添加邮箱功能已关闭',
|
||||
regDisabled: '注册功能已关闭',
|
||||
@@ -11,23 +16,26 @@ const zh = {
|
||||
delMyAccount: '不可以删除自己的邮箱',
|
||||
noUserAccount: '该邮箱不属于当前用户',
|
||||
usernameLengthLimit: '用户名长度超出限制',
|
||||
noOsDomainSendPic: '对象存储域名未配置不能发送正文图片',
|
||||
noOsSendPic: '对象存储未配置不能发送正文图片',
|
||||
noOsDomainSendAtt: '域名未配置不能发送附件',
|
||||
noOsDomainSendAtt: '对象存储域名未配置不能发送附件',
|
||||
noOsSendAtt: '对象存储未配置不能发送附件',
|
||||
disabledSend: '邮件发送功能已停用',
|
||||
noSeparateSend: '分别发送暂时不支持附件',
|
||||
daySendLimit: '发送次数已到达每日限制',
|
||||
totalSendLimit: '发送次数已到达限制',
|
||||
daySendLack: '当日剩余发送次数不足',
|
||||
totalSendLack: '剩余发送次数不足',
|
||||
senderAccountNotExist: '发件人邮箱不存在',
|
||||
noResendToken: 'resend密钥未配置',
|
||||
noResendToken: 'Resend未配置,只能给站内邮箱发件',
|
||||
noSendProvider: '发信服务未配置,只能给站内邮箱发件',
|
||||
sendEmailNotCurUser: '发件人邮箱非当前用户所有',
|
||||
notExistEmailReply: '邮件不存在无法回复',
|
||||
imageAttLimit: '图片不能超过10个',
|
||||
attLimit: '附件不能超过10个',
|
||||
pwdLengthLimit: '密码长度超出限制',
|
||||
emailLengthLimit: '邮箱长度超出限制',
|
||||
pwdMinLengthLimit: '密码不能小于6位',
|
||||
minEmailPrefix: '邮箱名至少{{msg}}位',
|
||||
banEmailPrefix: '邮箱名包含非法字符',
|
||||
pwdMinLength: '密码至少六位',
|
||||
notEmailDomain: '非法邮箱域名',
|
||||
emptyRegKey: '注册码不能为空',
|
||||
notExistRegKey: '注册码不存在',
|
||||
@@ -53,16 +61,16 @@ const zh = {
|
||||
botVerifyFail: '人机验证失败,请重试',
|
||||
authExpired: '身份认证失效,请重新登录',
|
||||
unauthorized: '权限不足',
|
||||
bannedSend: '你已被禁止发送邮件',
|
||||
initSuccess: '初始化成功',
|
||||
bannedSend: '你没有邮件发送权限',
|
||||
onlyInternalSend: '权限不足,只能给站内邮箱发件',
|
||||
noDomainPermAdd: '你没有权限添加该域名邮箱',
|
||||
noDomainPermReg: '你没有权限注册该域名邮箱',
|
||||
noDomainPermRegKey: '你的注册码没有权限注册该域名邮箱',
|
||||
noDomainPermSend: '你没有权限使用该域名邮箱发送邮件',
|
||||
JWTMismatch: 'jwt_secret 不匹配',
|
||||
publicTokenFail: 'token验证失败',
|
||||
notAdmin: '输入的邮箱不是管理员邮箱',
|
||||
emailExistDatabase: '有邮箱已存在数据库中',
|
||||
notConfigOss: '对象存储未配置',
|
||||
perms: {
|
||||
"邮件": "邮件",
|
||||
"邮件发送": "邮件发送",
|
||||
|
||||
@@ -3,10 +3,13 @@ import { email } from './email/email';
|
||||
import userService from './service/user-service';
|
||||
import verifyRecordService from './service/verify-record-service';
|
||||
import emailService from './service/email-service';
|
||||
import kvObjService from './service/kv-obj-service';
|
||||
import oauthService from "./service/oauth-service";
|
||||
import analysisService from './service/analysis-service';
|
||||
export default {
|
||||
async fetch(req, env, ctx) {
|
||||
const url = new URL(req.url)
|
||||
|
||||
const url = new URL(req.url)
|
||||
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
url.pathname = url.pathname.replace('/api', '')
|
||||
@@ -14,13 +17,23 @@ export default {
|
||||
return app.fetch(req, env, ctx);
|
||||
}
|
||||
|
||||
if (['/static/','/attachments/'].some(p => url.pathname.startsWith(p))) {
|
||||
return await kvObjService.toObjResp( { env }, url.pathname.substring(1));
|
||||
}
|
||||
|
||||
return env.assets.fetch(req);
|
||||
},
|
||||
email: email,
|
||||
async scheduled(c, env, ctx) {
|
||||
await verifyRecordService.clearRecord({env})
|
||||
if (c.cron === '*/30 * * * *') {
|
||||
await analysisService.refreshEchartsCache({ env })
|
||||
return;
|
||||
}
|
||||
|
||||
await verifyRecordService.clearRecord({ env })
|
||||
await userService.resetDaySendCount({ env })
|
||||
await emailService.completeReceiveAll({ env })
|
||||
await oauthService.clearNoBindOathUser({ env })
|
||||
await analysisService.refreshEchartsCache({ env })
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import settingService from '../service/setting-service';
|
||||
import emailUtils from '../utils/email-utils';
|
||||
import {emailConst} from "../const/entity-const";
|
||||
import { t } from '../i18n/i18n'
|
||||
|
||||
const init = {
|
||||
const dbInit = {
|
||||
async init(c) {
|
||||
|
||||
const secret = c.req.param('secret');
|
||||
|
||||
if (secret !== c.env.jwt_secret) {
|
||||
return c.text(t('JWTMismatch'));
|
||||
return c.text('❌ JWT secret mismatch');
|
||||
}
|
||||
|
||||
await this.intDB(c);
|
||||
@@ -22,8 +21,180 @@ const init = {
|
||||
await this.v1_6DB(c);
|
||||
await this.v1_7DB(c);
|
||||
await this.v2DB(c);
|
||||
await this.v2_3DB(c);
|
||||
await this.v2_4DB(c);
|
||||
await this.v2_5DB(c);
|
||||
await this.v2_6DB(c);
|
||||
await this.v2_7DB(c);
|
||||
await this.v2_8DB(c);
|
||||
await this.v2_9DB(c);
|
||||
await this.v3_0DB(c);
|
||||
await this.v3_1DB(c);
|
||||
await settingService.refresh(c);
|
||||
return c.text(t('initSuccess'));
|
||||
return c.text('success');
|
||||
},
|
||||
|
||||
async v3_1DB(c) {
|
||||
try {
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN passkey INTEGER NOT NULL DEFAULT 0;`).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
|
||||
await c.env.db.batch([
|
||||
c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS passkey_credential (
|
||||
passkey_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL UNIQUE,
|
||||
user_handle TEXT NOT NULL UNIQUE,
|
||||
credential_id TEXT NOT NULL UNIQUE,
|
||||
public_key TEXT NOT NULL,
|
||||
counter INTEGER NOT NULL DEFAULT 0,
|
||||
transports TEXT NOT NULL DEFAULT '[]',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`),
|
||||
c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS passkey_challenge (
|
||||
challenge_id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
challenge TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL DEFAULT 0,
|
||||
user_handle TEXT NOT NULL DEFAULT '',
|
||||
origin TEXT NOT NULL,
|
||||
rp_id TEXT NOT NULL,
|
||||
expires_at INTEGER NOT NULL
|
||||
)
|
||||
`),
|
||||
c.env.db.prepare(`CREATE INDEX IF NOT EXISTS idx_passkey_challenge_expires ON passkey_challenge(expires_at)`),
|
||||
]);
|
||||
},
|
||||
|
||||
async v3_0DB(c) {
|
||||
try {
|
||||
await c.env.db.batch([
|
||||
await c.env.db.prepare(`ALTER TABLE email ADD COLUMN code TEXT NOT NULL DEFAULT '';`),
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN ai_code INTEGER NOT NULL DEFAULT 1;`),
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN ai_code_filter TEXT NOT NULL DEFAULT '';`)
|
||||
]);
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await c.env.db.batch([
|
||||
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN black_subject TEXT NOT NULL DEFAULT '';`),
|
||||
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN black_content TEXT NOT NULL DEFAULT '';`),
|
||||
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN black_from TEXT NOT NULL DEFAULT '';`)
|
||||
]);
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async v2_9DB(c) {
|
||||
try {
|
||||
await c.env.db.prepare(`UPDATE setting SET auto_refresh = 5 WHERE auto_refresh = 1;`).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async v2_8DB(c) {
|
||||
try {
|
||||
await c.env.db.batch([
|
||||
c.env.db.prepare(`ALTER TABLE account ADD COLUMN sort INTEGER NOT NULL DEFAULT 0;`)
|
||||
]);
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async v2_7DB(c) {
|
||||
try {
|
||||
await c.env.db.batch([
|
||||
c.env.db.prepare(`ALTER TABLE setting RENAME COLUMN auto_refresh_time TO auto_refresh;`)
|
||||
]);
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async v2_6DB(c) {
|
||||
try {
|
||||
await c.env.db.prepare(`ALTER TABLE account ADD COLUMN all_receive INTEGER NOT NULL DEFAULT 0;`).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async v2_5DB(c) {
|
||||
|
||||
try {
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN email_prefix_filter text NOT NULL DEFAULT '';`).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await c.env.db.batch([
|
||||
c.env.db.prepare(`ALTER TABLE email ADD COLUMN unread INTEGER NOT NULL DEFAULT 0;`),
|
||||
c.env.db.prepare(`UPDATE email SET unread = 1;`)
|
||||
]);
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async v2_4DB(c) {
|
||||
try {
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS oauth (
|
||||
oauth_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
oauth_user_id TEXT,
|
||||
username TEXT,
|
||||
name TEXT,
|
||||
avatar TEXT,
|
||||
active INTEGER,
|
||||
trust_level INTEGER,
|
||||
silenced INTEGER,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
platform INTEGER NOT NULL DEFAULT 0,
|
||||
user_id INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
`).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN min_email_prefix INTEGER NOT NULL DEFAULT 1;`).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async v2_3DB(c) {
|
||||
try {
|
||||
await c.env.db.batch([
|
||||
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN force_path_style INTEGER NOT NULL DEFAULT 1;`),
|
||||
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN custom_domain TEXT NOT NULL DEFAULT '';`),
|
||||
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN tg_msg_to TEXT NOT NULL DEFAULT 'show';`),
|
||||
c.env.db.prepare(`ALTER TABLE setting ADD COLUMN tg_msg_from TEXT NOT NULL DEFAULT 'only-name';`)
|
||||
]);
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN tg_msg_text TEXT NOT NULL DEFAULT 'show';`).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async v2DB(c) {
|
||||
@@ -37,7 +208,7 @@ const init = {
|
||||
c.env.db.prepare(`DELETE FROM perm WHERE perm_key = 'setting:clean'`)
|
||||
]);
|
||||
} catch (e) {
|
||||
console.error(e.message)
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -45,7 +216,7 @@ const init = {
|
||||
try {
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN login_domain INTEGER NOT NULL DEFAULT 0;`).run();
|
||||
} catch (e) {
|
||||
console.error(e.message)
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -53,15 +224,7 @@ const init = {
|
||||
|
||||
const noticeContent = '本项目仅供学习交流,禁止用于违法业务\n' +
|
||||
'<br>\n' +
|
||||
'请遵守当地法规,作者不承担任何法律责任\n' +
|
||||
'<div style="display: flex;gap: 18px;margin-top: 10px;">\n' +
|
||||
'<a href="https://github.com/eoao/cloud-mail" target="_blank" >\n' +
|
||||
'<img src="https://api.iconify.design/codicon:github-inverted.svg" alt="GitHub" width="25" height="25" />\n' +
|
||||
'</a>\n' +
|
||||
'<a href="https://t.me/cloud_mail_tg" target="_blank" >\n' +
|
||||
'<img src="https://api.iconify.design/logos:telegram.svg" alt="GitHub" width="25" height="25" />\n' +
|
||||
'</a>\n' +
|
||||
'</div>\n'
|
||||
'请遵守当地法规,作者不承担任何法律责任'
|
||||
|
||||
const ADD_COLUMN_SQL_LIST = [
|
||||
`ALTER TABLE setting ADD COLUMN reg_verify_count INTEGER NOT NULL DEFAULT 1;`,
|
||||
@@ -83,7 +246,6 @@ const init = {
|
||||
`ALTER TABLE setting ADD COLUMN notice INTEGER NOT NULL DEFAULT 0;`,
|
||||
`ALTER TABLE setting ADD COLUMN no_recipient INTEGER NOT NULL DEFAULT 1;`,
|
||||
`UPDATE role SET avail_domain = '' WHERE role.avail_domain LIKE '@%';`,
|
||||
`UPDATE role SET ban_email = '';`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_email_user_id_account_id ON email(user_id, account_id);`
|
||||
];
|
||||
|
||||
@@ -91,7 +253,7 @@ const init = {
|
||||
try {
|
||||
await c.env.db.prepare(sql).run();
|
||||
} catch (e) {
|
||||
console.warn(`通过字段,原因:${e.message}`);
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -105,7 +267,7 @@ const init = {
|
||||
c.env.db.prepare(`CREATE UNIQUE INDEX IF NOT EXISTS idx_user_email_nocase ON user (email COLLATE NOCASE)`)
|
||||
]);
|
||||
} catch (e) {
|
||||
console.error(e.message)
|
||||
console.warn(e.message)
|
||||
}
|
||||
|
||||
},
|
||||
@@ -116,7 +278,7 @@ const init = {
|
||||
try {
|
||||
await c.env.db.prepare(`ALTER TABLE role ADD COLUMN avail_domain TEXT NOT NULL DEFAULT ''`).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段添加,原因:${e.message}`);
|
||||
console.warn(`跳过字段添加:${e.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -139,7 +301,7 @@ const init = {
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_setting_code ON reg_key(code COLLATE NOCASE)
|
||||
`).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过创建索引,原因:${e.message}`);
|
||||
console.warn(`跳过创建索引:${e.message}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +313,7 @@ const init = {
|
||||
(35,'密钥添加', 'reg-key:add', 33, 2, 1),
|
||||
(36,'密钥删除', 'reg-key:delete', 33, 2, 2)`).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过数据,原因:${e.message}`);
|
||||
console.warn(`跳过数据:${e.message}`);
|
||||
}
|
||||
|
||||
const ADD_COLUMN_SQL_LIST = [
|
||||
@@ -165,7 +327,7 @@ const init = {
|
||||
try {
|
||||
await c.env.db.prepare(sql).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段添加,原因:${e.message}`);
|
||||
console.warn(`跳过字段添加:${e.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -193,7 +355,7 @@ const init = {
|
||||
try {
|
||||
await c.env.db.prepare(sql).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段添加,原因:${e.message}`);
|
||||
console.warn(`跳过字段添加:${e.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -230,7 +392,7 @@ const init = {
|
||||
try {
|
||||
await c.env.db.prepare(sql).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段添加,原因:${e.message}`);
|
||||
console.warn(`跳过字段添加:${e.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -245,7 +407,7 @@ const init = {
|
||||
(31,'分析页', NULL, 0, 1, 2.1),
|
||||
(32,'数据查看', 'analysis:query', 31, 2, 1)`).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过数据,原因:${e.message}`);
|
||||
console.warn(`跳过数据:${e.message}`);
|
||||
}
|
||||
|
||||
},
|
||||
@@ -264,7 +426,7 @@ const init = {
|
||||
`ALTER TABLE setting ADD COLUMN site_key TEXT;`,
|
||||
`ALTER TABLE setting ADD COLUMN secret_key TEXT;`,
|
||||
`ALTER TABLE setting ADD COLUMN background TEXT;`,
|
||||
`ALTER TABLE setting ADD COLUMN login_opacity INTEGER NOT NULL DEFAULT 0.88;`,
|
||||
`ALTER TABLE setting ADD COLUMN login_opacity INTEGER NOT NULL DEFAULT 0.90;`,
|
||||
|
||||
`ALTER TABLE user ADD COLUMN create_ip TEXT;`,
|
||||
`ALTER TABLE user ADD COLUMN active_ip TEXT;`,
|
||||
@@ -282,7 +444,7 @@ const init = {
|
||||
try {
|
||||
await c.env.db.prepare(sql).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段添加,原因:${e.message}`);
|
||||
console.warn(`跳过字段添加:${e.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -396,93 +558,98 @@ const init = {
|
||||
async intDB(c) {
|
||||
// 初始化数据库表结构
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS email (
|
||||
email_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
send_email TEXT,
|
||||
name TEXT,
|
||||
account_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
subject TEXT,
|
||||
content TEXT,
|
||||
text TEXT,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
is_del INTEGER DEFAULT 0 NOT NULL
|
||||
)
|
||||
`).run();
|
||||
CREATE TABLE IF NOT EXISTS email (
|
||||
email_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
send_email TEXT,
|
||||
name TEXT,
|
||||
account_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
subject TEXT,
|
||||
content TEXT,
|
||||
text TEXT,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
is_del INTEGER DEFAULT 0 NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS star (
|
||||
star_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
email_id INTEGER NOT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
)
|
||||
`).run();
|
||||
CREATE TABLE IF NOT EXISTS star (
|
||||
star_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
email_id INTEGER NOT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS attachments (
|
||||
att_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
email_id INTEGER NOT NULL,
|
||||
account_id INTEGER NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
filename TEXT,
|
||||
mime_type TEXT,
|
||||
size INTEGER,
|
||||
disposition TEXT,
|
||||
related TEXT,
|
||||
content_id TEXT,
|
||||
encoding TEXT,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
)
|
||||
`).run();
|
||||
CREATE TABLE IF NOT EXISTS attachments (
|
||||
att_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
email_id INTEGER NOT NULL,
|
||||
account_id INTEGER NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
filename TEXT,
|
||||
mime_type TEXT,
|
||||
size INTEGER,
|
||||
disposition TEXT,
|
||||
related TEXT,
|
||||
content_id TEXT,
|
||||
encoding TEXT,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL,
|
||||
type INTEGER DEFAULT 1 NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
salt TEXT NOT NULL,
|
||||
status INTEGER DEFAULT 0 NOT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
active_time DATETIME,
|
||||
is_del INTEGER DEFAULT 0 NOT NULL
|
||||
)
|
||||
`).run();
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL,
|
||||
type INTEGER DEFAULT 1 NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
salt TEXT NOT NULL,
|
||||
status INTEGER DEFAULT 0 NOT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
active_time DATETIME,
|
||||
is_del INTEGER DEFAULT 0 NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS account (
|
||||
account_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL,
|
||||
status INTEGER DEFAULT 0 NOT NULL,
|
||||
latest_email_time DATETIME,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
user_id INTEGER NOT NULL,
|
||||
is_del INTEGER DEFAULT 0 NOT NULL
|
||||
)
|
||||
`).run();
|
||||
CREATE TABLE IF NOT EXISTS account (
|
||||
account_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL,
|
||||
status INTEGER DEFAULT 0 NOT NULL,
|
||||
latest_email_time DATETIME,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
user_id INTEGER NOT NULL,
|
||||
is_del INTEGER DEFAULT 0 NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS setting (
|
||||
register INTEGER NOT NULL,
|
||||
receive INTEGER NOT NULL,
|
||||
add_email INTEGER NOT NULL,
|
||||
many_email INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
auto_refresh_time INTEGER NOT NULL,
|
||||
register_verify INTEGER NOT NULL,
|
||||
add_email_verify INTEGER NOT NULL
|
||||
)
|
||||
`).run();
|
||||
CREATE TABLE IF NOT EXISTS setting (
|
||||
register INTEGER NOT NULL,
|
||||
receive INTEGER NOT NULL,
|
||||
add_email INTEGER NOT NULL,
|
||||
many_email INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
auto_refresh INTEGER NOT NULL,
|
||||
register_verify INTEGER NOT NULL,
|
||||
add_email_verify INTEGER NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
try {
|
||||
await c.env.db.prepare(`
|
||||
INSERT INTO setting (
|
||||
register, receive, add_email, many_email, title, auto_refresh, register_verify, add_email_verify
|
||||
)
|
||||
SELECT 0, 0, 0, 0, 'Cloud Mail', 0, 1, 1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM setting)
|
||||
`).run();
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
|
||||
await c.env.db.prepare(`
|
||||
INSERT INTO setting (
|
||||
register, receive, add_email, many_email, title, auto_refresh_time, register_verify, add_email_verify
|
||||
)
|
||||
SELECT 0, 0, 0, 1, 'Cloud Mail', 0, 1, 1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM setting)
|
||||
`).run();
|
||||
},
|
||||
|
||||
async receiveEmailToRecipient(c) {
|
||||
@@ -533,4 +700,4 @@ const init = {
|
||||
await c.env.db.batch(queryList);
|
||||
}
|
||||
};
|
||||
export default init;
|
||||
export { dbInit };
|
||||
|
||||
@@ -11,11 +11,15 @@ import app from '../hono/hono';
|
||||
const exclude = [
|
||||
'/login',
|
||||
'/register',
|
||||
'/file',
|
||||
'/oss',
|
||||
'/setting/websiteConfig',
|
||||
'/webhooks',
|
||||
'/init',
|
||||
'/public/genToken'
|
||||
'/public/genToken',
|
||||
'/telegram',
|
||||
'/test',
|
||||
'/oauth',
|
||||
'/passkey/auth/'
|
||||
];
|
||||
|
||||
const requirePerms = [
|
||||
@@ -25,6 +29,7 @@ const requirePerms = [
|
||||
'/account/delete',
|
||||
'/account/add',
|
||||
'/my/delete',
|
||||
'/analysis/echarts',
|
||||
'/role/add',
|
||||
'/role/list',
|
||||
'/role/delete',
|
||||
@@ -33,14 +38,19 @@ const requirePerms = [
|
||||
'/role/setDefault',
|
||||
'/allEmail/list',
|
||||
'/allEmail/delete',
|
||||
'/allEmail/batchDelete',
|
||||
'/allEmail/latest',
|
||||
'/setting/setBackground',
|
||||
'/setting/deleteBackground',
|
||||
'/setting/set',
|
||||
'/setting/query',
|
||||
'/setting/setBlacklist',
|
||||
'/user/delete',
|
||||
'/user/setPwd',
|
||||
'/user/setStatus',
|
||||
'/user/setType',
|
||||
'/user/list',
|
||||
'/user/restore',
|
||||
'/user/resetSendCount',
|
||||
'/user/add',
|
||||
'/user/deleteAccount',
|
||||
@@ -67,13 +77,13 @@ const premKey = {
|
||||
'user:add': ['/user/add'],
|
||||
'user:reset-send': ['/user/resetSendCount'],
|
||||
'user:set-pwd': ['/user/setPwd'],
|
||||
'user:set-status': ['/user/setStatus'],
|
||||
'user:set-status': ['/user/setStatus', '/user/restore'],
|
||||
'user:set-type': ['/user/setType'],
|
||||
'user:delete': ['/user/delete','/user/deleteAccount'],
|
||||
'all-email:query': ['/allEmail/list'],
|
||||
'all-email:query': ['/allEmail/list','/allEmail/latest'],
|
||||
'all-email:delete': ['/allEmail/delete','/allEmail/batchDelete'],
|
||||
'setting:query': ['/setting/query'],
|
||||
'setting:set': ['/setting/set', '/setting/setBackground'],
|
||||
'setting:set': ['/setting/set', '/setting/setBackground','/setting/deleteBackground','/setting/setBlacklist'],
|
||||
'analysis:query': ['/analysis/echarts'],
|
||||
'reg-key:add': ['/regKey/add'],
|
||||
'reg-key:query': ['/regKey/list','/regKey/history'],
|
||||
@@ -84,10 +94,6 @@ app.use('*', async (c, next) => {
|
||||
|
||||
const path = c.req.path;
|
||||
|
||||
if (path.startsWith('/test')) {
|
||||
return await next();
|
||||
}
|
||||
|
||||
const index = exclude.findIndex(item => {
|
||||
return path.startsWith(item);
|
||||
});
|
||||
@@ -161,7 +167,9 @@ app.use('*', async (c, next) => {
|
||||
});
|
||||
|
||||
function permKeyToPaths(permKeys) {
|
||||
|
||||
const paths = [];
|
||||
|
||||
for (const key of permKeys) {
|
||||
const routeList = premKey[key];
|
||||
if (routeList && Array.isArray(routeList)) {
|
||||
|
||||
@@ -12,8 +12,8 @@ const userContext = {
|
||||
|
||||
async getToken(c) {
|
||||
const jwt = c.req.header(constant.TOKEN_HEADER);
|
||||
const { token } = JwtUtils.verifyToken(c,jwt);
|
||||
return token;
|
||||
const result = await JwtUtils.verifyToken(c,jwt);
|
||||
return result?.token;
|
||||
},
|
||||
};
|
||||
export default userContext;
|
||||
|
||||
@@ -5,8 +5,8 @@ import userService from './user-service';
|
||||
import emailService from './email-service';
|
||||
import orm from '../entity/orm';
|
||||
import account from '../entity/account';
|
||||
import { and, asc, eq, gt, inArray, count, sql, ne } from 'drizzle-orm';
|
||||
import { isDel, settingConst } from '../const/entity-const';
|
||||
import { and, asc, eq, gt, inArray, count, sql, ne, or, lt, desc } from 'drizzle-orm';
|
||||
import {accountConst, isDel, settingConst} from '../const/entity-const';
|
||||
import settingService from './setting-service';
|
||||
import turnstileService from './turnstile-service';
|
||||
import roleService from './role-service';
|
||||
@@ -17,7 +17,7 @@ const accountService = {
|
||||
|
||||
async add(c, params, userId) {
|
||||
|
||||
const {addEmailVerify , addEmail, manyEmail, addVerifyCount} = await settingService.query(c);
|
||||
const { addEmailVerify , addEmail, manyEmail, addVerifyCount, minEmailPrefix, emailPrefixFilter } = await settingService.query(c);
|
||||
|
||||
let { email, token } = params;
|
||||
|
||||
@@ -39,6 +39,13 @@ const accountService = {
|
||||
throw new BizError(t('notExistDomain'));
|
||||
}
|
||||
|
||||
if (emailUtils.getName(email).length < minEmailPrefix) {
|
||||
throw new BizError(t('minEmailPrefix', { msg: minEmailPrefix } ));
|
||||
}
|
||||
|
||||
if (emailPrefixFilter.some(content => emailUtils.getName(email).includes(content))) {
|
||||
throw new BizError(t('banEmailPrefix'));
|
||||
}
|
||||
|
||||
let accountRow = await this.selectByEmailIncludeDel(c, email);
|
||||
|
||||
@@ -98,10 +105,11 @@ const accountService = {
|
||||
|
||||
list(c, params, userId) {
|
||||
|
||||
let { accountId, size } = params;
|
||||
let { accountId, size, lastSort } = params;
|
||||
|
||||
accountId = Number(accountId);
|
||||
size = Number(size);
|
||||
lastSort = Number(lastSort);
|
||||
|
||||
if (size > 30) {
|
||||
size = 30;
|
||||
@@ -110,12 +118,24 @@ const accountService = {
|
||||
if (!accountId) {
|
||||
accountId = 0;
|
||||
}
|
||||
|
||||
if(Number.isNaN(lastSort)) {
|
||||
lastSort = 9999999999;
|
||||
}
|
||||
|
||||
return orm(c).select().from(account).where(
|
||||
and(
|
||||
eq(account.userId, userId),
|
||||
eq(account.isDel, isDel.NORMAL),
|
||||
gt(account.accountId, accountId)))
|
||||
.orderBy(asc(account.accountId))
|
||||
or(
|
||||
lt(account.sort, lastSort),
|
||||
and(
|
||||
eq(account.sort, lastSort),
|
||||
gt(account.accountId, accountId)
|
||||
)
|
||||
))
|
||||
)
|
||||
.orderBy(desc(account.sort), asc(account.accountId))
|
||||
.limit(size)
|
||||
.all();
|
||||
},
|
||||
@@ -224,8 +244,27 @@ const accountService = {
|
||||
const { accountId } = params
|
||||
await emailService.physicsDeleteByAccountId(c, accountId)
|
||||
await orm(c).delete(account).where(eq(account.accountId, accountId)).run();
|
||||
}
|
||||
},
|
||||
|
||||
async setAllReceive(c, params, userId) {
|
||||
let a = null
|
||||
const { accountId } = params;
|
||||
const accountRow = await this.selectById(c, accountId);
|
||||
if (accountRow.userId !== userId) {
|
||||
return;
|
||||
}
|
||||
await orm(c).update(account).set({ allReceive: accountConst.allReceive.CLOSE }).where(eq(account.userId, userId)).run();
|
||||
await orm(c).update(account).set({ allReceive: accountRow.allReceive ? 0 : 1 }).where(eq(account.accountId, accountId)).run();
|
||||
},
|
||||
|
||||
async setAsTop(c, params, userId) {
|
||||
const { accountId } = params;
|
||||
const userRow = await userService.selectById(c, userId);
|
||||
const mainAccountRow = await accountService.selectByEmailIncludeDel(c, userRow.email);
|
||||
let mainSort = mainAccountRow.sort === 0 ? 2 : mainAccountRow.sort + 1;
|
||||
await orm(c).update(account).set({ sort: mainSort }).where(eq(account.email, userRow.email )).run();
|
||||
await orm(c).update(account).set({ sort: mainSort - 1 }).where(and(eq(account.accountId, accountId),eq(account.userId,userId))).run();
|
||||
}
|
||||
};
|
||||
|
||||
export default accountService;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import emailUtils from '../utils/email-utils';
|
||||
import { settingConst } from '../const/entity-const';
|
||||
|
||||
const aiService = {
|
||||
async extractCode(c, email, options = {}) {
|
||||
if (!this.shouldExtractCode(options.aiCode, options.aiCodeFilter, email)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const ai = c.env.ai;
|
||||
|
||||
try {
|
||||
const subject = email.subject || '';
|
||||
const text = emailUtils.formatText(email.text || '');
|
||||
const htmlText = emailUtils.htmlToText(email.html || '');
|
||||
const body = (htmlText || text).slice(0, 6000);
|
||||
|
||||
if (!subject && !body) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const result = await ai.run(c.env.ai_model || '@cf/meta/llama-3.1-8b-instruct', {
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: 'You extract verification codes from emails. Return only JSON like {"code":"12345678"} or {"code":""}. The code must be 8 characters or fewer and must not contain spaces. If the code is longer than 8 characters or contains spaces, return {"code":""}. Do not explain.'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Subject: ${subject}\n\n${body}`
|
||||
}
|
||||
],
|
||||
temperature: 0,
|
||||
max_tokens: 32
|
||||
});
|
||||
|
||||
const content = typeof result === 'string' ? result : result?.response || '';
|
||||
const json = JSON.parse(content);
|
||||
if (typeof json.code !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (json.code.length > 8 || /\s/.test(json.code)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return json.code;
|
||||
} catch (e) {
|
||||
console.error('验证码提取失败: ', e);
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
shouldExtractCode(aiCode, aiCodeFilterStr, email) {
|
||||
if (aiCode !== settingConst.aiCode.OPEN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const filterList = aiCodeFilterStr ? aiCodeFilterStr.split(',').map(item => item.trim().toLowerCase()).filter(Boolean) : [];
|
||||
|
||||
if (filterList.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const fromEmail = (email.from?.address || '').trim().toLowerCase();
|
||||
const fromDomain = emailUtils.getDomain(fromEmail).toLowerCase();
|
||||
|
||||
return filterList.some(item => item === fromEmail || item === fromDomain);
|
||||
}
|
||||
};
|
||||
|
||||
export default aiService;
|
||||
@@ -9,7 +9,38 @@ import { toUtc } from '../utils/date-uitil';
|
||||
const analysisService = {
|
||||
|
||||
async echarts(c, params) {
|
||||
if (!this.analysisCacheEnabled(c)) {
|
||||
return await this.queryEcharts(c, params);
|
||||
}
|
||||
|
||||
const cacheKey = this.echartsCacheKey(params);
|
||||
const cache = await c.env.kv.get(cacheKey, { type: 'json' });
|
||||
|
||||
if (cache) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
return await this.refreshEchartsCacheByKey(c, cacheKey);
|
||||
},
|
||||
|
||||
async refreshEchartsCacheByKey(c, cacheKey) {
|
||||
const params = this.echartsParamsByCacheKey(cacheKey);
|
||||
const data = await this.queryEcharts(c, params);
|
||||
await c.env.kv.put(cacheKey, JSON.stringify(data));
|
||||
return data;
|
||||
},
|
||||
|
||||
async refreshEchartsCache(c) {
|
||||
if (!this.analysisCacheEnabled(c)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { keys } = await c.env.kv.list({ prefix: kvConst.ANALYSIS_ECHARTS });
|
||||
|
||||
await Promise.all(keys.map(key => this.refreshEchartsCacheByKey(c, key.name)));
|
||||
},
|
||||
|
||||
async queryEcharts(c, params) {
|
||||
|
||||
const { timeZone } = params;
|
||||
|
||||
@@ -84,6 +115,20 @@ const analysisService = {
|
||||
return {date: day,total}
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
echartsCacheKey(params = {}) {
|
||||
return kvConst.ANALYSIS_ECHARTS + encodeURIComponent(params.timeZone || 'UTC');
|
||||
},
|
||||
|
||||
echartsParamsByCacheKey(cacheKey) {
|
||||
return {
|
||||
timeZone: decodeURIComponent(cacheKey.replace(kvConst.ANALYSIS_ECHARTS, ''))
|
||||
};
|
||||
},
|
||||
|
||||
analysisCacheEnabled(c) {
|
||||
return c.env.analysis_cache === true || c.env.analysis_cache === 'true';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import orm from '../entity/orm';
|
||||
import { att } from '../entity/att';
|
||||
import { and, eq, isNull, inArray } from 'drizzle-orm';
|
||||
import { and, eq, isNull, inArray, desc } from 'drizzle-orm';
|
||||
import r2Service from './r2-service';
|
||||
import constant from '../const/constant';
|
||||
import fileUtils from '../utils/file-utils';
|
||||
import { attConst } from '../const/entity-const';
|
||||
import { parseHTML } from 'linkedom';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import domainUtils from '../utils/domain-uitls';
|
||||
import BizError from '../error/biz-error';
|
||||
import settingService from "./setting-service";
|
||||
|
||||
const attService = {
|
||||
|
||||
@@ -46,22 +47,27 @@ const attService = {
|
||||
).all();
|
||||
},
|
||||
|
||||
async toImageUrlHtml(c, content, r2Domain) {
|
||||
async toImageUrlHtml(c, content) {
|
||||
|
||||
const { r2Domain } = await settingService.query(c);
|
||||
|
||||
const { document } = parseHTML(content);
|
||||
|
||||
const images = Array.from(document.querySelectorAll('img'));
|
||||
|
||||
const attDataList = [];
|
||||
let imageDataList = [];
|
||||
|
||||
for (const img of images) {
|
||||
|
||||
//邮件正文base64图片转cid附件
|
||||
const src = img.getAttribute('src');
|
||||
if (src && src.startsWith('data:image')) {
|
||||
const file = fileUtils.base64ToFile(src);
|
||||
const buff = await file.arrayBuffer();
|
||||
const cid = uuidv4().replace(/-/g, '');
|
||||
const key = constant.ATTACHMENT_PREFIX + await fileUtils.getBuffHash(buff) + fileUtils.getExtFileName(file.name);
|
||||
img.setAttribute('src', domainUtils.toOssDomain(r2Domain) + '/' + key);
|
||||
|
||||
img.setAttribute('src', 'cid:' + cid);
|
||||
|
||||
const attData = {};
|
||||
attData.key = key;
|
||||
@@ -69,8 +75,32 @@ const attService = {
|
||||
attData.mimeType = file.type;
|
||||
attData.size = file.size;
|
||||
attData.buff = buff;
|
||||
attData.content = fileUtils.base64ToDataStr(src);
|
||||
attData.contentId = cid;
|
||||
|
||||
imageDataList.push(attData);
|
||||
}
|
||||
|
||||
//邮件正文站内图片转cid附件
|
||||
if (src && (src.startsWith(domainUtils.toOssDomain(r2Domain)) || src.startsWith('attachments/'))) {
|
||||
|
||||
const cid = uuidv4().replace(/-/g, '')
|
||||
img.setAttribute('src', 'cid:' + cid);
|
||||
|
||||
const attData = {};
|
||||
|
||||
if (src.startsWith(domainUtils.toOssDomain(r2Domain))) {
|
||||
attData.key = src.replace(domainUtils.toOssDomain(r2Domain) + '/','');
|
||||
}
|
||||
|
||||
if (src.startsWith('attachments/')) {
|
||||
attData.key = src;
|
||||
}
|
||||
|
||||
attData.contentId = cid;
|
||||
attData.type = attConst.type.EMBED;
|
||||
imageDataList.push(attData);
|
||||
|
||||
attDataList.push(attData);
|
||||
}
|
||||
|
||||
const hasInlineWidth = img.hasAttribute('width');
|
||||
@@ -82,7 +112,38 @@ const attService = {
|
||||
img.setAttribute('style', newStyle);
|
||||
}
|
||||
}
|
||||
return { attDataList, html: document.toString() };
|
||||
|
||||
//查询已有内嵌url图片信息
|
||||
const keys = [...new Set(imageDataList.filter(item => !item.content).map(item => item.key))];
|
||||
const dbImageList = await this.selectOneByKeys(c, keys);
|
||||
|
||||
//设置给当前附件
|
||||
await Promise.all(imageDataList.map(async image => {
|
||||
if (image.content) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dbImage = dbImageList.find(dbImage => image.key === dbImage.key);
|
||||
if (!dbImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
image.size = dbImage.size;
|
||||
image.filename = dbImage.filename;
|
||||
image.mimeType = dbImage.mimeType;
|
||||
image.contentType = dbImage.mimeType;
|
||||
|
||||
const obj = await r2Service.getObj(c, image.key);
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
|
||||
image.content = obj instanceof ArrayBuffer ? obj : await obj.arrayBuffer();
|
||||
}))
|
||||
|
||||
imageDataList = imageDataList.filter(image => image.content);
|
||||
|
||||
return { imageDataList, html: document.toString() };
|
||||
},
|
||||
|
||||
async saveSendAtt(c, attList, userId, accountId, emailId) {
|
||||
@@ -119,11 +180,15 @@ const attService = {
|
||||
attData.emailId = emailId;
|
||||
attData.accountId = accountId;
|
||||
attData.type = attConst.type.EMBED;
|
||||
if (!attData.buff) {
|
||||
continue;
|
||||
}
|
||||
await r2Service.putObj(c, attData.key, attData.buff, {
|
||||
contentType: attData.mimeType,
|
||||
cacheControl: `max-age=259200`,
|
||||
contentDisposition: `inline;filename=${attData.filename}`
|
||||
});
|
||||
delete attData.buff;
|
||||
}
|
||||
|
||||
await orm(c).insert(att).values(attDataList).run();
|
||||
@@ -194,8 +259,14 @@ const attService = {
|
||||
},
|
||||
|
||||
async removeByAccountId(c, accountId) {
|
||||
console.log(accountId)
|
||||
await this.removeAttByField(c, "account_id", [accountId])
|
||||
},
|
||||
|
||||
selectOneByKeys(c, keys) {
|
||||
if (!keys || keys.length === 0) {
|
||||
return []
|
||||
}
|
||||
return orm(c).select().from(att).where(inArray(att.key, keys)).orderBy(desc(att.attId)).groupBy(att.key).all();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import settingService from './setting-service';
|
||||
import accountService from './account-service';
|
||||
import BizError from '../error/biz-error';
|
||||
import emailUtils from '../utils/email-utils';
|
||||
import fileUtils from '../utils/file-utils';
|
||||
import { Resend } from 'resend';
|
||||
import attService from './att-service';
|
||||
import { parseHTML } from 'linkedom';
|
||||
@@ -17,21 +18,25 @@ import starService from './star-service';
|
||||
import dayjs from 'dayjs';
|
||||
import kvConst from '../const/kv-const';
|
||||
import { t } from '../i18n/i18n'
|
||||
import r2Service from './r2-service';
|
||||
import domainUtils from '../utils/domain-uitls';
|
||||
import account from "../entity/account";
|
||||
import { att } from '../entity/att';
|
||||
import telegramService from './telegram-service';
|
||||
|
||||
const emailService = {
|
||||
|
||||
async list(c, params, userId) {
|
||||
|
||||
let { emailId, type, accountId, size, timeSort } = params;
|
||||
let { emailId, type, accountId, size, timeSort, allReceive } = params;
|
||||
|
||||
size = Number(size);
|
||||
emailId = Number(emailId);
|
||||
timeSort = Number(timeSort);
|
||||
accountId = Number(accountId);
|
||||
allReceive = Number(allReceive);
|
||||
|
||||
if (size > 30) {
|
||||
size = 30;
|
||||
if (size > 50) {
|
||||
size = 50;
|
||||
}
|
||||
|
||||
if (!emailId) {
|
||||
@@ -44,6 +49,10 @@ const emailService = {
|
||||
|
||||
}
|
||||
|
||||
if (isNaN(allReceive)) {
|
||||
let accountRow = await accountService.selectById(c, accountId);
|
||||
allReceive = accountRow.allReceive;
|
||||
}
|
||||
|
||||
const query = orm(c)
|
||||
.select({
|
||||
@@ -57,14 +66,18 @@ const emailService = {
|
||||
eq(star.emailId, email.emailId),
|
||||
eq(star.userId, userId)
|
||||
)
|
||||
).leftJoin(
|
||||
account,
|
||||
eq(account.accountId, email.accountId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
allReceive ? eq(1,1) : eq(email.accountId, accountId),
|
||||
eq(email.userId, userId),
|
||||
eq(email.accountId, accountId),
|
||||
timeSort ? gt(email.emailId, emailId) : lt(email.emailId, emailId),
|
||||
eq(email.type, type),
|
||||
eq(email.isDel, isDel.NORMAL)
|
||||
eq(email.isDel, isDel.NORMAL),
|
||||
eq(account.isDel, isDel.NORMAL)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -76,18 +89,24 @@ const emailService = {
|
||||
|
||||
const listQuery = query.limit(size).all();
|
||||
|
||||
const totalQuery = orm(c).select({ total: count() }).from(email).where(
|
||||
and(
|
||||
eq(email.accountId, accountId),
|
||||
eq(email.userId, userId),
|
||||
eq(email.type, type),
|
||||
eq(email.isDel, isDel.NORMAL)
|
||||
const totalQuery = orm(c).select({ total: count() }).from(email)
|
||||
.leftJoin(
|
||||
account,
|
||||
eq(account.accountId, email.accountId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
allReceive ? eq(1,1) : eq(email.accountId, accountId),
|
||||
eq(email.userId, userId),
|
||||
eq(email.type, type),
|
||||
eq(email.isDel, isDel.NORMAL),
|
||||
eq(account.isDel, isDel.NORMAL)
|
||||
)
|
||||
).get();
|
||||
|
||||
const latestEmailQuery = orm(c).select().from(email).where(
|
||||
and(
|
||||
eq(email.accountId, accountId),
|
||||
allReceive ? eq(1,1) : eq(email.accountId, accountId),
|
||||
eq(email.userId, userId),
|
||||
eq(email.type, type),
|
||||
eq(email.isDel, isDel.NORMAL)
|
||||
@@ -101,14 +120,16 @@ const emailService = {
|
||||
isStar: item.starId != null ? 1 : 0
|
||||
}));
|
||||
|
||||
const emailIds = list.map(item => item.emailId);
|
||||
|
||||
const attsList = await attService.selectByEmailIds(c, emailIds);
|
||||
await this.emailAddAtt(c, list);
|
||||
|
||||
list.forEach(emailRow => {
|
||||
const atts = attsList.filter(attsRow => attsRow.emailId === emailRow.emailId);
|
||||
emailRow.attList = atts;
|
||||
});
|
||||
if (!latestEmail) {
|
||||
latestEmail = {
|
||||
emailId: 0,
|
||||
accountId: accountId,
|
||||
userId: userId,
|
||||
}
|
||||
}
|
||||
|
||||
return { list, total: totalRow.total, latestEmail };
|
||||
},
|
||||
@@ -128,25 +149,26 @@ const emailService = {
|
||||
return orm(c).insert(email).values({ ...params }).returning().get();
|
||||
},
|
||||
|
||||
//邮件发送
|
||||
async send(c, params, userId) {
|
||||
|
||||
let {
|
||||
accountId,
|
||||
name,
|
||||
sendType,
|
||||
emailId,
|
||||
receiveEmail,
|
||||
manyType,
|
||||
text,
|
||||
content,
|
||||
subject,
|
||||
attachments
|
||||
accountId, //发送账号id
|
||||
name, //发件人名字
|
||||
sendType, //发件类型
|
||||
emailId, //邮件id,如果是回复邮件会带
|
||||
receiveEmail, //收件人邮箱
|
||||
text, //邮件纯文本
|
||||
content, //邮件内容
|
||||
subject, //邮件标题
|
||||
attachments = [] //附件
|
||||
} = params;
|
||||
|
||||
const { resendTokens, r2Domain, send } = await settingService.query(c);
|
||||
const { resendTokens, r2Domain, send, domainList } = await settingService.query(c);
|
||||
|
||||
let { attDataList, html } = await attService.toImageUrlHtml(c, content, r2Domain);
|
||||
let { imageDataList, html } = await attService.toImageUrlHtml(c, content);
|
||||
|
||||
//判断是否关闭发件功能
|
||||
if (send === settingConst.send.CLOSE) {
|
||||
throw new BizError(t('disabledSend'), 403);
|
||||
}
|
||||
@@ -154,10 +176,27 @@ const emailService = {
|
||||
const userRow = await userService.selectById(c, userId);
|
||||
const roleRow = await roleService.selectById(c, userRow.type);
|
||||
|
||||
if (c.env.admin !== userRow.email && roleRow.sendType === 'ban') {
|
||||
throw new BizError(t('bannedSend'), 403);
|
||||
//判断接收方是不是全部为站内邮箱
|
||||
const allInternal = receiveEmail.every(email => {
|
||||
const domain = '@' + emailUtils.getDomain(email);
|
||||
return domainList.includes(domain);
|
||||
});
|
||||
|
||||
if (c.env.admin !== userRow.email) {
|
||||
|
||||
//发件被禁用
|
||||
if (roleRow.sendType === 'ban') {
|
||||
throw new BizError(t('bannedSend'), 403);
|
||||
}
|
||||
|
||||
//发件被禁用
|
||||
if (roleRow.sendType === 'internal' && !allInternal) {
|
||||
throw new BizError(t('onlyInternalSend'), 403);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//如果不是管理员,权限设置了发送次数
|
||||
if (c.env.admin !== userRow.email && roleRow.sendCount) {
|
||||
|
||||
if (userRow.sendCount >= roleRow.sendCount) {
|
||||
@@ -172,28 +211,6 @@ const emailService = {
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (attDataList.length > 0 && !r2Domain) {
|
||||
throw new BizError(t('noOsDomainSendPic'));
|
||||
}
|
||||
|
||||
if (attDataList.length > 0 && !await r2Service.hasOSS(c)) {
|
||||
throw new BizError(t('noOsSendPic'));
|
||||
}
|
||||
|
||||
if (attachments.length > 0 && !r2Domain) {
|
||||
throw new BizError(t('noOsDomainSendAtt'));
|
||||
}
|
||||
|
||||
if (attachments.length > 0 && !await r2Service.hasOSS(c)) {
|
||||
throw new BizError(t('noOsSendAtt'));
|
||||
}
|
||||
|
||||
if (attachments.length > 0 && manyType === 'divide') {
|
||||
throw new BizError(t('noSeparateSend'));
|
||||
}
|
||||
|
||||
|
||||
const accountRow = await accountService.selectById(c, accountId);
|
||||
|
||||
if (!accountRow) {
|
||||
@@ -205,7 +222,7 @@ const emailService = {
|
||||
}
|
||||
|
||||
if (c.env.admin !== userRow.email) {
|
||||
|
||||
//用户没有这个域名的使用权限
|
||||
if(!roleService.hasAvailDomainPerm(roleRow.availDomain, accountRow.email)) {
|
||||
throw new BizError(t('noDomainPermSend'),403)
|
||||
}
|
||||
@@ -214,12 +231,14 @@ const emailService = {
|
||||
|
||||
const domain = emailUtils.getDomain(accountRow.email);
|
||||
const resendToken = resendTokens[domain];
|
||||
const useCloudflareEmail = !!c.env.email;
|
||||
|
||||
if (!resendToken) {
|
||||
throw new BizError(t('noResendToken'));
|
||||
//如果接收方存在站外邮箱,又没有发信服务
|
||||
if (!useCloudflareEmail && !resendToken && !allInternal) {
|
||||
throw new BizError(t('noSendProvider'));
|
||||
}
|
||||
|
||||
|
||||
//没有发件人名字自动截取
|
||||
if (!name) {
|
||||
name = emailUtils.getName(accountRow.email);
|
||||
}
|
||||
@@ -228,6 +247,7 @@ const emailService = {
|
||||
messageId: null
|
||||
};
|
||||
|
||||
//如果是回复邮件
|
||||
if (sendType === 'reply') {
|
||||
|
||||
emailRow = await this.selectById(c, emailId);
|
||||
@@ -238,66 +258,52 @@ const emailService = {
|
||||
|
||||
}
|
||||
|
||||
let resendResult = null;
|
||||
let sendResult = {};
|
||||
|
||||
const resend = new Resend(resendToken);
|
||||
//存在站外邮箱时,如果配置了 Cloudflare Email Service 就优先使用,否则使用 Resend
|
||||
if (!allInternal) {
|
||||
|
||||
if (manyType === 'divide') {
|
||||
|
||||
let sendFormList = [];
|
||||
|
||||
receiveEmail.forEach(email => {
|
||||
const sendForm = {
|
||||
from: `${name} <${accountRow.email}>`,
|
||||
to: [email],
|
||||
subject: subject,
|
||||
text: text,
|
||||
html: html
|
||||
};
|
||||
|
||||
if (sendType === 'reply') {
|
||||
sendForm.headers = {
|
||||
'in-reply-to': emailRow.messageId,
|
||||
'references': emailRow.messageId
|
||||
};
|
||||
}
|
||||
|
||||
sendFormList.push(sendForm);
|
||||
});
|
||||
|
||||
resendResult = await resend.batch.send(sendFormList);
|
||||
|
||||
} else {
|
||||
|
||||
const sendForm = {
|
||||
from: `${name} <${accountRow.email}>`,
|
||||
to: [...receiveEmail],
|
||||
subject: subject,
|
||||
text: text,
|
||||
html: html,
|
||||
attachments: attachments
|
||||
};
|
||||
|
||||
if (sendType === 'reply') {
|
||||
sendForm.headers = {
|
||||
'in-reply-to': emailRow.messageId,
|
||||
'references': emailRow.messageId
|
||||
};
|
||||
if (useCloudflareEmail) {
|
||||
sendResult = await this.sendByCloudflareEmail(c, {
|
||||
name,
|
||||
accountEmail: accountRow.email,
|
||||
receiveEmail,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
attachments: [...imageDataList, ...attachments],
|
||||
sendType,
|
||||
messageId: emailRow.messageId
|
||||
});
|
||||
} else {
|
||||
sendResult = await this.sendByResend(resendToken, {
|
||||
name,
|
||||
accountEmail: accountRow.email,
|
||||
receiveEmail,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
attachments: [...imageDataList, ...attachments],
|
||||
sendType,
|
||||
messageId: emailRow.messageId
|
||||
});
|
||||
}
|
||||
|
||||
resendResult = await resend.emails.send(sendForm);
|
||||
|
||||
}
|
||||
|
||||
const { data, error } = resendResult;
|
||||
const { data, error } = sendResult;
|
||||
|
||||
|
||||
if (error) {
|
||||
throw new BizError(error.message);
|
||||
}
|
||||
|
||||
html = this.imgReplace(html, null, r2Domain);
|
||||
imageDataList = imageDataList.map(item => ({...item, contentId: `<${item.contentId}>`}))
|
||||
|
||||
//把图片标签cid标签切换会通用url
|
||||
html = this.imgReplace(html, imageDataList, r2Domain);
|
||||
|
||||
//封装数据保存到数据库
|
||||
const emailData = {};
|
||||
emailData.sendEmail = accountRow.email;
|
||||
emailData.name = name;
|
||||
@@ -305,71 +311,60 @@ const emailService = {
|
||||
emailData.content = html;
|
||||
emailData.text = text;
|
||||
emailData.accountId = accountId;
|
||||
emailData.status = useCloudflareEmail ? emailConst.status.DELIVERED : emailConst.status.SENT;
|
||||
emailData.type = emailConst.type.SEND;
|
||||
emailData.userId = userId;
|
||||
emailData.status = emailConst.status.SENT;
|
||||
emailData.resendEmailId = data?.id;
|
||||
|
||||
const emailDataList = [];
|
||||
const recipient = [];
|
||||
|
||||
if (manyType === 'divide') {
|
||||
receiveEmail.forEach(item => {
|
||||
recipient.push({ address: item, name: '' });
|
||||
});
|
||||
|
||||
receiveEmail.forEach((item, index) => {
|
||||
const emailDataItem = { ...emailData };
|
||||
emailDataItem.resendEmailId = data.data[index].id;
|
||||
emailDataItem.recipient = JSON.stringify([{ address: item, name: '' }]);
|
||||
emailDataList.push(emailDataItem);
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
emailData.resendEmailId = data.id;
|
||||
|
||||
const recipient = [];
|
||||
|
||||
receiveEmail.forEach(item => {
|
||||
recipient.push({ address: item, name: '' });
|
||||
});
|
||||
|
||||
emailData.recipient = JSON.stringify(recipient);
|
||||
|
||||
emailDataList.push(emailData);
|
||||
}
|
||||
emailData.recipient = JSON.stringify(recipient);
|
||||
|
||||
if (sendType === 'reply') {
|
||||
emailDataList.forEach(emailData => {
|
||||
emailData.inReplyTo = emailRow.messageId;
|
||||
emailData.relation = emailRow.messageId;
|
||||
});
|
||||
emailData.inReplyTo = emailRow.messageId;
|
||||
emailData.relation = emailRow.messageId;
|
||||
}
|
||||
|
||||
|
||||
if (roleRow.sendCount) {
|
||||
//如果权限有发送次数增加用户发送次数
|
||||
if (roleRow.sendCount && roleRow.sendType !== 'internal') {
|
||||
await userService.incrUserSendCount(c, receiveEmail.length, userId);
|
||||
}
|
||||
|
||||
const emailRowList = await Promise.all(
|
||||
emailDataList.map(async (emailData) => {
|
||||
const emailRow = await orm(c).insert(email).values(emailData).returning().get();
|
||||
//保存到数据库并返回结果
|
||||
const emailResult = await orm(c).insert(email).values(emailData).returning().get();
|
||||
|
||||
if (attDataList.length > 0) {
|
||||
await attService.saveArticleAtt(c, attDataList, userId, accountId, emailRow.emailId);
|
||||
}
|
||||
//保存内嵌附件
|
||||
if (imageDataList.length > 0) {
|
||||
if (imageDataList.length > 10) {
|
||||
throw new BizError(t('imageAttLimit'));
|
||||
}
|
||||
await attService.saveArticleAtt(c, imageDataList, userId, accountId, emailResult.emailId);
|
||||
}
|
||||
|
||||
if (attachments?.length > 0 && await r2Service.hasOSS(c)) {
|
||||
await attService.saveSendAtt(c, attachments, userId, accountId, emailRow.emailId);
|
||||
}
|
||||
//保存普通附件
|
||||
if (attachments?.length > 0) {
|
||||
if (attachments.length > 10) {
|
||||
throw new BizError(t('attLimit'));
|
||||
}
|
||||
await attService.saveSendAtt(c, attachments, userId, accountId, emailResult.emailId);
|
||||
}
|
||||
|
||||
const attsList = await attService.selectByEmailIds(c, [emailRow.emailId]);
|
||||
emailRow.attList = attsList;
|
||||
const attList = await attService.selectByEmailIds(c, [emailResult.emailId]);
|
||||
emailResult.attList = attList;
|
||||
|
||||
return emailRow;
|
||||
})
|
||||
);
|
||||
//如果全是站内接收方,直接写入数据库
|
||||
if (allInternal) {
|
||||
await this.HandleOnSiteEmail(c, receiveEmail, emailResult, attList);
|
||||
}
|
||||
|
||||
const dateStr = dayjs().format('YYYY-MM-DD');
|
||||
|
||||
let daySendTotal = await c.env.kv.get(kvConst.SEND_DAY_COUNT + dateStr);
|
||||
|
||||
//记录每天发件次数统计
|
||||
if (!daySendTotal) {
|
||||
await c.env.kv.put(kvConst.SEND_DAY_COUNT + dateStr, JSON.stringify(receiveEmail.length), { expirationTtl: 60 * 60 * 24 });
|
||||
} else {
|
||||
@@ -377,7 +372,282 @@ const emailService = {
|
||||
await c.env.kv.put(kvConst.SEND_DAY_COUNT + dateStr, JSON.stringify(daySendTotal), { expirationTtl: 60 * 60 * 24 });
|
||||
}
|
||||
|
||||
return emailRowList;
|
||||
return [ emailResult ];
|
||||
},
|
||||
|
||||
async sendByCloudflareEmail(c, params) {
|
||||
const sendForm = {
|
||||
from: { email: params.accountEmail, name: params.name },
|
||||
to: [...params.receiveEmail],
|
||||
subject: params.subject
|
||||
};
|
||||
|
||||
if (params.text) {
|
||||
sendForm.text = params.text;
|
||||
}
|
||||
|
||||
if (params.html) {
|
||||
sendForm.html = params.html;
|
||||
}
|
||||
|
||||
const attachments = await this.toCloudflareAttachments(params.attachments);
|
||||
if (attachments.length > 0) {
|
||||
sendForm.attachments = attachments;
|
||||
}
|
||||
|
||||
if (params.sendType === 'reply' && params.messageId) {
|
||||
sendForm.headers = {
|
||||
'in-reply-to': params.messageId,
|
||||
'references': params.messageId
|
||||
};
|
||||
}
|
||||
|
||||
const result = await c.env.email.send(sendForm);
|
||||
|
||||
return {
|
||||
data: {
|
||||
id: result.messageId
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
async sendByResend(resendToken, params) {
|
||||
const resend = new Resend(resendToken);
|
||||
|
||||
const sendForm = {
|
||||
from: `${params.name} <${params.accountEmail}>`,
|
||||
to: [...params.receiveEmail],
|
||||
subject: params.subject,
|
||||
text: params.text,
|
||||
html: params.html,
|
||||
attachments: await this.toResendAttachments(params.attachments)
|
||||
};
|
||||
|
||||
if (params.sendType === 'reply') {
|
||||
sendForm.headers = {
|
||||
'in-reply-to': params.messageId,
|
||||
'references': params.messageId
|
||||
};
|
||||
}
|
||||
|
||||
return await resend.emails.send(sendForm);
|
||||
},
|
||||
|
||||
async toCloudflareAttachments(attachments) {
|
||||
const arrayBufferAttachments = await this.toArrayBufferAttachments(attachments);
|
||||
|
||||
return arrayBufferAttachments.map(attachment => {
|
||||
const item = {
|
||||
content: attachment.content,
|
||||
filename: attachment.filename,
|
||||
type: attachment.mimeType || attachment.contentType || attachment.type || 'application/octet-stream',
|
||||
disposition: attachment.contentId ? 'inline' : 'attachment'
|
||||
};
|
||||
|
||||
if (attachment.contentId) {
|
||||
item.contentId = attachment.contentId.replace(/^<|>$/g, '');
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
},
|
||||
|
||||
async toResendAttachments(attachments = []) {
|
||||
const result = [];
|
||||
|
||||
for (const attachment of attachments) {
|
||||
const content = await this.toAttachmentBase64(attachment);
|
||||
if (!content) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push({
|
||||
...attachment,
|
||||
content,
|
||||
contentType: attachment.contentType || attachment.mimeType || attachment.type || 'application/octet-stream'
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
async toArrayBufferAttachments(attachments = []) {
|
||||
const result = [];
|
||||
|
||||
for (const attachment of attachments) {
|
||||
const content = await this.toAttachmentArrayBuffer(attachment);
|
||||
if (!content) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push({ ...attachment, content });
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
async toAttachmentBase64(attachment) {
|
||||
let content = attachment.content;
|
||||
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof content === 'string') {
|
||||
if (content.startsWith('data:')) {
|
||||
content = content.split(',')[1] || content;
|
||||
}
|
||||
return content.replace(/\s+/g, '');
|
||||
}
|
||||
|
||||
const arrayBuffer = await this.toAttachmentArrayBuffer(attachment);
|
||||
if (!arrayBuffer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(arrayBuffer);
|
||||
let binary = '';
|
||||
|
||||
for (let i = 0; i < bytes.length; i += 0x8000) {
|
||||
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
||||
}
|
||||
|
||||
return btoa(binary);
|
||||
},
|
||||
|
||||
async toAttachmentArrayBuffer(attachment) {
|
||||
let content = attachment.content;
|
||||
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (content instanceof ArrayBuffer) {
|
||||
return content;
|
||||
}
|
||||
|
||||
if (content instanceof Uint8Array) {
|
||||
return content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);
|
||||
}
|
||||
|
||||
if (typeof content === 'string') {
|
||||
if (content.startsWith('data:')) {
|
||||
content = content.split(',')[1] || content;
|
||||
}
|
||||
return fileUtils.base64ToUint8Array(content.replace(/\s+/g, '')).buffer;
|
||||
}
|
||||
|
||||
return content;
|
||||
},
|
||||
|
||||
//处理站内邮件发送
|
||||
async HandleOnSiteEmail(c, receiveEmail, sendEmailData, attList) {
|
||||
|
||||
const { noRecipient } = await settingService.query(c);
|
||||
|
||||
//查询所有收件人账号信息
|
||||
let accountList = await orm(c).select().from(account).where(inArray(account.email, receiveEmail)).all();
|
||||
|
||||
//查询所有收件人权限身份
|
||||
const userIds = accountList.map(accountRow => accountRow.userId);
|
||||
let roleList = await roleService.selectByUserIds(c, userIds);
|
||||
|
||||
//封装数据库准备保存到数据库
|
||||
const emailDataList = [];
|
||||
|
||||
for (const email of receiveEmail) {
|
||||
|
||||
//把发件人邮件改成收件
|
||||
const emailValues = {...sendEmailData}
|
||||
emailValues.status = emailConst.status.RECEIVE;
|
||||
emailValues.type = emailConst.type.RECEIVE;
|
||||
emailValues.toEmail = email;
|
||||
emailValues.toName = emailUtils.getName(email);
|
||||
emailValues.emailId = null;
|
||||
|
||||
const accountRow = accountList.find(accountRow => accountRow.email === email);
|
||||
|
||||
//如果收件人存在就把邮件信息改成收件人的
|
||||
if (accountRow) {
|
||||
|
||||
//设置给收件人保存
|
||||
emailValues.userId = accountRow.userId;
|
||||
emailValues.accountId = accountRow.accountId;
|
||||
emailValues.type = emailConst.type.RECEIVE;
|
||||
emailValues.status = emailConst.status.RECEIVE;
|
||||
|
||||
const roleRow = roleList.find(roleRow => roleRow.userId === accountRow.userId);
|
||||
|
||||
let { banEmail, availDomain } = roleRow;
|
||||
|
||||
//如果收件人没有这个域名的使用权限和有邮件拦截,就把邮件改为拒收状态
|
||||
if (email !== c.env.admin) {
|
||||
|
||||
if (!roleService.hasAvailDomainPerm(availDomain, email)) {
|
||||
emailValues.status = emailConst.status.BOUNCED;
|
||||
emailValues.message = `The recipient <${email}> is not authorized to use this domain.`;
|
||||
} else if(roleService.isBanEmail(banEmail, sendEmailData.sendEmail)) {
|
||||
emailValues.status = emailConst.status.BOUNCED;
|
||||
emailValues.message = `The recipient <${email}> is disabled from receiving emails.`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
emailDataList.push(emailValues);
|
||||
|
||||
} else {
|
||||
|
||||
//设置无收件人邮件信息
|
||||
emailValues.userId = 0;
|
||||
emailValues.accountId = 0;
|
||||
emailValues.type = emailConst.type.RECEIVE;
|
||||
emailValues.status = emailConst.status.NOONE;
|
||||
|
||||
//如果无人收件关闭改为拒收
|
||||
if (noRecipient === settingConst.noRecipient.CLOSE) {
|
||||
emailValues.status = emailConst.status.BOUNCED;
|
||||
emailValues.message = `Recipient not found: <${email}>`;
|
||||
}
|
||||
|
||||
emailDataList.push(emailValues);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//保存邮件
|
||||
const receiveEmailList = emailDataList.filter(emailRow => emailRow.status === emailConst.status.RECEIVE || emailRow.status === emailConst.status.NOONE);
|
||||
|
||||
for (const emailData of receiveEmailList) {
|
||||
|
||||
const emailRow = await orm(c).insert(email).values(emailData).returning().get();
|
||||
|
||||
//设置附件保存
|
||||
for (const attRow of attList) {
|
||||
const attValues = {...attRow};
|
||||
attValues.emailId = emailRow.emailId;
|
||||
attValues.accountId = emailRow.accountId;
|
||||
attValues.userId = emailRow.userId;
|
||||
attValues.attId = null;
|
||||
await orm(c).insert(att).values(attValues).run();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const bouncedEmail = emailDataList.find(emailRow => emailRow.status === emailConst.status.BOUNCED);
|
||||
|
||||
|
||||
let status = emailConst.status.DELIVERED;
|
||||
let message = ''
|
||||
//如果有拒收邮件,就把发件人的邮件改成拒收
|
||||
if (bouncedEmail) {
|
||||
const messageJson = { message: bouncedEmail.message };
|
||||
message = JSON.stringify(messageJson);
|
||||
status = emailConst.status.BOUNCED;
|
||||
}
|
||||
|
||||
await orm(c).update(email).set({ status, message: message }).where(eq(email.emailId, sendEmailData.emailId)).run();
|
||||
|
||||
},
|
||||
|
||||
imgReplace(content, cidAttList, r2domain) {
|
||||
@@ -431,29 +701,32 @@ const emailService = {
|
||||
},
|
||||
|
||||
async latest(c, params, userId) {
|
||||
let { emailId, accountId } = params;
|
||||
const list = await orm(c).select().from(email).where(
|
||||
and(
|
||||
eq(email.userId, userId),
|
||||
eq(email.isDel, isDel.NORMAL),
|
||||
eq(email.accountId, accountId),
|
||||
eq(email.type, emailConst.type.RECEIVE),
|
||||
gt(email.emailId, emailId)
|
||||
))
|
||||
let { emailId, accountId, allReceive } = params;
|
||||
allReceive = Number(allReceive);
|
||||
|
||||
if (isNaN(allReceive)) {
|
||||
let accountRow = await accountService.selectById(c, accountId);
|
||||
allReceive = accountRow.allReceive;
|
||||
}
|
||||
|
||||
let list = await orm(c).select({...email}).from(email)
|
||||
.leftJoin(
|
||||
account,
|
||||
eq(account.accountId, email.accountId)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
gt(email.emailId, emailId),
|
||||
eq(email.userId, userId),
|
||||
eq(email.isDel, isDel.NORMAL),
|
||||
eq(account.isDel, isDel.NORMAL),
|
||||
allReceive ? eq(1,1) : eq(email.accountId, accountId),
|
||||
eq(email.type, emailConst.type.RECEIVE)
|
||||
))
|
||||
.orderBy(desc(email.emailId))
|
||||
.limit(20);
|
||||
|
||||
const emailIds = list.map(item => item.emailId);
|
||||
|
||||
if (emailIds.length > 0) {
|
||||
|
||||
const attsList = await attService.selectByEmailIds(c, emailIds);
|
||||
|
||||
list.forEach(emailRow => {
|
||||
const atts = attsList.filter(attsRow => attsRow.emailId === emailRow.emailId);
|
||||
emailRow.attList = atts;
|
||||
});
|
||||
}
|
||||
await this.emailAddAtt(c, list);
|
||||
|
||||
return list;
|
||||
},
|
||||
@@ -505,8 +778,8 @@ const emailService = {
|
||||
emailId = Number(emailId);
|
||||
timeSort = Number(timeSort);
|
||||
|
||||
if (size > 30) {
|
||||
size = 30;
|
||||
if (size > 50) {
|
||||
size = 50;
|
||||
}
|
||||
|
||||
if (!emailId) {
|
||||
@@ -521,7 +794,6 @@ const emailService = {
|
||||
|
||||
const conditions = [];
|
||||
|
||||
|
||||
if (type === 'send') {
|
||||
conditions.push(eq(email.type, emailConst.type.SEND));
|
||||
}
|
||||
@@ -539,24 +811,24 @@ const emailService = {
|
||||
}
|
||||
|
||||
if (userEmail) {
|
||||
conditions.push(sql`${user.email} COLLATE NOCASE LIKE ${userEmail + '%'}`);
|
||||
conditions.push(sql`${user.email} COLLATE NOCASE LIKE ${'%'+ userEmail + '%'}`);
|
||||
}
|
||||
|
||||
if (accountEmail) {
|
||||
conditions.push(
|
||||
or(
|
||||
sql`${email.toEmail} COLLATE NOCASE LIKE ${accountEmail + '%'}`,
|
||||
sql`${email.sendEmail} COLLATE NOCASE LIKE ${accountEmail + '%'}`,
|
||||
sql`${email.toEmail} COLLATE NOCASE LIKE ${'%'+ accountEmail + '%'}`,
|
||||
sql`${email.sendEmail} COLLATE NOCASE LIKE ${'%'+ accountEmail + '%'}`,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (name) {
|
||||
conditions.push(sql`${email.name} COLLATE NOCASE LIKE ${name + '%'}`);
|
||||
conditions.push(sql`${email.name} COLLATE NOCASE LIKE ${'%'+ name + '%'}`);
|
||||
}
|
||||
|
||||
if (subject) {
|
||||
conditions.push(sql`${email.subject} COLLATE NOCASE LIKE ${subject + '%'}`);
|
||||
conditions.push(sql`${email.subject} COLLATE NOCASE LIKE ${'%'+ subject + '%'}`);
|
||||
}
|
||||
|
||||
conditions.push(ne(email.status, emailConst.status.SAVING));
|
||||
@@ -564,9 +836,9 @@ const emailService = {
|
||||
const countConditions = [...conditions];
|
||||
|
||||
if (timeSort) {
|
||||
conditions.push(gt(email.emailId, emailId));
|
||||
conditions.unshift(gt(email.emailId, emailId));
|
||||
} else {
|
||||
conditions.push(lt(email.emailId, emailId));
|
||||
conditions.unshift(lt(email.emailId, emailId));
|
||||
}
|
||||
|
||||
const query = orm(c).select({ ...email, userEmail: user.email })
|
||||
@@ -585,20 +857,63 @@ const emailService = {
|
||||
query.orderBy(desc(email.emailId));
|
||||
}
|
||||
|
||||
const listQuery = await query.limit(size).all();
|
||||
const totalQuery = await queryCount.get();
|
||||
const listQuery = query.limit(size).all();
|
||||
const totalQuery = queryCount.get();
|
||||
const latestEmailQuery = orm(c).select().from(email)
|
||||
.where(and(
|
||||
eq(email.type, emailConst.type.RECEIVE),
|
||||
ne(email.status, emailConst.status.SAVING)
|
||||
))
|
||||
.orderBy(desc(email.emailId)).limit(1).get();
|
||||
|
||||
const [list, totalRow] = await Promise.all([listQuery, totalQuery]);
|
||||
let [list, totalRow, latestEmail] = await Promise.all([listQuery, totalQuery, latestEmailQuery]);
|
||||
|
||||
await this.emailAddAtt(c, list);
|
||||
|
||||
if (!latestEmail) {
|
||||
latestEmail = {
|
||||
emailId: 0,
|
||||
accountId: 0,
|
||||
userId: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return { list: list, total: totalRow.total, latestEmail };
|
||||
},
|
||||
|
||||
async allEmailLatest(c, params) {
|
||||
|
||||
const { emailId } = params;
|
||||
|
||||
let list = await orm(c).select({...email, userEmail: user.email}).from(email)
|
||||
.leftJoin(user, eq(email.userId, user.userId))
|
||||
.where(
|
||||
and(
|
||||
gt(email.emailId, emailId),
|
||||
eq(email.type, emailConst.type.RECEIVE),
|
||||
ne(email.status, emailConst.status.SAVING)
|
||||
))
|
||||
.orderBy(desc(email.emailId))
|
||||
.limit(20);
|
||||
|
||||
await this.emailAddAtt(c, list);
|
||||
|
||||
return list;
|
||||
},
|
||||
|
||||
async emailAddAtt(c, list) {
|
||||
|
||||
const emailIds = list.map(item => item.emailId);
|
||||
const attsList = await attService.selectByEmailIds(c, emailIds);
|
||||
|
||||
list.forEach(emailRow => {
|
||||
const atts = attsList.filter(attsRow => attsRow.emailId === emailRow.emailId);
|
||||
emailRow.attList = atts;
|
||||
});
|
||||
if (emailIds.length > 0) {
|
||||
|
||||
return { list: list, total: totalRow.total };
|
||||
const attList = await attService.selectByEmailIds(c, emailIds);
|
||||
|
||||
list.forEach(emailRow => {
|
||||
const atts = attList.filter(attRow => attRow.emailId === emailRow.emailId);
|
||||
emailRow.attList = atts;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async restoreByUserId(c, userId) {
|
||||
@@ -613,8 +928,8 @@ const emailService = {
|
||||
},
|
||||
|
||||
async completeReceiveAll(c) {
|
||||
await c.env.db.prepare(`UPDATE email as e SET status = ${emailConst.status.RECEIVE} WHERE status = ${emailConst.status.SAVING} AND EXISTS (SELECT 1 FROM account WHERE account_id = e.account_id)`).run();
|
||||
await c.env.db.prepare(`UPDATE email as e SET status = ${emailConst.status.NOONE} WHERE status = ${emailConst.status.SAVING} AND NOT EXISTS (SELECT 1 FROM account WHERE account_id = e.account_id)`).run();
|
||||
await c.env.db.prepare(`UPDATE email as e SET status = ${emailConst.status.RECEIVE} WHERE status = ${emailConst.status.SAVING} AND EXISTS (SELECT 1 FROM account WHERE account_id = e.account_id)`).run();
|
||||
await c.env.db.prepare(`UPDATE email as e SET status = ${emailConst.status.NOONE} WHERE status = ${emailConst.status.SAVING} AND NOT EXISTS (SELECT 1 FROM account WHERE account_id = e.account_id)`).run();
|
||||
},
|
||||
|
||||
async batchDelete(c, params) {
|
||||
@@ -666,6 +981,11 @@ const emailService = {
|
||||
async physicsDeleteByAccountId(c, accountId) {
|
||||
await attService.removeByAccountId(c, accountId);
|
||||
await orm(c).delete(email).where(eq(email.accountId, accountId)).run();
|
||||
},
|
||||
|
||||
async read(c, params, userId) {
|
||||
const { emailIds } = params;
|
||||
await orm(c).update(email).set({ unread: emailConst.unread.READ }).where(and(eq(email.userId, userId), inArray(email.emailId, emailIds)));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
const kvObjService = {
|
||||
|
||||
async putObj(c, key, content, metadata) {
|
||||
await c.env.kv.put(key, content, { metadata: metadata });
|
||||
},
|
||||
|
||||
async deleteObj(c, keys) {
|
||||
|
||||
if (typeof keys === 'string') {
|
||||
keys = [keys];
|
||||
}
|
||||
|
||||
if (keys.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(keys.map( key => c.env.kv.delete(key)));
|
||||
},
|
||||
|
||||
async getObj(c, key) {
|
||||
const obj = await c.env.kv.getWithMetadata(key, { type: "arrayBuffer"});
|
||||
if (!obj.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Response(obj.value, {
|
||||
headers: {
|
||||
'Content-Type': obj.metadata?.contentType || 'application/octet-stream',
|
||||
'Content-Disposition': obj.metadata?.contentDisposition || null,
|
||||
'Cache-Control': obj.metadata?.cacheControl || null
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async toObjResp(c, key) {
|
||||
|
||||
return await this.getObj(c, key);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default kvObjService;
|
||||
@@ -22,12 +22,16 @@ import verifyRecordService from './verify-record-service';
|
||||
|
||||
const loginService = {
|
||||
|
||||
async register(c, params) {
|
||||
async register(c, params, oauth = false) {
|
||||
|
||||
const { email, password, token, code } = params;
|
||||
|
||||
const {regKey, register, registerVerify, regVerifyCount} = await settingService.query(c)
|
||||
let { regKey, register, registerVerify, regVerifyCount, minEmailPrefix, emailPrefixFilter } = await settingService.query(c)
|
||||
|
||||
if (oauth) {
|
||||
registerVerify = settingConst.registerVerify.CLOSE;
|
||||
register = settingConst.register.OPEN;
|
||||
}
|
||||
|
||||
if (register === settingConst.register.CLOSE) {
|
||||
throw new BizError(t('regDisabled'));
|
||||
@@ -37,16 +41,24 @@ const loginService = {
|
||||
throw new BizError(t('notEmail'));
|
||||
}
|
||||
|
||||
if (emailUtils.getName(email).length < minEmailPrefix) {
|
||||
throw new BizError(t('minEmailPrefix', { msg: minEmailPrefix } ));
|
||||
}
|
||||
|
||||
if (emailPrefixFilter.some(content => emailUtils.getName(email).includes(content))) {
|
||||
throw new BizError(t('banEmailPrefix'));
|
||||
}
|
||||
|
||||
if (emailUtils.getName(email).length > 64) {
|
||||
throw new BizError(t('emailLengthLimit'));
|
||||
}
|
||||
|
||||
if (password.length > 30) {
|
||||
throw new BizError(t('pwdLengthLimit'));
|
||||
}
|
||||
|
||||
if (emailUtils.getName(email).length > 30) {
|
||||
throw new BizError(t('emailLengthLimit'));
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
throw new BizError(t('pwdMinLengthLimit'));
|
||||
throw new BizError(t('pwdMinLength'));
|
||||
}
|
||||
|
||||
if (!c.env.domain.includes(emailUtils.getDomain(email))) {
|
||||
@@ -78,7 +90,6 @@ const loginService = {
|
||||
throw new BizError(t('isRegAccount'));
|
||||
}
|
||||
|
||||
|
||||
let defType = null
|
||||
|
||||
if (!type) {
|
||||
@@ -188,11 +199,11 @@ const loginService = {
|
||||
return { type: regKeyRow.roleId, regKeyId: regKeyRow.regKeyId };
|
||||
},
|
||||
|
||||
async login(c, params) {
|
||||
async login(c, params, noVerifyPwd = false) {
|
||||
|
||||
const { email, password } = params;
|
||||
|
||||
if (!email || !password) {
|
||||
if ((!email || !password) && !noVerifyPwd) {
|
||||
throw new BizError(t('emailAndPwdEmpty'));
|
||||
}
|
||||
|
||||
@@ -202,6 +213,18 @@ const loginService = {
|
||||
throw new BizError(t('notExistUser'));
|
||||
}
|
||||
|
||||
if (!await cryptoUtils.verifyPassword(password, userRow.salt, userRow.password) && !noVerifyPwd) {
|
||||
throw new BizError(t('IncorrectPwd'));
|
||||
}
|
||||
|
||||
return this.loginUser(c, userRow);
|
||||
},
|
||||
|
||||
async loginUser(c, userRow) {
|
||||
if (!userRow) {
|
||||
throw new BizError(t('notExistUser'));
|
||||
}
|
||||
|
||||
if(userRow.isDel === isDel.DELETE) {
|
||||
throw new BizError(t('isDelUser'));
|
||||
}
|
||||
@@ -210,16 +233,12 @@ const loginService = {
|
||||
throw new BizError(t('isBanUser'));
|
||||
}
|
||||
|
||||
if (!await cryptoUtils.verifyPassword(password, userRow.salt, userRow.password)) {
|
||||
throw new BizError(t('IncorrectPwd'));
|
||||
}
|
||||
|
||||
const uuid = uuidv4();
|
||||
const jwt = await JwtUtils.generateToken(c,{ userId: userRow.userId, token: uuid });
|
||||
|
||||
let authInfo = await c.env.kv.get(KvConst.AUTH_INFO + userRow.userId, { type: 'json' });
|
||||
|
||||
if (authInfo) {
|
||||
if (authInfo && (authInfo.user.email === userRow.email)) {
|
||||
|
||||
if (authInfo.tokens.length > 10) {
|
||||
authInfo.tokens.shift();
|
||||
|
||||