修复gmail附件和回复编辑器图片不显示

This commit is contained in:
eoao
2025-07-10 09:00:14 +08:00
parent f2c12216c8
commit 7e5a383bc6
10 changed files with 102 additions and 70 deletions

View File

@@ -55,7 +55,7 @@
effect="dark" effect="dark"
content="已发送" content="已发送"
> >
<Icon icon="bi:send-arrow-up" style="color: #67C23A" width="20" height="20" <Icon icon="bi:send-arrow-up-fill" style="color: #67C23A" width="20" height="20"
/> />
</el-tooltip> </el-tooltip>
@@ -87,7 +87,7 @@
effect="dark" effect="dark"
content="发送延迟" content="发送延迟"
> >
<Icon icon="quill:send-later" style="color:#FBBD08" width="20" <Icon icon="bi:send-arrow-up-fill" style="color:#FBBD08" width="20"
height="20"/> height="20"/>
</el-tooltip> </el-tooltip>

View File

@@ -77,7 +77,6 @@ function initEditor() {
toolbar_mode: 'scrolling', toolbar_mode: 'scrolling',
mobile: { mobile: {
toolbar: 'fullscreen bold emoticons forecolor backcolor italic fontsize | alignleft aligncenter alignright alignjustify | outdent indent | bullist numlist | link image | table code preview ', toolbar: 'fullscreen bold emoticons forecolor backcolor italic fontsize | alignleft aligncenter alignright alignjustify | outdent indent | bullist numlist | link image | table code preview ',
}, },
font_size_formats: '8px 10px 12px 14px 16px 18px 24px 36px', font_size_formats: '8px 10px 12px 14px 16px 18px 24px 36px',
emoticons_search: false, emoticons_search: false,
@@ -85,8 +84,9 @@ function initEditor() {
language_url: '/tinymce/langs/zh_CN.js', language_url: '/tinymce/langs/zh_CN.js',
menubar: false, menubar: false,
license_key: 'gpl', license_key: 'gpl',
noneditable_class: 'mceNonEditable',
content_style: ` .tox-dialog__body-content { margin: 0 !important; } content_style: ` .tox-dialog__body-content { margin: 0 !important; }
img { max-width: 100%; height: auto; } img { max-width: 100% !important; height: auto !important; }
body {margin: 10px 8px 0 5px !important; font-family: 'HarmonyOS'; font-size: 14px;} body {margin: 10px 8px 0 5px !important; font-family: 'HarmonyOS'; font-size: 14px;}
@media (pointer: fine) and (hover: hover) { @media (pointer: fine) and (hover: hover) {
::-webkit-scrollbar { ::-webkit-scrollbar {
@@ -110,9 +110,6 @@ function initEditor() {
.mce-item-table:not([border]), .mce-item-table:not([border]) caption, .mce-item-table:not([border]) td, .mce-item-table:not([border]) th, .mce-item-table[border="0"], .mce-item-table[border="0"] caption, .mce-item-table[border="0"] td, .mce-item-table[border="0"] th, table[style*="border-width: 0px"], table[style*="border-width: 0px"] caption, table[style*="border-width: 0px"] td, table[style*="border-width: 0px"] th { .mce-item-table:not([border]), .mce-item-table:not([border]) caption, .mce-item-table:not([border]) td, .mce-item-table:not([border]) th, .mce-item-table[border="0"], .mce-item-table[border="0"] caption, .mce-item-table[border="0"] td, .mce-item-table[border="0"] th, table[style*="border-width: 0px"], table[style*="border-width: 0px"] caption, table[style*="border-width: 0px"] td, table[style*="border-width: 0px"] th {
border: none; border: none;
} }
a {
color: #409EFF !important;
}
`, `,
setup: (ed) => { setup: (ed) => {
editor.value = ed; editor.value = ed;

View File

@@ -67,12 +67,14 @@ import {fileToBase64, formatBytes} from "@/utils/file-utils.js";
import {getIconByName} from "@/utils/icon-utils.js"; import {getIconByName} from "@/utils/icon-utils.js";
import sendPercent from "@/components/send-percent/index.vue" import sendPercent from "@/components/send-percent/index.vue"
import {formatDetailDate} from "@/utils/day.js"; import {formatDetailDate} from "@/utils/day.js";
import {useSettingStore} from "@/store/setting.js";
defineExpose({ defineExpose({
open, open,
openReply openReply
}) })
const settingStore = useSettingStore()
const emailStore = useEmailStore(); const emailStore = useEmailStore();
const accountStore = useAccountStore() const accountStore = useAccountStore()
const editor = ref({}) const editor = ref({})
@@ -283,11 +285,22 @@ function openReply(email) {
<br> <br>
${ formatDetailDate(email.createTime) }${email.name} &lt${email.sendEmail}&gt 来信: ${ formatDetailDate(email.createTime) }${email.name} &lt${email.sendEmail}&gt 来信:
</div> </div>
<blockquote style="margin: 0 0 0 0.8ex;border-left: 1px solid rgb(204,204,204);padding-left: 1ex;">${email.content}</blockquote>` <blockquote class="mceNonEditable" style="margin: 0 0 0 0.8ex;border-left: 1px solid rgb(204,204,204);padding-left: 1ex;">
<articl>
${formatImage(email.content) || `<pre style="font-family: inherit;word-break: break-word;white-space: pre-wrap;margin: 0">${email.text}</pre>`}
</article>
</blockquote>`
open() open()
console.log(defValue.value)
}) })
}
function formatImage(content) {
content = content || '';
const domain = settingStore.settings.r2Domain;
console.log(content)
return content.replace(/{{domain}}/g, domain + '/');
} }
function open() { function open() {

View File

@@ -35,7 +35,7 @@
</div> </div>
<el-scrollbar class="htm-scrollbar" :class="email.attList.length === 0 ? 'bottom-distance' : ''"> <el-scrollbar class="htm-scrollbar" :class="email.attList.length === 0 ? 'bottom-distance' : ''">
<ShadowHtml :html="formatImage(email.content)" v-if="email.content" /> <ShadowHtml :html="formatImage(email.content)" v-if="email.content" />
<span v-else class="email-text" >{{email.text}}</span> <pre v-else class="email-text" >{{email.text}}</pre>
</el-scrollbar> </el-scrollbar>
<div class="att" v-if="email.attList.length > 0"> <div class="att" v-if="email.attList.length > 0">
<div class="att-title"> <div class="att-title">
@@ -380,8 +380,10 @@ const handleDelete = () => {
} }
.email-text { .email-text {
font-family: inherit;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
margin: 0;
} }
.bottom-distance { .bottom-distance {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,8 +6,8 @@
<title></title> <title></title>
<link rel="icon" href="/assets/favicon-C5dAZutX.svg" type="image/svg+xml"> <link rel="icon" href="/assets/favicon-C5dAZutX.svg" type="image/svg+xml">
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script type="module" crossorigin src="/assets/index-C4HVi8Hj.js"></script> <script type="module" crossorigin src="/assets/index-BXq2c1cR.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DI8kV-nO.css"> <link rel="stylesheet" crossorigin href="/assets/index-DAVUbrwg.css">
</head> </head>
<body> <body>
<div id="loading-first"> <div id="loading-first">

View File

@@ -25,7 +25,8 @@ export async function email(message, env, ctx) {
forwardStatus, forwardStatus,
forwardEmail, forwardEmail,
ruleEmail, ruleEmail,
ruleType ruleType,
r2Domain
} = await settingService.query({ env }); } = await settingService.query({ env });
if (receive === settingConst.receive.CLOSE) { if (receive === settingConst.receive.CLOSE) {
@@ -45,8 +46,6 @@ export async function email(message, env, ctx) {
const email = await PostalMime.parse(content); const email = await PostalMime.parse(content);
console.log(email)
const toName = email.to.find(item => item.address === message.to)?.name || ''; const toName = email.to.find(item => item.address === message.to)?.name || '';
const params = { const params = {
@@ -82,13 +81,12 @@ export async function email(message, env, ctx) {
} }
} }
let emailRow = await emailService.receive({ env }, params, cidAttachments); let emailRow = await emailService.receive({ env }, params, cidAttachments, r2Domain);
attachments.forEach(attachment => { attachments.forEach(attachment => {
attachment.emailId = emailRow.emailId; attachment.emailId = emailRow.emailId;
attachment.userId = emailRow.userId; attachment.userId = emailRow.userId;
attachment.accountId = emailRow.accountId; attachment.accountId = emailRow.accountId;
attachment.type = attachment.contentId ? attConst.type.EMBED : attConst.type.ATT;
}); });
if (attachments.length > 0 && env.r2) { if (attachments.length > 0 && env.r2) {

View File

@@ -157,8 +157,8 @@ const attService = {
return orm(c).select().from(att).where( return orm(c).select().from(att).where(
and( and(
inArray(att.emailId,emailIds), inArray(att.emailId,emailIds),
eq(att.type, attConst.type.ATT), eq(att.type, attConst.type.ATT)
isNull(att.contentId))) ))
.all(); .all();
} }
}; };

View File

@@ -1,6 +1,6 @@
import orm from '../entity/orm'; import orm from '../entity/orm';
import email from '../entity/email'; import email from '../entity/email';
import { emailConst, isDel, settingConst } from '../const/entity-const'; import { attConst, emailConst, isDel, settingConst } from '../const/entity-const';
import { and, desc, eq, gt, inArray, lt, count, asc, sql, ne, or } from 'drizzle-orm'; import { and, desc, eq, gt, inArray, lt, count, asc, sql, ne, or } from 'drizzle-orm';
import { star } from '../entity/star'; import { star } from '../entity/star';
import settingService from './setting-service'; import settingService from './setting-service';
@@ -120,28 +120,8 @@ const emailService = {
.run(); .run();
}, },
receive(c, params, cidAttList) { receive(c, params, cidAttList, r2domain) {
params.content = this.imgReplace(params.content, cidAttList, r2domain)
const { document } = parseHTML(params.content);
const images = Array.from(document.querySelectorAll('img'));
for (const img of images) {
const src = img.getAttribute('src');
if (src && src.startsWith('cid:')) {
const cid = src.replace(/^cid:/, '');
const attCidIndex = cidAttList.findIndex(cidAtt => cidAtt.contentId.replace(/^<|>$/g, '') === cid);
if (attCidIndex > -1) {
const cidAtt = cidAttList[attCidIndex];
img.setAttribute('src', '{{domain}}' + cidAtt.key);
}
}
}
params.content = document.toString();
return orm(c).insert(email).values({ ...params }).returning().get(); return orm(c).insert(email).values({ ...params }).returning().get();
}, },
@@ -151,7 +131,7 @@ const emailService = {
const { resendTokens, r2Domain, send } = await settingService.query(c); const { resendTokens, r2Domain, send } = await settingService.query(c);
const { attDataList, html } = await attService.toImageUrlHtml(c, content, r2Domain); let { attDataList, html } = await attService.toImageUrlHtml(c, content, r2Domain);
if (attDataList.length > 0 && !r2Domain) { if (attDataList.length > 0 && !r2Domain) {
throw new BizError('r2域名未配置不能发送正文图片'); throw new BizError('r2域名未配置不能发送正文图片');
@@ -216,7 +196,6 @@ const emailService = {
name = emailUtils.getName(accountRow.email); name = emailUtils.getName(accountRow.email);
} }
let emailRow = { let emailRow = {
messageId: null messageId: null
}; };
@@ -290,6 +269,7 @@ const emailService = {
throw new BizError(error.message); throw new BizError(error.message);
} }
html = this.imgReplace(html, null, r2Domain);
const emailData = {}; const emailData = {};
emailData.sendEmail = accountRow.email; emailData.sendEmail = accountRow.email;
@@ -373,6 +353,47 @@ const emailService = {
return emailRowList; return emailRowList;
}, },
imgReplace(content, cidAttList, r2domain) {
if (!content) {
return ''
}
const { document } = parseHTML(content);
const images = Array.from(document.querySelectorAll('img'));
const useAtts = []
for (const img of images) {
const src = img.getAttribute('src');
if (src && src.startsWith('cid:') && cidAttList) {
const cid = src.replace(/^cid:/, '');
const attCidIndex = cidAttList.findIndex(cidAtt => cidAtt.contentId.replace(/^<|>$/g, '') === cid);
if (attCidIndex > -1) {
const cidAtt = cidAttList[attCidIndex];
img.setAttribute('src', '{{domain}}' + cidAtt.key);
useAtts.push(cidAtt)
}
}
if (src && src.startsWith(r2domain + '/')) {
img.setAttribute('src', src.replace(r2domain + '/', '{{domain}}'));
}
}
useAtts.forEach(att => {
att.type = attConst.type.EMBED
})
return document.toString();
},
selectById(c, emailId) { selectById(c, emailId) {
return orm(c).select().from(email).where( return orm(c).select().from(email).where(
and(eq(email.emailId, emailId), and(eq(email.emailId, emailId),