diff --git a/email/azure_acs.go b/email/azure_acs.go
new file mode 100644
index 00000000..edd1f1c3
--- /dev/null
+++ b/email/azure_acs.go
@@ -0,0 +1,227 @@
+// Copyright 2023 The Casdoor Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package email
+
+import (
+ "bytes"
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/google/uuid"
+)
+
+const (
+ importanceNormal = "normal"
+ sendEmailEndpoint = "/emails:send"
+ apiVersion = "2023-03-31"
+)
+
+type Email struct {
+ Recipients Recipients `json:"recipients"`
+ SenderAddress string `json:"senderAddress"`
+ Content Content `json:"content"`
+ Headers []CustomHeader `json:"headers"`
+ Tracking bool `json:"disableUserEngagementTracking"`
+ Importance string `json:"importance"`
+ ReplyTo []EmailAddress `json:"replyTo"`
+ Attachments []Attachment `json:"attachments"`
+}
+
+type Recipients struct {
+ To []EmailAddress `json:"to"`
+ CC []EmailAddress `json:"cc"`
+ BCC []EmailAddress `json:"bcc"`
+}
+
+type EmailAddress struct {
+ DisplayName string `json:"displayName"`
+ Address string `json:"address"`
+}
+
+type Content struct {
+ Subject string `json:"subject"`
+ HTML string `json:"html"`
+ PlainText string `json:"plainText"`
+}
+
+type CustomHeader struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+}
+
+type Attachment struct {
+ Content string `json:"contentBytesBase64"`
+ AttachmentType string `json:"attachmentType"`
+ Name string `json:"name"`
+}
+
+type ErrorResponse struct {
+ Error CommunicationError `json:"error"`
+}
+
+// CommunicationError contains the error code and message
+type CommunicationError struct {
+ Code string `json:"code"`
+ Message string `json:"message"`
+}
+
+type AzureACSEmailProvider struct {
+ AccessKey string
+ Endpoint string
+}
+
+func NewAzureACSEmailProvider(accessKey string, endpoint string) *AzureACSEmailProvider {
+ return &AzureACSEmailProvider{
+ AccessKey: accessKey,
+ Endpoint: endpoint,
+ }
+}
+
+func newEmail(fromAddress string, toAddress string, subject string, content string) *Email {
+ return &Email{
+ Recipients: Recipients{
+ To: []EmailAddress{
+ {
+ DisplayName: toAddress,
+ Address: toAddress,
+ },
+ },
+ },
+ SenderAddress: fromAddress,
+ Content: Content{
+ Subject: subject,
+ HTML: content,
+ },
+ Importance: importanceNormal,
+ }
+}
+
+func (a *AzureACSEmailProvider) sendEmail(e *Email) error {
+ postBody, err := json.Marshal(e)
+ if err != nil {
+ return fmt.Errorf("email JSON marshall failed: %s", err)
+ }
+
+ bodyBuffer := bytes.NewBuffer(postBody)
+
+ req, err := http.NewRequest("POST", a.Endpoint+sendEmailEndpoint+"?api-version="+apiVersion, bodyBuffer)
+ if err != nil {
+ return fmt.Errorf("error creating AzureACS API request: %s", err)
+ }
+
+ // Sign the request using the AzureACS access key and HMAC-SHA256
+ err = signRequestHMAC(a.AccessKey, req)
+ if err != nil {
+ return fmt.Errorf("error signing AzureACS API request: %s", err)
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+
+ // Some important header
+ req.Header.Set("repeatability-request-id", uuid.New().String())
+ req.Header.Set("repeatability-first-sent", time.Now().UTC().Format(http.TimeFormat))
+
+ // Send request
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ return fmt.Errorf("error sending AzureACS API request: %s", err)
+ }
+ defer resp.Body.Close()
+
+ // Response error Handling
+ if resp.StatusCode == http.StatusBadRequest {
+ commError := ErrorResponse{}
+
+ err = json.NewDecoder(resp.Body).Decode(&commError)
+ if err != nil {
+ return err
+ }
+
+ return fmt.Errorf("error sending email: %s", commError.Error.Message)
+ }
+
+ if resp.StatusCode != http.StatusAccepted {
+ return fmt.Errorf("error sending email: status: %d", resp.StatusCode)
+ }
+
+ return nil
+}
+
+func signRequestHMAC(secret string, req *http.Request) error {
+ method := req.Method
+ host := req.URL.Host
+ pathAndQuery := req.URL.Path
+
+ if req.URL.RawQuery != "" {
+ pathAndQuery = pathAndQuery + "?" + req.URL.RawQuery
+ }
+
+ var content []byte
+ var err error
+ if req.Body != nil {
+ content, err = io.ReadAll(req.Body)
+ if err != nil {
+ // return err
+ content = []byte{}
+ }
+ }
+
+ req.Body = io.NopCloser(bytes.NewBuffer(content))
+
+ key, err := base64.StdEncoding.DecodeString(secret)
+ if err != nil {
+ return fmt.Errorf("error decoding secret: %s", err)
+ }
+
+ timestamp := time.Now().UTC().Format(http.TimeFormat)
+ contentHash := GetContentHashBase64(content)
+ stringToSign := fmt.Sprintf("%s\n%s\n%s;%s;%s", strings.ToUpper(method), pathAndQuery, timestamp, host, contentHash)
+ signature := GetHmac(stringToSign, key)
+
+ req.Header.Set("x-ms-content-sha256", contentHash)
+ req.Header.Set("x-ms-date", timestamp)
+
+ req.Header.Set("Authorization", "HMAC-SHA256 SignedHeaders=x-ms-date;host;x-ms-content-sha256&Signature="+signature)
+
+ return nil
+}
+
+func GetContentHashBase64(content []byte) string {
+ hasher := sha256.New()
+ hasher.Write(content)
+
+ return base64.StdEncoding.EncodeToString(hasher.Sum(nil))
+}
+
+func GetHmac(content string, key []byte) string {
+ hmac := hmac.New(sha256.New, key)
+ hmac.Write([]byte(content))
+
+ return base64.StdEncoding.EncodeToString(hmac.Sum(nil))
+}
+
+func (a *AzureACSEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
+ e := newEmail(fromAddress, toAddress, subject, content)
+
+ return a.sendEmail(e)
+}
diff --git a/email/provider.go b/email/provider.go
new file mode 100644
index 00000000..56b5ff7d
--- /dev/null
+++ b/email/provider.go
@@ -0,0 +1,27 @@
+// Copyright 2023 The Casdoor Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package email
+
+type EmailProvider interface {
+ Send(fromAddress string, fromName, toAddress string, subject string, content string) error
+}
+
+func GetEmailProvider(typ string, clientId string, clientSecret string, appId string, host string, port int, disableSsl bool) EmailProvider {
+ if typ == "Azure ACS" {
+ return NewAzureACSEmailProvider(appId, host)
+ } else {
+ return NewSmtpEmailProvider(clientId, clientSecret, host, port, typ, disableSsl)
+ }
+}
diff --git a/email/smtp.go b/email/smtp.go
new file mode 100644
index 00000000..d26d357d
--- /dev/null
+++ b/email/smtp.go
@@ -0,0 +1,49 @@
+// Copyright 2023 The Casdoor Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package email
+
+import (
+ "crypto/tls"
+
+ "github.com/casdoor/gomail/v2"
+)
+
+type SmtpEmailProvider struct {
+ Dialer *gomail.Dialer
+}
+
+func NewSmtpEmailProvider(userName string, password string, host string, port int, typ string, disableSsl bool) *SmtpEmailProvider {
+ dialer := &gomail.Dialer{}
+ dialer = gomail.NewDialer(host, port, userName, password)
+ if typ == "SUBMAIL" {
+ dialer.TLSConfig = &tls.Config{InsecureSkipVerify: true}
+ }
+
+ dialer.SSL = !disableSsl
+
+ return &SmtpEmailProvider{Dialer: dialer}
+}
+
+func (s *SmtpEmailProvider) Send(fromAddress string, fromName string, toAddress string, subject string, content string) error {
+ message := gomail.NewMessage()
+
+ message.SetAddressHeader("From", fromAddress, fromName)
+ message.SetHeader("To", toAddress)
+ message.SetHeader("Subject", subject)
+ message.SetBody("text/html", content)
+
+ message.SkipUsernameCheck = true
+ return s.Dialer.DialAndSend(message)
+}
diff --git a/object/email.go b/object/email.go
index 55bbb0ff..e393295b 100644
--- a/object/email.go
+++ b/object/email.go
@@ -19,6 +19,7 @@ package object
import (
"crypto/tls"
+ "github.com/casdoor/casdoor/email"
"github.com/casdoor/gomail/v2"
)
@@ -35,9 +36,7 @@ func getDialer(provider *Provider) *gomail.Dialer {
}
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
- dialer := getDialer(provider)
-
- message := gomail.NewMessage()
+ emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.AppId, provider.Host, provider.Port, provider.DisableSsl)
fromAddress := provider.ClientId2
if fromAddress == "" {
@@ -49,14 +48,7 @@ func SendEmail(provider *Provider, title string, content string, dest string, se
fromName = sender
}
- message.SetAddressHeader("From", fromAddress, fromName)
- message.SetHeader("To", dest)
- message.SetHeader("Subject", title)
- message.SetBody("text/html", content)
-
- message.SkipUsernameCheck = true
-
- return dialer.DialAndSend(message)
+ return emailProvider.Send(fromAddress, fromName, dest, title, content)
}
// DailSmtpServer Dail Smtp server
diff --git a/web/src/ProviderEditPage.js b/web/src/ProviderEditPage.js
index 72f3b531..801daae6 100644
--- a/web/src/ProviderEditPage.js
+++ b/web/src/ProviderEditPage.js
@@ -297,7 +297,7 @@ class ProviderEditPage extends React.Component {
tooltip = i18next.t("provider:Project Id - Tooltip");
}
} else if (provider.category === "Email") {
- if (provider.type === "SUBMAIL") {
+ if (provider.type === "SUBMAIL" || provider.type === "Azure ACS") {
text = i18next.t("provider:App ID");
tooltip = i18next.t("provider:App ID - Tooltip");
}
@@ -626,6 +626,7 @@ class ProviderEditPage extends React.Component {
}
{
(this.state.provider.category === "Captcha" && this.state.provider.type === "Default") ||
+ (this.state.provider.category === "Email" && this.state.provider.type === "Azure ACS") ||
(this.state.provider.category === "Web3") ||
(this.state.provider.category === "Storage" && this.state.provider.type === "Local File System" ||
(this.state.provider.category === "Notification" && this.state.provider.type !== "Webpush" && this.state.provider.type !== "Line" && this.state.provider.type !== "Matrix" && this.state.provider.type !== "Twitter" && this.state.provider.type !== "Reddit" && this.state.provider.type !== "Rocket Chat" && this.state.provider.type !== "Viber")) ? null : (
@@ -671,7 +672,7 @@ class ProviderEditPage extends React.Component {
{
- this.state.provider.type === "WeChat Pay" ? null : (
+ (this.state.provider.type === "WeChat Pay") || (this.state.provider.category === "Email" && this.state.provider.type === "Azure ACS") ? null : (
{this.getClientSecret2Label(this.state.provider)} :
@@ -866,26 +867,30 @@ class ProviderEditPage extends React.Component {
}} />
-
-
- {Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
-
-
- {
- this.updateProviderField("port", value);
- }} />
-
-
-
-
- {Setting.getLabel(i18next.t("provider:Disable SSL"), i18next.t("provider:Disable SSL - Tooltip"))} :
-
-
- {
- this.updateProviderField("disableSsl", checked);
- }} />
-
-
+ {["Azure ACS"].includes(this.state.provider.type) ? null : (
+
+
+ {Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
+
+
+ {
+ this.updateProviderField("port", value);
+ }} />
+
+
+ )}
+ {["Azure ACS"].includes(this.state.provider.type) ? null : (
+
+
+ {Setting.getLabel(i18next.t("provider:Disable SSL"), i18next.t("provider:Disable SSL - Tooltip"))} :
+
+
+ {
+ this.updateProviderField("disableSsl", checked);
+ }} />
+
+
+ )}
{Setting.getLabel(i18next.t("provider:Email title"), i18next.t("provider:Email title - Tooltip"))} :
@@ -915,9 +920,11 @@ class ProviderEditPage extends React.Component {
this.updateProviderField("receiver", e.target.value);
}} />
-
+ {["Azure ACS"].includes(this.state.provider.type) ? null : (
+
+ )}