补齐任务列表状态筛选和邮件HTML安全字段

This commit is contained in:
andy
2026-07-09 00:15:51 +08:00
parent a0cb3a05d7
commit 855396e553
13 changed files with 203 additions and 43 deletions

View File

@@ -16,7 +16,9 @@ import java.util.List;
* @param sourceSentAt 邮件来源发送时间UTC
* @param textBody 完整纯文本正文
* @param htmlBody 完整 HTML 正文,前端展示前必须 sanitize
* @param htmlBodySanitized 后端第一版清洗后的 HTML前端生产展示应优先使用
* @param htmlSanitizeRequired 是否要求前端 sanitize HTML
* @param htmlRenderMode HTML 渲染建议模式,例如 SANITIZED_HTML 或 TEXT_ONLY
* @param inlineImages 内联图片外链
* @param attachments 附件外链
* @param relatedOrders 关联订单摘要
@@ -39,8 +41,12 @@ public record SourceMessageConversationMessageResult(
String textBody,
@JsonProperty("html_body")
String htmlBody,
@JsonProperty("html_body_sanitized")
String htmlBodySanitized,
@JsonProperty("html_sanitize_required")
Boolean htmlSanitizeRequired,
@JsonProperty("html_render_mode")
String htmlRenderMode,
@JsonProperty("inline_images")
List<SourceMessageOriginalMediaResponse> inlineImages,
List<SourceMessageOriginalMediaResponse> attachments,

View File

@@ -18,6 +18,7 @@ import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,6 +32,20 @@ public class SourceMessageConversationServiceImpl implements SourceMessageConver
private static final String ACCESS_SCENE = "source-message-conversation";
private static final String MEDIA_TYPE_INLINE_IMAGE = "INLINE_IMAGE";
private static final String MEDIA_TYPE_ATTACHMENT = "ATTACHMENT";
private static final String HTML_RENDER_MODE_SANITIZED = "SANITIZED_HTML";
private static final String HTML_RENDER_MODE_TEXT_ONLY = "TEXT_ONLY";
private static final Pattern DANGEROUS_TAG_PATTERN = Pattern.compile(
"(?is)<\\s*(script|style|iframe|object|embed|meta|link|base|form)[^>]*>.*?<\\s*/\\s*\\1\\s*>"
+ "|<\\s*(script|style|iframe|object|embed|meta|link|base|form)[^>]*/?\\s*>");
private static final Pattern EVENT_ATTRIBUTE_PATTERN = Pattern.compile(
"(?i)\\s+on[a-z0-9_-]+\\s*=\\s*(\"[^\"]*\"|'[^']*'|[^\\s>]+)");
private static final Pattern STYLE_ATTRIBUTE_PATTERN = Pattern.compile(
"(?i)\\s+style\\s*=\\s*(\"[^\"]*\"|'[^']*'|[^\\s>]+)");
private static final Pattern DANGEROUS_URL_ATTRIBUTE_PATTERN = Pattern.compile(
"(?i)\\s+(href|src|xlink:href|formaction)\\s*=\\s*"
+ "(\"\\s*(?:javascript|data|vbscript):[^\"]*\""
+ "|'\\s*(?:javascript|data|vbscript):[^']*'"
+ "|\\s*(?:javascript|data|vbscript):[^\\s>]+)");
private final SourceMessageInboxRepository inboxRepository;
private final List<SourceMessageRelatedContextProvider> relatedContextProviders;
@@ -101,6 +116,7 @@ public class SourceMessageConversationServiceImpl implements SourceMessageConver
private SourceMessageConversationMessageResult toConversationMessage(SourceMessageInboxSnapshot message) {
SourceMessageOriginalContent originalContent = readOriginalAndAudit(message);
SourceMessageRelatedContextResult relatedContext = findRelatedContext(message);
String htmlBody = originalContent.htmlBody();
return new SourceMessageConversationMessageResult(
message.id().toString(),
message.externalMessageId(),
@@ -110,8 +126,10 @@ public class SourceMessageConversationServiceImpl implements SourceMessageConver
UtcTimeFormatter.toUtcOffsetDateTime(message.receivedAt()),
UtcTimeFormatter.toUtcOffsetDateTime(message.sourceSentAt()),
originalContent.textBody(),
originalContent.htmlBody(),
htmlBody,
sanitizeHtml(htmlBody),
true,
htmlRenderMode(htmlBody),
filterMedia(originalContent.mediaItems(), MEDIA_TYPE_INLINE_IMAGE),
filterMedia(originalContent.mediaItems(), MEDIA_TYPE_ATTACHMENT),
relatedContext.relatedOrders(),
@@ -173,4 +191,27 @@ public class SourceMessageConversationServiceImpl implements SourceMessageConver
item.externalUrl(),
item.externalMediaId());
}
/**
* 第一版 HTML 清洗:保留邮件正文基本结构,移除脚本标签、事件属性和危险协议链接。
*/
private String sanitizeHtml(String htmlBody) {
if (htmlBody == null) {
return null;
}
String sanitized = DANGEROUS_TAG_PATTERN.matcher(htmlBody).replaceAll("");
sanitized = EVENT_ATTRIBUTE_PATTERN.matcher(sanitized).replaceAll("");
sanitized = STYLE_ATTRIBUTE_PATTERN.matcher(sanitized).replaceAll("");
return DANGEROUS_URL_ATTRIBUTE_PATTERN.matcher(sanitized).replaceAll("");
}
/**
* 返回前端渲染建议;有 HTML 时优先使用后端清洗后的安全 HTML。
*/
private String htmlRenderMode(String htmlBody) {
if (htmlBody == null || htmlBody.isBlank()) {
return HTML_RENDER_MODE_TEXT_ONLY;
}
return HTML_RENDER_MODE_SANITIZED;
}
}