feat(mail): 支持外部 IMAP 邮件翻译
Docker Release / Check web and api (push) Waiting to run
Docker Release / Resolve release tag (push) Blocked by required conditions
Docker Release / Build and publish all-in-one (push) Blocked by required conditions
Docker Release / Build and publish api (push) Blocked by required conditions
Docker Release / Build and publish web (push) Blocked by required conditions
Docker Release / Build and publish dovecot (push) Blocked by required conditions
Docker Release / Build and publish postfix (push) Blocked by required conditions
Docker Release / Build and publish rspamd (push) Blocked by required conditions
Docker Release / Create GitHub release (push) Blocked by required conditions
Docker Release / Check web and api (push) Waiting to run
Docker Release / Resolve release tag (push) Blocked by required conditions
Docker Release / Build and publish all-in-one (push) Blocked by required conditions
Docker Release / Build and publish api (push) Blocked by required conditions
Docker Release / Build and publish web (push) Blocked by required conditions
Docker Release / Build and publish dovecot (push) Blocked by required conditions
Docker Release / Build and publish postfix (push) Blocked by required conditions
Docker Release / Build and publish rspamd (push) Blocked by required conditions
Docker Release / Create GitHub release (push) Blocked by required conditions
- 新增外部账号邮件翻译接口与路由。 - 前端翻译请求优先走外部账号邮件翻译接口。
This commit is contained in:
@@ -70,6 +70,69 @@ func (a *App) handleTranslateMailMessage(w http.ResponseWriter, r *http.Request)
|
||||
respondJSON(w, http.StatusOK, translateMailMessageResponse{TranslatedText: translated, SourceLanguage: source, TargetLanguage: target, Truncated: truncated})
|
||||
}
|
||||
|
||||
func (a *App) handleTranslateExternalIMAPMessage(w http.ResponseWriter, r *http.Request) {
|
||||
if !a.cfg.MailTranslateEnabled {
|
||||
respondError(w, http.StatusForbidden, "mail translation is disabled")
|
||||
return
|
||||
}
|
||||
var req translateMailMessageRequest
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
badRequest(w, err)
|
||||
return
|
||||
}
|
||||
target := normalizeTranslateTarget(req.TargetLanguage)
|
||||
if target == "" {
|
||||
respondError(w, http.StatusBadRequest, "unsupported target language")
|
||||
return
|
||||
}
|
||||
account, ok := a.externalIMAPAccountForMailRequest(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
folder, uid, ok := decodeExternalRemoteID(w, chi.URLParam(r, "remoteId"))
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
client, err := a.externalIMAP.openExternalIMAPClient(r.Context(), account)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "connection failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
raw, remote, err := client.FetchRaw(r.Context(), folder, uid)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "failed to load remote message")
|
||||
return
|
||||
}
|
||||
stored, _, err := a.parseMaildirMessage(raw, account.Username)
|
||||
text := ""
|
||||
if err == nil {
|
||||
text = strings.TrimSpace(stored.BodyText)
|
||||
if text == "" {
|
||||
text = strings.TrimSpace(stored.Snippet)
|
||||
}
|
||||
}
|
||||
if text == "" {
|
||||
text = strings.TrimSpace(remote.Snippet)
|
||||
}
|
||||
if text == "" {
|
||||
respondError(w, http.StatusBadRequest, "message has no translatable text")
|
||||
return
|
||||
}
|
||||
maxChars := a.cfg.MailTranslateMaxChars
|
||||
if maxChars <= 0 {
|
||||
maxChars = 8000
|
||||
}
|
||||
text, truncated := truncateRunes(text, maxChars)
|
||||
translated, source, err := googleFreeTranslate(r.Context(), text, target)
|
||||
if err != nil {
|
||||
a.log.Warn("external mail translation failed", "account_id", account.ID, "remote_id", chi.URLParam(r, "remoteId"), "target", target, "error", err)
|
||||
respondError(w, http.StatusBadGateway, "translation failed")
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusOK, translateMailMessageResponse{TranslatedText: translated, SourceLanguage: source, TargetLanguage: target, Truncated: truncated})
|
||||
}
|
||||
|
||||
func normalizeTranslateTarget(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "zh", "zh-cn", "zh-hans", "zh_cn":
|
||||
|
||||
@@ -89,6 +89,7 @@ func (a *App) Router() http.Handler {
|
||||
r.With(a.requirePermission(PermissionMailRead), a.requireExternalIMAPEnabled).Get("/mail/external-accounts/{id}/folders", a.handleExternalIMAPFolders)
|
||||
r.With(a.requirePermission(PermissionMailRead), a.requireExternalIMAPEnabled).Get("/mail/external-accounts/{id}/messages", a.handleExternalIMAPMessages)
|
||||
r.With(a.requirePermission(PermissionMailRead), a.requireExternalIMAPEnabled).Get("/mail/external-accounts/{id}/messages/{remoteId}", a.handleExternalIMAPMessage)
|
||||
r.With(a.requirePermission(PermissionMailRead), a.requireExternalIMAPEnabled).Post("/mail/external-accounts/{id}/messages/{remoteId}/translate", a.handleTranslateExternalIMAPMessage)
|
||||
r.With(a.requirePermission(PermissionMailAttachments), a.requireExternalIMAPEnabled).Get("/mail/external-accounts/{id}/attachments/{remoteId}/{partId}", a.handleExternalIMAPAttachment)
|
||||
r.With(a.requirePermission(PermissionMailOrganize), a.requireExternalIMAPEnabled).Post("/mail/external-accounts/{id}/messages/{remoteId}/mark-read", a.handleExternalIMAPMarkRead)
|
||||
r.With(a.requirePermission(PermissionMailSend)).Post("/mail/send", a.handleMailSend)
|
||||
|
||||
@@ -165,6 +165,7 @@ export const api = {
|
||||
},
|
||||
message: (id: string, options: { markRead?: boolean } = {}) => request<MailMessage>(`/api/mail/messages/${id}${options.markRead === false ? "?markRead=0" : ""}`),
|
||||
translateMessage: (id: string, targetLanguage: string) => request<MailTranslation>(`/api/mail/messages/${id}/translate`, { method: "POST", body: JSON.stringify({ targetLanguage }), timeoutMs: MAIL_DELIVERY_TIMEOUT_MS }),
|
||||
translateExternalMessage: (id: string, remoteId: string, targetLanguage: string) => request<MailTranslation>(`/api/mail/external-accounts/${id}/messages/${encodeURIComponent(remoteId)}/translate`, { method: "POST", body: JSON.stringify({ targetLanguage }), timeoutMs: MAIL_DELIVERY_TIMEOUT_MS }),
|
||||
send: (payload: SendPayload) => request<MailMessage>("/api/mail/send", { method: "POST", body: JSON.stringify(payload), timeoutMs: MAIL_DELIVERY_TIMEOUT_MS }),
|
||||
scheduledSends: (mailboxId?: string) => request<ListResponse<ScheduledSend>>(`/api/mail/scheduled-sends${mailboxId ? `?mailboxId=${encodeURIComponent(mailboxId)}` : ""}`),
|
||||
scheduleSend: (payload: ScheduleSendPayload) => request<ScheduledSend>("/api/mail/schedule-send", { method: "POST", body: JSON.stringify(payload), timeoutMs: MAIL_DELIVERY_TIMEOUT_MS }),
|
||||
|
||||
@@ -2478,7 +2478,7 @@ function MessageTranslateBox({ message, language }: { message: MailMessage; lang
|
||||
const sourceText = React.useMemo(() => (message.bodyText || stripHtml(message.bodyHtml || message.snippet || "")).trim(), [message.bodyHtml, message.bodyText, message.snippet])
|
||||
const shouldShow = targetLanguage && shouldOfferMessageTranslation(sourceText, language)
|
||||
const translate = useMutation({
|
||||
mutationFn: () => api.translateMessage(message.id, targetLanguage!),
|
||||
mutationFn: () => message.externalAccountId ? api.translateExternalMessage(message.externalAccountId, message.id, targetLanguage!) : api.translateMessage(message.id, targetLanguage!),
|
||||
onSuccess: (result) => {
|
||||
setTranslatedText(result.translatedText)
|
||||
setTruncated(result.truncated)
|
||||
|
||||
Reference in New Issue
Block a user