mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-21 08:10:48 +08:00
feat: use rust mail-parser (#104)
* feat: imp worker v2 * feat: add rust mail-parser * feat: imp frontend v2 * feat: imp frontend v2 * feat: update doc * feat: add mailV1Alert * feat: remove unused
This commit is contained in:
@@ -75,7 +75,8 @@ const getSettings = async () => {
|
||||
const res = await apiFetch("/api/settings");;
|
||||
settings.value = {
|
||||
address: res["address"],
|
||||
auto_reply: res["auto_reply"]
|
||||
auto_reply: res["auto_reply"],
|
||||
has_v1_mails: res["has_v1_mails"],
|
||||
};
|
||||
} finally {
|
||||
settings.value.fetched = true;
|
||||
|
||||
@@ -14,6 +14,7 @@ export const useGlobalState = createGlobalState(
|
||||
})
|
||||
const settings = ref({
|
||||
fetched: false,
|
||||
has_v1_mails: false,
|
||||
address: '',
|
||||
auto_reply: {
|
||||
subject: '',
|
||||
|
||||
71
frontend/src/utils/email-parser.js
Normal file
71
frontend/src/utils/email-parser.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import PostalMime from 'postal-mime';
|
||||
import { parse_message } from 'mail-parser-wasm'
|
||||
|
||||
export async function processItem(item) {
|
||||
// Try to parse the email using mail-parser-wasm
|
||||
try {
|
||||
const parsedEmail = parse_message(item.raw);
|
||||
item.source = parsedEmail.sender || item.source;
|
||||
item.subject = parsedEmail.subject || '';
|
||||
item.message = parsedEmail.body_html || parsedEmail.text || '';
|
||||
item.attachments = parsedEmail.attachments?.map((a_item) => {
|
||||
const blob_url = URL.createObjectURL(
|
||||
new Blob(
|
||||
[a_item.content],
|
||||
{ type: a_item.content_type || 'application/octet-stream' }
|
||||
))
|
||||
if (a_item.content_id && a_item.content_id.length > 0) {
|
||||
item.message = item.message.replace(`cid:${a_item.content_id}`, blob_url);
|
||||
}
|
||||
return {
|
||||
id: a_item.content_id || Math.random().toString(36).substring(2, 15),
|
||||
filename: a_item.filename || a_item.content_id || "",
|
||||
size: a_item.content?.length || 0,
|
||||
url: blob_url
|
||||
}
|
||||
}) || [];
|
||||
} catch (error) {
|
||||
console.log('Error parsing email with mail-parser-wasm');
|
||||
console.error(error);
|
||||
}
|
||||
if (item.subject && item.subject.length > 0 && item.message && item.message.length > 0) {
|
||||
return item;
|
||||
}
|
||||
// Fallback to PostalMime
|
||||
try {
|
||||
const parsedEmail = await PostalMime.parse(item.raw);
|
||||
item.source = parsedEmail.from.address || item.source;
|
||||
if (parsedEmail.from.address && parsedEmail.from.name) {
|
||||
item.source = `${parsedEmail.from.name} <${parsedEmail.from.address}>`;
|
||||
}
|
||||
item.subject = parsedEmail.subject || 'No Subject';
|
||||
item.message = parsedEmail.html || parsedEmail.text || item.raw;
|
||||
item.attachments = parsedEmail.attachments?.map((a_item) => {
|
||||
const blob_url = URL.createObjectURL(
|
||||
new Blob(
|
||||
[a_item.content],
|
||||
{ type: a_item.mimeType || 'application/octet-stream' }
|
||||
))
|
||||
if (a_item.contentId && a_item.contentId.length > 0) {
|
||||
item.message = item.message.replace(`cid:${a_item.contentId}`, blob_url);
|
||||
}
|
||||
return {
|
||||
id: a_item.contentId || Math.random().toString(36).substring(2, 15),
|
||||
filename: a_item.filename || a_item.contentId || "",
|
||||
size: a_item.content?.length || 0,
|
||||
url: blob_url
|
||||
}
|
||||
}) || [];
|
||||
} catch (error) {
|
||||
console.log('Error parsing email with PostalMime');
|
||||
console.error(error);
|
||||
item.subject = 'No Subject';
|
||||
item.message = item.raw;
|
||||
}
|
||||
}
|
||||
|
||||
export function getDownloadEmlUrl(raw) {
|
||||
return URL.createObjectURL(
|
||||
new Blob([raw], { type: 'text/plain' }
|
||||
))
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { User, UserCheck, MailBulk } from '@vicons/fa'
|
||||
|
||||
import { useGlobalState } from '../store'
|
||||
import { api } from '../api'
|
||||
import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
|
||||
|
||||
const { localeCache, adminAuth, showAdminAuth } = useGlobalState()
|
||||
const router = useRouter()
|
||||
@@ -222,7 +223,9 @@ const fetchMailData = async () => {
|
||||
+ `&limit=${mailPageSize.value}`
|
||||
+ `&offset=${(mailPage.value - 1) * mailPageSize.value}`
|
||||
);
|
||||
mailData.value = results;
|
||||
mailData.value = await Promise.all(results.map(async (item) => {
|
||||
return await processItem(item);
|
||||
}));
|
||||
if (count > 0) {
|
||||
mailCount.value = count;
|
||||
}
|
||||
@@ -249,7 +252,9 @@ const fetchMailUnknowData = async () => {
|
||||
+ `?limit=${mailPageSize.value}`
|
||||
+ `&offset=${(mailPage.value - 1) * mailPageSize.value}`
|
||||
);
|
||||
mailUnknowData.value = results;
|
||||
mailUnknowData.value = await Promise.all(results.map(async (item) => {
|
||||
return await processItem(item);
|
||||
}));
|
||||
if (count > 0) {
|
||||
mailUnknowCount.value = count;
|
||||
}
|
||||
@@ -268,9 +273,7 @@ const fetchMailUnknowData = async () => {
|
||||
<div>{{ t('auth') }}</div>
|
||||
</template>
|
||||
<p>{{ t('authTip') }}</p>
|
||||
<n-input v-model:value="adminAuth" type="textarea" :autosize="{
|
||||
minRows: 3
|
||||
}" />
|
||||
<n-input v-model:value="adminAuth" type="textarea" :autosize="{ minRows: 3 }" />
|
||||
<template #action>
|
||||
<n-button @click="authFunc" size="small" tertiary round type="primary">
|
||||
{{ t('auth') }}
|
||||
|
||||
@@ -29,8 +29,8 @@ const emailDomain = ref("")
|
||||
|
||||
const login = async () => {
|
||||
try {
|
||||
await api.getSettings()
|
||||
jwt.value = password.value;
|
||||
await api.getSettings()
|
||||
location.reload()
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
@@ -85,6 +85,7 @@ const { t } = useI18n({
|
||||
copied: 'Copied',
|
||||
showPassword: 'Show Password',
|
||||
fetchAddressError: 'Fetch address error, maybe your jwt is invalid or network error.',
|
||||
mailV1Alert: 'You have some mails in v1, please click here to login and visit your history mails.',
|
||||
},
|
||||
zh: {
|
||||
title: 'Cloudflare 临时邮件',
|
||||
@@ -114,6 +115,7 @@ const { t } = useI18n({
|
||||
copied: '已复制',
|
||||
showPassword: '查看密码',
|
||||
fetchAddressError: '获取地址失败, 请检查你的 jwt 是否有效 或 网络是否正常。',
|
||||
mailV1Alert: '你有一些 v1 版本的邮件,请点击此处登录查看。',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -351,14 +353,24 @@ onMounted(async () => {
|
||||
<n-card v-if="!settings.fetched">
|
||||
<n-skeleton style="height: 50vh" />
|
||||
</n-card>
|
||||
<n-alert v-else-if="settings.address" type="info" show-icon>
|
||||
<span>
|
||||
<b>{{ t('yourAddress') }} <b>{{ settings.address }}</b></b>
|
||||
<n-button style="margin-left: 10px" @click="copy" size="small" tertiary round type="primary">
|
||||
<n-icon :component="Copy" /> {{ t('copy') }}
|
||||
</n-button>
|
||||
</span>
|
||||
</n-alert>
|
||||
<div v-else-if="settings.address">
|
||||
<n-alert v-if="settings.has_v1_mails" type="warning" show-icon closable>
|
||||
<span>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small"
|
||||
href="https://temp-email-v1.dreamhunter2333.xyz/">
|
||||
<b>{{ t('mailV1Alert') }} </b>
|
||||
</n-button>
|
||||
</span>
|
||||
</n-alert>
|
||||
<n-alert type="info" show-icon>
|
||||
<span>
|
||||
<b>{{ t('yourAddress') }} <b>{{ settings.address }}</b></b>
|
||||
<n-button style="margin-left: 10px" @click="copy" size="small" tertiary round type="primary">
|
||||
<n-icon :component="Copy" /> {{ t('copy') }}
|
||||
</n-button>
|
||||
</span>
|
||||
</n-alert>
|
||||
</div>
|
||||
<n-card v-else>
|
||||
<n-result status="info" :description="t('pleaseGetNewEmail')">
|
||||
<template #footer>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useGlobalState } from '../store'
|
||||
import { api } from '../api'
|
||||
import { CloudDownloadRound } from '@vicons/material'
|
||||
import { useIsMobile } from '../utils/composables'
|
||||
import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
|
||||
|
||||
const message = useMessage()
|
||||
const isMobile = useIsMobile()
|
||||
@@ -30,11 +31,13 @@ const { t } = useI18n({
|
||||
autoRefresh: 'Auto Refresh',
|
||||
refresh: 'Refresh',
|
||||
attachments: 'Show Attachments',
|
||||
downloadMail: 'Download Mail',
|
||||
pleaseSelectMail: "Please select a mail to view."
|
||||
},
|
||||
zh: {
|
||||
autoRefresh: '自动刷新',
|
||||
refresh: '刷新',
|
||||
downloadMail: '下载邮件',
|
||||
attachments: '查看附件',
|
||||
pleaseSelectMail: "请选择一封邮件查看。"
|
||||
}
|
||||
@@ -72,12 +75,14 @@ const refresh = async () => {
|
||||
+ `?limit=${pageSize.value}`
|
||||
+ `&offset=${(page.value - 1) * pageSize.value}`
|
||||
);
|
||||
data.value = results;
|
||||
data.value = await Promise.all(results.map(async (item) => {
|
||||
return await processItem(item);
|
||||
}));
|
||||
if (totalCount > 0) {
|
||||
count.value = totalCount;
|
||||
}
|
||||
if (!isMobile.value && !curMail.value && data.value.length > 0) {
|
||||
curMail.value = results[0];
|
||||
curMail.value = data.value[0];
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
@@ -89,29 +94,9 @@ const clickRow = async (row) => {
|
||||
curMail.value = row;
|
||||
};
|
||||
|
||||
const getAttachments = async (attachment_id) => {
|
||||
try {
|
||||
const res = await api.fetch(
|
||||
`/api/attachment/${attachment_id}`
|
||||
);
|
||||
curAttachments.value = res
|
||||
.filter((item) => item?.content?.data)
|
||||
.map((item) => {
|
||||
return {
|
||||
id: item.contentId || Math.random().toString(36).substring(2, 15),
|
||||
filename: item.filename || "",
|
||||
size: item.size,
|
||||
url: URL.createObjectURL(
|
||||
new Blob(
|
||||
[new Uint8Array(item.content.data)],
|
||||
{ type: item.contentType || 'application/octet-stream' }
|
||||
))
|
||||
}
|
||||
});
|
||||
showAttachments.value = true;
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
const getAttachments = (attachments) => {
|
||||
curAttachments.value = attachments;
|
||||
showAttachments.value = true;
|
||||
};
|
||||
|
||||
const mailItemClass = (row) => {
|
||||
@@ -177,12 +162,17 @@ onMounted(async () => {
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.source }}
|
||||
</n-tag>
|
||||
<n-button v-if="curMail.attachment_id" size="small" tertiary type="info"
|
||||
@click="getAttachments(curMail.attachment_id)">
|
||||
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
||||
@click="getAttachments(curMail.attachments)">
|
||||
{{ t('attachments') }}
|
||||
</n-button>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
|
||||
:href="getDownloadEmlUrl(curMail.raw)">
|
||||
<n-icon :component="CloudDownloadRound" />
|
||||
{{ t('downloadMail') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
<div v-html="curMail.message" style="max-height: 100vh;"></div>
|
||||
<div v-html="curMail.message" style="margin-top: 10px;max-height: 100vh;"></div>
|
||||
</n-card>
|
||||
<n-card class="mail-item" v-else>
|
||||
<n-result status="info" :title="t('pleaseSelectMail')">
|
||||
@@ -238,10 +228,15 @@ onMounted(async () => {
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.source }}
|
||||
</n-tag>
|
||||
<n-button v-if="curMail.attachment_id" size="small" tertiary type="info"
|
||||
@click="getAttachments(curMail.attachment_id)">
|
||||
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
||||
@click="getAttachments(curMail.attachments)">
|
||||
{{ t('attachments') }}
|
||||
</n-button>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small'" :download="curMail.id + '.eml'"
|
||||
:href="getDownloadEmlUrl(curMail)">
|
||||
{{ t('downloadMail') }}
|
||||
<n-icon :component="CloudDownloadRound" />
|
||||
</n-button>
|
||||
</n-space>
|
||||
<div v-html="curMail.message" style="max-height: 100vh;"></div>
|
||||
</n-card>
|
||||
|
||||
Reference in New Issue
Block a user