Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Zetian.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<Folder Name="/tests/">
<Project Path="tests/Zetian.Clustering.Tests/Zetian.Clustering.Tests.csproj" />
<Project Path="tests/Zetian.HealthCheck.Tests/Zetian.HealthCheck.Tests.csproj" />
<Project Path="tests/Zetian.Relay.Tests/Zetian.Relay.Tests.csproj" />
<Project Path="tests/Zetian.Tests/Zetian.Tests.csproj" />
</Folder>
</Solution>
23 changes: 23 additions & 0 deletions examples/Zetian.Relay.Examples/BasicRelayExample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,29 @@ public static async Task RunAsync(ILoggerFactory loggerFactory)
Console.WriteLine("[INFO] Starting SMTP server with relay on port 25025...");
RelayService relayService = await server.StartWithRelayAsync();

// Subscribe to relay delivery lifecycle events.
// These are ideal for persisting delivery outcomes to an external store (e.g. a database).
relayService.MessageDelivered += (sender, e) =>
{
Console.WriteLine($"[RELAY] DELIVERED {e.QueueId} from {e.From?.Address} via {e.SmartHost}");
};

relayService.MessageBounced += (sender, e) =>
{
Console.WriteLine($"[RELAY] BOUNCED {e.QueueId} from {e.From?.Address}: {e.Error} (attempts: {e.RetryCount})");
// e.g. persist the failure: await failureStore.SaveAsync(e.QueueId, e.From?.Address, e.Error);
};

relayService.MessageDeferred += (sender, e) =>
{
Console.WriteLine($"[RELAY] DEFERRED {e.QueueId}: {e.Error} — next attempt at {e.NextRetryTime:HH:mm:ss} UTC");
};

relayService.MessageExpired += (sender, e) =>
{
Console.WriteLine($"[RELAY] EXPIRED {e.QueueId} from {e.From?.Address} after {e.RetryCount} attempts");
};

// Handle message received event - just for logging
// The actual relay queuing is handled by EnableRelay's event handler
server.MessageReceived += (sender, e) =>
Expand Down
73 changes: 73 additions & 0 deletions src/Zetian.Relay/Models/EventArgs/RelayDeliveryEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using System.Net.Mail;
using Zetian.Relay.Abstractions;
using Zetian.Relay.Enums;

namespace Zetian.Relay.Models.EventArgs
{
/// <summary>
/// Event arguments raised by the relay service for delivery lifecycle
/// events (delivered, bounced, deferred and expired). Useful for logging
/// delivery outcomes to an external store such as a database.
/// </summary>
/// <remarks>
/// Initializes a new instance of <see cref="RelayDeliveryEventArgs"/>.
/// </remarks>
public class RelayDeliveryEventArgs(IRelayMessage message) : System.EventArgs
{
/// <summary>
/// Gets the relay message the event relates to.
/// </summary>
public IRelayMessage Message { get; } = message ?? throw new ArgumentNullException(nameof(message));

/// <summary>
/// Gets the unique queue ID of the message.
/// </summary>
public string QueueId => Message.QueueId;

/// <summary>
/// Gets the sender address of the message, if any.
/// </summary>
public MailAddress? From => Message.From;

/// <summary>
/// Gets the current status of the message at the time the event was raised.
/// </summary>
public RelayStatus Status => Message.Status;

/// <summary>
/// Gets the target smart host the message was routed to, when known.
/// </summary>
public string? SmartHost => Message.SmartHost;

/// <summary>
/// Gets the number of delivery attempts made so far.
/// </summary>
public int RetryCount => Message.RetryCount;

/// <summary>
/// Gets the result of the SMTP delivery attempt, when available.
/// Populated for the delivered, bounced and deferred events that follow
/// an actual delivery attempt; <c>null</c> when no attempt produced a result
/// (for example the expired event, or a transport-level exception).
/// </summary>
public SmtpDeliveryResult? Result { get; init; }

/// <summary>
/// Gets the error or diagnostic message, when available.
/// Populated for the bounced, deferred and expired events.
/// </summary>
public string? Error { get; init; }

/// <summary>
/// Gets the scheduled time of the next delivery attempt.
/// Populated only for the deferred event.
/// </summary>
public DateTime? NextRetryTime { get; init; }

/// <summary>
/// Gets the UTC timestamp at which the event was raised.
/// </summary>
public DateTime Timestamp { get; } = DateTime.UtcNow;
}
}
62 changes: 62 additions & 0 deletions src/Zetian.Relay/README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ SMTP relay and proxy extension for Zetian SMTP Server with smart host support, q
- ⚖️ **Load Balancing** - Distribute load across multiple relay servers
- 🔄 **Queue Management** - Persistent queue with retry mechanisms
- 📬 **Smart Host Support** - Route messages through configured relay servers
- 📡 **Delivery Events** - Lifecycle events for delivered, bounced, deferred and expired messages

