Skip to content

Commit

Permalink
feat: support acs email provider (casdoor#2323)
Browse files Browse the repository at this point in the history
* feat: support acs email provider

* feat: support acs email provider

* hide Test SMTP Connection button

* fix name acs
  • Loading branch information
UsherFall authored Sep 11, 2023
1 parent a7cb202 commit dc57c47
Show file tree
Hide file tree
Showing 6 changed files with 343 additions and 36 deletions.
227 changes: 227 additions & 0 deletions email/azure_acs.go
Original file line number Diff line number Diff line change
@@ -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)
}
27 changes: 27 additions & 0 deletions email/provider.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
49 changes: 49 additions & 0 deletions email/smtp.go
Original file line number Diff line number Diff line change
@@ -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)
}
14 changes: 3 additions & 11 deletions object/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package object
import (
"crypto/tls"

"github.com/casdoor/casdoor/email"
"github.com/casdoor/gomail/v2"
)

Expand All @@ -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 == "" {
Expand All @@ -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
Expand Down
Loading

0 comments on commit dc57c47

Please sign in to comment.