diff --git a/ApiMailSenderOptions.cs b/ApiMailSenderOptions.cs new file mode 100644 index 000000000..c2a427119 --- /dev/null +++ b/ApiMailSenderOptions.cs @@ -0,0 +1,8 @@ +namespace DevBetterWeb.Infrastructure.Services; + +public class ApiMailSenderOptions +{ + public string? ApiBaseUrl { get; set; } + public string? ApiKey { get; set; } + public string? Sender { get; set; } +} \ No newline at end of file diff --git a/README.md b/README.md index 5c878262f..fb7568b90 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Head over to [devBetter.com](https://devbetter.com) to see the live site. Scroll - Register - Login - View Public Questions/Topics -- Validate Accounts via Email (SendGrid) +- Validate Accounts via Email (SMTP2Go) ### Members Only diff --git a/src/DevBetterWeb.Infrastructure/DevBetterWeb.Infrastructure.csproj b/src/DevBetterWeb.Infrastructure/DevBetterWeb.Infrastructure.csproj index c5bbe5347..5cb794017 100644 --- a/src/DevBetterWeb.Infrastructure/DevBetterWeb.Infrastructure.csproj +++ b/src/DevBetterWeb.Infrastructure/DevBetterWeb.Infrastructure.csproj @@ -20,7 +20,6 @@ - diff --git a/src/DevBetterWeb.Infrastructure/InfrastructureServiceCollectionExtensions.cs b/src/DevBetterWeb.Infrastructure/InfrastructureServiceCollectionExtensions.cs index 9a66d34b9..fefa61fc8 100644 --- a/src/DevBetterWeb.Infrastructure/InfrastructureServiceCollectionExtensions.cs +++ b/src/DevBetterWeb.Infrastructure/InfrastructureServiceCollectionExtensions.cs @@ -26,7 +26,7 @@ public static IServiceCollection AddInfrastructureServices(this IServiceCollecti } else { - services.AddTransient(); + services.AddTransient(); } // Common Dependencies diff --git a/src/DevBetterWeb.Infrastructure/Services/ApiMailSenderOptions.cs b/src/DevBetterWeb.Infrastructure/Services/ApiMailSenderOptions.cs new file mode 100644 index 000000000..187124ae3 --- /dev/null +++ b/src/DevBetterWeb.Infrastructure/Services/ApiMailSenderOptions.cs @@ -0,0 +1,8 @@ +namespace DevBetterWeb.Infrastructure.Services; + +public class ApiMailSenderOptions +{ + public string? ApiBaseUrl { get; set; } + public string? ApiKey { get; set; } + public string? Sender { get; set; } +} diff --git a/src/DevBetterWeb.Infrastructure/Services/AuthMessageSenderOptions.cs b/src/DevBetterWeb.Infrastructure/Services/AuthMessageSenderOptions.cs index 965796be3..f01b607ac 100644 --- a/src/DevBetterWeb.Infrastructure/Services/AuthMessageSenderOptions.cs +++ b/src/DevBetterWeb.Infrastructure/Services/AuthMessageSenderOptions.cs @@ -2,6 +2,7 @@ public class AuthMessageSenderOptions { - public string? SendGridUser { get; set; } - public string? SendGridKey { get; set; } + public string? SmtpServer { get; set; } + public int SmtpPort { get; set; } = 587; + public string? ApiKey { get; set; } } diff --git a/src/DevBetterWeb.Infrastructure/Services/SendGridEmailService.cs b/src/DevBetterWeb.Infrastructure/Services/SendGridEmailService.cs deleted file mode 100644 index 67684c41f..000000000 --- a/src/DevBetterWeb.Infrastructure/Services/SendGridEmailService.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Threading.Tasks; -using Ardalis.GuardClauses; -using DevBetterWeb.Core.Interfaces; -using Microsoft.Extensions.Options; -using SendGrid; -using SendGrid.Helpers.Mail; - -namespace DevBetterWeb.Infrastructure.Services; - -public class SendGridEmailService : IEmailService -{ - public SendGridEmailService(IOptions optionsAccessor) - { - Guard.Against.Null(optionsAccessor.Value, nameof(optionsAccessor.Value)); - Options = optionsAccessor.Value; - } - - public AuthMessageSenderOptions Options { get; } //set only via Secret Manager - - public async Task SendEmailAsync(string email, string subject, string message) - { - if (Options.SendGridKey == null) throw new Exception("SendGridKey not set."); - var response = await Execute(Options.SendGridKey, subject, message, email); - - if (response.StatusCode != System.Net.HttpStatusCode.Accepted) - { - // log or throw - throw new Exception("Could not send email: " + await response.Body.ReadAsStringAsync()); - } - } - - private async Task Execute(string apiKey, string subject, string message, string email) - { - var client = new SendGridClient(apiKey); - var msg = new SendGridMessage() - { - From = new EmailAddress("donotreply@devbetter.com", "devBetter Admin"), - Subject = subject, - PlainTextContent = message, - HtmlContent = message - }; - msg.AddTo(new EmailAddress(email)); - - // Disable click tracking. - // See https://sendgrid.com/docs/User_Guide/Settings/tracking.html - msg.SetClickTracking(false, false); - - return await client.SendEmailAsync(msg); - } -} diff --git a/src/DevBetterWeb.Infrastructure/Services/Smtp2GoEmailService.cs b/src/DevBetterWeb.Infrastructure/Services/Smtp2GoEmailService.cs new file mode 100644 index 000000000..27c1884cc --- /dev/null +++ b/src/DevBetterWeb.Infrastructure/Services/Smtp2GoEmailService.cs @@ -0,0 +1,50 @@ +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Ardalis.GuardClauses; +using DevBetterWeb.Core.Interfaces; +using Microsoft.Extensions.Options; + +namespace DevBetterWeb.Infrastructure.Services; + +public class Smtp2GoEmailService : IEmailService +{ + private readonly HttpClient _httpClient; + public Smtp2GoEmailService(IOptions optionsAccessor) + { + Guard.Against.Null(optionsAccessor, nameof(optionsAccessor)); + Guard.Against.Null(optionsAccessor.Value, nameof(optionsAccessor.Value)); + Options = optionsAccessor.Value; + _httpClient = new HttpClient(); + } + + public AuthMessageSenderOptions Options { get; } + + public async Task SendEmailAsync(string email, string subject, string message) + { + if (string.IsNullOrEmpty(Options.ApiKey)) throw new Exception("SMTP API Key not set."); + + var request = new HttpRequestMessage(HttpMethod.Post, "https://api.smtp2go.com/v3/email/send"); + request.Headers.Add("Authorization", $"Bearer {Options.ApiKey}"); + + var payload = new + { + sender = "donotreply@devbetter.com", + to = new[] { email }, + subject = subject, + text_body = message, + html_body = message + }; + string json = JsonSerializer.Serialize(payload); + request.Content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _httpClient.SendAsync(request); + if (!response.IsSuccessStatusCode) + { + var error = await response.Content.ReadAsStringAsync(); + throw new Exception($"SMTP2GO API error: {response.StatusCode} - {error}"); + } + } +} diff --git a/src/DevBetterWeb.Web/Program.cs b/src/DevBetterWeb.Web/Program.cs index f91ba67a0..b9d78115d 100644 --- a/src/DevBetterWeb.Web/Program.cs +++ b/src/DevBetterWeb.Web/Program.cs @@ -60,7 +60,7 @@ builder.Services.AddLogging(); -builder.Services.Configure(builder.Configuration.GetSection("AuthMessageSenderOptions")); +builder.Services.Configure(builder.Configuration.GetSection("ApiMailSenderOptions")); builder.Services.Configure(builder.Configuration.GetSection("DiscordWebhookUrls")); builder.Services.Configure(builder.Configuration.GetSection("StripeOptions")); builder.Services.Configure(builder.Configuration.GetSection("SubscriptionPlanOptions")); diff --git a/src/DevBetterWeb.Web/appsettings.Template.json b/src/DevBetterWeb.Web/appsettings.Template.json index c6334dda5..d194cc39b 100644 --- a/src/DevBetterWeb.Web/appsettings.Template.json +++ b/src/DevBetterWeb.Web/appsettings.Template.json @@ -14,6 +14,11 @@ "ApiSettings": { "ApiKey": "[api key string goes here]" }, + "ApiMailSenderOptions": { + "ApiBaseUrl": "https://api.smtp2go.com/v3/", + "ApiKey": "[smtp2go api key goes here]", + "Sender": "donotreply@devbetter.com" + }, "Logging": { "ApplicationInsights": { "LogLevel": { diff --git a/src/DevBetterWeb.Web/appsettings.Testing.Template.json b/src/DevBetterWeb.Web/appsettings.Testing.Template.json index c6334dda5..bbb89fcce 100644 --- a/src/DevBetterWeb.Web/appsettings.Testing.Template.json +++ b/src/DevBetterWeb.Web/appsettings.Testing.Template.json @@ -14,6 +14,12 @@ "ApiSettings": { "ApiKey": "[api key string goes here]" }, + "AuthMessageSenderOptions": { + "SmtpServer": "mail.smtp2go.com", + "SmtpPort": 587, + "SmtpUsername": "[SMTP2Go username goes here]", + "SmtpPassword": "[SMTP2Go password goes here]" + }, "Logging": { "ApplicationInsights": { "LogLevel": { diff --git a/src/DevBetterWeb.Web/appsettings.json b/src/DevBetterWeb.Web/appsettings.json index 060bc51b0..b2acaabe1 100644 --- a/src/DevBetterWeb.Web/appsettings.json +++ b/src/DevBetterWeb.Web/appsettings.json @@ -14,6 +14,11 @@ "ApiSettings": { "ApiKey": "[api key string goes here]" }, + "ApiMailSenderOptions": { + "ApiBaseUrl": "https://api.smtp2go.com/v3/", + "ApiKey": "[smtp2go api key goes here]", + "Sender": "donotreply@devbetter.com" + }, "Logging": { "ApplicationInsights": { "LogLevel": { diff --git a/tests/DevBetterWeb.Tests/Services/EmailServiceTests/Smtp2GoEmailServiceTests.cs b/tests/DevBetterWeb.Tests/Services/EmailServiceTests/Smtp2GoEmailServiceTests.cs new file mode 100644 index 000000000..88b064b13 --- /dev/null +++ b/tests/DevBetterWeb.Tests/Services/EmailServiceTests/Smtp2GoEmailServiceTests.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading.Tasks; +using DevBetterWeb.Infrastructure.Services; +using Microsoft.Extensions.Options; +using Xunit; + +namespace DevBetterWeb.Tests.Services.EmailServiceTests; + +public class Smtp2GoEmailServiceTests +{ + [Fact] + public void Constructor_WithNullOptions_ThrowsArgumentNullException() + { + Assert.Throws(() => new Smtp2GoEmailService(null!)); + } + + [Fact] + public async Task SendEmailAsync_WithNullApiKey_ThrowsException() + { + var options = new AuthMessageSenderOptions + { + SmtpServer = "mail.smtp2go.com" + }; + var service = new Smtp2GoEmailService(Options.Create(options)); + + var exception = await Assert.ThrowsAsync(() => + service.SendEmailAsync("test@example.com", "Test Subject", "Test Message")); + + Assert.Equal("SMTP API Key not set.", exception.Message); + } +}