feat: 完成邮件通知的html渲染功能 (#1183)

This commit is contained in:
Allyx 2026-02-05 11:32:43 +08:00 committed by GitHub
parent 1dd48e04da
commit 34e2dae3d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 103 additions and 5 deletions

3
go.mod
View File

@ -49,6 +49,7 @@ require (
github.com/jdcloud-api/jdcloud-sdk-go v1.64.0
github.com/kong/go-kong v0.71.0
github.com/luthermonson/go-proxmox v0.3.2
github.com/microcosm-cc/bluemonday v1.0.27
github.com/minio/minio-go/v7 v7.0.97
github.com/mohuatech/mohuacloud-go-sdk v0.0.0-20251115182757-6fba4d0a4c47
github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0
@ -192,6 +193,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
@ -209,6 +211,7 @@ require (
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect

6
go.sum
View File

@ -238,6 +238,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfm
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/baidubce/bce-sdk-go v0.9.256 h1:/6UwBzDp+dRFpKRIb5WsvxfSiG4SLOIOghvagOK/q4Y=
github.com/baidubce/bce-sdk-go v0.9.256/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFTbwh4eIsqjVdg=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@ -495,6 +497,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
@ -648,6 +652,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=

View File

@ -25,6 +25,7 @@ func init() {
SenderAddress: credentials.SenderAddress,
SenderName: credentials.SenderName,
ReceiverAddress: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "receiverAddress", credentials.ReceiverAddress),
MessageFormat: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "format", email.MESSAGE_FORMAT_PLAIN),
AllowInsecureConnections: credentials.AllowInsecureConnections,
})
return provider, err

View File

@ -0,0 +1,6 @@
package email
const (
MESSAGE_FORMAT_PLAIN = "plain"
MESSAGE_FORMAT_HTML = "html"
)

View File

