mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-06 16:07:14 +08:00
refactor: hide mail flags from frontend
This commit is contained in:
+2
-2
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- feat: |邮件状态| 新增可选 Mail Flags 功能,新邮件支持已读/未读状态、打开自动已读、手动切换状态、本页全部已读及按状态筛选;位运算和状态映射统一由后端处理,并为系统状态位及每地址自定义规则 Flag 预留扩展空间
|
- feat: |邮件状态| 新增可选邮件状态功能,新邮件支持已读/未读、打开自动已读、手动切换状态、本页全部已读及按状态筛选;前端仅使用已读状态,底层 Flag 存储、计算与后续分组归属统一由后端处理
|
||||||
- feat: |Admin| 数据库页面新增 D1 存储容量展示,支持选择并保存 Free 或 Workers Paid 套餐,对比当前数据库大小和容量上限
|
- feat: |Admin| 数据库页面新增 D1 存储容量展示,支持选择并保存 Free 或 Workers Paid 套餐,对比当前数据库大小和容量上限
|
||||||
- feat: |用户系统| 用户中心新增发送邮件、与收件箱一致的可按绑定地址过滤的发件箱,以及地址管理凭证弹框;提供使用用户 JWT 的地址设置、发信权限申请、发信及发件箱 API
|
- feat: |用户系统| 用户中心新增发送邮件、与收件箱一致的可按绑定地址过滤的发件箱,以及地址管理凭证弹框;提供使用用户 JWT 的地址设置、发信权限申请、发信及发件箱 API
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
- test: |E2E| 覆盖新邮件默认未读、地址隔离、标记已读及非法 Flag 操作校验
|
- test: |E2E| 覆盖新邮件默认未读、地址隔离、标记已读及非法状态操作校验
|
||||||
- test: |E2E| 覆盖 D1 数据库大小响应、配置键隔离,以及数据库页面套餐选择的持久化与刷新恢复
|
- test: |E2E| 覆盖 D1 数据库大小响应、配置键隔离,以及数据库页面套餐选择的持久化与刷新恢复
|
||||||
- fix: |E2E| 覆盖发信页面草稿编辑、正文格式切换及 HTML 预览
|
- fix: |E2E| 覆盖发信页面草稿编辑、正文格式切换及 HTML 预览
|
||||||
- fix: |E2E| 覆盖用户 JWT 发信接口的地址归属、额度扣减、实际投递和发件箱操作,以及用户中心查看地址凭证、切换发件地址和按地址过滤发件箱的完整流程
|
- fix: |E2E| 覆盖用户 JWT 发信接口的地址归属、额度扣减、实际投递和发件箱操作,以及用户中心查看地址凭证、切换发件地址和按地址过滤发件箱的完整流程
|
||||||
|
|||||||
+2
-2
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- feat: |Mail State| Add optional Mail Flags with unread state for new mail, automatic read-on-open, manual state toggling, mark-current-page-read and state filters; bit operations and state mapping are handled by the backend, with reserved system/custom bits for future per-address rule assignment
|
- feat: |Mail State| Add optional read state for new mail, automatic read-on-open, manual state toggling, mark-current-page-read and state filters; the frontend only consumes read state, while backend Flags own storage, calculation, and future group membership
|
||||||
- feat: |Admin| Add D1 storage capacity details to the database page, with persistent Free and Workers Paid plan selection and a comparison between the current database size and capacity limit
|
- feat: |Admin| Add D1 storage capacity details to the database page, with persistent Free and Workers Paid plan selection and a comparison between the current database size and capacity limit
|
||||||
- feat: |User| Add mail composition, inbox-style sent-item filtering by bound address, and the shared address-credentials dialog to the user center, backed by User JWT APIs for address settings, send-access requests, sending, and sent-item management
|
- feat: |User| Add mail composition, inbox-style sent-item filtering by bound address, and the shared address-credentials dialog to the user center, backed by User JWT APIs for address settings, send-access requests, sending, and sent-item management
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
- test: |E2E| Cover unread state on new mail, mailbox isolation, marking mail read, and invalid flag-operation validation
|
- test: |E2E| Cover unread state on new mail, mailbox isolation, marking mail read, and invalid status-operation validation
|
||||||
- test: |E2E| Cover the D1 database-size response, config-key isolation, and persistence of the database-page plan selection across reloads
|
- test: |E2E| Cover the D1 database-size response, config-key isolation, and persistence of the database-page plan selection across reloads
|
||||||
- fix: |E2E| Cover draft editing, content-format switching, and HTML preview in the send-mail composer
|
- fix: |E2E| Cover draft editing, content-format switching, and HTML preview in the send-mail composer
|
||||||
- fix: |E2E| Cover address ownership, balance decrement, delivery, and sent-item operations through the User JWT API, plus user-center credential display, sender switching, and sent-item filtering by address
|
- fix: |E2E| Cover address ownership, balance decrement, delivery, and sent-item operations through the User JWT API, plus user-center credential display, sender switching, and sent-item filtering by address
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
seedTestMail,
|
seedTestMail,
|
||||||
} from '../../fixtures/test-helpers';
|
} from '../../fixtures/test-helpers';
|
||||||
|
|
||||||
test.describe('Mail Flags', () => {
|
test.describe('Mail Read Status', () => {
|
||||||
test('new mail is unread and can be marked as read without changing other mailboxes', async ({ request }) => {
|
test('new mail is unread and can be marked as read without changing other mailboxes', async ({ request }) => {
|
||||||
const first = await createTestAddress(request, 'mail-flags-first');
|
const first = await createTestAddress(request, 'mail-flags-first');
|
||||||
const second = await createTestAddress(request, 'mail-flags-second');
|
const second = await createTestAddress(request, 'mail-flags-second');
|
||||||
@@ -20,7 +20,7 @@ test.describe('Mail Flags', () => {
|
|||||||
const { results } = await listRes.json();
|
const { results } = await listRes.json();
|
||||||
expect(results).toHaveLength(1);
|
expect(results).toHaveLength(1);
|
||||||
expect(results[0].flags).toBeUndefined();
|
expect(results[0].flags).toBeUndefined();
|
||||||
expect(results[0].mail_flags).toEqual({ unread: true });
|
expect(results[0].unread).toBe(true);
|
||||||
|
|
||||||
const unreadRes = await request.get(
|
const unreadRes = await request.get(
|
||||||
`${WORKER_URL}/api/mails?limit=10&offset=0&read_status=unread`,
|
`${WORKER_URL}/api/mails?limit=10&offset=0&read_status=unread`,
|
||||||
@@ -28,26 +28,26 @@ test.describe('Mail Flags', () => {
|
|||||||
);
|
);
|
||||||
expect((await unreadRes.json()).results).toHaveLength(1);
|
expect((await unreadRes.json()).results).toHaveLength(1);
|
||||||
|
|
||||||
const deniedRes = await request.patch(`${WORKER_URL}/api/mails/flags`, {
|
const deniedRes = await request.patch(`${WORKER_URL}/api/mails/read-status`, {
|
||||||
headers: { Authorization: `Bearer ${second.jwt}` },
|
headers: { Authorization: `Bearer ${second.jwt}` },
|
||||||
data: { ids: [results[0].id], flag: 'unread', action: 'clear' },
|
data: { ids: [results[0].id], action: 'read' },
|
||||||
});
|
});
|
||||||
expect(deniedRes.ok()).toBe(true);
|
expect(deniedRes.ok()).toBe(true);
|
||||||
expect((await deniedRes.json()).changes).toBe(0);
|
expect((await deniedRes.json()).changes).toBe(0);
|
||||||
|
|
||||||
const updateRes = await request.patch(`${WORKER_URL}/api/mails/flags`, {
|
const updateRes = await request.patch(`${WORKER_URL}/api/mails/read-status`, {
|
||||||
headers: { Authorization: `Bearer ${first.jwt}` },
|
headers: { Authorization: `Bearer ${first.jwt}` },
|
||||||
data: { ids: [results[0].id], flag: 'unread', action: 'clear' },
|
data: { ids: [results[0].id], action: 'read' },
|
||||||
});
|
});
|
||||||
expect(updateRes.ok()).toBe(true);
|
expect(updateRes.ok()).toBe(true);
|
||||||
const updateResult = await updateRes.json();
|
const updateResult = await updateRes.json();
|
||||||
expect(updateResult.changes).toBe(1);
|
expect(updateResult.changes).toBe(1);
|
||||||
expect(updateResult.results[0].mail_flags).toEqual({ unread: false });
|
expect(updateResult.results[0].unread).toBe(false);
|
||||||
|
|
||||||
const updatedListRes = await request.get(`${WORKER_URL}/api/mails?limit=10&offset=0`, {
|
const updatedListRes = await request.get(`${WORKER_URL}/api/mails?limit=10&offset=0`, {
|
||||||
headers: { Authorization: `Bearer ${first.jwt}` },
|
headers: { Authorization: `Bearer ${first.jwt}` },
|
||||||
});
|
});
|
||||||
expect((await updatedListRes.json()).results[0].mail_flags).toEqual({ unread: false });
|
expect((await updatedListRes.json()).results[0].unread).toBe(false);
|
||||||
|
|
||||||
const unreadAfterUpdateRes = await request.get(
|
const unreadAfterUpdateRes = await request.get(
|
||||||
`${WORKER_URL}/api/mails?limit=10&offset=0&read_status=unread`,
|
`${WORKER_URL}/api/mails?limit=10&offset=0&read_status=unread`,
|
||||||
@@ -55,26 +55,26 @@ test.describe('Mail Flags', () => {
|
|||||||
);
|
);
|
||||||
expect((await unreadAfterUpdateRes.json()).results).toHaveLength(0);
|
expect((await unreadAfterUpdateRes.json()).results).toHaveLength(0);
|
||||||
|
|
||||||
const toggleRes = await request.patch(`${WORKER_URL}/api/mails/flags`, {
|
const toggleRes = await request.patch(`${WORKER_URL}/api/mails/read-status`, {
|
||||||
headers: { Authorization: `Bearer ${first.jwt}` },
|
headers: { Authorization: `Bearer ${first.jwt}` },
|
||||||
data: { ids: [results[0].id], flag: 'unread', action: 'toggle' },
|
data: { ids: [results[0].id], action: 'toggle' },
|
||||||
});
|
});
|
||||||
expect(toggleRes.ok()).toBe(true);
|
expect(toggleRes.ok()).toBe(true);
|
||||||
expect((await toggleRes.json()).results[0].mail_flags).toEqual({ unread: true });
|
expect((await toggleRes.json()).results[0].unread).toBe(true);
|
||||||
} finally {
|
} finally {
|
||||||
await deleteAddress(request, first.jwt);
|
await deleteAddress(request, first.jwt);
|
||||||
await deleteAddress(request, second.jwt);
|
await deleteAddress(request, second.jwt);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rejects unsupported flag names and actions', async ({ request }) => {
|
test('rejects unsupported read-status actions', async ({ request }) => {
|
||||||
const { jwt } = await createTestAddress(request, 'mail-flags-invalid');
|
const { jwt } = await createTestAddress(request, 'mail-flags-invalid');
|
||||||
try {
|
try {
|
||||||
for (const data of [
|
for (const data of [
|
||||||
{ ids: [1], flag: 'flagged', action: 'set' },
|
{ ids: [1], action: 'invalid' },
|
||||||
{ ids: [1], flag: 'unread', action: 'invalid' },
|
{ ids: [1] },
|
||||||
]) {
|
]) {
|
||||||
const res = await request.patch(`${WORKER_URL}/api/mails/flags`, {
|
const res = await request.patch(`${WORKER_URL}/api/mails/read-status`, {
|
||||||
headers: { Authorization: `Bearer ${jwt}` },
|
headers: { Authorization: `Bearer ${jwt}` },
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ const props = defineProps({
|
|||||||
default: false,
|
default: false,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
enableMailFlags: {
|
enableReadStatus: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
updateMailFlags: {
|
updateMailReadStatus: {
|
||||||
type: Function,
|
type: Function,
|
||||||
default: () => { },
|
default: () => { },
|
||||||
required: false
|
required: false
|
||||||
@@ -68,7 +68,7 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const localFilterKeyword = ref('')
|
const localFilterKeyword = ref('')
|
||||||
const mailFlagFilter = ref('all')
|
const readStatusFilter = ref('all')
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isDark, mailboxSplitSize, mailListView, mailListPreviewLineClamp, indexTab, loading, useUTCDate,
|
isDark, mailboxSplitSize, mailListView, mailListPreviewLineClamp, indexTab, loading, useUTCDate,
|
||||||
@@ -106,11 +106,11 @@ const data = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isMailUnread = (mail) => {
|
const isMailUnread = (mail) => {
|
||||||
return props.enableMailFlags && mail?.mail_flags?.unread === true
|
return props.enableReadStatus && mail?.unread === true
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPageHasUnread = computed(() => rawData.value.some(isMailUnread))
|
const currentPageHasUnread = computed(() => rawData.value.some(isMailUnread))
|
||||||
const mailFlagFilterOptions = computed(() => [
|
const readStatusFilterOptions = computed(() => [
|
||||||
{ label: t('allMail'), value: 'all' },
|
{ label: t('allMail'), value: 'all' },
|
||||||
{ label: t('unread'), value: 'unread' },
|
{ label: t('unread'), value: 'unread' },
|
||||||
{ label: t('read'), value: 'read' },
|
{ label: t('read'), value: 'read' },
|
||||||
@@ -119,12 +119,12 @@ const mailFlagFilterOptions = computed(() => [
|
|||||||
const updateUnreadState = async (mails, action) => {
|
const updateUnreadState = async (mails, action) => {
|
||||||
if (mails.length === 0) return true
|
if (mails.length === 0) return true
|
||||||
try {
|
try {
|
||||||
const response = await props.updateMailFlags(mails.map(mail => mail.id), 'unread', action)
|
const response = await props.updateMailReadStatus(mails.map(mail => mail.id), action)
|
||||||
const results = response?.results ?? []
|
const results = response?.results ?? []
|
||||||
const resultById = new Map(results.map(result => [result.id, result]))
|
const resultById = new Map(results.map(result => [result.id, result]))
|
||||||
mails.forEach(mail => {
|
mails.forEach(mail => {
|
||||||
const result = resultById.get(mail.id)
|
const result = resultById.get(mail.id)
|
||||||
if (result) mail.mail_flags = result.mail_flags
|
if (result) mail.unread = result.unread
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -133,7 +133,7 @@ const updateUnreadState = async (mails, action) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const markMailsRead = async (mails) => updateUnreadState(mails, 'clear')
|
const markMailsRead = async (mails) => updateUnreadState(mails, 'read')
|
||||||
|
|
||||||
const toggleCurrentMailUnread = async () => {
|
const toggleCurrentMailUnread = async () => {
|
||||||
if (!curMail.value) return
|
if (!curMail.value) return
|
||||||
@@ -148,7 +148,7 @@ const openMail = async (mail) => {
|
|||||||
const markCurrentPageRead = async () => {
|
const markCurrentPageRead = async () => {
|
||||||
if (!await markMailsRead(rawData.value)) return
|
if (!await markMailsRead(rawData.value)) return
|
||||||
message.success(t("success"))
|
message.success(t("success"))
|
||||||
if (mailFlagFilter.value === 'unread') await refresh()
|
if (readStatusFilter.value === 'unread') await refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
const canGoPrevMail = computed(() => {
|
const canGoPrevMail = computed(() => {
|
||||||
@@ -232,14 +232,14 @@ watch([page, pageSize], async ([page, pageSize], [oldPage, oldPageSize]) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(mailFlagFilter, async () => {
|
watch(readStatusFilter, async () => {
|
||||||
await backFirstPageAndRefresh()
|
await backFirstPageAndRefresh()
|
||||||
})
|
})
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
try {
|
try {
|
||||||
const { results, count: totalCount } = await props.fetchMailData(
|
const { results, count: totalCount } = await props.fetchMailData(
|
||||||
pageSize.value, (page.value - 1) * pageSize.value, mailFlagFilter.value
|
pageSize.value, (page.value - 1) * pageSize.value, readStatusFilter.value
|
||||||
);
|
);
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
rawData.value = await Promise.all(results.map(async (item) => {
|
rawData.value = await Promise.all(results.map(async (item) => {
|
||||||
@@ -440,10 +440,10 @@ onBeforeUnmount(() => {
|
|||||||
<n-button @click="backFirstPageAndRefresh" type="primary" tertiary>
|
<n-button @click="backFirstPageAndRefresh" type="primary" tertiary>
|
||||||
{{ t('refresh') }}
|
{{ t('refresh') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button v-if="enableMailFlags && currentPageHasUnread" @click="markCurrentPageRead" tertiary>
|
<n-button v-if="enableReadStatus && currentPageHasUnread" @click="markCurrentPageRead" tertiary>
|
||||||
{{ t('markCurrentPageRead') }}
|
{{ t('markCurrentPageRead') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-select v-if="enableMailFlags" v-model:value="mailFlagFilter" :options="mailFlagFilterOptions"
|
<n-select v-if="enableReadStatus" v-model:value="readStatusFilter" :options="readStatusFilterOptions"
|
||||||
style="width: 120px" />
|
style="width: 120px" />
|
||||||
<n-input v-if="showFilterInput" v-model:value="localFilterKeyword"
|
<n-input v-if="showFilterInput" v-model:value="localFilterKeyword"
|
||||||
:placeholder="t('keywordQueryTip')" style="width: 200px; display: flex; align-items: center;"
|
:placeholder="t('keywordQueryTip')" style="width: 200px; display: flex; align-items: center;"
|
||||||
@@ -528,7 +528,7 @@ onBeforeUnmount(() => {
|
|||||||
style="overflow: auto; max-height: 100vh;">
|
style="overflow: auto; max-height: 100vh;">
|
||||||
<MailContentRenderer :mail="curMail" :showEMailTo="showEMailTo"
|
<MailContentRenderer :mail="curMail" :showEMailTo="showEMailTo"
|
||||||
:enableUserDeleteEmail="enableUserDeleteEmail" :showReply="showReply" :showSaveS3="showSaveS3"
|
:enableUserDeleteEmail="enableUserDeleteEmail" :showReply="showReply" :showSaveS3="showSaveS3"
|
||||||
:enableMailFlags="enableMailFlags" :onToggleUnread="toggleCurrentMailUnread"
|
:enableReadStatus="enableReadStatus" :onToggleUnread="toggleCurrentMailUnread"
|
||||||
:onDelete="deleteMail" :onReply="replyMail" :onForward="forwardMail" :onSaveToS3="saveToS3Proxy" />
|
:onDelete="deleteMail" :onReply="replyMail" :onForward="forwardMail" :onSaveToS3="saveToS3Proxy" />
|
||||||
</n-card>
|
</n-card>
|
||||||
<n-card :bordered="false" embedded class="mail-item" v-else>
|
<n-card :bordered="false" embedded class="mail-item" v-else>
|
||||||
@@ -600,7 +600,7 @@ onBeforeUnmount(() => {
|
|||||||
<n-button @click="backFirstPageAndRefresh" tertiary size="small" type="primary">
|
<n-button @click="backFirstPageAndRefresh" tertiary size="small" type="primary">
|
||||||
{{ t('refresh') }}
|
{{ t('refresh') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button v-if="enableMailFlags && currentPageHasUnread" @click="markCurrentPageRead" tertiary size="small">
|
<n-button v-if="enableReadStatus && currentPageHasUnread" @click="markCurrentPageRead" tertiary size="small">
|
||||||
{{ t('markCurrentPageRead') }}
|
{{ t('markCurrentPageRead') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
@@ -608,8 +608,8 @@ onBeforeUnmount(() => {
|
|||||||
<n-input v-model:value="localFilterKeyword"
|
<n-input v-model:value="localFilterKeyword"
|
||||||
:placeholder="t('keywordQueryTip')" size="small" clearable />
|
:placeholder="t('keywordQueryTip')" size="small" clearable />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="enableMailFlags" style="padding: 0 10px; margin-bottom: 10px;">
|
<div v-if="enableReadStatus" style="padding: 0 10px; margin-bottom: 10px;">
|
||||||
<n-select v-model:value="mailFlagFilter" :options="mailFlagFilterOptions" size="small" />
|
<n-select v-model:value="readStatusFilter" :options="readStatusFilterOptions" size="small" />
|
||||||
</div>
|
</div>
|
||||||
<div style="overflow: auto; min-height: 60vh; max-height: 100vh;">
|
<div style="overflow: auto; min-height: 60vh; max-height: 100vh;">
|
||||||
<n-list hoverable clickable>
|
<n-list hoverable clickable>
|
||||||
@@ -649,7 +649,7 @@ onBeforeUnmount(() => {
|
|||||||
<MailContentRenderer :mail="curMail" :showEMailTo="showEMailTo"
|
<MailContentRenderer :mail="curMail" :showEMailTo="showEMailTo"
|
||||||
:enableUserDeleteEmail="enableUserDeleteEmail" :showReply="showReply" :showSaveS3="showSaveS3"
|
:enableUserDeleteEmail="enableUserDeleteEmail" :showReply="showReply" :showSaveS3="showSaveS3"
|
||||||
:useUTCDate="useUTCDate" :onDelete="deleteMail" :onReply="replyMail" :onForward="forwardMail"
|
:useUTCDate="useUTCDate" :onDelete="deleteMail" :onReply="replyMail" :onForward="forwardMail"
|
||||||
:enableMailFlags="enableMailFlags" :onToggleUnread="toggleCurrentMailUnread"
|
:enableReadStatus="enableReadStatus" :onToggleUnread="toggleCurrentMailUnread"
|
||||||
:onSaveToS3="saveToS3Proxy" />
|
:onSaveToS3="saveToS3Proxy" />
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-drawer-content>
|
</n-drawer-content>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
enableMailFlags: {
|
enableReadStatus: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
@@ -154,8 +154,8 @@ const handleSaveToS3 = async (filename, blob) => {
|
|||||||
{{ t('downloadMail') }}
|
{{ t('downloadMail') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|
||||||
<n-button v-if="enableMailFlags" size="small" tertiary type="info" @click="onToggleUnread">
|
<n-button v-if="enableReadStatus" size="small" tertiary type="info" @click="onToggleUnread">
|
||||||
{{ mail.mail_flags?.unread ? t('markRead') : t('markUnread') }}
|
{{ mail.unread ? t('markRead') : t('markUnread') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|
||||||
<n-button v-if="showReply" size="small" tertiary type="info" @click="handleReply">
|
<n-button v-if="showReply" size="small" tertiary type="info" @click="handleReply">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const useGlobalState = createGlobalState(
|
|||||||
disableAnonymousUserCreateEmail: false,
|
disableAnonymousUserCreateEmail: false,
|
||||||
disableCustomAddressName: false,
|
disableCustomAddressName: false,
|
||||||
enableUserDeleteEmail: false,
|
enableUserDeleteEmail: false,
|
||||||
enableMailFlags: false,
|
enableReadStatus: false,
|
||||||
enableAutoReply: false,
|
enableAutoReply: false,
|
||||||
enableIndexAbout: false,
|
enableIndexAbout: false,
|
||||||
/** @type {string[]} */
|
/** @type {string[]} */
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ const SendMail = defineAsyncComponent(() => {
|
|||||||
|
|
||||||
const { t } = useScopedI18n('views.Index')
|
const { t } = useScopedI18n('views.Index')
|
||||||
|
|
||||||
const fetchMailData = async (limit, offset, mailFlagFilter = 'all') => {
|
const fetchMailData = async (limit, offset, readStatus = 'all') => {
|
||||||
if (mailIdQuery.value > 0) {
|
if (mailIdQuery.value > 0) {
|
||||||
const singleMail = await api.fetch(`/api/mail/${mailIdQuery.value}`);
|
const singleMail = await api.fetch(`/api/mail/${mailIdQuery.value}`);
|
||||||
if (singleMail) return { results: [singleMail], count: 1 };
|
if (singleMail) return { results: [singleMail], count: 1 };
|
||||||
return { results: [], count: 0 };
|
return { results: [], count: 0 };
|
||||||
}
|
}
|
||||||
const readStatusQuery = mailFlagFilter === 'all' ? '' : `&read_status=${mailFlagFilter}`
|
const readStatusQuery = readStatus === 'all' ? '' : `&read_status=${readStatus}`
|
||||||
return await api.fetch(
|
return await api.fetch(
|
||||||
`/api/mails?limit=${limit}&offset=${offset}${readStatusQuery}`
|
`/api/mails?limit=${limit}&offset=${offset}${readStatusQuery}`
|
||||||
);
|
);
|
||||||
@@ -48,10 +48,10 @@ const deleteMail = async (curMailId) => {
|
|||||||
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
|
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateMailFlags = async (ids, flag, action) => {
|
const updateMailReadStatus = async (ids, action) => {
|
||||||
return await api.fetch(`/api/mails/flags`, {
|
return await api.fetch(`/api/mails/read-status`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: JSON.stringify({ ids, flag, action })
|
body: JSON.stringify({ ids, action })
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ onMounted(() => {
|
|||||||
<MailBox :key="mailBoxKey" :showEMailTo="false" :showReply="openSettings.enableSendMail" :showSaveS3="openSettings.isS3Enabled"
|
<MailBox :key="mailBoxKey" :showEMailTo="false" :showReply="openSettings.enableSendMail" :showSaveS3="openSettings.isS3Enabled"
|
||||||
:saveToS3="saveToS3" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
:saveToS3="saveToS3" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||||
:fetchMailData="fetchMailData" :deleteMail="deleteMail" :showFilterInput="true"
|
:fetchMailData="fetchMailData" :deleteMail="deleteMail" :showFilterInput="true"
|
||||||
:enableMailFlags="openSettings.enableMailFlags" :updateMailFlags="updateMailFlags" />
|
:enableReadStatus="openSettings.enableReadStatus" :updateMailReadStatus="updateMailReadStatus" />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane v-if="openSettings.enableSendMail" name="sendbox" :tab="t('sendbox')">
|
<n-tab-pane v-if="openSettings.enableSendMail" name="sendbox" :tab="t('sendbox')">
|
||||||
<SendBox :fetchMailData="fetchSenboxData" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
<SendBox :fetchMailData="fetchSenboxData" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||||
|
|||||||
@@ -50,13 +50,13 @@ const fetchMails = async () => {
|
|||||||
totalCount.value = count > 0 ? count : totalCount.value;
|
totalCount.value = count > 0 ? count : totalCount.value;
|
||||||
const rawMail = results && results.length > 0 ? results[0] : null
|
const rawMail = results && results.length > 0 ? results[0] : null
|
||||||
currentMail.value = rawMail ? await processItem(rawMail) : null
|
currentMail.value = rawMail ? await processItem(rawMail) : null
|
||||||
if (openSettings.value.enableMailFlags && rawMail) {
|
if (openSettings.value.enableReadStatus && rawMail) {
|
||||||
const response = await api.fetch(`/api/mails/flags`, {
|
const response = await api.fetch(`/api/mails/read-status`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: JSON.stringify({ ids: [rawMail.id], flag: 'unread', action: 'clear' })
|
body: JSON.stringify({ ids: [rawMail.id], action: 'read' })
|
||||||
})
|
})
|
||||||
const updatedMail = response.results?.[0]
|
const updatedMail = response.results?.[0]
|
||||||
if (updatedMail) currentMail.value.mail_flags = updatedMail.mail_flags
|
if (updatedMail) currentMail.value.unread = updatedMail.unread
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch mails:', error)
|
console.error('Failed to fetch mails:', error)
|
||||||
@@ -65,18 +65,17 @@ const fetchMails = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toggleCurrentMailUnread = async () => {
|
const toggleCurrentMailUnread = async () => {
|
||||||
if (!currentMail.value || !openSettings.value.enableMailFlags) return
|
if (!currentMail.value || !openSettings.value.enableReadStatus) return
|
||||||
try {
|
try {
|
||||||
const response = await api.fetch(`/api/mails/flags`, {
|
const response = await api.fetch(`/api/mails/read-status`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
ids: [currentMail.value.id],
|
ids: [currentMail.value.id],
|
||||||
flag: 'unread',
|
|
||||||
action: 'toggle',
|
action: 'toggle',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
const updatedMail = response.results?.[0]
|
const updatedMail = response.results?.[0]
|
||||||
if (updatedMail) currentMail.value.mail_flags = updatedMail.mail_flags
|
if (updatedMail) currentMail.value.unread = updatedMail.unread
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.message || 'error')
|
message.error(error.message || 'error')
|
||||||
}
|
}
|
||||||
@@ -246,7 +245,7 @@ onBeforeUnmount(() => {
|
|||||||
<div style="margin-top: 16px;">
|
<div style="margin-top: 16px;">
|
||||||
<MailContentRenderer :mail="currentMail" :showEMailTo="false" :showReply="false"
|
<MailContentRenderer :mail="currentMail" :showEMailTo="false" :showReply="false"
|
||||||
:enableUserDeleteEmail="openSettings.enableUserDeleteEmail" :showSaveS3="false"
|
:enableUserDeleteEmail="openSettings.enableUserDeleteEmail" :showSaveS3="false"
|
||||||
:enableMailFlags="openSettings.enableMailFlags" :onToggleUnread="toggleCurrentMailUnread"
|
:enableReadStatus="openSettings.enableReadStatus" :onToggleUnread="toggleCurrentMailUnread"
|
||||||
:onDelete="deleteMail" />
|
:onDelete="deleteMail" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ const queryMail = () => {
|
|||||||
mailBoxKey.value = Date.now();
|
mailBoxKey.value = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchMailData = async (limit, offset, mailFlagFilter = 'all') => {
|
const fetchMailData = async (limit, offset, readStatus = 'all') => {
|
||||||
return await api.fetch(
|
return await api.fetch(
|
||||||
`/user_api/mails`
|
`/user_api/mails`
|
||||||
+ `?limit=${limit}`
|
+ `?limit=${limit}`
|
||||||
+ `&offset=${offset}`
|
+ `&offset=${offset}`
|
||||||
+ (mailFlagFilter === 'all' ? '' : `&read_status=${mailFlagFilter}`)
|
+ (readStatus === 'all' ? '' : `&read_status=${readStatus}`)
|
||||||
+ (addressFilter.value ? `&address=${addressFilter.value}` : '')
|
+ (addressFilter.value ? `&address=${addressFilter.value}` : '')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -51,10 +51,10 @@ const deleteMail = async (curMailId) => {
|
|||||||
await api.fetch(`/user_api/mails/${curMailId}`, { method: 'DELETE' });
|
await api.fetch(`/user_api/mails/${curMailId}`, { method: 'DELETE' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateMailFlags = async (ids, flag, action) => {
|
const updateMailReadStatus = async (ids, action) => {
|
||||||
return await api.fetch(`/user_api/mails/flags`, {
|
return await api.fetch(`/user_api/mails/read-status`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: JSON.stringify({ ids, flag, action })
|
body: JSON.stringify({ ids, action })
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ onMounted(() => {
|
|||||||
</n-input-group>
|
</n-input-group>
|
||||||
<div style="margin-top: 10px;"></div>
|
<div style="margin-top: 10px;"></div>
|
||||||
<MailBox :key="mailBoxKey" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail" :fetchMailData="fetchMailData"
|
<MailBox :key="mailBoxKey" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail" :fetchMailData="fetchMailData"
|
||||||
:deleteMail="deleteMail" :showFilterInput="true" :enableMailFlags="openSettings.enableMailFlags"
|
:deleteMail="deleteMail" :showFilterInput="true" :enableReadStatus="openSettings.enableReadStatus"
|
||||||
:updateMailFlags="updateMailFlags" />
|
:updateMailReadStatus="updateMailReadStatus" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -19,21 +19,21 @@ res = requests.get(
|
|||||||
|
|
||||||
**Note**: `/api/mails` returns raw RFC822 data by design (for example `source`/`raw`), and it does not guarantee parsed fields such as `subject`, `text`, or `html`. Parse the raw source on the client side (for example with `mail-parser-wasm` or `postal-mime`) if you need readable message content.
|
**Note**: `/api/mails` returns raw RFC822 data by design (for example `source`/`raw`), and it does not guarantee parsed fields such as `subject`, `text`, or `html`. Parse the raw source on the client side (for example with `mail-parser-wasm` or `postal-mime`) if you need readable message content.
|
||||||
|
|
||||||
## Mail Flags API
|
## Mail Read Status API
|
||||||
|
|
||||||
After enabling `ENABLE_MAIL_FLAGS` and running the database migration, each mail response includes `mail_flags`, for example `{"unread": true}`. The backend owns bitmask mapping, historical `NULL` compatibility, and state calculation; clients do not need to know bit positions.
|
After enabling `ENABLE_MAIL_FLAGS` and running the database migration, each mail response includes the boolean field `unread`. The backend owns storage, historical `NULL` compatibility, and state calculation.
|
||||||
|
|
||||||
With an Address JWT, use `PATCH /api/mails/flags` to update up to 100 mail IDs. The supported flag is currently `unread`, and its action can be `set`, `clear`, or `toggle`.
|
With an Address JWT, use `PATCH /api/mails/read-status` to update up to 100 mail IDs. The `action` can be `read`, `unread`, or `toggle`.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
requests.patch(
|
requests.patch(
|
||||||
"https://<your-worker-address>/api/mails/flags",
|
"https://<your-worker-address>/api/mails/read-status",
|
||||||
headers={"Authorization": f"Bearer {your-JWT-password}"},
|
headers={"Authorization": f"Bearer {your-JWT-password}"},
|
||||||
json={"ids": [1, 2], "flag": "unread", "action": "clear"}
|
json={"ids": [1, 2], "action": "read"}
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
With a User JWT, send the same body to `PATCH /user_api/mails/flags`. Only mail belonging to addresses bound to that user can be changed. The response contains the updated `mail_flags`, and all unrelated bits are preserved by the server.
|
With a User JWT, send the same body to `PATCH /user_api/mails/read-status`. Only mail belonging to addresses bound to that user can be changed. The response contains the updated `unread` state.
|
||||||
|
|
||||||
Mail-list endpoints use the semantic `read_status` filter. For example, list unread mail with:
|
Mail-list endpoints use the semantic `read_status` filter. For example, list unread mail with:
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ Mail-list endpoints use the semantic `read_status` filter. For example, list unr
|
|||||||
GET /api/mails?limit=20&offset=0&read_status=unread
|
GET /api/mails?limit=20&offset=0&read_status=unread
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `read_status=read` for read mail. `/user_api/mails` accepts the same parameter. Future system and custom flags will still be mapped from names to bits by the backend.
|
Use `read_status=read` for read mail. `/user_api/mails` accepts the same parameter. Future mail groups will also be returned by the backend as group definitions and mail membership; clients only render them.
|
||||||
|
|
||||||
## Admin Mail API
|
## Admin Mail API
|
||||||
|
|
||||||
|
|||||||
@@ -19,21 +19,21 @@ res = requests.get(
|
|||||||
|
|
||||||
**注意**:`/api/mails` 按设计返回的是原始 RFC822 数据(如 `source`/`raw`),不保证直接包含 `subject`、`text`、`html` 等已解析字段。若要直接读取正文,请在客户端侧解析 `raw`(例如 `mail-parser-wasm`、`postal-mime`)。
|
**注意**:`/api/mails` 按设计返回的是原始 RFC822 数据(如 `source`/`raw`),不保证直接包含 `subject`、`text`、`html` 等已解析字段。若要直接读取正文,请在客户端侧解析 `raw`(例如 `mail-parser-wasm`、`postal-mime`)。
|
||||||
|
|
||||||
## 邮件 Flag API
|
## 邮件已读状态 API
|
||||||
|
|
||||||
启用 `ENABLE_MAIL_FLAGS` 并完成数据库迁移后,邮件响应会包含 `mail_flags`,例如 `{"unread": true}`。数据库位掩码、历史 `NULL` 兼容和状态计算全部由后端处理,客户端不需要了解具体 bit。
|
启用 `ENABLE_MAIL_FLAGS` 并完成数据库迁移后,邮件响应会包含布尔字段 `unread`。数据库存储、历史 `NULL` 兼容和状态计算全部由后端处理。
|
||||||
|
|
||||||
地址 JWT 使用 `PATCH /api/mails/flags` 批量操作状态。每次最多传入 100 个邮件 ID;当前支持 `unread`,操作可为 `set`、`clear` 或 `toggle`。
|
地址 JWT 使用 `PATCH /api/mails/read-status` 批量操作状态。每次最多传入 100 个邮件 ID;`action` 可为 `read`、`unread` 或 `toggle`。
|
||||||
|
|
||||||
```python
|
```python
|
||||||
requests.patch(
|
requests.patch(
|
||||||
"https://<你的worker地址>/api/mails/flags",
|
"https://<你的worker地址>/api/mails/read-status",
|
||||||
headers={"Authorization": f"Bearer {你的JWT密码}"},
|
headers={"Authorization": f"Bearer {你的JWT密码}"},
|
||||||
json={"ids": [1, 2], "flag": "unread", "action": "clear"}
|
json={"ids": [1, 2], "action": "read"}
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
用户 JWT 使用相同请求体访问 `PATCH /user_api/mails/flags`,只能修改该用户已绑定地址的邮件。接口返回更新后的 `mail_flags`;服务端始终保留请求未涉及的其他 Flag 位。
|
用户 JWT 使用相同请求体访问 `PATCH /user_api/mails/read-status`,只能修改该用户已绑定地址的邮件。接口返回更新后的 `unread` 状态。
|
||||||
|
|
||||||
邮件列表使用语义化的 `read_status` 查询已读状态。例如查询未读邮件:
|
邮件列表使用语义化的 `read_status` 查询已读状态。例如查询未读邮件:
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ requests.patch(
|
|||||||
GET /api/mails?limit=20&offset=0&read_status=unread
|
GET /api/mails?limit=20&offset=0&read_status=unread
|
||||||
```
|
```
|
||||||
|
|
||||||
查询已读邮件使用 `read_status=read`。`/user_api/mails` 支持相同参数。后续扩展其他系统或自定义 Flag 时仍由后端负责名称到 bit 的映射。
|
查询已读邮件使用 `read_status=read`,`/user_api/mails` 支持相同参数。后续邮件分组也由后端返回分组定义和邮件归属,客户端只负责展示。
|
||||||
|
|
||||||
## admin 邮件 API
|
## admin 邮件 API
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Context } from "hono";
|
import { Context } from "hono";
|
||||||
import { handleMailListQuery } from "../common";
|
import { handleMailListQuery } from "../common";
|
||||||
import { resolveRawEmailRow } from "../gzip";
|
import { resolveRawEmailRow } from "../gzip";
|
||||||
import { serializeMailFlags } from "../mail_flags";
|
import { serializeMailState } from "../mail_flags";
|
||||||
import { getBooleanValue } from "../utils";
|
import { getBooleanValue } from "../utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -34,7 +34,7 @@ export default {
|
|||||||
).bind(id).first();
|
).bind(id).first();
|
||||||
if (!result) return c.json(null);
|
if (!result) return c.json(null);
|
||||||
const resolved = await resolveRawEmailRow(result);
|
const resolved = await resolveRawEmailRow(result);
|
||||||
return c.json(serializeMailFlags(resolved, getBooleanValue(c.env.ENABLE_MAIL_FLAGS)));
|
return c.json(serializeMailState(resolved, getBooleanValue(c.env.ENABLE_MAIL_FLAGS)));
|
||||||
},
|
},
|
||||||
deleteMail: async (c: Context<HonoCustomType>) => {
|
deleteMail: async (c: Context<HonoCustomType>) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ api.get('/open_api/settings', async (c) => {
|
|||||||
"disableAnonymousUserCreateEmail": utils.getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL),
|
"disableAnonymousUserCreateEmail": utils.getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL),
|
||||||
"disableCustomAddressName": utils.getBooleanValue(c.env.DISABLE_CUSTOM_ADDRESS_NAME),
|
"disableCustomAddressName": utils.getBooleanValue(c.env.DISABLE_CUSTOM_ADDRESS_NAME),
|
||||||
"enableUserDeleteEmail": utils.getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL),
|
"enableUserDeleteEmail": utils.getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL),
|
||||||
"enableMailFlags": utils.getBooleanValue(c.env.ENABLE_MAIL_FLAGS),
|
"enableReadStatus": utils.getBooleanValue(c.env.ENABLE_MAIL_FLAGS),
|
||||||
"enableAutoReply": utils.getBooleanValue(c.env.ENABLE_AUTO_REPLY),
|
"enableAutoReply": utils.getBooleanValue(c.env.ENABLE_AUTO_REPLY),
|
||||||
"enableIndexAbout": utils.getBooleanValue(c.env.ENABLE_INDEX_ABOUT),
|
"enableIndexAbout": utils.getBooleanValue(c.env.ENABLE_INDEX_ABOUT),
|
||||||
"copyright": c.env.COPYRIGHT,
|
"copyright": c.env.COPYRIGHT,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { unbindTelegramByAddress } from './telegram_api/common';
|
|||||||
import { CONSTANTS } from './constants';
|
import { CONSTANTS } from './constants';
|
||||||
import { AddressCreationSettings, AdminWebhookSettings, ExtractResult, WebhookMail, WebhookSettings } from './models';
|
import { AddressCreationSettings, AdminWebhookSettings, ExtractResult, WebhookMail, WebhookSettings } from './models';
|
||||||
import i18n from './i18n';
|
import i18n from './i18n';
|
||||||
import { serializeMailFlags } from './mail_flags';
|
import { serializeMailState } from './mail_flags';
|
||||||
|
|
||||||
const DEFAULT_NAME_REGEX = /[^a-z0-9]/g;
|
const DEFAULT_NAME_REGEX = /[^a-z0-9]/g;
|
||||||
const DEFAULT_RANDOM_SUBDOMAIN_LENGTH = 8;
|
const DEFAULT_RANDOM_SUBDOMAIN_LENGTH = 8;
|
||||||
@@ -722,7 +722,7 @@ export const handleMailListQuery = async (
|
|||||||
...params, limit, offset
|
...params, limit, offset
|
||||||
).all();
|
).all();
|
||||||
const resolvedResults = (await resolveRawEmailList(results)).map(row =>
|
const resolvedResults = (await resolveRawEmailList(results)).map(row =>
|
||||||
serializeMailFlags(row, getBooleanValue(c.env.ENABLE_MAIL_FLAGS))
|
serializeMailState(row, getBooleanValue(c.env.ENABLE_MAIL_FLAGS))
|
||||||
);
|
);
|
||||||
const count = offset == 0 ? await c.env.DB.prepare(
|
const count = offset == 0 ? await c.env.DB.prepare(
|
||||||
countQuery
|
countQuery
|
||||||
|
|||||||
+14
-39
@@ -10,17 +10,7 @@ export const MAIL_FLAGS = {
|
|||||||
export const CUSTOM_MAIL_FLAG_OFFSET = 10;
|
export const CUSTOM_MAIL_FLAG_OFFSET = 10;
|
||||||
export const CUSTOM_MAIL_FLAG_COUNT = 10;
|
export const CUSTOM_MAIL_FLAG_COUNT = 10;
|
||||||
|
|
||||||
const MAIL_FLAG_MASKS = {
|
type MailReadStatusAction = 'read' | 'unread' | 'toggle';
|
||||||
unread: MAIL_FLAGS.UNREAD,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
type MailFlagName = keyof typeof MAIL_FLAG_MASKS;
|
|
||||||
type MailFlagAction = 'set' | 'clear' | 'toggle';
|
|
||||||
|
|
||||||
const isMailFlagName = (value: unknown): value is MailFlagName => {
|
|
||||||
return typeof value === 'string'
|
|
||||||
&& Object.prototype.hasOwnProperty.call(MAIL_FLAG_MASKS, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCustomMailFlag = (slot: number): number => {
|
export const getCustomMailFlag = (slot: number): number => {
|
||||||
if (!Number.isInteger(slot) || slot < 0 || slot >= CUSTOM_MAIL_FLAG_COUNT) {
|
if (!Number.isInteger(slot) || slot < 0 || slot >= CUSTOM_MAIL_FLAG_COUNT) {
|
||||||
@@ -29,7 +19,7 @@ export const getCustomMailFlag = (slot: number): number => {
|
|||||||
return 1 << (CUSTOM_MAIL_FLAG_OFFSET + slot);
|
return 1 << (CUSTOM_MAIL_FLAG_OFFSET + slot);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const serializeMailFlags = <T extends Record<string, unknown>>(
|
export const serializeMailState = <T extends Record<string, unknown>>(
|
||||||
row: T,
|
row: T,
|
||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
): T => {
|
): T => {
|
||||||
@@ -39,9 +29,7 @@ export const serializeMailFlags = <T extends Record<string, unknown>>(
|
|||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
result.mail_flags = {
|
result.unread = (flags & MAIL_FLAGS.UNREAD) !== 0;
|
||||||
unread: (flags & MAIL_FLAGS.UNREAD) !== 0,
|
|
||||||
};
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,38 +67,27 @@ export const insertRawMail = async (
|
|||||||
).bind(source, address, content, messageId, flags).run();
|
).bind(source, address, content, messageId, flags).run();
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MailFlagUpdate = {
|
export type MailReadStatusUpdate = {
|
||||||
ids: number[];
|
ids: number[];
|
||||||
flag: MailFlagName;
|
|
||||||
mask: number;
|
mask: number;
|
||||||
action: MailFlagAction;
|
action: MailReadStatusAction;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MailFlagFilter = {
|
export type MailReadStatusFilter = {
|
||||||
mask: number;
|
mask: number;
|
||||||
state: 'set' | 'unset';
|
state: 'set' | 'unset';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseMailFlagFilter = (
|
|
||||||
flagValue: string | undefined,
|
|
||||||
stateValue: string | undefined,
|
|
||||||
): MailFlagFilter | undefined | null => {
|
|
||||||
if (flagValue === undefined && stateValue === undefined) return undefined;
|
|
||||||
if (!isMailFlagName(flagValue)) return null;
|
|
||||||
if (stateValue !== 'set' && stateValue !== 'unset') return null;
|
|
||||||
return { mask: MAIL_FLAG_MASKS[flagValue], state: stateValue };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const parseReadStatusFilter = (
|
export const parseReadStatusFilter = (
|
||||||
value: string | undefined,
|
value: string | undefined,
|
||||||
): MailFlagFilter | undefined | null => {
|
): MailReadStatusFilter | undefined | null => {
|
||||||
if (value === undefined || value === 'all') return undefined;
|
if (value === undefined || value === 'all') return undefined;
|
||||||
if (value === 'unread') return { mask: MAIL_FLAGS.UNREAD, state: 'set' };
|
if (value === 'unread') return { mask: MAIL_FLAGS.UNREAD, state: 'set' };
|
||||||
if (value === 'read') return { mask: MAIL_FLAGS.UNREAD, state: 'unset' };
|
if (value === 'read') return { mask: MAIL_FLAGS.UNREAD, state: 'unset' };
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseMailFlagUpdate = (value: unknown): MailFlagUpdate | null => {
|
export const parseMailReadStatusUpdate = (value: unknown): MailReadStatusUpdate | null => {
|
||||||
if (!value || typeof value !== 'object') return null;
|
if (!value || typeof value !== 'object') return null;
|
||||||
const body = value as Record<string, unknown>;
|
const body = value as Record<string, unknown>;
|
||||||
if (!Array.isArray(body.ids) || body.ids.length === 0 || body.ids.length > 100) return null;
|
if (!Array.isArray(body.ids) || body.ids.length === 0 || body.ids.length > 100) return null;
|
||||||
@@ -119,18 +96,16 @@ export const parseMailFlagUpdate = (value: unknown): MailFlagUpdate | null => {
|
|||||||
const ids = [...new Set(body.ids.map(Number))];
|
const ids = [...new Set(body.ids.map(Number))];
|
||||||
if (ids.some(id => !Number.isInteger(id) || id <= 0)) return null;
|
if (ids.some(id => !Number.isInteger(id) || id <= 0)) return null;
|
||||||
|
|
||||||
if (!isMailFlagName(body.flag)) return null;
|
if (body.action !== 'read' && body.action !== 'unread' && body.action !== 'toggle') return null;
|
||||||
if (body.action !== 'set' && body.action !== 'clear' && body.action !== 'toggle') return null;
|
|
||||||
|
|
||||||
const flag = body.flag;
|
return { ids, mask: MAIL_FLAGS.UNREAD, action: body.action };
|
||||||
return { ids, flag, mask: MAIL_FLAG_MASKS[flag], action: body.action };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMailFlagUpdateExpression = (
|
export const getMailReadStatusUpdateExpression = (
|
||||||
update: MailFlagUpdate,
|
update: MailReadStatusUpdate,
|
||||||
column = 'flags',
|
column = 'flags',
|
||||||
): { expression: string; params: number[]; condition?: string; conditionParams?: number[] } => {
|
): { expression: string; params: number[]; condition?: string; conditionParams?: number[] } => {
|
||||||
if (update.action === 'set') {
|
if (update.action === 'unread') {
|
||||||
return {
|
return {
|
||||||
expression: `(COALESCE(${column}, 0) | ?)`,
|
expression: `(COALESCE(${column}, 0) | ?)`,
|
||||||
params: [update.mask],
|
params: [update.mask],
|
||||||
@@ -138,7 +113,7 @@ export const getMailFlagUpdateExpression = (
|
|||||||
conditionParams: [update.mask],
|
conditionParams: [update.mask],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (update.action === 'clear') {
|
if (update.action === 'read') {
|
||||||
return {
|
return {
|
||||||
expression: `(COALESCE(${column}, 0) & ~?)`,
|
expression: `(COALESCE(${column}, 0) & ~?)`,
|
||||||
params: [update.mask],
|
params: [update.mask],
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ api.post('/api/attachment/get_url', s3_attachment.getSignedGetUrl)
|
|||||||
// mail crud
|
// mail crud
|
||||||
api.get('/api/mails', mails_crud.listMails)
|
api.get('/api/mails', mails_crud.listMails)
|
||||||
api.get('/api/mail/:mail_id', mails_crud.getMail)
|
api.get('/api/mail/:mail_id', mails_crud.getMail)
|
||||||
api.patch('/api/mails/flags', mails_crud.updateMailFlags)
|
api.patch('/api/mails/read-status', mails_crud.updateMailReadStatus)
|
||||||
api.delete('/api/mails/:id', mails_crud.deleteMail)
|
api.delete('/api/mails/:id', mails_crud.deleteMail)
|
||||||
|
|
||||||
// parsed mail (server-side parsed subject/text/html/attachments)
|
// parsed mail (server-side parsed subject/text/html/attachments)
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ import { handleMailListQuery, deleteAddressWithData, updateAddressUpdatedAt } fr
|
|||||||
import { resolveRawEmailRow } from '../gzip'
|
import { resolveRawEmailRow } from '../gzip'
|
||||||
import { getSendBalanceState } from './send_balance';
|
import { getSendBalanceState } from './send_balance';
|
||||||
import {
|
import {
|
||||||
getMailFlagUpdateExpression,
|
getMailReadStatusUpdateExpression,
|
||||||
parseMailFlagFilter,
|
parseMailReadStatusUpdate,
|
||||||
parseMailFlagUpdate,
|
|
||||||
parseReadStatusFilter,
|
parseReadStatusFilter,
|
||||||
serializeMailFlags,
|
serializeMailState,
|
||||||
} from '../mail_flags';
|
} from '../mail_flags';
|
||||||
|
|
||||||
const listMails = async (c: Context<HonoCustomType>) => {
|
const listMails = async (c: Context<HonoCustomType>) => {
|
||||||
@@ -18,24 +17,19 @@ const listMails = async (c: Context<HonoCustomType>) => {
|
|||||||
if (!address) {
|
if (!address) {
|
||||||
return c.json({ "error": "No address" }, 400)
|
return c.json({ "error": "No address" }, 400)
|
||||||
}
|
}
|
||||||
const { limit, offset, flag, flag_state, read_status } = c.req.query();
|
const { limit, offset, read_status } = c.req.query();
|
||||||
if (Number.parseInt(offset) <= 0) updateAddressUpdatedAt(c, address);
|
if (Number.parseInt(offset) <= 0) updateAddressUpdatedAt(c, address);
|
||||||
if (read_status !== undefined && (flag !== undefined || flag_state !== undefined)) {
|
const readStatusFilter = parseReadStatusFilter(read_status);
|
||||||
return c.json({ error: "Conflicting mail flag filters" }, 400);
|
if (readStatusFilter === null) return c.json({ error: "Invalid mail read status filter" }, 400);
|
||||||
}
|
if (readStatusFilter && !getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
||||||
const flagFilter = read_status === undefined
|
return c.json({ error: "Mail read status is disabled" }, 403);
|
||||||
? parseMailFlagFilter(flag, flag_state)
|
|
||||||
: parseReadStatusFilter(read_status);
|
|
||||||
if (flagFilter === null) return c.json({ error: "Invalid mail flag filter" }, 400);
|
|
||||||
if (flagFilter && !getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
|
||||||
return c.json({ error: "Mail flags are disabled" }, 403);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = [`address = ?`];
|
const filters = [`address = ?`];
|
||||||
const params = [address];
|
const params = [address];
|
||||||
if (flagFilter) {
|
if (readStatusFilter) {
|
||||||
filters.push(`(COALESCE(flags, 0) & ?) ${flagFilter.state === 'set' ? '!=' : '='} 0`);
|
filters.push(`(COALESCE(flags, 0) & ?) ${readStatusFilter.state === 'set' ? '!=' : '='} 0`);
|
||||||
params.push(String(flagFilter.mask));
|
params.push(String(readStatusFilter.mask));
|
||||||
}
|
}
|
||||||
const whereClause = filters.join(' AND ');
|
const whereClause = filters.join(' AND ');
|
||||||
return await handleMailListQuery(c,
|
return await handleMailListQuery(c,
|
||||||
@@ -53,7 +47,7 @@ const getMail = async (c: Context<HonoCustomType>) => {
|
|||||||
).bind(mail_id, address).first();
|
).bind(mail_id, address).first();
|
||||||
if (!result) return c.json(null);
|
if (!result) return c.json(null);
|
||||||
const resolved = await resolveRawEmailRow(result);
|
const resolved = await resolveRawEmailRow(result);
|
||||||
return c.json(serializeMailFlags(resolved, getBooleanValue(c.env.ENABLE_MAIL_FLAGS)));
|
return c.json(serializeMailState(resolved, getBooleanValue(c.env.ENABLE_MAIL_FLAGS)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteMail = async (c: Context<HonoCustomType>) => {
|
const deleteMail = async (c: Context<HonoCustomType>) => {
|
||||||
@@ -70,26 +64,26 @@ const deleteMail = async (c: Context<HonoCustomType>) => {
|
|||||||
return c.json({ success });
|
return c.json({ success });
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateMailFlags = async (c: Context<HonoCustomType>) => {
|
const updateMailReadStatus = async (c: Context<HonoCustomType>) => {
|
||||||
if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
||||||
return c.json({ error: "Mail flags are disabled" }, 403);
|
return c.json({ error: "Mail read status is disabled" }, 403);
|
||||||
}
|
}
|
||||||
const update = parseMailFlagUpdate(await c.req.json().catch(() => null));
|
const update = parseMailReadStatusUpdate(await c.req.json().catch(() => null));
|
||||||
if (!update) return c.json({ error: "Invalid mail flags request" }, 400);
|
if (!update) return c.json({ error: "Invalid mail read status request" }, 400);
|
||||||
|
|
||||||
const { address } = c.get("jwtPayload");
|
const { address } = c.get("jwtPayload");
|
||||||
const placeholders = update.ids.map(() => '?').join(',');
|
const placeholders = update.ids.map(() => '?').join(',');
|
||||||
const flagUpdate = getMailFlagUpdateExpression(update);
|
const statusUpdate = getMailReadStatusUpdateExpression(update);
|
||||||
const condition = flagUpdate.condition ? ` AND ${flagUpdate.condition}` : '';
|
const condition = statusUpdate.condition ? ` AND ${statusUpdate.condition}` : '';
|
||||||
const result = await c.env.DB.prepare(
|
const result = await c.env.DB.prepare(
|
||||||
`UPDATE raw_mails`
|
`UPDATE raw_mails`
|
||||||
+ ` SET flags = ${flagUpdate.expression}`
|
+ ` SET flags = ${statusUpdate.expression}`
|
||||||
+ ` WHERE address = ? AND id IN (${placeholders})${condition}`
|
+ ` WHERE address = ? AND id IN (${placeholders})${condition}`
|
||||||
).bind(
|
).bind(
|
||||||
...flagUpdate.params,
|
...statusUpdate.params,
|
||||||
address,
|
address,
|
||||||
...update.ids,
|
...update.ids,
|
||||||
...(flagUpdate.conditionParams ?? []),
|
...(statusUpdate.conditionParams ?? []),
|
||||||
).run();
|
).run();
|
||||||
if (!result.success) return c.json({ success: false, changes: 0, results: [] }, 500);
|
if (!result.success) return c.json({ success: false, changes: 0, results: [] }, 500);
|
||||||
|
|
||||||
@@ -99,7 +93,7 @@ const updateMailFlags = async (c: Context<HonoCustomType>) => {
|
|||||||
return c.json({
|
return c.json({
|
||||||
success: true,
|
success: true,
|
||||||
changes: result.meta.changes ?? 0,
|
changes: result.meta.changes ?? 0,
|
||||||
results: results.map(row => serializeMailFlags(row, true)),
|
results: results.map(row => serializeMailState(row, true)),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -177,6 +171,6 @@ const clearSentItems = async (c: Context<HonoCustomType>) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
listMails, getMail, deleteMail, updateMailFlags,
|
listMails, getMail, deleteMail, updateMailReadStatus,
|
||||||
getSettings, deleteAddress, clearInbox, clearSentItems
|
getSettings, deleteAddress, clearInbox, clearSentItems
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Context } from 'hono'
|
|||||||
|
|
||||||
import { commonParseMail, handleMailListQuery, updateAddressUpdatedAt } from '../common'
|
import { commonParseMail, handleMailListQuery, updateAddressUpdatedAt } from '../common'
|
||||||
import { resolveRawEmailRow } from '../gzip'
|
import { resolveRawEmailRow } from '../gzip'
|
||||||
import { serializeMailFlags } from '../mail_flags';
|
import { serializeMailState } from '../mail_flags';
|
||||||
import { getBooleanValue } from '../utils';
|
import { getBooleanValue } from '../utils';
|
||||||
|
|
||||||
const toParsedMailRow = async (row: Record<string, unknown>): Promise<Record<string, unknown>> => {
|
const toParsedMailRow = async (row: Record<string, unknown>): Promise<Record<string, unknown>> => {
|
||||||
@@ -48,7 +48,7 @@ const getParsedMail = async (c: Context<HonoCustomType>) => {
|
|||||||
).bind(mail_id, address).first();
|
).bind(mail_id, address).first();
|
||||||
if (!row) return c.json(null);
|
if (!row) return c.json(null);
|
||||||
const resolved = await resolveRawEmailRow(row);
|
const resolved = await resolveRawEmailRow(row);
|
||||||
const serialized = serializeMailFlags(resolved, getBooleanValue(c.env.ENABLE_MAIL_FLAGS));
|
const serialized = serializeMailState(resolved, getBooleanValue(c.env.ENABLE_MAIL_FLAGS));
|
||||||
return c.json(await toParsedMailRow(serialized));
|
return c.json(await toParsedMailRow(serialized));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ api.get('/user_api/settings', settings.settings);
|
|||||||
|
|
||||||
// mail api
|
// mail api
|
||||||
api.get('/user_api/mails', user_mail_api.getMails);
|
api.get('/user_api/mails', user_mail_api.getMails);
|
||||||
api.patch('/user_api/mails/flags', user_mail_api.updateMailFlags);
|
api.patch('/user_api/mails/read-status', user_mail_api.updateMailReadStatus);
|
||||||
api.delete('/user_api/mails/:id', user_mail_api.deleteMail);
|
api.delete('/user_api/mails/:id', user_mail_api.deleteMail);
|
||||||
|
|
||||||
// send mail api
|
// send mail api
|
||||||
|
|||||||
@@ -3,36 +3,30 @@ import i18n from "../i18n";
|
|||||||
import { handleMailListQuery } from "../common";
|
import { handleMailListQuery } from "../common";
|
||||||
import { getBooleanValue } from "../utils";
|
import { getBooleanValue } from "../utils";
|
||||||
import {
|
import {
|
||||||
getMailFlagUpdateExpression,
|
getMailReadStatusUpdateExpression,
|
||||||
parseMailFlagFilter,
|
parseMailReadStatusUpdate,
|
||||||
parseMailFlagUpdate,
|
|
||||||
parseReadStatusFilter,
|
parseReadStatusFilter,
|
||||||
serializeMailFlags,
|
serializeMailState,
|
||||||
} from "../mail_flags";
|
} from "../mail_flags";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getMails: async (c: Context<HonoCustomType>) => {
|
getMails: async (c: Context<HonoCustomType>) => {
|
||||||
const { user_id } = c.get("userPayload");
|
const { user_id } = c.get("userPayload");
|
||||||
const { address, limit, offset, flag, flag_state, read_status } = c.req.query();
|
const { address, limit, offset, read_status } = c.req.query();
|
||||||
const filterQuerys = [`ua.user_id = ?`];
|
const filterQuerys = [`ua.user_id = ?`];
|
||||||
const filterParams = [String(user_id)];
|
const filterParams = [String(user_id)];
|
||||||
if (address) {
|
if (address) {
|
||||||
filterQuerys.push(`rm.address = ?`);
|
filterQuerys.push(`rm.address = ?`);
|
||||||
filterParams.push(address);
|
filterParams.push(address);
|
||||||
}
|
}
|
||||||
if (read_status !== undefined && (flag !== undefined || flag_state !== undefined)) {
|
const readStatusFilter = parseReadStatusFilter(read_status);
|
||||||
return c.json({ error: "Conflicting mail flag filters" }, 400);
|
if (readStatusFilter === null) return c.json({ error: "Invalid mail read status filter" }, 400);
|
||||||
|
if (readStatusFilter && !getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
||||||
|
return c.json({ error: "Mail read status is disabled" }, 403);
|
||||||
}
|
}
|
||||||
const flagFilter = read_status === undefined
|
if (readStatusFilter) {
|
||||||
? parseMailFlagFilter(flag, flag_state)
|
filterQuerys.push(`(COALESCE(rm.flags, 0) & ?) ${readStatusFilter.state === 'set' ? '!=' : '='} 0`);
|
||||||
: parseReadStatusFilter(read_status);
|
filterParams.push(String(readStatusFilter.mask));
|
||||||
if (flagFilter === null) return c.json({ error: "Invalid mail flag filter" }, 400);
|
|
||||||
if (flagFilter && !getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
|
||||||
return c.json({ error: "Mail flags are disabled" }, 403);
|
|
||||||
}
|
|
||||||
if (flagFilter) {
|
|
||||||
filterQuerys.push(`(COALESCE(rm.flags, 0) & ?) ${flagFilter.state === 'set' ? '!=' : '='} 0`);
|
|
||||||
filterParams.push(String(flagFilter.mask));
|
|
||||||
}
|
}
|
||||||
const fromQuery = ` FROM users_address ua`
|
const fromQuery = ` FROM users_address ua`
|
||||||
+ ` JOIN address a ON a.id = ua.address_id`
|
+ ` JOIN address a ON a.id = ua.address_id`
|
||||||
@@ -63,20 +57,20 @@ export default {
|
|||||||
success: success
|
success: success
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateMailFlags: async (c: Context<HonoCustomType>) => {
|
updateMailReadStatus: async (c: Context<HonoCustomType>) => {
|
||||||
if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
|
||||||
return c.json({ error: "Mail flags are disabled" }, 403);
|
return c.json({ error: "Mail read status is disabled" }, 403);
|
||||||
}
|
}
|
||||||
const update = parseMailFlagUpdate(await c.req.json().catch(() => null));
|
const update = parseMailReadStatusUpdate(await c.req.json().catch(() => null));
|
||||||
if (!update) return c.json({ error: "Invalid mail flags request" }, 400);
|
if (!update) return c.json({ error: "Invalid mail read status request" }, 400);
|
||||||
|
|
||||||
const { user_id } = c.get("userPayload");
|
const { user_id } = c.get("userPayload");
|
||||||
const placeholders = update.ids.map(() => '?').join(',');
|
const placeholders = update.ids.map(() => '?').join(',');
|
||||||
const flagUpdate = getMailFlagUpdateExpression(update);
|
const statusUpdate = getMailReadStatusUpdateExpression(update);
|
||||||
const condition = flagUpdate.condition ? ` AND ${flagUpdate.condition}` : '';
|
const condition = statusUpdate.condition ? ` AND ${statusUpdate.condition}` : '';
|
||||||
const result = await c.env.DB.prepare(
|
const result = await c.env.DB.prepare(
|
||||||
`UPDATE raw_mails`
|
`UPDATE raw_mails`
|
||||||
+ ` SET flags = ${flagUpdate.expression}`
|
+ ` SET flags = ${statusUpdate.expression}`
|
||||||
+ ` WHERE id IN (${placeholders})`
|
+ ` WHERE id IN (${placeholders})`
|
||||||
+ ` AND EXISTS (`
|
+ ` AND EXISTS (`
|
||||||
+ `SELECT 1 FROM users_address ua`
|
+ `SELECT 1 FROM users_address ua`
|
||||||
@@ -84,10 +78,10 @@ export default {
|
|||||||
+ ` WHERE ua.user_id = ? AND a.name = raw_mails.address`
|
+ ` WHERE ua.user_id = ? AND a.name = raw_mails.address`
|
||||||
+ `)${condition}`
|
+ `)${condition}`
|
||||||
).bind(
|
).bind(
|
||||||
...flagUpdate.params,
|
...statusUpdate.params,
|
||||||
...update.ids,
|
...update.ids,
|
||||||
user_id,
|
user_id,
|
||||||
...(flagUpdate.conditionParams ?? []),
|
...(statusUpdate.conditionParams ?? []),
|
||||||
).run();
|
).run();
|
||||||
if (!result.success) return c.json({ success: false, changes: 0, results: [] }, 500);
|
if (!result.success) return c.json({ success: false, changes: 0, results: [] }, 500);
|
||||||
|
|
||||||
@@ -103,7 +97,7 @@ export default {
|
|||||||
return c.json({
|
return c.json({
|
||||||
success: true,
|
success: true,
|
||||||
changes: result.meta.changes ?? 0,
|
changes: result.meta.changes ?? 0,
|
||||||
results: results.map(row => serializeMailFlags(row, true)),
|
results: results.map(row => serializeMailState(row, true)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user