From 8d5a0b0c0e716c7ff47fbda4e2708ecc3c52cc5e Mon Sep 17 00:00:00 2001 From: Marvin Zhang Date: Fri, 26 Jul 2024 18:15:15 +0800 Subject: [PATCH] refactor: Add Outlook authentication support for sending emails --- .../models/v2/notification_channel_v2.go | 3 + core/notification/mail.go | 21 ++++++- core/notification/mail_outlook.go | 55 +++++++++++++++++++ core/notification/oauth2_auth.go | 21 +++++++ 4 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 core/notification/mail_outlook.go create mode 100644 core/notification/oauth2_auth.go diff --git a/core/models/models/v2/notification_channel_v2.go b/core/models/models/v2/notification_channel_v2.go index 95507618..b93de702 100644 --- a/core/models/models/v2/notification_channel_v2.go +++ b/core/models/models/v2/notification_channel_v2.go @@ -14,4 +14,7 @@ type NotificationChannelV2 struct { WebhookUrl string `json:"webhook_url,omitempty" bson:"webhook_url,omitempty"` TelegramBotToken string `json:"telegram_bot_token,omitempty" bson:"telegram_bot_token,omitempty"` TelegramChatId string `json:"telegram_chat_id,omitempty" bson:"telegram_chat_id,omitempty"` + OutlookTenantId string `json:"outlook_tenant_id,omitempty" bson:"outlook_tenant_id,omitempty"` + OutlookClientId string `json:"outlook_client_id,omitempty" bson:"outlook_client_id,omitempty"` + OutlookClientSecret string `json:"outlook_client_secret,omitempty" bson:"outlook_client_secret,omitempty"` } diff --git a/core/notification/mail.go b/core/notification/mail.go index 549cc0e6..88c4a716 100644 --- a/core/notification/mail.go +++ b/core/notification/mail.go @@ -13,6 +13,20 @@ import ( ) func SendMail(s *models.NotificationSettingV2, ch *models.NotificationChannelV2, to, cc, bcc, title, content string) error { + // compatibility for different providers + var auth *XOAuth2Auth + if ch.Provider == ChannelMailProviderOutlook { + token, err := getOutlookToken(ch.OutlookTenantId, ch.OutlookClientId, ch.OutlookClientSecret) + if err != nil { + log.Errorf("failed to get outlook token: %v", err) + return err + } + auth = &XOAuth2Auth{ + Username: ch.SMTPUsername, + Token: token, + } + } + // config smtpConfig := smtpAuthentication{ Server: ch.SMTPServer, @@ -42,7 +56,7 @@ func SendMail(s *models.NotificationSettingV2, ch *models.NotificationChannelV2, } // send the email - if err := send(smtpConfig, options, content, text); err != nil { + if err := sendMail(smtpConfig, options, content, text, auth); err != nil { log.Errorf("failed to send email: %v", err) trace.PrintError(err) return err @@ -84,7 +98,7 @@ type sendOptions struct { } // send email -func send(smtpConfig smtpAuthentication, options sendOptions, htmlBody string, txtBody string) error { +func sendMail(smtpConfig smtpAuthentication, options sendOptions, htmlBody string, txtBody string, auth *XOAuth2Auth) error { if smtpConfig.Server == "" { return errors.New("SMTP server config is empty") } @@ -132,6 +146,9 @@ func send(smtpConfig smtpAuthentication, options sendOptions, htmlBody string, t m.AddAlternative("text/html", htmlBody) d := gomail.NewDialer(smtpConfig.Server, smtpConfig.Port, smtpConfig.SMTPUser, smtpConfig.SMTPPassword) + if auth != nil { + d.Auth = auth + } return d.DialAndSend(m) } diff --git a/core/notification/mail_outlook.go b/core/notification/mail_outlook.go new file mode 100644 index 00000000..7e5b33da --- /dev/null +++ b/core/notification/mail_outlook.go @@ -0,0 +1,55 @@ +package notification + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" +) + +func getOutlookToken(tenantID, clientID, clientSecret string) (string, error) { + url := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantID) + data := map[string]string{ + "grant_type": "client_credentials", + "client_id": clientID, + "client_secret": clientSecret, + "scope": "https://outlook.office365.com/.default", + } + + formData := "" + for key, value := range data { + if formData != "" { + formData += "&" + } + formData += fmt.Sprintf("%s=%s", key, value) + } + + req, err := http.NewRequest("POST", url, bytes.NewBufferString(formData)) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + return "", err + } + + if token, ok := result["access_token"].(string); ok { + return token, nil + } + return "", fmt.Errorf("no access token found") +} diff --git a/core/notification/oauth2_auth.go b/core/notification/oauth2_auth.go new file mode 100644 index 00000000..4b80784a --- /dev/null +++ b/core/notification/oauth2_auth.go @@ -0,0 +1,21 @@ +package notification + +import ( + "fmt" + "net/smtp" +) + +type XOAuth2Auth struct { + Username, Token string +} + +func (a *XOAuth2Auth) Start(_ *smtp.ServerInfo) (string, []byte, error) { + return "XOAUTH2", []byte("user=" + a.Username + "\x01auth=Bearer " + a.Token + "\x01\x01"), nil +} + +func (a *XOAuth2Auth) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + return nil, fmt.Errorf("unexpected server challenge: %s", fromServer) + } + return nil, nil +}