@ -6,6 +6,8 @@ import (
"fmt"
"log/slog"
"github.com/microcosm-cc/bluemonday"
"github.com/certimate-go/certimate/internal/tools/smtp"
"github.com/certimate-go/certimate/pkg/core/notifier"
)
@ -28,6 +30,10 @@ type NotifierConfig struct {
SenderName string `json:"senderName,omitempty"`
// 收件人邮箱。
ReceiverAddress string `json:"receiverAddress"`
// 消息格式。
// 可取值 [MESSAGE_FORMAT_PLAIN]、[MESSAGE_FORMAT_HTML]。
// 零值时默认值 [MESSAGE_FORMAT_PLAIN]。
MessageFormat string `json:"messageFormat,omitempty"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
@ -75,7 +81,16 @@ func (n *Notifier) Notify(ctx context.Context, subject string, message string) (
msg := smtp.NewMessage()
msg.Subject(subject)
msg.SetBodyString(smtp.MIMETypeTextPlain, message)
switch n.config.MessageFormat {
case "", MESSAGE_FORMAT_PLAIN:
msg.SetBodyString(smtp.MIMETypeTextPlain, message)
case MESSAGE_FORMAT_HTML:
msg.SetBodyString(smtp.MIMETypeTextHTML, bluemonday.UGCPolicy().Sanitize(message))
msg.AddAlternativeString(smtp.MIMETypeTextPlain, bluemonday.StrictPolicy().Sanitize(message))
default:
return nil, fmt.Errorf("unsupported message format: '%s'", n.config.MessageFormat)
}
if n.config.SenderName == "" {
msg.From(n.config.SenderAddress)
} else {

View File

@ -11,8 +11,9 @@ import (
)
const (
mockSubject = "test_subject"
mockMessage = "test_message"
mockSubject = "test_subject"
mockMessage = "test_message"
mockHtmlMessage = "<h1>Hello Certimate</h1><a onblur=\"alert(secret)\" href=\"http://www.google.com\">Google</a>"
)
var (
@ -86,4 +87,40 @@ func TestNotify(t *testing.T) {
t.Logf("ok: %v", res)
})
t.Run("Notify_Html", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("SMTPHOST: %v", fSmtpHost),
fmt.Sprintf("SMTPPORT: %v", fSmtpPort),
fmt.Sprintf("SMTPTLS: %v", fSmtpTLS),
fmt.Sprintf("USERNAME: %v", fUsername),
fmt.Sprintf("PASSWORD: %v", fPassword),
fmt.Sprintf("SENDERADDRESS: %v", fSenderAddress),
fmt.Sprintf("RECEIVERADDRESS: %v", fReceiverAddress),
}, "\n"))
provider, err := provider.NewNotifier(&provider.NotifierConfig{
SmtpHost: fSmtpHost,
SmtpPort: int32(fSmtpPort),
SmtpTls: fSmtpTLS,
Username: fUsername,
Password: fPassword,
SenderAddress: fSenderAddress,
ReceiverAddress: fReceiverAddress,
MessageFormat: provider.MESSAGE_FORMAT_HTML,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
res, err := provider.Notify(context.Background(), mockSubject, mockHtmlMessage)
if err != nil {
t.Errorf("err: %+v", err)
return
}
t.Logf("ok: %v", res)
})
}

View File

@ -1,5 +1,5 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { Form, Input, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
@ -7,6 +7,9 @@ import { isEmail } from "@/utils/validator";
import { useFormNestedFieldsContext } from "./_context";
const MESSAGE_FORMAT_PLAIN = "plain" as const;
const MESSAGE_FORMAT_HTML = "html" as const;
const BizNotifyNodeConfigFieldsProviderEmail = () => {
const { i18n, t } = useTranslation();
@ -19,6 +22,22 @@ const BizNotifyNodeConfigFieldsProviderEmail = () => {
return (
<>
<Form.Item
name={[parentNamePath, "format"]}
initialValue={initialValues.format}
label={t("workflow_node.notify.form.email_format.label")}
rules={[formRule]}
>
<Select
options={[MESSAGE_FORMAT_PLAIN, MESSAGE_FORMAT_HTML].map((s) => ({
key: s,
label: t(`workflow_node.notify.form.email_format.option.${s}.label`),
value: s,
}))}
placeholder={t("workflow_node.notify.form.email_format.placeholder")}
/>
</Form.Item>
<Form.Item
name={[parentNamePath, "receiverAddress"]}
initialValue={initialValues.receiverAddress}
@ -33,13 +52,16 @@ const BizNotifyNodeConfigFieldsProviderEmail = () => {
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {};
return {
format: MESSAGE_FORMAT_PLAIN,
};
};
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
const { t } = i18n;
return z.object({
format: z.enum([MESSAGE_FORMAT_PLAIN, MESSAGE_FORMAT_HTML]).nullish(),
receiverAddress: z
.string()
.nullish()

View File

@ -1197,6 +1197,10 @@
"workflow_node.notify.form.discordbot_channel_id.label": "Discord channel ID (Optional)",
"workflow_node.notify.form.discordbot_channel_id.placeholder": "Please enter Discord channel ID",
"workflow_node.notify.form.discordbot_channel_id.help": "Notes: Leave it blank to use the default channel ID provided by the credential.",
"workflow_node.notify.form.email_format.label": "Message format (Optional)",
"workflow_node.notify.form.email_format.placeholder": "Please select message format",
"workflow_node.notify.form.email_format.option.plain.label": "Plain text",
"workflow_node.notify.form.email_format.option.html.label": "HTML",
"workflow_node.notify.form.email_receiver_address.label": "Receiver email address (Optional)",
"workflow_node.notify.form.email_receiver_address.placeholder": "Please enter receiver email address",
"workflow_node.notify.form.email_receiver_address.help": "Notes: Leave it blank to use the default receiver email address provided by the selected credential.",

View File

@ -1195,6 +1195,10 @@
"workflow_node.notify.form.discordbot_channel_id.label": "Discord 频道 ID可选",
"workflow_node.notify.form.discordbot_channel_id.placeholder": "请输入 Discord 频道 ID",
"workflow_node.notify.form.discordbot_channel_id.help": "提示:不填写时,将使用所选通知渠道授权的默认频道 ID。",
"workflow_node.notify.form.email_format.label": "消息格式(可选)",
"workflow_node.notify.form.email_format.placeholder": "请选择消息格式",
"workflow_node.notify.form.email_format.option.plain.label": "纯文本",
"workflow_node.notify.form.email_format.option.html.label": "HTML",
"workflow_node.notify.form.email_receiver_address.label": "收件人邮箱(可选)",
"workflow_node.notify.form.email_receiver_address.placeholder": "请输入收件人邮箱以覆盖默认值",
"workflow_node.notify.form.email_receiver_address.help": "提示:不填写时,将使用所选通知渠道授权的默认收件人邮箱。",