MailForge

Communication

The MailForge.Communication package generalizes MailForge from an email-only library into a channel-aware notification framework — without changing the email API. It sits on top of the core MailForge pipeline: the EmailChannel delegates to the existing IEmailSender, so every email behavior (validation, retries, auditing, failover) is preserved as-is. Channels live in their own packages — MailForge.Telegram and MailForge.Webhook ship the first non-email ones, and any channel plugs into the same INotificationProvider contract.

The Model

A communication is a single Notification addressed to one channel:

using MailForge.Communication.Abstractions;
using MailForge.Communication.Models;

var notification = new Notification(
    ChannelType.Email,
    new EmailContent(emailMessage),
    NotificationPriority.High,
    headers: new Dictionary<string, string> { { "X-Sev", "2" } },
    tags: new Dictionary<string, string> { { "kind", "order" } });

Sending

INotificationSender routes a notification to the channel registered for its ChannelType:

INotificationSender sender = ...;
NotificationDeliveryResult result = await sender.SendAsync(notification);

NotificationDeliveryResult mirrors EmailDeliveryResult: Status (Sent | FailedValidation | Failed), Succeeded, ProviderResult (the underlying ProviderDeliveryResult), and Details. Unknown channels and provider failures are mapped to Failed; unexpected channel exceptions are wrapped in a NotificationException with IsTransient = true.

Channels and Middleware

Registration

services.AddCommunication(builder => builder
    .UseEmailChannel()
    .AddMiddleware(new KillSwitchMiddleware()));

UseChannel(IChannel) and UseChannel(Func<IServiceProvider, IChannel>) register any channel; UseEmailChannel() wires the email adapter over the IEmailSender registered by AddMailForge. AddCommunication registers INotificationSender, IChannelRegistry, and every configured channel and middleware as singletons.

See Core for the email pipeline and Architecture for where communication fits.

Telegram Channel

MailForge.Telegram adds Telegram as a first-class channel: TelegramContent carries one or more chat ids plus the message text, an optional parse mode, media, and an inline keyboard. TelegramChannel hands the notification to TelegramNotificationProvider, which calls the Bot API (sendMessage, sendPhoto, sendDocument). Transient failures (429, 5xx, transport errors) are thrown as NotificationException with IsTransient == true; permanent API rejections (unknown chat, invalid payload) return a Failed result.

using MailForge.Communication.Extensions;
using MailForge.Communication.Models;
using MailForge.Telegram;
using MailForge.Telegram.Extensions;

services.AddCommunication(builder => builder
    .UseEmailChannel()
    .UseTelegramChannel(new TelegramOptions { BotToken = "123:ABC" }));

INotificationSender sender = ...;

var result = await sender.SendAsync(new Notification(
    ChannelType.Telegram,
    new TelegramContent(
        chatId: "@ops-alerts",
        text: "<b>Build failed</b> on <i>main</i>",
        parseMode: TelegramParseMode.Html,
        keyboard: new TelegramKeyboard(
            TelegramKeyboardButton.WithUrl("Open build", "https://ci.example.com/builds/42")))));

UseTelegramChannel(INotificationProvider) registers the channel over an existing provider — useful for custom providers or future failover chains.

Webhook Channel

MailForge.Webhook delivers a notification as an HTTP POST to any endpoint. WebhookContent carries the raw request body (typically JSON) and its content type; WebhookNotificationProvider posts it verbatim to WebhookOptions.Endpoint, applying any configured request headers. When SigningSecret is set, the body is signed with HMAC-SHA256 and the lowercase hex digest is written to the X-MailForge-Signature header (configurable via SignatureHeader, optional SignaturePrefix). Transient failures (429, 5xx, transport errors) are thrown as NotificationException with IsTransient == true; endpoint rejections (4xx) return a Failed result.

using MailForge.Communication.Extensions;
using MailForge.Communication.Models;
using MailForge.Webhook;
using MailForge.Webhook.Extensions;

services.AddCommunication(builder => builder
    .UseEmailChannel()
    .UseWebhookChannel(new WebhookOptions
    {
        Endpoint = new Uri("https://hooks.example.com/notify"),
        SigningSecret = "shared-secret",
        Headers = { ["Authorization"] = "Bearer token" },
    }));

INotificationSender sender = ...;

var result = await sender.SendAsync(new Notification(
    ChannelType.Webhook,
    new WebhookContent("{\"event\":\"build.failed\",\"project\":\"main\"}")));

UseWebhookChannel(INotificationProvider) registers the channel over an existing provider — useful for custom providers or future failover chains.

Notification Routing

When you need to select channels based on event type, severity, or other context, configure routing on the communication builder. A router evaluates rules in priority order and can fan out a notification to multiple channels; if no rules match, the original Notification.Channel is used as the fallback (or an explicit default).

using MailForge.Communication.Extensions;
using MailForge.Communication.Models;
using MailForge.Telegram.Extensions;
using MailForge.Webhook.Extensions;

services.AddCommunication(builder => builder
    .UseEmailChannel()
    .UseTelegramChannel(new TelegramOptions { BotToken = "123:ABC" })
    .UseWebhookChannel(new WebhookOptions { Endpoint = new Uri("https://hooks.example.com/alerts") })
    .UseRouting(routing => routing
        .WhenEvent("security.breach", NotificationPriority.Urgent)
            .To(ChannelType.Telegram, ChannelType.Webhook)
        .WhenPriority(NotificationPriority.High, ChannelType.Email)
        .WhenTag("severity", "warning", ChannelType.Email)
        .Default(ChannelType.Email)));

UseRouting(INotificationRouter) also accepts a custom router implementation for advanced scenarios.