| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 | 
							- // Copyright (C) 2019-2023 Nicola Murino
 
- //
 
- // This program is free software: you can redistribute it and/or modify
 
- // it under the terms of the GNU Affero General Public License as published
 
- // by the Free Software Foundation, version 3.
 
- //
 
- // This program is distributed in the hope that it will be useful,
 
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
 
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 
- // GNU Affero General Public License for more details.
 
- //
 
- // You should have received a copy of the GNU Affero General Public License
 
- // along with this program. If not, see <https://www.gnu.org/licenses/>.
 
- // Package smtp provides supports for sending emails
 
- package smtp
 
- import (
 
- 	"context"
 
- 	"errors"
 
- 	"fmt"
 
- 	"sync"
 
- 	"time"
 
- 	"golang.org/x/oauth2"
 
- 	"golang.org/x/oauth2/google"
 
- 	"golang.org/x/oauth2/microsoft"
 
- 	"github.com/drakkan/sftpgo/v2/internal/logger"
 
- 	"github.com/drakkan/sftpgo/v2/internal/util"
 
- )
 
- // Supported OAuth2 providers
 
- const (
 
- 	OAuth2ProviderGoogle = iota
 
- 	OAuth2ProviderMicrosoft
 
- )
 
- var supportedOAuth2Providers = []int{OAuth2ProviderGoogle, OAuth2ProviderMicrosoft}
 
- // OAuth2Config defines OAuth2 settings
 
- type OAuth2Config struct {
 
- 	Provider int `json:"provider" mapstructure:"provider"`
 
- 	// Tenant for Microsoft provider, if empty "common" is used
 
- 	Tenant string `json:"tenant" mapstructure:"tenant"`
 
- 	// ClientID is the application's ID
 
- 	ClientID string `json:"client_id" mapstructure:"client_id"`
 
- 	// ClientSecret is the application's secret
 
- 	ClientSecret string `json:"client_secret" mapstructure:"client_secret"`
 
- 	// Token to use to get/renew access tokens
 
- 	RefreshToken string `json:"refresh_token" mapstructure:"refresh_token"`
 
- 	mu           *sync.RWMutex
 
- 	config       *oauth2.Config
 
- 	accessToken  *oauth2.Token
 
- }
 
- // Validate validates and initializes the configuration
 
- func (c *OAuth2Config) Validate() error {
 
- 	if !util.Contains(supportedOAuth2Providers, c.Provider) {
 
- 		return fmt.Errorf("smtp oauth2: unsupported provider %d", c.Provider)
 
- 	}
 
- 	if c.ClientID == "" {
 
- 		return errors.New("smtp oauth2: client id is required")
 
- 	}
 
- 	if c.ClientSecret == "" {
 
- 		return errors.New("smtp oauth2: client secret is required")
 
- 	}
 
- 	if c.RefreshToken == "" {
 
- 		return errors.New("smtp oauth2: refresh token is required")
 
- 	}
 
- 	c.initialize()
 
- 	return nil
 
- }
 
- func (c *OAuth2Config) isEqual(other *OAuth2Config) bool {
 
- 	if c.Provider != other.Provider {
 
- 		return false
 
- 	}
 
- 	if c.Tenant != other.Tenant {
 
- 		return false
 
- 	}
 
- 	if c.ClientID != other.ClientID {
 
- 		return false
 
- 	}
 
- 	if c.ClientSecret != other.ClientSecret {
 
- 		return false
 
- 	}
 
- 	if c.RefreshToken != other.RefreshToken {
 
- 		return false
 
- 	}
 
- 	return true
 
- }
 
- func (c *OAuth2Config) getAccessToken() (string, error) {
 
- 	c.mu.RLock()
 
- 	if c.accessToken.Expiry.After(time.Now().Add(30 * time.Second)) {
 
- 		accessToken := c.accessToken.AccessToken
 
- 		c.mu.RUnlock()
 
- 		return accessToken, nil
 
- 	}
 
- 	logger.Debug(logSender, "", "renew oauth2 token required, current token expires at %s", c.accessToken.Expiry)
 
- 	token := new(oauth2.Token)
 
- 	*token = *c.accessToken
 
- 	c.mu.RUnlock()
 
- 	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
 
- 	defer cancel()
 
- 	newToken, err := c.config.TokenSource(ctx, token).Token()
 
- 	if err != nil {
 
- 		logger.Error(logSender, "", "unable to get new token: %v", err)
 
- 		return "", err
 
- 	}
 
- 	accessToken := newToken.AccessToken
 
- 	refreshToken := newToken.RefreshToken
 
- 	if refreshToken != "" && refreshToken != token.RefreshToken {
 
- 		c.mu.Lock()
 
- 		c.RefreshToken = refreshToken
 
- 		c.accessToken = newToken
 
- 		c.mu.Unlock()
 
- 		logger.Debug(logSender, "", "oauth2 refresh token changed")
 
- 		go updateRefreshToken(refreshToken)
 
- 	}
 
- 	if accessToken != token.AccessToken {
 
- 		c.mu.Lock()
 
- 		c.accessToken = newToken
 
- 		c.mu.Unlock()
 
- 		logger.Debug(logSender, "", "new oauth2 token saved, expires at %s", c.accessToken.Expiry)
 
- 	}
 
- 	return accessToken, nil
 
- }
 
- func (c *OAuth2Config) initialize() {
 
- 	c.mu = new(sync.RWMutex)
 
- 	c.config = c.GetOAuth2()
 
- 	c.accessToken = &oauth2.Token{
 
- 		TokenType:    "Bearer",
 
- 		RefreshToken: c.RefreshToken,
 
- 	}
 
- }
 
- // GetOAuth2 returns the oauth2 configuration for the provided parameters.
 
- func (c *OAuth2Config) GetOAuth2() *oauth2.Config {
 
- 	var endpoint oauth2.Endpoint
 
- 	var scopes []string
 
- 	switch c.Provider {
 
- 	case OAuth2ProviderMicrosoft:
 
- 		endpoint = microsoft.AzureADEndpoint(c.Tenant)
 
- 		scopes = []string{"offline_access", "https://outlook.office.com/SMTP.Send"}
 
- 	default:
 
- 		endpoint = google.Endpoint
 
- 		scopes = []string{"https://mail.google.com/"}
 
- 	}
 
- 	return &oauth2.Config{
 
- 		ClientID:     c.ClientID,
 
- 		ClientSecret: c.ClientSecret,
 
- 		Scopes:       scopes,
 
- 		Endpoint:     endpoint,
 
- 	}
 
- }
 
 
  |