✨ graph api发送邮件

This commit is contained in:
sugarBecomer
2026-03-26 15:19:51 +08:00
parent 9fa83657e3
commit 39cb44bc22
+169
View File
@@ -0,0 +1,169 @@
package email
import (
"context"
"encoding/json"
"fmt"
"html"
"net/url"
"strings"
"sync"
"time"
v1 "likedream-goframe/internal/api/v1"
"github.com/google/uuid"
"golang.org/x/oauth2"
)
type outlookOAuthSession struct {
Config EmailConfig
FrontendOrigin string
CallbackURL string
ExpiresAt time.Time
}
var outlookOAuthStateStore = struct {
mu sync.Mutex
sessions map[string]outlookOAuthSession
}{
sessions: make(map[string]outlookOAuthSession),
}
func (s *EmailServiceV1) BuildOutlookAuthorizeURL(cfg EmailConfig, callbackBaseURL, frontendOrigin string) (*v1.OutlookOAuthAuthorizeURLResp, error) {
cfg = NormalizeConfig(cfg)
if cfg.AuthMode != AuthModeOutlookOAuth2 {
cfg.AuthMode = AuthModeOutlookOAuth2
}
if cfg.OAuthClientID == "" || cfg.OAuthClientSecret == "" {
return nil, fmt.Errorf("client id and client secret are required")
}
if cfg.FromEmail == "" {
return nil, fmt.Errorf("fromEmail is required")
}
callbackURL, err := buildCallbackURL(callbackBaseURL)
if err != nil {
return nil, err
}
if _, err = url.ParseRequestURI(frontendOrigin); err != nil {
return nil, fmt.Errorf("frontendOrigin is invalid")
}
state := uuid.NewString()
oauthCfg := newOutlookOAuthConfig(cfg, callbackURL)
authURL := oauthCfg.AuthCodeURL(
state,
oauth2.SetAuthURLParam("prompt", "consent"),
oauth2.SetAuthURLParam("response_mode", "query"),
)
outlookOAuthStateStore.mu.Lock()
defer outlookOAuthStateStore.mu.Unlock()
now := time.Now()
for key, session := range outlookOAuthStateStore.sessions {
if session.ExpiresAt.Before(now) {
delete(outlookOAuthStateStore.sessions, key)
}
}
outlookOAuthStateStore.sessions[state] = outlookOAuthSession{
Config: cfg,
FrontendOrigin: frontendOrigin,
CallbackURL: callbackURL,
ExpiresAt: now.Add(10 * time.Minute),
}
return &v1.OutlookOAuthAuthorizeURLResp{
AuthURL: authURL,
CallbackURL: callbackURL,
State: state,
}, nil
}
func (s *EmailServiceV1) ExchangeOutlookAuthorizeCode(ctx context.Context, state, code string) (string, string, error) {
outlookOAuthStateStore.mu.Lock()
session, ok := outlookOAuthStateStore.sessions[state]
if ok {
delete(outlookOAuthStateStore.sessions, state)
}
outlookOAuthStateStore.mu.Unlock()
if !ok {
return "", "", fmt.Errorf("oauth state is invalid or expired")
}
if time.Now().After(session.ExpiresAt) {
return "", "", fmt.Errorf("oauth state has expired")
}
if code == "" {
return "", "", fmt.Errorf("authorization code is empty")
}
token, err := newOutlookOAuthConfig(session.Config, session.CallbackURL).Exchange(ctx, code)
if err != nil {
return "", session.FrontendOrigin, err
}
if token.RefreshToken == "" {
return "", session.FrontendOrigin, fmt.Errorf("no refresh token returned, please ensure offline_access scope is granted")
}
return token.RefreshToken, session.FrontendOrigin, nil
}
func BuildOutlookOAuthPopupHTML(targetOrigin string, success bool, message string, refreshToken string) string {
payload := map[string]any{
"source": "likedream-outlook-oauth",
"success": success,
"message": message,
}
if refreshToken != "" {
payload["refreshToken"] = refreshToken
}
jsonPayload, _ := json.Marshal(payload)
return fmt.Sprintf(`<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Outlook OAuth</title>
</head>
<body>
<script>
(function () {
const payload = %s;
const targetOrigin = %q;
if (window.opener && targetOrigin) {
window.opener.postMessage(payload, targetOrigin);
}
document.body.innerText = payload.success ? '授权成功,窗口即将关闭。' : ('授权失败:' + payload.message);
setTimeout(function () { window.close(); }, 300);
})();
</script>
</body>
</html>`, string(jsonPayload), targetOrigin)
}
func BuildOutlookOAuthErrorHTML(message string) string {
return fmt.Sprintf(`<!doctype html>
<html>
<head><meta charset="utf-8" /><title>Outlook OAuth Error</title></head>
<body>%s</body>
</html>`, html.EscapeString(message))
}
func buildCallbackURL(base string) (string, error) {
return DefaultOutlookRedirectURL, nil
}
func newOutlookOAuthConfig(cfg EmailConfig, callbackURL string) *oauth2.Config {
cfg = NormalizeConfig(cfg)
return &oauth2.Config{
ClientID: cfg.OAuthClientID,
ClientSecret: cfg.OAuthClientSecret,
RedirectURL: callbackURL,
Scopes: strings.Fields(cfg.OAuthScope),
Endpoint: oauth2.Endpoint{
AuthURL: fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/authorize", cfg.OAuthTenant),
TokenURL: fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", cfg.OAuthTenant),
},
}
}