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);
+ }
+}