refactor: serve mail states from backend

This commit is contained in:
dreamhunter2333
2026-08-26 00:05:02 +08:00
parent b5d8f64320
commit 04138ebcdb
18 changed files with 291 additions and 155 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
### Features ### Features
- feat: |邮件状态| 新增可选邮件状态功能,新邮件支持已读/未读、打开自动已读、手动切换状态、本页全部已读及按状态筛选;前端仅使用已读状态,底层 Flag 存储、计算与后续分组归属统一由后端处理 - feat: |邮件状态| 新增可选邮件状态功能,新邮件支持已读/未读、打开自动已读、手动切换状态、本页全部已读及按状态筛选;后端统一返回系统状态枚举与自定义分组,前端直接使用返回值显示和移动邮件
- feat: |Admin| 数据库页面新增 D1 存储容量展示,支持选择并保存 Free 或 Workers Paid 套餐,对比当前数据库大小和容量上限 - feat: |Admin| 数据库页面新增 D1 存储容量展示,支持选择并保存 Free 或 Workers Paid 套餐,对比当前数据库大小和容量上限
- feat: |用户系统| 用户中心新增发送邮件、与收件箱一致的可按绑定地址过滤的发件箱,以及地址管理凭证弹框;提供使用用户 JWT 的地址设置、发信权限申请、发信及发件箱 API - feat: |用户系统| 用户中心新增发送邮件、与收件箱一致的可按绑定地址过滤的发件箱,以及地址管理凭证弹框;提供使用用户 JWT 的地址设置、发信权限申请、发信及发件箱 API
+1 -1
View File
@@ -10,7 +10,7 @@
### Features ### Features
- 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: |Mail State| Add optional read state for new mail, automatic read-on-open, manual state toggling, mark-current-page-read and state filters; the backend returns the combined system-state enum and custom groups for clients to render and use as move targets
- 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
+20 -13
View File
@@ -12,6 +12,13 @@ test.describe('Mail Read Status', () => {
const second = await createTestAddress(request, 'mail-flags-second'); const second = await createTestAddress(request, 'mail-flags-second');
try { try {
const statesRes = await request.get(`${WORKER_URL}/api/mail-states`, {
headers: { Authorization: `Bearer ${first.jwt}` },
});
expect(statesRes.ok()).toBe(true);
expect((await statesRes.json()).results.map((state: { value: string }) => state.value))
.toEqual(['all', 'unread', 'read']);
await seedTestMail(request, first.address, { subject: 'Unread mail' }); await seedTestMail(request, first.address, { subject: 'Unread mail' });
const listRes = await request.get(`${WORKER_URL}/api/mails?limit=10&offset=0`, { const listRes = await request.get(`${WORKER_URL}/api/mails?limit=10&offset=0`, {
headers: { Authorization: `Bearer ${first.jwt}` }, headers: { Authorization: `Bearer ${first.jwt}` },
@@ -23,21 +30,21 @@ test.describe('Mail Read Status', () => {
expect(results[0].unread).toBe(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&mail_state=unread`,
{ headers: { Authorization: `Bearer ${first.jwt}` } }, { headers: { Authorization: `Bearer ${first.jwt}` } },
); );
expect((await unreadRes.json()).results).toHaveLength(1); expect((await unreadRes.json()).results).toHaveLength(1);
const deniedRes = await request.patch(`${WORKER_URL}/api/mails/read-status`, { const deniedRes = await request.patch(`${WORKER_URL}/api/mails/state`, {
headers: { Authorization: `Bearer ${second.jwt}` }, headers: { Authorization: `Bearer ${second.jwt}` },
data: { ids: [results[0].id], action: 'read' }, data: { ids: [results[0].id], state: '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/read-status`, { const updateRes = await request.patch(`${WORKER_URL}/api/mails/state`, {
headers: { Authorization: `Bearer ${first.jwt}` }, headers: { Authorization: `Bearer ${first.jwt}` },
data: { ids: [results[0].id], action: 'read' }, data: { ids: [results[0].id], state: 'read' },
}); });
expect(updateRes.ok()).toBe(true); expect(updateRes.ok()).toBe(true);
const updateResult = await updateRes.json(); const updateResult = await updateRes.json();
@@ -50,31 +57,31 @@ test.describe('Mail Read Status', () => {
expect((await updatedListRes.json()).results[0].unread).toBe(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&mail_state=unread`,
{ headers: { Authorization: `Bearer ${first.jwt}` } }, { headers: { Authorization: `Bearer ${first.jwt}` } },
); );
expect((await unreadAfterUpdateRes.json()).results).toHaveLength(0); expect((await unreadAfterUpdateRes.json()).results).toHaveLength(0);
const toggleRes = await request.patch(`${WORKER_URL}/api/mails/read-status`, { const unreadStateRes = await request.patch(`${WORKER_URL}/api/mails/state`, {
headers: { Authorization: `Bearer ${first.jwt}` }, headers: { Authorization: `Bearer ${first.jwt}` },
data: { ids: [results[0].id], action: 'toggle' }, data: { ids: [results[0].id], state: 'unread' },
}); });
expect(toggleRes.ok()).toBe(true); expect(unreadStateRes.ok()).toBe(true);
expect((await toggleRes.json()).results[0].unread).toBe(true); expect((await unreadStateRes.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 read-status actions', async ({ request }) => { test('rejects unsupported mail states', 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], action: 'invalid' }, { ids: [1], state: 'invalid' },
{ ids: [1] }, { ids: [1] },
]) { ]) {
const res = await request.patch(`${WORKER_URL}/api/mails/read-status`, { const res = await request.patch(`${WORKER_URL}/api/mails/state`, {
headers: { Authorization: `Bearer ${jwt}` }, headers: { Authorization: `Bearer ${jwt}` },
data, data,
}); });
+39 -24
View File
@@ -55,20 +55,26 @@ const props = defineProps({
default: false, default: false,
required: false required: false
}, },
enableReadStatus: { enableMailStates: {
type: Boolean, type: Boolean,
default: false, default: false,
required: false required: false
}, },
updateMailReadStatus: { updateMailState: {
type: Function, type: Function,
default: () => { }, default: () => { },
required: false required: false
}, },
fetchMailStates: {
type: Function,
default: () => ({ results: [] }),
required: false
},
}) })
const localFilterKeyword = ref('') const localFilterKeyword = ref('')
const readStatusFilter = ref('all') const mailStateFilter = ref(null)
const mailStates = ref([])
const { const {
isDark, mailboxSplitSize, mailListView, mailListPreviewLineClamp, indexTab, loading, useUTCDate, isDark, mailboxSplitSize, mailListView, mailListPreviewLineClamp, indexTab, loading, useUTCDate,
@@ -106,20 +112,23 @@ const data = computed(() => {
}) })
const isMailUnread = (mail) => { const isMailUnread = (mail) => {
return props.enableReadStatus && mail?.unread === true return props.enableMailStates && mail?.unread === true
} }
const currentPageHasUnread = computed(() => rawData.value.some(isMailUnread)) const currentPageHasUnread = computed(() => rawData.value.some(isMailUnread))
const readStatusFilterOptions = computed(() => [ const mailStateFilterOptions = computed(() => mailStates.value.map(state => ({
{ label: t('allMail'), value: 'all' }, label: state.label || t(state.label_key),
{ label: t('unread'), value: 'unread' }, value: state.value,
{ label: t('read'), value: 'read' }, })))
])
const updateUnreadState = async (mails, action) => { const getReadStateValue = (unread) => {
if (mails.length === 0) return true return mailStates.value.find(state => state.unread === unread)?.value
}
const updateUnreadState = async (mails, state) => {
if (mails.length === 0 || !state) return true
try { try {
const response = await props.updateMailReadStatus(mails.map(mail => mail.id), action) const response = await props.updateMailState(mails.map(mail => mail.id), state)
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 => {
@@ -133,11 +142,11 @@ const updateUnreadState = async (mails, action) => {
} }
} }
const markMailsRead = async (mails) => updateUnreadState(mails, 'read') const markMailsRead = async (mails) => updateUnreadState(mails, getReadStateValue(false))
const toggleCurrentMailUnread = async () => { const toggleCurrentMailUnread = async () => {
if (!curMail.value) return if (!curMail.value) return
await updateUnreadState([curMail.value], 'toggle') await updateUnreadState([curMail.value], getReadStateValue(!curMail.value.unread))
} }
const openMail = async (mail) => { const openMail = async (mail) => {
@@ -148,7 +157,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 (readStatusFilter.value === 'unread') await refresh() if (mailStateFilter.value === getReadStateValue(true)) await refresh()
} }
const canGoPrevMail = computed(() => { const canGoPrevMail = computed(() => {
@@ -232,14 +241,15 @@ watch([page, pageSize], async ([page, pageSize], [oldPage, oldPageSize]) => {
} }
}) })
watch(readStatusFilter, async () => { watch(mailStateFilter, async (_value, oldValue) => {
if (oldValue === null) return
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, readStatusFilter.value pageSize.value, (page.value - 1) * pageSize.value, mailStateFilter.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) => {
@@ -388,6 +398,11 @@ const multiActionDownload = async () => {
} }
onMounted(async () => { onMounted(async () => {
if (props.enableMailStates) {
const { results = [] } = await props.fetchMailStates()
mailStates.value = results
mailStateFilter.value = results.find(state => state.default)?.value ?? results[0]?.value ?? null
}
await refresh(); await refresh();
}); });
@@ -440,10 +455,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="enableReadStatus && currentPageHasUnread" @click="markCurrentPageRead" tertiary> <n-button v-if="enableMailStates && currentPageHasUnread" @click="markCurrentPageRead" tertiary>
{{ t('markCurrentPageRead') }} {{ t('markCurrentPageRead') }}
</n-button> </n-button>
<n-select v-if="enableReadStatus" v-model:value="readStatusFilter" :options="readStatusFilterOptions" <n-select v-if="enableMailStates" v-model:value="mailStateFilter" :options="mailStateFilterOptions"
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 +543,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"
:enableReadStatus="enableReadStatus" :onToggleUnread="toggleCurrentMailUnread" :enableMailStates="enableMailStates" :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 +615,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="enableReadStatus && currentPageHasUnread" @click="markCurrentPageRead" tertiary size="small"> <n-button v-if="enableMailStates && currentPageHasUnread" @click="markCurrentPageRead" tertiary size="small">
{{ t('markCurrentPageRead') }} {{ t('markCurrentPageRead') }}
</n-button> </n-button>
</n-space> </n-space>
@@ -608,8 +623,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="enableReadStatus" style="padding: 0 10px; margin-bottom: 10px;"> <div v-if="enableMailStates" style="padding: 0 10px; margin-bottom: 10px;">
<n-select v-model:value="readStatusFilter" :options="readStatusFilterOptions" size="small" /> <n-select v-model:value="mailStateFilter" :options="mailStateFilterOptions" 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 +664,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"
:enableReadStatus="enableReadStatus" :onToggleUnread="toggleCurrentMailUnread" :enableMailStates="enableMailStates" :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
}, },
enableReadStatus: { enableMailStates: {
type: Boolean, type: Boolean,
default: false default: false
}, },
@@ -154,7 +154,7 @@ const handleSaveToS3 = async (filename, blob) => {
{{ t('downloadMail') }} {{ t('downloadMail') }}
</n-button> </n-button>
<n-button v-if="enableReadStatus" size="small" tertiary type="info" @click="onToggleUnread"> <n-button v-if="enableMailStates" size="small" tertiary type="info" @click="onToggleUnread">
{{ mail.unread ? t('markRead') : t('markUnread') }} {{ mail.unread ? t('markRead') : t('markUnread') }}
</n-button> </n-button>
+1 -1
View File
@@ -24,7 +24,7 @@ export const useGlobalState = createGlobalState(
disableAnonymousUserCreateEmail: false, disableAnonymousUserCreateEmail: false,
disableCustomAddressName: false, disableCustomAddressName: false,
enableUserDeleteEmail: false, enableUserDeleteEmail: false,
enableReadStatus: false, enableMailStates: false,
enableAutoReply: false, enableAutoReply: false,
enableIndexAbout: false, enableIndexAbout: false,
/** @type {string[]} */ /** @type {string[]} */
+12 -7
View File
@@ -32,15 +32,15 @@ const SendMail = defineAsyncComponent(() => {
const { t } = useScopedI18n('views.Index') const { t } = useScopedI18n('views.Index')
const fetchMailData = async (limit, offset, readStatus = 'all') => { const fetchMailData = async (limit, offset, mailState) => {
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 = readStatus === 'all' ? '' : `&read_status=${readStatus}` const mailStateQuery = mailState ? `&mail_state=${encodeURIComponent(mailState)}` : ''
return await api.fetch( return await api.fetch(
`/api/mails?limit=${limit}&offset=${offset}${readStatusQuery}` `/api/mails?limit=${limit}&offset=${offset}${mailStateQuery}`
); );
}; };
@@ -48,13 +48,17 @@ const deleteMail = async (curMailId) => {
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' }); await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
}; };
const updateMailReadStatus = async (ids, action) => { const updateMailState = async (ids, state) => {
return await api.fetch(`/api/mails/read-status`, { return await api.fetch(`/api/mails/state`, {
method: 'PATCH', method: 'PATCH',
body: JSON.stringify({ ids, action }) body: JSON.stringify({ ids, state })
}); });
}; };
const fetchMailStates = async () => {
return await api.fetch(`/api/mail-states`)
}
const deleteSenboxMail = async (curMailId) => { const deleteSenboxMail = async (curMailId) => {
await api.fetch(`/api/sendbox/${curMailId}`, { method: 'DELETE' }); await api.fetch(`/api/sendbox/${curMailId}`, { method: 'DELETE' });
}; };
@@ -138,7 +142,8 @@ 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"
:enableReadStatus="openSettings.enableReadStatus" :updateMailReadStatus="updateMailReadStatus" /> :enableMailStates="openSettings.enableMailStates" :updateMailState="updateMailState"
:fetchMailStates="fetchMailStates" />
</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"
+20 -7
View File
@@ -26,12 +26,17 @@ const message = useMessage()
const currentPage = ref(1) const currentPage = ref(1)
const totalCount = ref(0) const totalCount = ref(0)
const currentMail = ref(null) const currentMail = ref(null)
const mailStates = ref([])
const showAccountSettingsCard = ref(false) const showAccountSettingsCard = ref(false)
const currentAutoRefreshInterval = ref(60) const currentAutoRefreshInterval = ref(60)
const timer = ref(null) const timer = ref(null)
const { t } = useScopedI18n('views.index.SimpleIndex') const { t } = useScopedI18n('views.index.SimpleIndex')
const getReadStateValue = (unread) => {
return mailStates.value.find(state => state.unread === unread)?.value
}
// 复制地址 // 复制地址
const copyAddress = async () => { const copyAddress = async () => {
try { try {
@@ -50,10 +55,12 @@ 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.enableReadStatus && rawMail) { if (openSettings.value.enableMailStates && rawMail) {
const response = await api.fetch(`/api/mails/read-status`, { const state = getReadStateValue(false)
if (!state) return
const response = await api.fetch(`/api/mails/state`, {
method: 'PATCH', method: 'PATCH',
body: JSON.stringify({ ids: [rawMail.id], action: 'read' }) body: JSON.stringify({ ids: [rawMail.id], state })
}) })
const updatedMail = response.results?.[0] const updatedMail = response.results?.[0]
if (updatedMail) currentMail.value.unread = updatedMail.unread if (updatedMail) currentMail.value.unread = updatedMail.unread
@@ -65,13 +72,15 @@ const fetchMails = async () => {
} }
const toggleCurrentMailUnread = async () => { const toggleCurrentMailUnread = async () => {
if (!currentMail.value || !openSettings.value.enableReadStatus) return if (!currentMail.value || !openSettings.value.enableMailStates) return
try { try {
const response = await api.fetch(`/api/mails/read-status`, { const state = getReadStateValue(!currentMail.value.unread)
if (!state) return
const response = await api.fetch(`/api/mails/state`, {
method: 'PATCH', method: 'PATCH',
body: JSON.stringify({ body: JSON.stringify({
ids: [currentMail.value.id], ids: [currentMail.value.id],
action: 'toggle', state,
}) })
}) })
const updatedMail = response.results?.[0] const updatedMail = response.results?.[0]
@@ -131,6 +140,10 @@ watch(currentPage, () => {
onMounted(async () => { onMounted(async () => {
await api.getSettings() await api.getSettings()
if (openSettings.value.enableMailStates) {
const { results = [] } = await api.fetch(`/api/mail-states`)
mailStates.value = results
}
await fetchMails() await fetchMails()
// 启动自动刷新 // 启动自动刷新
@@ -245,7 +258,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"
:enableReadStatus="openSettings.enableReadStatus" :onToggleUnread="toggleCurrentMailUnread" :enableMailStates="openSettings.enableMailStates" :onToggleUnread="toggleCurrentMailUnread"
:onDelete="deleteMail" /> :onDelete="deleteMail" />
</div> </div>
</div> </div>
+11 -7
View File
@@ -20,12 +20,12 @@ const queryMail = () => {
mailBoxKey.value = Date.now(); mailBoxKey.value = Date.now();
} }
const fetchMailData = async (limit, offset, readStatus = 'all') => { const fetchMailData = async (limit, offset, mailState) => {
return await api.fetch( return await api.fetch(
`/user_api/mails` `/user_api/mails`
+ `?limit=${limit}` + `?limit=${limit}`
+ `&offset=${offset}` + `&offset=${offset}`
+ (readStatus === 'all' ? '' : `&read_status=${readStatus}`) + (mailState ? `&mail_state=${encodeURIComponent(mailState)}` : '')
+ (addressFilter.value ? `&address=${addressFilter.value}` : '') + (addressFilter.value ? `&address=${addressFilter.value}` : '')
); );
} }
@@ -51,13 +51,17 @@ const deleteMail = async (curMailId) => {
await api.fetch(`/user_api/mails/${curMailId}`, { method: 'DELETE' }); await api.fetch(`/user_api/mails/${curMailId}`, { method: 'DELETE' });
}; };
const updateMailReadStatus = async (ids, action) => { const updateMailState = async (ids, state) => {
return await api.fetch(`/user_api/mails/read-status`, { return await api.fetch(`/user_api/mails/state`, {
method: 'PATCH', method: 'PATCH',
body: JSON.stringify({ ids, action }) body: JSON.stringify({ ids, state })
}); });
}; };
const fetchMailStates = async () => {
return await api.fetch(`/user_api/mail-states`)
}
watch(addressFilter, async (newValue) => { watch(addressFilter, async (newValue) => {
queryMail(); queryMail();
}); });
@@ -78,7 +82,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" :enableReadStatus="openSettings.enableReadStatus" :deleteMail="deleteMail" :showFilterInput="true" :enableMailStates="openSettings.enableMailStates"
:updateMailReadStatus="updateMailReadStatus" /> :updateMailState="updateMailState" :fetchMailStates="fetchMailStates" />
</div> </div>
</template> </template>
@@ -19,29 +19,31 @@ 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 Read Status API ## Mail State API
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. 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/read-status` to update up to 100 mail IDs. The `action` can be `read`, `unread`, or `toggle`. With an Address JWT, use `GET /api/mail-states` to retrieve available states. The backend combines its system-state enum with address-specific custom states. The frontend uses each returned `value` directly for filtering and moving, and displays its `label_key` or `label`.
Use `PATCH /api/mails/state` to move the state of up to 100 mail IDs:
```python ```python
requests.patch( requests.patch(
"https://<your-worker-address>/api/mails/read-status", "https://<your-worker-address>/api/mails/state",
headers={"Authorization": f"Bearer {your-JWT-password}"}, headers={"Authorization": f"Bearer {your-JWT-password}"},
json={"ids": [1, 2], "action": "read"} json={"ids": [1, 2], "state": "read"}
) )
``` ```
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. With a User JWT, use `GET /user_api/mail-states` and `PATCH /user_api/mails/state`. 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 accept a state `value` returned by the backend. For example, list unread mail with:
```text ```text
GET /api/mails?limit=20&offset=0&read_status=unread GET /api/mails?limit=20&offset=0&mail_state=unread
``` ```
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. `/user_api/mails` accepts the same parameter. Future custom groups only require the backend to append custom states and resolve their values; clients do not add another enum.
## Admin Mail API ## Admin Mail API
@@ -19,29 +19,31 @@ 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`)。
## 邮件已读状态 API ## 邮件状态 API
启用 `ENABLE_MAIL_FLAGS` 并完成数据库迁移后,邮件响应会包含布尔字段 `unread`。数据库存储、历史 `NULL` 兼容和状态计算全部由后端处理。 启用 `ENABLE_MAIL_FLAGS` 并完成数据库迁移后,邮件响应会包含布尔字段 `unread`。数据库存储、历史 `NULL` 兼容和状态计算全部由后端处理。
地址 JWT 使用 `PATCH /api/mails/read-status` 批量操作状态。每次最多传入 100 个邮件 ID;`action` 可为 `read``unread``toggle` 地址 JWT 使用 `GET /api/mail-states` 获取状态列表。后端将系统状态枚举与地址自定义状态合并后返回;前端直接使用其中的 `value` 作为筛选和移动参数,并使用 `label_key``label` 显示名称
使用 `PATCH /api/mails/state` 批量移动邮件状态,每次最多传入 100 个邮件 ID:
```python ```python
requests.patch( requests.patch(
"https://<你的worker地址>/api/mails/read-status", "https://<你的worker地址>/api/mails/state",
headers={"Authorization": f"Bearer {你的JWT密码}"}, headers={"Authorization": f"Bearer {你的JWT密码}"},
json={"ids": [1, 2], "action": "read"} json={"ids": [1, 2], "state": "read"}
) )
``` ```
用户 JWT 使用相同请求体访问 `PATCH /user_api/mails/read-status`,只能修改该用户已绑定地址的邮件。接口返回更新后的 `unread` 状态。 用户 JWT 使用 `GET /user_api/mail-states` `PATCH /user_api/mails/state`,只能修改该用户已绑定地址的邮件。接口返回更新后的 `unread` 状态。
邮件列表使用语义化的 `read_status` 查询已读状态。例如查询未读邮件: 邮件列表使用后端返回的状态 `value` 查询。例如查询未读邮件:
```text ```text
GET /api/mails?limit=20&offset=0&read_status=unread GET /api/mails?limit=20&offset=0&mail_state=unread
``` ```
查询已读邮件使用 `read_status=read``/user_api/mails` 支持相同参数。后续邮件分组也由后端返回分组定义和邮件归属,客户端只负责展示 `/user_api/mails` 支持相同参数。后续增加自定义分组时,只需由后端将自定义状态拼接到状态列表并解析其 `value`,客户端无需增加枚举
## admin 邮件 API ## admin 邮件 API
+1 -1
View File
@@ -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),
"enableReadStatus": utils.getBooleanValue(c.env.ENABLE_MAIL_FLAGS), "enableMailStates": 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,
+4 -4
View File
@@ -37,11 +37,11 @@ export async function resolveRawEmail(row: RawMailRow): Promise<string> {
*/ */
export async function resolveRawEmailRow( export async function resolveRawEmailRow(
row: RawMailRow, row: RawMailRow,
enableReadStatus = false, enableMailStates = false,
): Promise<RawMailRow> { ): Promise<RawMailRow> {
const raw = await resolveRawEmail(row); const raw = await resolveRawEmail(row);
const { raw_blob: _, ...rest } = row; const { raw_blob: _, ...rest } = row;
return serializeMailState({ ...rest, raw }, enableReadStatus); return serializeMailState({ ...rest, raw }, enableMailStates);
} }
/** /**
@@ -49,7 +49,7 @@ export async function resolveRawEmailRow(
*/ */
export async function resolveRawEmailList( export async function resolveRawEmailList(
rows: RawMailRow[], rows: RawMailRow[],
enableReadStatus = false, enableMailStates = false,
): Promise<RawMailRow[]> { ): Promise<RawMailRow[]> {
return Promise.all(rows.map(row => resolveRawEmailRow(row, enableReadStatus))); return Promise.all(rows.map(row => resolveRawEmailRow(row, enableMailStates)));
} }
+111 -40
View File
@@ -10,7 +10,59 @@ 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;
type MailReadStatusAction = 'read' | 'unread' | 'toggle'; export enum MailState {
ALL = 'all',
UNREAD = 'unread',
READ = 'read',
}
export type MailStateOption = {
value: string;
label_key?: string;
label?: string;
unread?: boolean;
default?: boolean;
};
export type MailStateDefinition = MailStateOption & {
filter?: { mask: number; set: boolean };
mutation?: { add: number; remove: number };
};
const SYSTEM_MAIL_STATES: MailStateDefinition[] = [
{ value: MailState.ALL, label_key: 'allMail', default: true },
{
value: MailState.UNREAD,
label_key: 'unread',
unread: true,
filter: { mask: MAIL_FLAGS.UNREAD, set: true },
mutation: { add: MAIL_FLAGS.UNREAD, remove: 0 },
},
{
value: MailState.READ,
label_key: 'read',
unread: false,
filter: { mask: MAIL_FLAGS.UNREAD, set: false },
mutation: { add: 0, remove: MAIL_FLAGS.UNREAD },
},
];
export const getMailStateOptions = (
customStates: MailStateDefinition[] = [],
): MailStateOption[] => {
return [...SYSTEM_MAIL_STATES, ...customStates].map(state => {
const { filter: _filter, mutation: _mutation, ...option } = state;
return option;
});
};
const getMailStateDefinition = (
value: unknown,
customStates: MailStateDefinition[] = [],
) => {
if (typeof value !== 'string') return undefined;
return [...SYSTEM_MAIL_STATES, ...customStates].find(state => state.value === 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) {
@@ -19,6 +71,27 @@ export const getCustomMailFlag = (slot: number): number => {
return 1 << (CUSTOM_MAIL_FLAG_OFFSET + slot); return 1 << (CUSTOM_MAIL_FLAG_OFFSET + slot);
}; };
export type CustomMailStateConfig = {
slot: number;
name: string;
};
const CUSTOM_MAIL_FLAGS_MASK = ((1 << CUSTOM_MAIL_FLAG_COUNT) - 1) << CUSTOM_MAIL_FLAG_OFFSET;
export const createCustomMailStateDefinitions = (
configs: CustomMailStateConfig[],
): MailStateDefinition[] => {
return configs.map(config => {
const flag = getCustomMailFlag(config.slot);
return {
value: `custom:${config.slot}`,
label: config.name,
filter: { mask: flag, set: true },
mutation: { add: flag, remove: CUSTOM_MAIL_FLAGS_MASK & ~flag },
};
});
};
export const serializeMailState = <T extends Record<string, unknown>>( export const serializeMailState = <T extends Record<string, unknown>>(
row: T, row: T,
enabled: boolean, enabled: boolean,
@@ -54,10 +127,11 @@ export const updateInitialMailFlags = async (
await db.prepare(`UPDATE raw_mails SET flags = ? WHERE id = ?`).bind(flags, mailId).run(); await db.prepare(`UPDATE raw_mails SET flags = ? WHERE id = ?`).bind(flags, mailId).run();
}; };
export type MailReadStatusUpdate = { export type MailStateUpdate = {
ids: number[]; ids: number[];
mask: number; state: string;
action: MailReadStatusAction; add: number;
remove: number;
}; };
export type MailReadStatusQuery = { export type MailReadStatusQuery = {
@@ -65,21 +139,26 @@ export type MailReadStatusQuery = {
params: string[]; params: string[];
}; };
export const getReadStatusQuery = ( export const getMailStateQuery = (
value: string | undefined, value: string | undefined,
column: 'flags' | 'rm.flags', column: 'flags' | 'rm.flags',
customStates: MailStateDefinition[] = [],
): MailReadStatusQuery | undefined | null => { ): MailReadStatusQuery | undefined | null => {
if (value === undefined || value === 'all') return undefined; if (value === undefined) return undefined;
if (value === 'unread') { const definition = getMailStateDefinition(value, customStates);
return { clause: `(COALESCE(${column}, 0) & ?) != 0`, params: [String(MAIL_FLAGS.UNREAD)] }; if (!definition) return null;
} if (!definition.filter) return undefined;
if (value === 'read') { const operator = definition.filter.set ? '!=' : '=';
return { clause: `(COALESCE(${column}, 0) & ?) = 0`, params: [String(MAIL_FLAGS.UNREAD)] }; return {
} clause: `(COALESCE(${column}, 0) & ?) ${operator} 0`,
return null; params: [String(definition.filter.mask)],
};
}; };
const parseMailReadStatusUpdate = (value: unknown): MailReadStatusUpdate | null => { const parseMailStateUpdate = (
value: unknown,
customStates: MailStateDefinition[] = [],
): MailStateUpdate | 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;
@@ -88,34 +167,25 @@ const parseMailReadStatusUpdate = (value: unknown): MailReadStatusUpdate | 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 (body.action !== 'read' && body.action !== 'unread' && body.action !== 'toggle') return null; const definition = getMailStateDefinition(body.state, customStates);
if (!definition?.mutation) return null;
return { ids, mask: MAIL_FLAGS.UNREAD, action: body.action }; return {
ids,
state: definition.value,
add: definition.mutation.add,
remove: definition.mutation.remove,
};
}; };
const getMailReadStatusUpdateExpression = ( const getMailStateUpdateExpression = (
update: MailReadStatusUpdate, update: MailStateUpdate,
column = 'flags', column = 'flags',
): { expression: string; params: number[]; condition?: string; conditionParams?: number[] } => { ): { expression: string; params: number[]; condition?: string; conditionParams?: number[] } => {
if (update.action === 'unread') {
return { return {
expression: `(COALESCE(${column}, 0) | ?)`, expression: `((COALESCE(${column}, 0) | ?) & ~?)`,
params: [update.mask], params: [update.add, update.remove],
condition: `(COALESCE(${column}, 0) & ?) = 0`, condition: `((COALESCE(${column}, 0) & ?) != ? OR (COALESCE(${column}, 0) & ?) != 0)`,
conditionParams: [update.mask], conditionParams: [update.add, update.add, update.remove],
};
}
if (update.action === 'read') {
return {
expression: `(COALESCE(${column}, 0) & ~?)`,
params: [update.mask],
condition: `(COALESCE(${column}, 0) & ?) != 0`,
conditionParams: [update.mask],
};
}
return {
expression: `((COALESCE(${column}, 0) | ?) - (COALESCE(${column}, 0) & ?))`,
params: [update.mask, update.mask],
}; };
}; };
@@ -124,16 +194,17 @@ type MailScope = {
params: (string | number)[]; params: (string | number)[];
}; };
export const applyMailReadStatusUpdate = async ( export const applyMailStateUpdate = async (
db: D1Database, db: D1Database,
scope: MailScope, scope: MailScope,
value: unknown, value: unknown,
customStates: MailStateDefinition[] = [],
) => { ) => {
const update = parseMailReadStatusUpdate(value); const update = parseMailStateUpdate(value, customStates);
if (!update) return null; if (!update) return null;
const placeholders = update.ids.map(() => '?').join(','); const placeholders = update.ids.map(() => '?').join(',');
const statusUpdate = getMailReadStatusUpdateExpression(update); const statusUpdate = getMailStateUpdateExpression(update);
const condition = statusUpdate.condition ? ` AND ${statusUpdate.condition}` : ''; const condition = statusUpdate.condition ? ` AND ${statusUpdate.condition}` : '';
const result = await db.prepare( const result = await db.prepare(
`UPDATE raw_mails SET flags = ${statusUpdate.expression}` `UPDATE raw_mails SET flags = ${statusUpdate.expression}`
+2 -1
View File
@@ -27,8 +27,9 @@ 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-states', mails_crud.getMailStates)
api.get('/api/mail/:mail_id', mails_crud.getMail) api.get('/api/mail/:mail_id', mails_crud.getMail)
api.patch('/api/mails/read-status', mails_crud.updateMailReadStatus) api.patch('/api/mails/state', mails_crud.updateMailState)
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)
+23 -15
View File
@@ -6,8 +6,9 @@ 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 {
getReadStatusQuery, getMailStateQuery,
applyMailReadStatusUpdate, getMailStateOptions,
applyMailStateUpdate,
} from '../mail_flags'; } from '../mail_flags';
const listMails = async (c: Context<HonoCustomType>) => { const listMails = async (c: Context<HonoCustomType>) => {
@@ -15,19 +16,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, read_status } = c.req.query(); const { limit, offset, mail_state } = c.req.query();
if (Number.parseInt(offset) <= 0) updateAddressUpdatedAt(c, address); if (Number.parseInt(offset) <= 0) updateAddressUpdatedAt(c, address);
const readStatusQuery = getReadStatusQuery(read_status, 'flags'); const stateQuery = getMailStateQuery(mail_state, 'flags');
if (readStatusQuery === null) return c.json({ error: "Invalid mail read status filter" }, 400); if (stateQuery === null) return c.json({ error: "Invalid mail state filter" }, 400);
if (readStatusQuery && !getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) { if (stateQuery && !getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
return c.json({ error: "Mail read status is disabled" }, 403); return c.json({ error: "Mail states are disabled" }, 403);
} }
const filters = [`address = ?`]; const filters = [`address = ?`];
const params = [address]; const params = [address];
if (readStatusQuery) { if (stateQuery) {
filters.push(readStatusQuery.clause); filters.push(stateQuery.clause);
params.push(...readStatusQuery.params); params.push(...stateQuery.params);
} }
const whereClause = filters.join(' AND '); const whereClause = filters.join(' AND ');
return await handleMailListQuery(c, return await handleMailListQuery(c,
@@ -64,21 +65,28 @@ const deleteMail = async (c: Context<HonoCustomType>) => {
return c.json({ success }); return c.json({ success });
}; };
const updateMailReadStatus = async (c: Context<HonoCustomType>) => { const updateMailState = async (c: Context<HonoCustomType>) => {
if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) { if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
return c.json({ error: "Mail read status is disabled" }, 403); return c.json({ error: "Mail states are disabled" }, 403);
} }
const { address } = c.get("jwtPayload"); const { address } = c.get("jwtPayload");
const result = await applyMailReadStatusUpdate( const result = await applyMailStateUpdate(
c.env.DB, c.env.DB,
{ clause: 'address = ?', params: [address] }, { clause: 'address = ?', params: [address] },
await c.req.json().catch(() => null), await c.req.json().catch(() => null),
); );
if (!result) return c.json({ error: "Invalid mail read status request" }, 400); if (!result) return c.json({ error: "Invalid mail state request" }, 400);
if (!result.success) return c.json(result, 500); if (!result.success) return c.json(result, 500);
return c.json(result); return c.json(result);
}; };
const getMailStates = (c: Context<HonoCustomType>) => {
if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
return c.json({ error: "Mail states are disabled" }, 403);
}
return c.json({ results: getMailStateOptions() });
};
const getSettings = async (c: Context<HonoCustomType>) => { const getSettings = async (c: Context<HonoCustomType>) => {
const { address, address_id } = c.get("jwtPayload") const { address, address_id } = c.get("jwtPayload")
const msgs = i18n.getMessagesbyContext(c); const msgs = i18n.getMessagesbyContext(c);
@@ -153,6 +161,6 @@ const clearSentItems = async (c: Context<HonoCustomType>) => {
}; };
export default { export default {
listMails, getMail, deleteMail, updateMailReadStatus, listMails, getMail, deleteMail, updateMailState, getMailStates,
getSettings, deleteAddress, clearInbox, clearSentItems getSettings, deleteAddress, clearInbox, clearSentItems
}; };
+2 -1
View File
@@ -16,7 +16,8 @@ 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/read-status', user_mail_api.updateMailReadStatus); api.get('/user_api/mail-states', user_mail_api.getMailStates);
api.patch('/user_api/mails/state', user_mail_api.updateMailState);
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
+21 -14
View File
@@ -3,28 +3,35 @@ import i18n from "../i18n";
import { handleMailListQuery } from "../common"; import { handleMailListQuery } from "../common";
import { getBooleanValue } from "../utils"; import { getBooleanValue } from "../utils";
import { import {
getReadStatusQuery, getMailStateQuery,
applyMailReadStatusUpdate, getMailStateOptions,
applyMailStateUpdate,
} from "../mail_flags"; } from "../mail_flags";
export default { export default {
getMailStates: (c: Context<HonoCustomType>) => {
if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
return c.json({ error: "Mail states are disabled" }, 403);
}
return c.json({ results: getMailStateOptions() });
},
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, read_status } = c.req.query(); const { address, limit, offset, mail_state } = 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);
} }
const readStatusQuery = getReadStatusQuery(read_status, 'rm.flags'); const stateQuery = getMailStateQuery(mail_state, 'rm.flags');
if (readStatusQuery === null) return c.json({ error: "Invalid mail read status filter" }, 400); if (stateQuery === null) return c.json({ error: "Invalid mail state filter" }, 400);
if (readStatusQuery && !getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) { if (stateQuery && !getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
return c.json({ error: "Mail read status is disabled" }, 403); return c.json({ error: "Mail states are disabled" }, 403);
} }
if (readStatusQuery) { if (stateQuery) {
filterQuerys.push(readStatusQuery.clause); filterQuerys.push(stateQuery.clause);
filterParams.push(...readStatusQuery.params); filterParams.push(...stateQuery.params);
} }
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`
@@ -55,12 +62,12 @@ export default {
success: success success: success
}) })
}, },
updateMailReadStatus: async (c: Context<HonoCustomType>) => { updateMailState: async (c: Context<HonoCustomType>) => {
if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) { if (!getBooleanValue(c.env.ENABLE_MAIL_FLAGS)) {
return c.json({ error: "Mail read status is disabled" }, 403); return c.json({ error: "Mail states are disabled" }, 403);
} }
const { user_id } = c.get("userPayload"); const { user_id } = c.get("userPayload");
const result = await applyMailReadStatusUpdate( const result = await applyMailStateUpdate(
c.env.DB, c.env.DB,
{ {
clause: `EXISTS (` clause: `EXISTS (`
@@ -72,7 +79,7 @@ export default {
}, },
await c.req.json().catch(() => null), await c.req.json().catch(() => null),
); );
if (!result) return c.json({ error: "Invalid mail read status request" }, 400); if (!result) return c.json({ error: "Invalid mail state request" }, 400);
if (!result.success) return c.json(result, 500); if (!result.success) return c.json(result, 500);
return c.json(result); return c.json(result);
} }