using System.Net.Security; using System.Security.Cryptography.X509Certificates; using Google.Apis.Auth.OAuth2; using Google.Apis.Auth.OAuth2.Flows; using Google.Apis.Auth.OAuth2.Web; using Google.Apis.Util.Store; using MailKit.Net.Smtp; using MailKit.Security; using Microsoft.Identity.Client; using Nop.Core; using Nop.Core.Domain.Messages; using Nop.Core.Infrastructure; using Nop.Services.Localization; namespace Nop.Services.Messages; /// /// SMTP Builder /// public partial class SmtpBuilder : ISmtpBuilder { #region Fields protected readonly EmailAccountSettings _emailAccountSettings; protected readonly IEmailAccountService _emailAccountService; protected readonly ILocalizationService _localizationService; protected readonly INopFileProvider _fileProvider; #endregion #region Ctor public SmtpBuilder(EmailAccountSettings emailAccountSettings, IEmailAccountService emailAccountService, ILocalizationService localizationService, INopFileProvider fileProvider) { _emailAccountSettings = emailAccountSettings; _emailAccountService = emailAccountService; _localizationService = localizationService; _fileProvider = fileProvider; } #endregion #region Utilities protected virtual async Task GetGmailCredentialsAsync(EmailAccount emailAccount) { ArgumentNullException.ThrowIfNull(emailAccount); if (string.IsNullOrEmpty(emailAccount.ClientId)) throw new NopException(await _localizationService.GetResourceAsync("Admin.Configuration.EmailAccounts.Fields.ClientId.Required")); if (string.IsNullOrEmpty(emailAccount.ClientSecret)) throw new NopException(await _localizationService.GetResourceAsync("Admin.Configuration.EmailAccounts.Fields.ClientSecret.Required")); var tokenFilePath = _fileProvider.MapPath(NopMessageDefaults.GmailAuthStorePath); var credentialRoot = _fileProvider.Combine(tokenFilePath, emailAccount.Email); var codeFlow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer { ClientSecrets = new ClientSecrets { ClientId = emailAccount.ClientId, ClientSecret = emailAccount.ClientSecret }, Scopes = NopMessageDefaults.GmailScopes, DataStore = new FileDataStore(credentialRoot, true) }); var authCode = new AuthorizationCodeWebApp(codeFlow, null, null); var authResult = await authCode.AuthorizeAsync(emailAccount.Email, CancellationToken.None); if (authResult.Credential is null) throw new NopException("Failed to obtain user credentials for the authorization server. Check the client secrets and allow the application to perform required operations."); if (authResult.Credential.Token?.IsStale == true) await authResult.Credential.RefreshTokenAsync(CancellationToken.None); return new SaslMechanismOAuth2(authResult.Credential.UserId, authResult.Credential.Token.AccessToken); } protected virtual async Task GetExchangeCredentialsAsync(EmailAccount emailAccount) { ArgumentNullException.ThrowIfNull(emailAccount); if (string.IsNullOrEmpty(emailAccount.ClientId)) throw new NopException(await _localizationService.GetResourceAsync("Admin.Configuration.EmailAccounts.Fields.ClientId.Required")); if (string.IsNullOrEmpty(emailAccount.ClientSecret)) throw new NopException(await _localizationService.GetResourceAsync("Admin.Configuration.EmailAccounts.Fields.ClientSecret.Required")); if (string.IsNullOrEmpty(emailAccount.TenantId)) throw new NopException(await _localizationService.GetResourceAsync("Admin.Configuration.EmailAccounts.Fields.TenantId.Required")); var confidentialClientApplication = ConfidentialClientApplicationBuilder.Create(emailAccount.ClientId) .WithAuthority(string.Format(NopMessageDefaults.MSALTenantPattern, emailAccount.TenantId)) .WithClientSecret(emailAccount.ClientSecret) .Build(); var authToken = await confidentialClientApplication.AcquireTokenForClient(NopMessageDefaults.MSALScopes).ExecuteAsync(); return new SaslMechanismOAuth2(emailAccount.Email, authToken.AccessToken); } #endregion #region Methods /// /// Create a new SMTP client for a specific email account /// /// Email account to use. If null, then would be used EmailAccount by default /// /// A task that represents the asynchronous operation /// The task result contains the an SMTP client that can be used to send email messages /// public virtual async Task BuildAsync(EmailAccount emailAccount = null) { emailAccount ??= await _emailAccountService.GetEmailAccountByIdAsync(_emailAccountSettings.DefaultEmailAccountId) ?? throw new NopException("Email account could not be loaded"); var client = new SmtpClient { ServerCertificateValidationCallback = ValidateServerCertificate }; try { await client.ConnectAsync( emailAccount.Host, emailAccount.Port, emailAccount.EnableSsl ? SecureSocketOptions.SslOnConnect : SecureSocketOptions.StartTlsWhenAvailable); switch (emailAccount.EmailAuthenticationMethod) { case EmailAuthenticationMethod.Login: await client.AuthenticateAsync(new SaslMechanismLogin(emailAccount.Username, emailAccount.Password)); break; case EmailAuthenticationMethod.GmailOAuth2: await client.AuthenticateAsync(await GetGmailCredentialsAsync(emailAccount)); break; case EmailAuthenticationMethod.MicrosoftOAuth2: await client.AuthenticateAsync(await GetExchangeCredentialsAsync(emailAccount)); break; case EmailAuthenticationMethod.Ntlm: await client.AuthenticateAsync(new SaslMechanismNtlm()); break; } return client; } catch (Exception ex) { client.Dispose(); throw new NopException(ex.Message, ex); } } /// /// Validates the remote Secure Sockets Layer (SSL) certificate used for authentication. /// /// An object that contains state information for this validation. /// The certificate used to authenticate the remote party. /// The chain of certificate authorities associated with the remote certificate. /// One or more errors associated with the remote certificate. /// A System.Boolean value that determines whether the specified certificate is accepted for authentication public virtual bool ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { //By default, server certificate verification is disabled. return true; } #endregion }