## 📦 Installation

Expand Down Expand Up @@ -307,6 +308,67 @@ var server = SmtpServerBuilder
});
```

## 📡 Delivery Events

The relay service raises events for every stage of a message's delivery lifecycle.
These are ideal for logging delivery outcomes to an external store (such as a database),
metrics, or alerting. All events share the `RelayDeliveryEventArgs` payload.

| Event | Raised when |
| --- | --- |
| `MessageDelivered` | The message was successfully delivered to all recipients. |
| `MessageBounced` | Delivery permanently failed (a permanent SMTP error or the retry limit was reached). Fires regardless of whether a bounce/NDR message is generated. |
| `MessageDeferred` | A delivery attempt failed temporarily and the message was rescheduled for retry. |
| `MessageExpired` | The message exceeded its lifetime before being delivered. |

```csharp
using Zetian.Relay.Services;
using Zetian.Relay.Extensions;

RelayService relayService = await server.StartWithRelayAsync();
// (or retrieve it later with: var relayService = server.GetRelayService();)

relayService.MessageDelivered += (sender, e) =>
{
Console.WriteLine($"Delivered {e.QueueId} from {e.From?.Address} via {e.SmartHost}");
};

relayService.MessageBounced += async (sender, e) =>
{
// Persist the failure to another database
await failureStore.SaveAsync(e.QueueId, e.From?.Address, e.Error, e.RetryCount);
};
Comment on lines +336 to +340

relayService.MessageDeferred += (sender, e) =>
{
Console.WriteLine($"Deferred {e.QueueId}: {e.Error} — next attempt at {e.NextRetryTime:u}");
};

relayService.MessageExpired += (sender, e) =>
{
Console.WriteLine($"Expired {e.QueueId} after {e.RetryCount} attempts");
};
```

### RelayDeliveryEventArgs

| Property | Description |
| --- | --- |
| `Message` | The underlying `IRelayMessage` (queue ID, recipients, status, metadata, …). |
| `QueueId` | Unique queue ID of the message. |
| `From` | The sender address, if any. |
| `Status` | The message status at the time the event was raised. |
| `SmartHost` | The target smart host the message was routed to, when known. |
| `RetryCount` | Number of delivery attempts made so far. |
| `Result` | The `SmtpDeliveryResult` of the attempt, when available (delivered / bounced / deferred). |
| `Error` | The error or diagnostic message (bounced / deferred / expired). |
| `NextRetryTime` | The scheduled time of the next attempt (deferred only). |
| `Timestamp` | UTC time the event was raised. |

> Event handlers are invoked synchronously from the delivery pipeline and any exception
> they throw is caught and logged, so a faulty handler never interrupts delivery. Keep
> handlers fast; offload heavy work (database writes, network calls) to a background queue.

## 🔐 Authentication Methods

### AUTH PLAIN
Expand Down
Loading
Loading