From 0860cbf343cbd750c80630149ba066dafe3a6f4c Mon Sep 17 00:00:00 2001 From: DacongDA Date: Thu, 17 Apr 2025 01:59:11 +0800 Subject: [PATCH] feat: can specify content type and http body field mapping for Custom HTTP Email provider (#3730) --- email/custom_http.go | 68 ++++++++++++++---- email/provider.go | 4 +- object/email.go | 2 +- web/src/ProviderEditPage.js | 138 ++++++++++++++++++++++++++++-------- 4 files changed, 168 insertions(+), 44 deletions(-) diff --git a/email/custom_http.go b/email/custom_http.go index 33f71f59..d2e855f6 100644 --- a/email/custom_http.go +++ b/email/custom_http.go @@ -15,6 +15,8 @@ package email import ( + "bytes" + "encoding/json" "fmt" "net/http" "net/url" @@ -27,13 +29,21 @@ type HttpEmailProvider struct { endpoint string method string httpHeaders map[string]string + bodyMapping map[string]string + contentType string } -func NewHttpEmailProvider(endpoint string, method string, httpHeaders map[string]string) *HttpEmailProvider { +func NewHttpEmailProvider(endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string) *HttpEmailProvider { + if contentType == "" { + contentType = "application/x-www-form-urlencoded" + } + client := &HttpEmailProvider{ endpoint: endpoint, method: method, httpHeaders: httpHeaders, + bodyMapping: bodyMapping, + contentType: contentType, } return client } @@ -41,18 +51,52 @@ func NewHttpEmailProvider(endpoint string, method string, httpHeaders map[string func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error { var req *http.Request var err error + + fromNameField := "fromName" + toAddressField := "toAddress" + subjectField := "subject" + contentField := "content" + + for k, v := range c.bodyMapping { + switch k { + case "fromName": + fromNameField = v + case "toAddress": + toAddressField = v + case "subject": + subjectField = v + case "content": + contentField = v + } + } + if c.method == "POST" || c.method == "PUT" || c.method == "DELETE" { - formValues := url.Values{} - formValues.Set("fromName", fromName) - formValues.Set("toAddress", toAddress) - formValues.Set("subject", subject) - formValues.Set("content", content) - req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode())) + bodyMap := make(map[string]string) + bodyMap[fromNameField] = fromName + bodyMap[toAddressField] = toAddress + bodyMap[subjectField] = subject + bodyMap[contentField] = content + + var fromValueBytes []byte + if c.contentType == "application/json" { + fromValueBytes, err = json.Marshal(bodyMap) + if err != nil { + return err + } + req, err = http.NewRequest(c.method, c.endpoint, bytes.NewBuffer(fromValueBytes)) + } else { + formValues := url.Values{} + for k, v := range bodyMap { + formValues.Add(k, v) + } + req, err = http.NewRequest(c.method, c.endpoint, strings.NewReader(formValues.Encode())) + } + if err != nil { return err } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Content-Type", c.contentType) } else if c.method == "GET" { req, err = http.NewRequest(c.method, c.endpoint, nil) if err != nil { @@ -60,10 +104,10 @@ func (c *HttpEmailProvider) Send(fromAddress string, fromName string, toAddress } q := req.URL.Query() - q.Add("fromName", fromName) - q.Add("toAddress", toAddress) - q.Add("subject", subject) - q.Add("content", content) + q.Add(fromNameField, fromName) + q.Add(toAddressField, toAddress) + q.Add(subjectField, subject) + q.Add(contentField, content) req.URL.RawQuery = q.Encode() } else { return fmt.Errorf("HttpEmailProvider's Send() error, unsupported method: %s", c.method) diff --git a/email/provider.go b/email/provider.go index 3ed0a5d8..f0d3bddf 100644 --- a/email/provider.go +++ b/email/provider.go @@ -18,11 +18,11 @@ type EmailProvider interface { Send(fromAddress string, fromName, toAddress string, subject string, content string) error } -func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool, endpoint string, method string, httpHeaders map[string]string) EmailProvider { +func GetEmailProvider(typ string, clientId string, clientSecret string, host string, port int, disableSsl bool, endpoint string, method string, httpHeaders map[string]string, bodyMapping map[string]string, contentType string) EmailProvider { if typ == "Azure ACS" { return NewAzureACSEmailProvider(clientSecret, host) } else if typ == "Custom HTTP Email" { - return NewHttpEmailProvider(endpoint, method, httpHeaders) + return NewHttpEmailProvider(endpoint, method, httpHeaders, bodyMapping, contentType) } else if typ == "SendGrid" { return NewSendgridEmailProvider(clientSecret, host, endpoint) } else { diff --git a/object/email.go b/object/email.go index 51dfa6ec..b63a2b02 100644 --- a/object/email.go +++ b/object/email.go @@ -31,7 +31,7 @@ func TestSmtpServer(provider *Provider) error { } func SendEmail(provider *Provider, title string, content string, dest string, sender string) error { - emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl, provider.Endpoint, provider.Method, provider.HttpHeaders) + emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.Host, provider.Port, provider.DisableSsl, provider.Endpoint, provider.Method, provider.HttpHeaders, provider.UserMapping, provider.IssuerUrl) fromAddress := provider.ClientId2 if fromAddress == "" { diff --git a/web/src/ProviderEditPage.js b/web/src/ProviderEditPage.js index ed150a1a..e94a58a2 100644 --- a/web/src/ProviderEditPage.js +++ b/web/src/ProviderEditPage.js @@ -42,6 +42,13 @@ const defaultUserMapping = { avatarUrl: "avatarUrl", }; +const defaultEmailMapping = { + fromName: "fromName", + toAddress: "toAddress", + subject: "subject", + content: "content", +}; + class ProviderEditPage extends React.Component { constructor(props) { super(props); @@ -72,7 +79,16 @@ class ProviderEditPage extends React.Component { if (res.status === "ok") { const provider = res.data; - provider.userMapping = provider.userMapping || defaultUserMapping; + if (provider.type === "Custom HTTP Email") { + if (!provider.userMapping) { + provider.userMapping = provider.userMapping || defaultEmailMapping; + } + if (!provider.userMapping?.fromName) { + provider.userMapping = defaultEmailMapping; + } + } else { + provider.userMapping = provider.userMapping || defaultUserMapping; + } this.setState({ provider: provider, }); @@ -146,9 +162,16 @@ class ProviderEditPage extends React.Component { const requiredKeys = ["id", "username", "displayName"]; const provider = this.state.provider; - if (value === "" && requiredKeys.includes(key)) { - Setting.showMessage("error", i18next.t("provider:This field is required")); - return; + if (provider.type === "Custom HTTP Email") { + if (value === "") { + Setting.showMessage("error", i18next.t("provider:This field is required")); + return; + } + } else { + if (value === "" && requiredKeys.includes(key)) { + Setting.showMessage("error", i18next.t("provider:This field is required")); + return; + } } provider.userMapping[key] = value; @@ -184,6 +207,30 @@ class ProviderEditPage extends React.Component { ); } + + renderEmailMappingInput() { + return ( + + {Setting.getLabel(i18next.t("provider:From name"), i18next.t("provider:From name - Tooltip"))} : + { + this.updateUserMappingField("fromName", e.target.value); + }} /> + {Setting.getLabel(i18next.t("provider:From address"), i18next.t("provider:From address - Tooltip"))} : + { + this.updateUserMappingField("toAddress", e.target.value); + }} /> + {Setting.getLabel(i18next.t("provider:Subject"), i18next.t("provider:Subject - Tooltip"))} : + { + this.updateUserMappingField("subject", e.target.value); + }} /> + {Setting.getLabel(i18next.t("provider:Email content"), i18next.t("provider:Email content - Tooltip"))} : + { + this.updateUserMappingField("content", e.target.value); + }} /> + + ); + } + getClientIdLabel(provider) { switch (provider.category) { case "OAuth": @@ -1097,33 +1144,66 @@ class ProviderEditPage extends React.Component { )} - - - {Setting.getLabel(i18next.t("general:Method"), i18next.t("provider:Method - Tooltip"))} : - - - { + this.updateProviderField("method", value); + }}> + { + [ + {id: "GET", name: "GET"}, + {id: "POST", name: "POST"}, + {id: "PUT", name: "PUT"}, + {id: "DELETE", name: "DELETE"}, + ].map((method, index) => ) + } + + + { - [ - {id: "GET", name: "GET"}, - {id: "POST", name: "POST"}, - {id: "PUT", name: "PUT"}, - {id: "DELETE", name: "DELETE"}, - ].map((method, index) => ) + this.state.provider.method !== "GET" ? ( + + {Setting.getLabel(i18next.t("webhook:Content type"), i18next.t("webhook:Content type - Tooltip"))} : + + + + + ) : null } - - - - - - {Setting.getLabel(i18next.t("provider:HTTP header"), i18next.t("provider:HTTP header - Tooltip"))} : - - - {this.updateProviderField("httpHeaders", value);}} /> - - + + + {Setting.getLabel(i18next.t("provider:HTTP header"), i18next.t("provider:HTTP header - Tooltip"))} : + + + {this.updateProviderField("httpHeaders", value);}} /> + + + {this.state.provider.method !== "GET" ? + + {Setting.getLabel(i18next.t("provider:HTTP body mapping"), i18next.t("provider:HTTP body mapping - Tooltip"))} : + + + {this.renderEmailMappingInput()} + + : null} + + ) + } {Setting.getLabel(i18next.t("provider:Email title"), i18next.t("provider:Email title - Tooltip"))} :