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})
|
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 {
|
func normalizeTranslateTarget(value string) string {
|
||||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||||
case "zh", "zh-cn", "zh-hans", "zh_cn":
|
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}/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", 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).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(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(PermissionMailOrganize), a.requireExternalIMAPEnabled).Post("/mail/external-accounts/{id}/messages/{remoteId}/mark-read", a.handleExternalIMAPMarkRead)
|
||||||
r.With(a.requirePermission(PermissionMailSend)).Post("/mail/send", a.handleMailSend)
|
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" : ""}`),
|
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 }),
|
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 }),
|
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)}` : ""}`),
|
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 }),
|
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 sourceText = React.useMemo(() => (message.bodyText || stripHtml(message.bodyHtml || message.snippet || "")).trim(), [message.bodyHtml, message.bodyText, message.snippet])
|
||||||
const shouldShow = targetLanguage && shouldOfferMessageTranslation(sourceText, language)
|
const shouldShow = targetLanguage && shouldOfferMessageTranslation(sourceText, language)
|
||||||
const translate = useMutation({
|
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) => {
|
onSuccess: (result) => {
|
||||||
setTranslatedText(result.translatedText)
|
setTranslatedText(result.translatedText)
|
||||||
setTruncated(result.truncated)
|
setTruncated(result.truncated)
|
||||||
|
|||||||
Reference in New Issue
Block a user