mirror of
https://github.com/casdoor/casdoor.git
synced 2025-05-23 10:45:47 +08:00
feat: support acs email provider (#2323)
* feat: support acs email provider * feat: support acs email provider * hide Test SMTP Connection button * fix name acs
This commit is contained in:
parent
a7cb202ee9
commit
dc57c476b7
227
email/azure_acs.go
Normal file
227
email/azure_acs.go
Normal file
@ -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
email/provider.go
Normal file
27
email/provider.go
Normal file
@ -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
email/smtp.go
Normal file
49
email/smtp.go
Normal file
@ -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)
|
||||||
|
}
|
@ -19,6 +19,7 @@ package object
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
|
||||||
|
"github.com/casdoor/casdoor/email"
|
||||||
"github.com/casdoor/gomail/v2"
|
"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 {
|
func SendEmail(provider *Provider, title string, content string, dest string, sender string) error {
|
||||||
dialer := getDialer(provider)
|
emailProvider := email.GetEmailProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.AppId, provider.Host, provider.Port, provider.DisableSsl)
|
||||||
|
|
||||||
message := gomail.NewMessage()
|
|
||||||
|
|
||||||
fromAddress := provider.ClientId2
|
fromAddress := provider.ClientId2
|
||||||
if fromAddress == "" {
|
if fromAddress == "" {
|
||||||
@ -49,14 +48,7 @@ func SendEmail(provider *Provider, title string, content string, dest string, se
|
|||||||
fromName = sender
|
fromName = sender
|
||||||
}
|
}
|
||||||
|
|
||||||
message.SetAddressHeader("From", fromAddress, fromName)
|
return emailProvider.Send(fromAddress, fromName, dest, title, content)
|
||||||
message.SetHeader("To", dest)
|
|
||||||
message.SetHeader("Subject", title)
|
|
||||||
message.SetBody("text/html", content)
|
|
||||||
|
|
||||||
message.SkipUsernameCheck = true
|
|
||||||
|
|
||||||
return dialer.DialAndSend(message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DailSmtpServer Dail Smtp server
|
// DailSmtpServer Dail Smtp server
|
||||||
|
@ -297,7 +297,7 @@ class ProviderEditPage extends React.Component {
|
|||||||
tooltip = i18next.t("provider:Project Id - Tooltip");
|
tooltip = i18next.t("provider:Project Id - Tooltip");
|
||||||
}
|
}
|
||||||
} else if (provider.category === "Email") {
|
} else if (provider.category === "Email") {
|
||||||
if (provider.type === "SUBMAIL") {
|
if (provider.type === "SUBMAIL" || provider.type === "Azure ACS") {
|
||||||
text = i18next.t("provider:App ID");
|
text = i18next.t("provider:App ID");
|
||||||
tooltip = i18next.t("provider:App ID - Tooltip");
|
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 === "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 === "Web3") ||
|
||||||
(this.state.provider.category === "Storage" && this.state.provider.type === "Local File System" ||
|
(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 : (
|
(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 {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
{
|
{
|
||||||
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 : (
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{this.getClientSecret2Label(this.state.provider)} :
|
{this.getClientSecret2Label(this.state.provider)} :
|
||||||
@ -866,26 +867,30 @@ class ProviderEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
{["Azure ACS"].includes(this.state.provider.type) ? null : (
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Row style={{marginTop: "20px"}} >
|
||||||
{Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
</Col>
|
{Setting.getLabel(i18next.t("provider:Port"), i18next.t("provider:Port - Tooltip"))} :
|
||||||
<Col span={22} >
|
</Col>
|
||||||
<InputNumber value={this.state.provider.port} onChange={value => {
|
<Col span={22} >
|
||||||
this.updateProviderField("port", value);
|
<InputNumber value={this.state.provider.port} onChange={value => {
|
||||||
}} />
|
this.updateProviderField("port", value);
|
||||||
</Col>
|
}} />
|
||||||
</Row>
|
</Col>
|
||||||
<Row style={{marginTop: "20px"}} >
|
</Row>
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
)}
|
||||||
{Setting.getLabel(i18next.t("provider:Disable SSL"), i18next.t("provider:Disable SSL - Tooltip"))} :
|
{["Azure ACS"].includes(this.state.provider.type) ? null : (
|
||||||
</Col>
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col span={1} >
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
<Switch checked={this.state.provider.disableSsl} onChange={checked => {
|
{Setting.getLabel(i18next.t("provider:Disable SSL"), i18next.t("provider:Disable SSL - Tooltip"))} :
|
||||||
this.updateProviderField("disableSsl", checked);
|
</Col>
|
||||||
}} />
|
<Col span={1} >
|
||||||
</Col>
|
<Switch checked={this.state.provider.disableSsl} onChange={checked => {
|
||||||
</Row>
|
this.updateProviderField("disableSsl", checked);
|
||||||
|
}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Email title"), i18next.t("provider:Email title - Tooltip"))} :
|
{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);
|
this.updateProviderField("receiver", e.target.value);
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" onClick={() => ProviderEditTestEmail.connectSmtpServer(this.state.provider)} >
|
{["Azure ACS"].includes(this.state.provider.type) ? null : (
|
||||||
{i18next.t("provider:Test SMTP Connection")}
|
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary" onClick={() => ProviderEditTestEmail.connectSmtpServer(this.state.provider)} >
|
||||||
</Button>
|
{i18next.t("provider:Test SMTP Connection")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary"
|
<Button style={{marginLeft: "10px", marginBottom: "5px"}} type="primary"
|
||||||
disabled={!Setting.isValidEmail(this.state.provider.receiver)}
|
disabled={!Setting.isValidEmail(this.state.provider.receiver)}
|
||||||
onClick={() => ProviderEditTestEmail.sendTestEmail(this.state.provider, this.state.provider.receiver)} >
|
onClick={() => ProviderEditTestEmail.sendTestEmail(this.state.provider, this.state.provider.receiver)} >
|
||||||
|
@ -161,6 +161,10 @@ export const OtherProviderInfo = {
|
|||||||
logo: `${StaticBaseUrl}/img/email_mailtrap.png`,
|
logo: `${StaticBaseUrl}/img/email_mailtrap.png`,
|
||||||
url: "https://mailtrap.io",
|
url: "https://mailtrap.io",
|
||||||
},
|
},
|
||||||
|
"Azure ACS": {
|
||||||
|
logo: `${StaticBaseUrl}/img/social_azure.png`,
|
||||||
|
url: "https://learn.microsoft.com/zh-cn/azure/communication-services",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Storage: {
|
Storage: {
|
||||||
"Local File System": {
|
"Local File System": {
|
||||||
@ -972,6 +976,7 @@ export function getProviderTypeOptions(category) {
|
|||||||
{id: "Default", name: "Default"},
|
{id: "Default", name: "Default"},
|
||||||
{id: "SUBMAIL", name: "SUBMAIL"},
|
{id: "SUBMAIL", name: "SUBMAIL"},
|
||||||
{id: "Mailtrap", name: "Mailtrap"},
|
{id: "Mailtrap", name: "Mailtrap"},
|
||||||
|
{id: "Azure ACS", name: "Azure ACS"},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} else if (category === "SMS") {
|
} else if (category === "SMS") {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user