From d04cacf67c7d8b118d644633c2cd2cd72c5b7193 Mon Sep 17 00:00:00 2001
From: Alexis <3876562+alexis-@users.noreply.github.com>
Date: Sat, 7 Feb 2026 20:04:33 +0100
Subject: [PATCH 1/3] Fixed payload - smtp2go dev docs have errors
---
README.md | 26 ++--
.../Models/Webhooks/WebhookCallbackPayload.cs | 79 ++++++++----
src/Smtp2Go.NET/Smtp2Go.NET.csproj | 2 +-
.../WebhookPayloadDeserializationTests.cs | 115 ++++++++++++++----
4 files changed, 160 insertions(+), 62 deletions(-)
diff --git a/README.md b/README.md
index 4e65d3a..69c792c 100644
--- a/README.md
+++ b/README.md
@@ -90,16 +90,16 @@ public IActionResult HandleWebhook([FromBody] WebhookCallbackPayload payload)
switch (payload.Event)
{
case WebhookCallbackEvent.Delivered:
- logger.LogInformation("Delivered to {Email}", payload.Email);
+ logger.LogInformation("Delivered to {Recipient}", payload.Recipient);
break;
case WebhookCallbackEvent.Bounce:
- logger.LogWarning("Bounce ({Type}) for {Email}: {Context}",
- payload.BounceType, payload.Email, payload.BounceContext);
+ logger.LogWarning("Bounce ({Type}) for {Recipient}: {Context}",
+ payload.BounceType, payload.Recipient, payload.BounceContext);
break;
case WebhookCallbackEvent.SpamComplaint:
- logger.LogWarning("Spam complaint from {Email}", payload.Email);
+ logger.LogWarning("Spam complaint from {Recipient}", payload.Recipient);
break;
}
@@ -129,14 +129,16 @@ SMTP2GO uses different event names for **subscriptions** vs **callback payloads*
|-------|------|-------------|
| `Event` | `WebhookCallbackEvent` | The event type that triggered this callback |
| `EmailId` | `string?` | SMTP2GO email identifier (correlates with send response) |
-| `Email` | `string?` | Recipient email address for this event |
+| `Recipient` | `string?` | Per-event recipient (`rcpt`); present for delivered/bounce events |
+| `Recipients` | `string[]?` | All recipients from the original send; present for processed events |
| `Sender` | `string?` | Sender email address |
-| `Timestamp` | `int` | Unix timestamp (seconds since epoch) |
-| `Hostname` | `string?` | SMTP2GO server that processed the email |
-| `RecipientsList` | `string[]?` | All recipients from the original send |
+| `Time` | `DateTimeOffset?` | ISO 8601 timestamp when the event occurred |
+| `SendTime` | `DateTimeOffset?` | ISO 8601 timestamp when the email was sent by SMTP2GO |
+| `SourceHost` | `string?` | Source host IP of the SMTP2GO server that processed the email |
| `BounceType` | `BounceType?` | `Hard` or `Soft` (bounce events only) |
-| `BounceContext` | `string?` | SMTP transaction context (bounce events only) |
-| `Host` | `string?` | Target mail server host and IP (bounce events only) |
+| `BounceContext` | `string?` | SMTP transaction context (bounce and delivered events) |
+| `Host` | `string?` | Target mail server host and IP (bounce and delivered events) |
+| `SmtpResponse` | `string?` | SMTP 250 response from receiving server (delivered events only) |
| `ClickUrl` | `string?` | Original URL clicked (click events only) |
| `Link` | `string?` | Tracked link URL (click events only) |
@@ -209,7 +211,7 @@ dotnet build Smtp2Go.NET.slnx
### Testing
```bash
-# Unit tests (73 tests, no network required)
+# Unit tests (74 tests, no network required)
tests/Smtp2Go.NET.UnitTests/bin/Debug/net10.0/Smtp2Go.NET.UnitTests
# Integration tests (15 tests, requires API keys configured via user secrets)
@@ -244,7 +246,7 @@ Smtp2Go.NET/
│ ├── Smtp2GoClient.cs # Main client implementation
│ └── ServiceCollectionExtensions.cs # DI registration
└── tests/
- ├── Smtp2Go.NET.UnitTests/ # 73 unit tests (Moq-based)
+ ├── Smtp2Go.NET.UnitTests/ # 77 unit tests (Moq-based)
└── Smtp2Go.NET.IntegrationTests/ # 15 integration tests (live API)
```
diff --git a/src/Smtp2Go.NET/Models/Webhooks/WebhookCallbackPayload.cs b/src/Smtp2Go.NET/Models/Webhooks/WebhookCallbackPayload.cs
index c112bef..f84388f 100644
--- a/src/Smtp2Go.NET/Models/Webhooks/WebhookCallbackPayload.cs
+++ b/src/Smtp2Go.NET/Models/Webhooks/WebhookCallbackPayload.cs
@@ -13,7 +13,10 @@ namespace Smtp2Go.NET.Models.Webhooks;
///
/// The fields populated depend on the event type:
///
-/// - , , and are only present for bounce events.
+/// - (rcpt) is present for delivered and bounce events.
+/// - is present for processed events (array of all recipients).
+/// - , , and
+/// are present for bounce and delivered events.
/// - and are only present for click events.
///
///
@@ -27,7 +30,7 @@ namespace Smtp2Go.NET.Models.Webhooks;
/// switch (payload.Event)
/// {
/// case WebhookCallbackEvent.Delivered:
-/// // Handle delivery confirmation
+/// // Handle delivery confirmation — payload.Recipient has the recipient
/// break;
/// case WebhookCallbackEvent.Bounce:
/// // Handle bounce — check payload.BounceType for hard/soft
@@ -40,10 +43,13 @@ namespace Smtp2Go.NET.Models.Webhooks;
public class WebhookCallbackPayload
{
///
- /// Gets the hostname of the SMTP2GO sending server that processed the email.
+ /// Gets the source host IP address of the SMTP2GO server that processed the email.
///
- [JsonPropertyName("hostname")]
- public string? Hostname { get; init; }
+ ///
+ /// Maps to the srchost field in the SMTP2GO webhook JSON payload.
+ ///
+ [JsonPropertyName("srchost")]
+ public string? SourceHost { get; init; }
///
/// Gets the unique SMTP2GO identifier for the email associated with this event.
@@ -72,29 +78,37 @@ public class WebhookCallbackPayload
public WebhookCallbackEvent Event { get; init; }
///
- /// Gets the Unix timestamp (seconds since epoch) when the event occurred.
+ /// Gets the ISO 8601 timestamp when the event occurred.
///
///
- ///
- /// Convert to using
- /// .
- ///
+ /// Maps to the time field in the SMTP2GO webhook JSON payload.
+ /// Format example: 2026-02-07T18:05:02Z.
+ ///
+ [JsonPropertyName("time")]
+ public DateTimeOffset? Time { get; init; }
+
+ ///
+ /// Gets the ISO 8601 timestamp when the email was sent by SMTP2GO.
+ ///
+ ///
+ /// Maps to the sendtime field in the SMTP2GO webhook JSON payload.
+ /// Format example: 2026-02-07T18:05:02.199324+00:00.
///
- [JsonPropertyName("timestamp")]
- public int Timestamp { get; init; }
+ [JsonPropertyName("sendtime")]
+ public DateTimeOffset? SendTime { get; init; }
///
- /// Gets the recipient email address associated with this event.
+ /// Gets the per-event recipient email address.
///
///
///
- /// The specific recipient that this event applies to. For example,
- /// a delivered event for a multi-recipient email will generate one
- /// webhook per recipient.
+ /// Maps to the rcpt field in the SMTP2GO webhook JSON payload.
+ /// Present for delivered and bounce events (one webhook per recipient).
+ /// Not present for processed events — use instead.
///
///
- [JsonPropertyName("email")]
- public string? Email { get; init; }
+ [JsonPropertyName("rcpt")]
+ public string? Recipient { get; init; }
///
/// Gets the sender email address of the original email.
@@ -107,11 +121,13 @@ public class WebhookCallbackPayload
///
///
///
- /// Contains all To, CC, and BCC recipients from the original send request.
+ /// Maps to the recipients field in the SMTP2GO webhook JSON payload.
+ /// Present for processed events. For delivered/bounce events, use
+ /// (rcpt) which has the per-event recipient.
///
///
- [JsonPropertyName("recipients_list")]
- public string[]? RecipientsList { get; init; }
+ [JsonPropertyName("recipients")]
+ public string[]? Recipients { get; init; }
///
/// Gets the bounce type when the event is a bounce.
@@ -132,12 +148,13 @@ public class WebhookCallbackPayload
public BounceType? BounceType { get; init; }
///
- /// Gets the bounce diagnostic context from the recipient's mail server.
+ /// Gets the diagnostic context from the recipient's mail server.
///
///
///
- /// Only populated for events. Contains
+ /// Present for bounce and delivered events. For bounce events, contains
/// the SMTP transaction context (e.g., "RCPT TO:<user@example.com>").
+ /// For delivered events, may contain "Unavailable".
///
///
[JsonPropertyName("context")]
@@ -148,13 +165,25 @@ public class WebhookCallbackPayload
///
///
///
- /// Only populated for events. Contains the
- /// MX host and IP address (e.g., "gmail-smtp-in.l.google.com [209.85.233.26]").
+ /// Present for bounce and delivered events. Contains the MX host and IP address
+ /// (e.g., "mail.protonmail.ch [176.119.200.128]").
///
///
[JsonPropertyName("host")]
public string? Host { get; init; }
+ ///
+ /// Gets the SMTP response message from the receiving mail server.
+ ///
+ ///
+ ///
+ /// Present for delivered events. Contains the SMTP 250 response
+ /// (e.g., "250 2.0.0 Ok: 2788 bytes queued as 4f7f4b3tWbzKy").
+ ///
+ ///
+ [JsonPropertyName("message")]
+ public string? SmtpResponse { get; init; }
+
///
/// Gets the URL that was clicked by the recipient.
///
diff --git a/src/Smtp2Go.NET/Smtp2Go.NET.csproj b/src/Smtp2Go.NET/Smtp2Go.NET.csproj
index 20e8f78..df55b91 100644
--- a/src/Smtp2Go.NET/Smtp2Go.NET.csproj
+++ b/src/Smtp2Go.NET/Smtp2Go.NET.csproj
@@ -5,7 +5,7 @@
Smtp2Go.NET
- 1.0.0
+ 1.1.0
A .NET client library for the SMTP2GO email delivery API. Supports sending emails, webhook management, and email statistics with built-in resilience.
smtp2go;email;smtp;api;webhook;dotnet
diff --git a/tests/Smtp2Go.NET.UnitTests/Models/WebhookPayloadDeserializationTests.cs b/tests/Smtp2Go.NET.UnitTests/Models/WebhookPayloadDeserializationTests.cs
index 4e2a439..e7ba467 100644
--- a/tests/Smtp2Go.NET.UnitTests/Models/WebhookPayloadDeserializationTests.cs
+++ b/tests/Smtp2Go.NET.UnitTests/Models/WebhookPayloadDeserializationTests.cs
@@ -9,24 +9,82 @@ namespace Smtp2Go.NET.UnitTests.Models;
/// including the custom JSON converters for
/// and .
///
+///
+/// JSON fixtures in these tests use the actual SMTP2GO webhook format,
+/// captured from live webhook callbacks (not documentation — the docs are inaccurate).
+/// Key differences from SMTP2GO docs:
+///
+/// - Recipient field is "rcpt" (delivered/bounce), not "email".
+/// - Recipients array is "recipients" (processed), not "recipients_list".
+/// - Timestamp is "time" (ISO 8601 string), not "timestamp" (int).
+/// - Source host is "srchost", not "hostname".
+///
+///
[Trait("Category", "Unit")]
public sealed class WebhookPayloadDeserializationTests
{
+ #region Processed Event
+
+ [Fact]
+ public void Deserialize_ProcessedEvent_ParsesCorrectly()
+ {
+ // Arrange — Actual SMTP2GO processed event format. Note: processed events have "recipients"
+ // array but NOT "rcpt".
+ const string json = """
+ {
+ "srchost": "146.70.170.30",
+ "email_id": "1vomg2-abc123",
+ "event": "processed",
+ "time": "2026-02-07T18:05:02Z",
+ "sender": "noreply@example.com",
+ "recipients": ["user@example.com", "user2@example.com"],
+ "sendtime": "2026-02-07T18:05:02.199324+00:00"
+ }
+ """;
+
+ // Act
+ var payload = JsonSerializer.Deserialize(json, Smtp2GoJsonDefaults.Options);
+
+ // Assert
+ payload.Should().NotBeNull();
+ payload!.SourceHost.Should().Be("146.70.170.30");
+ payload.EmailId.Should().Be("1vomg2-abc123");
+ payload.Event.Should().Be(WebhookCallbackEvent.Processed);
+ payload.Time.Should().Be(new DateTimeOffset(2026, 2, 7, 18, 5, 2, TimeSpan.Zero));
+ payload.SendTime.Should().NotBeNull();
+ payload.Sender.Should().Be("noreply@example.com");
+
+ // Processed events have "recipients" array, not "rcpt".
+ payload.Recipient.Should().BeNull();
+ payload.Recipients.Should().HaveCount(2);
+ payload.Recipients![0].Should().Be("user@example.com");
+
+ payload.BounceType.Should().BeNull();
+ payload.BounceContext.Should().BeNull();
+ }
+
+ #endregion
+
+
#region Delivered Event
[Fact]
public void Deserialize_DeliveredEvent_ParsesCorrectly()
{
- // Arrange
+ // Arrange — Actual SMTP2GO delivered event format. Note: delivered events have "rcpt"
+ // (single recipient) but NOT "recipients" array.
const string json = """
{
- "hostname": "mail01.smtp2go.com",
- "email_id": "abc-123",
+ "srchost": "146.70.170.30",
+ "email_id": "1vomg2-abc123",
"event": "delivered",
- "timestamp": 1700000000,
- "email": "user@example.com",
+ "time": "2026-02-07T18:05:06Z",
+ "rcpt": "user@example.com",
"sender": "noreply@alos.app",
- "recipients_list": ["user@example.com", "user2@example.com"]
+ "host": "mail.protonmail.ch [176.119.200.128]",
+ "context": "Unavailable",
+ "message": "250 2.0.0 Ok: 2788 bytes queued as 4f7f4b3tWbzKy",
+ "sendtime": "2026-02-07T18:05:06.215451+00:00"
}
""";
@@ -35,15 +93,21 @@ public void Deserialize_DeliveredEvent_ParsesCorrectly()
// Assert
payload.Should().NotBeNull();
- payload!.Hostname.Should().Be("mail01.smtp2go.com");
- payload.EmailId.Should().Be("abc-123");
+ payload!.SourceHost.Should().Be("146.70.170.30");
+ payload.EmailId.Should().Be("1vomg2-abc123");
payload.Event.Should().Be(WebhookCallbackEvent.Delivered);
- payload.Timestamp.Should().Be(1700000000);
- payload.Email.Should().Be("user@example.com");
+ payload.Time.Should().Be(new DateTimeOffset(2026, 2, 7, 18, 5, 6, TimeSpan.Zero));
+ payload.SendTime.Should().NotBeNull();
+
+ // Delivered events have "rcpt" (per-event recipient), not "recipients" array.
+ payload.Recipient.Should().Be("user@example.com");
+ payload.Recipients.Should().BeNull();
+
payload.Sender.Should().Be("noreply@alos.app");
- payload.RecipientsList.Should().HaveCount(2);
+ payload.Host.Should().Be("mail.protonmail.ch [176.119.200.128]");
+ payload.BounceContext.Should().Be("Unavailable");
+ payload.SmtpResponse.Should().Be("250 2.0.0 Ok: 2788 bytes queued as 4f7f4b3tWbzKy");
payload.BounceType.Should().BeNull();
- payload.BounceContext.Should().BeNull();
}
#endregion
@@ -61,9 +125,9 @@ public void Deserialize_BounceEvent_HardBounce_ParsesBounceFields()
{
"email_id": "bounce-456",
"event": "bounce",
- "timestamp": 1700000100,
- "email": "invalid@nonexistent.com",
- "from": "noreply@alos.app",
+ "time": "2026-02-07T18:15:00Z",
+ "rcpt": "invalid@nonexistent.com",
+ "sender": "noreply@alos.app",
"bounce": "hard",
"context": "RCPT TO:",
"host": "gmail-smtp-in.l.google.com [209.85.233.26]"
@@ -79,6 +143,7 @@ public void Deserialize_BounceEvent_HardBounce_ParsesBounceFields()
payload.BounceType.Should().Be(BounceType.Hard);
payload.BounceContext.Should().Be("RCPT TO:");
payload.Host.Should().Be("gmail-smtp-in.l.google.com [209.85.233.26]");
+ payload.Recipient.Should().Be("invalid@nonexistent.com");
}
@@ -89,8 +154,8 @@ public void Deserialize_BounceEvent_SoftBounce_ParsesBounceFields()
const string json = """
{
"event": "bounce",
- "timestamp": 1700000200,
- "email": "user@example.com",
+ "time": "2026-02-07T18:18:00Z",
+ "rcpt": "user@example.com",
"bounce": "soft",
"context": "DATA: 452 Mailbox full"
}
@@ -104,6 +169,7 @@ public void Deserialize_BounceEvent_SoftBounce_ParsesBounceFields()
payload!.Event.Should().Be(WebhookCallbackEvent.Bounce);
payload.BounceType.Should().Be(BounceType.Soft);
payload.BounceContext.Should().Be("DATA: 452 Mailbox full");
+ payload.Recipient.Should().Be("user@example.com");
}
#endregion
@@ -118,8 +184,8 @@ public void Deserialize_ClickedEvent_ParsesClickFields()
const string json = """
{
"event": "clicked",
- "timestamp": 1700000300,
- "email": "user@example.com",
+ "time": "2026-02-07T18:20:00Z",
+ "rcpt": "user@example.com",
"click_url": "https://alos.app/dashboard",
"link": "https://track.smtp2go.com/abc123"
}
@@ -133,6 +199,7 @@ public void Deserialize_ClickedEvent_ParsesClickFields()
payload!.Event.Should().Be(WebhookCallbackEvent.Clicked);
payload.ClickUrl.Should().Be("https://alos.app/dashboard");
payload.Link.Should().Be("https://track.smtp2go.com/abc123");
+ payload.Recipient.Should().Be("user@example.com");
}
#endregion
@@ -151,7 +218,7 @@ public void Deserialize_ClickedEvent_ParsesClickFields()
public void CallbackEventConverter_DeserializesKnownEvents(string jsonValue, WebhookCallbackEvent expected)
{
// Arrange
- var json = $$"""{"event": "{{jsonValue}}", "timestamp": 0}""";
+ var json = $$"""{"event": "{{jsonValue}}"}""";
// Act
var payload = JsonSerializer.Deserialize(json, Smtp2GoJsonDefaults.Options);
@@ -171,7 +238,7 @@ public void CallbackEventConverter_DeserializesUnknownEvent_AsUnknown(string jso
// Arrange — The API may introduce new event types in the future.
// Also verifies that the removed legacy values ("hard_bounced", "soft_bounced")
// now correctly fall through to Unknown instead of being mapped to dead enum values.
- var json = $$"""{"event": "{{jsonValue}}", "timestamp": 0}""";
+ var json = $$"""{"event": "{{jsonValue}}"}""";
// Act
var payload = JsonSerializer.Deserialize(json, Smtp2GoJsonDefaults.Options);
@@ -214,7 +281,7 @@ public void CallbackEventConverter_SerializesToSnakeCase(WebhookCallbackEvent va
public void BounceTypeConverter_DeserializesKnownTypes(string jsonValue, BounceType expected)
{
// Arrange — The "bounce" field contains the bounce classification (hard/soft).
- var json = $$"""{"event": "bounce", "timestamp": 0, "bounce": "{{jsonValue}}"}""";
+ var json = $$"""{"event": "bounce", "bounce": "{{jsonValue}}"}""";
// Act
var payload = JsonSerializer.Deserialize(json, Smtp2GoJsonDefaults.Options);
@@ -229,7 +296,7 @@ public void BounceTypeConverter_DeserializesKnownTypes(string jsonValue, BounceT
public void BounceTypeConverter_DeserializesUnknownType_AsUnknown()
{
// Arrange
- const string json = """{"event": "bounce", "timestamp": 0, "bounce": "future_type"}""";
+ const string json = """{"event": "bounce", "bounce": "future_type"}""";
// Act
var payload = JsonSerializer.Deserialize(json, Smtp2GoJsonDefaults.Options);
@@ -244,7 +311,7 @@ public void BounceTypeConverter_DeserializesUnknownType_AsUnknown()
public void BounceTypeConverter_DeserializesNull_AsNull()
{
// Arrange — Non-bounce events have no "bounce" field.
- const string json = """{"event": "delivered", "timestamp": 0}""";
+ const string json = """{"event": "delivered"}""";
// Act
var payload = JsonSerializer.Deserialize(json, Smtp2GoJsonDefaults.Options);
From 9173a4839fe415209a0f1ad447dd775cc0c033e7 Mon Sep 17 00:00:00 2001
From: Alexis <3876562+alexis-@users.noreply.github.com>
Date: Sat, 7 Feb 2026 20:12:25 +0100
Subject: [PATCH 2/3] Fixed integration test issue
---
.../WebhookDeliveryIntegrationTests.cs | 35 ++++++++++++++++++-
1 file changed, 34 insertions(+), 1 deletion(-)
diff --git a/tests/Smtp2Go.NET.IntegrationTests/Webhooks/WebhookDeliveryIntegrationTests.cs b/tests/Smtp2Go.NET.IntegrationTests/Webhooks/WebhookDeliveryIntegrationTests.cs
index 3413791..cfa66e8 100644
--- a/tests/Smtp2Go.NET.IntegrationTests/Webhooks/WebhookDeliveryIntegrationTests.cs
+++ b/tests/Smtp2Go.NET.IntegrationTests/Webhooks/WebhookDeliveryIntegrationTests.cs
@@ -290,7 +290,11 @@ private async Task SetupWebhookPipelineAsync(
};
var webhookUrl = webhookUri.Uri.AbsoluteUri;
- // Step 3: Register the webhook with SMTP2GO.
+ // Step 3: Delete any stale webhooks from previous runs.
+ // SMTP2GO free tier allows only 1 webhook — a stale webhook from a failed run blocks creation.
+ await DeleteAllExistingWebhooksAsync(ct);
+
+ // Step 4: Register the webhook with SMTP2GO.
var createRequest = new WebhookCreateRequest
{
WebhookUrl = webhookUrl,
@@ -308,6 +312,35 @@ private async Task SetupWebhookPipelineAsync(
}
+ ///
+ /// Deletes all existing webhooks on the SMTP2GO account.
+ /// SMTP2GO free tier limits accounts to 1 webhook — stale webhooks from
+ /// previous failed runs block creation of new ones.
+ ///
+ private async Task DeleteAllExistingWebhooksAsync(CancellationToken ct)
+ {
+ var listResponse = await _fixture.Client.Webhooks.ListAsync(ct);
+
+ if (listResponse.Data is not { Length: > 0 })
+ return;
+
+ foreach (var webhook in listResponse.Data)
+ {
+ if (webhook.WebhookId is { } id)
+ {
+ try
+ {
+ await _fixture.Client.Webhooks.DeleteAsync(id, ct);
+ }
+ catch
+ {
+ // Best-effort cleanup — continue with remaining webhooks.
+ }
+ }
+ }
+ }
+
+
///
/// Best-effort webhook cleanup. Silently ignores errors to prevent masking test failures.
///
From c558458d2cea874c2e87797ec144aadd56074447 Mon Sep 17 00:00:00 2001
From: Alexis <3876562+alexis-@users.noreply.github.com>
Date: Sat, 7 Feb 2026 20:12:32 +0100
Subject: [PATCH 3/3] Added missing file
---
.../WebhookManagementIntegrationTests.cs | 39 +++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/tests/Smtp2Go.NET.IntegrationTests/Webhooks/WebhookManagementIntegrationTests.cs b/tests/Smtp2Go.NET.IntegrationTests/Webhooks/WebhookManagementIntegrationTests.cs
index 9345483..a459a52 100644
--- a/tests/Smtp2Go.NET.IntegrationTests/Webhooks/WebhookManagementIntegrationTests.cs
+++ b/tests/Smtp2Go.NET.IntegrationTests/Webhooks/WebhookManagementIntegrationTests.cs
@@ -45,6 +45,39 @@ public WebhookManagementIntegrationTests(Smtp2GoLiveFixture fixture)
#endregion
+ #region Methods - Helpers
+
+ ///
+ /// Deletes all existing webhooks on the SMTP2GO account.
+ /// SMTP2GO free tier limits accounts to 1 webhook — stale webhooks from
+ /// previous failed runs or E2E tests block creation of new ones.
+ ///
+ private async Task DeleteAllExistingWebhooksAsync(CancellationToken ct)
+ {
+ var listResponse = await _fixture.Client.Webhooks.ListAsync(ct);
+
+ if (listResponse.Data is not { Length: > 0 })
+ return;
+
+ foreach (var webhook in listResponse.Data)
+ {
+ if (webhook.WebhookId is { } id)
+ {
+ try
+ {
+ await _fixture.Client.Webhooks.DeleteAsync(id, ct);
+ }
+ catch
+ {
+ // Best-effort cleanup — continue with remaining webhooks.
+ }
+ }
+ }
+ }
+
+ #endregion
+
+
#region Webhook Lifecycle
[Fact]
@@ -56,6 +89,9 @@ public async Task WebhookLifecycle_CreateListDelete_Succeeds()
var ct = TestContext.Current.CancellationToken;
int? webhookId = null;
+ // SMTP2GO free tier allows only 1 webhook — clear stale webhooks from previous runs.
+ await DeleteAllExistingWebhooksAsync(ct);
+
try
{
// Step 1: Create a webhook.
@@ -133,6 +169,9 @@ public async Task WebhookCreate_WithSpecificEvents_ConfiguresCorrectly()
var ct = TestContext.Current.CancellationToken;
int? webhookId = null;
+ // SMTP2GO free tier allows only 1 webhook — clear stale webhooks from previous runs.
+ await DeleteAllExistingWebhooksAsync(ct);
+
try
{
// Arrange — Create a webhook with a specific set of event types.