Portal Community
What You'll Learn How to add a new email provider (SendGrid, Mailgun, custom service, etc.) without modifying core email system code. Plugin architecture allows complete provider isolation.

10-Step Implementation Guide

Step 1: Create a New Service Project

Create a new C# class library project for your provider:

BizFirst.Integration.YourProvider.Services

Structure:
├── BizFirst.Integration.YourProvider.Services.csproj
├── DependencyInjection.cs
├── Email/
│   ├── YourProviderEmailDispatcher.cs
│   └── IYourProviderEmailService.cs
├── Services/
│   └── YourProviderEmailService.cs
└── GlobalUsings.cs

Step 2: Add Project References

<ItemGroup>
  <ProjectReference Include=".../BizFirst.Ai.Email.Domain.csproj" />
</ItemGroup>

Step 3: Define Your Service Interface

// Email/IYourProviderEmailService.cs
namespace BizFirst.Integration.YourProvider.Services.Email;

public interface IYourProviderEmailService
{
    Task<YourProviderEmailResult> SendAsync(
        YourProviderEmailRequest request,
        CancellationToken ct = default);
}

public record YourProviderEmailRequest(
    string ApiKey,
    string FromEmail,
    string ToEmail,
    string Subject,
    string Html,
    string? Text = null,
    string? FromName = null,
    List<string>? Cc = null,
    List<string>? Bcc = null,
    string? ReplyTo = null);

public record YourProviderEmailResult(
    bool Success,
    string? MessageId,
    string? ErrorCode,
    string? ErrorMessage);

Step 4: Implement the Service

// Services/YourProviderEmailService.cs
public class YourProviderEmailService : IYourProviderEmailService
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<YourProviderEmailService> _logger;

    public YourProviderEmailService(HttpClient httpClient, ILogger<YourProviderEmailService> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
    }

    public async Task<YourProviderEmailResult> SendAsync(
        YourProviderEmailRequest request,
        CancellationToken ct = default)
    {
        try
        {
            // 1. Build request payload for your provider's API
            var payload = new
            {
                from = request.FromEmail,
                to = new[] { request.ToEmail },
                subject = request.Subject,
                html = request.Html,
                text = request.Text,
                cc = request.Cc ?? new(),
                bcc = request.Bcc ?? new(),
                reply_to = request.ReplyTo
            };

            // 2. Call your provider's API
            var content = new StringContent(
                JsonSerializer.Serialize(payload),
                Encoding.UTF8,
                "application/json");

            var response = await _httpClient.PostAsync(
                "https://api.yourprovider.com/v1/messages/send",
                content,
                ct);

            // 3. Handle response
            if (response.IsSuccessStatusCode)
            {
                var result = await response.Content.ReadAsAsync<dynamic>(ct);
                return new YourProviderEmailResult(
                    Success: true,
                    MessageId: result.id,
                    ErrorCode: null,
                    ErrorMessage: null);
            }

            var error = await response.Content.ReadAsStringAsync(ct);
            _logger.LogError($"SendGrid API error: {error}");

            return new YourProviderEmailResult(
                Success: false,
                MessageId: null,
                ErrorCode: response.StatusCode.ToString(),
                ErrorMessage: error);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error sending email via provider");
            return new YourProviderEmailResult(
                Success: false,
                MessageId: null,
                ErrorCode: "EXCEPTION",
                ErrorMessage: ex.Message);
        }
    }
}

Step 5: Create the Dispatcher

// Email/YourProviderEmailDispatcher.cs
public class YourProviderEmailDispatcher : IEmailDispatcher
{
    private readonly IYourProviderEmailService _service;
    private readonly ILogger<YourProviderEmailDispatcher> _logger;

    public YourProviderEmailDispatcher(
        IYourProviderEmailService service,
        ILogger<YourProviderEmailDispatcher> logger)
    {
        _service = service;
        _logger = logger;
    }

    public async Task<EmailDispatchResult> SendAsync(
        ISecretRecord credential,
        EmailProviderMetadata metadata,
        EmailMessage message,
        CancellationToken ct = default)
    {
        // 1. Cast credential to expected type
        if (credential is not ApiKeyRecord apiKeyRecord)
            return EmailDispatchResult.Fail("INVALID_CREDENTIAL", "Credential must be ApiKeyRecord");

        // 2. Build provider-specific request
        var request = new YourProviderEmailRequest(
            ApiKey: apiKeyRecord.ApiKey,
            FromEmail: metadata.FromEmail,
            ToEmail: message.To,
            Subject: message.Subject,
            Html: message.Html,
            Text: message.Plain,
            FromName: metadata.FromName,
            Cc: message.Cc?.Split(';').ToList(),
            Bcc: message.Bcc?.Split(';').ToList(),
            ReplyTo: message.ReplyTo);

        // 3. Call service
        var result = await _service.SendAsync(request, ct);

        // 4. Map to EmailDispatchResult
        if (result.Success)
            return EmailDispatchResult.Ok(result.MessageId!);

        _logger.LogError($"Email send failed: {result.ErrorMessage}");
        return EmailDispatchResult.Fail(
            result.ErrorCode ?? "UNKNOWN",
            result.ErrorMessage ?? "Unknown error");
    }
}

Step 6: Register in Dependency Injection

// DependencyInjection.cs
namespace BizFirst.Integration.YourProvider.Services;

public static class YourProviderEmailDependencyInjection
{
    public static IServiceCollection AddYourProviderEmailDispatcher(
        this IServiceCollection services)
    {
        services.AddScoped<IYourProviderEmailService, YourProviderEmailService>();
        services.AddScoped<YourProviderEmailDispatcher>();

        // Register HttpClient for your provider's API
        services.AddHttpClient<IYourProviderEmailService, YourProviderEmailService>(client =>
        {
            client.BaseAddress = new Uri("https://api.yourprovider.com");
            client.DefaultRequestHeaders.Add("User-Agent", "BizFirst/1.0");
        });

        return services;
    }
}

Step 7: Register Dispatcher in Factory

Modify EmailDispatcherFactory.cs to handle your new provider:

public class EmailDispatcherFactory : IEmailDispatcherFactory
{
    private readonly IServiceProvider _serviceProvider;

    public IEmailDispatcher Get(string provider)
    {
        return provider.ToOrdinalIgnoreCase() switch
        {
            "smtp" => _serviceProvider.GetRequiredService<SmtpEmailDispatcher>(),
            "ses" => _serviceProvider.GetRequiredService<SesEmailDispatcher>(),
            "gmail" => _serviceProvider.GetRequiredService<GmailEmailDispatcher>(),
            "yourprovider" => _serviceProvider.GetRequiredService<YourProviderEmailDispatcher>(),
            _ => throw new InvalidOperationException($"Unknown provider: {provider}")
        };
    }
}

Step 8: Wire Up in Main Application

Add to your application's DI startup:

// Program.cs or Startup.cs
services.AddAiEmailServices();           // Core
services.AddSmtpEmailDispatcher();       // SMTP
services.AddSesEmailDispatcher();        // SES
services.AddGmailEmailDispatcher();      // Gmail
services.AddYourProviderEmailDispatcher(); // Your new provider

Step 9: Create Credential in Database

INSERT INTO AIExt_Credentials (
  Name,
  DisplayMetadata,
  EncryptedData,  -- Your API key (encrypted)
  TenantID,
  CredentialTypeID,  -- 2 for ApiKeyRecord
  Enabled
) VALUES (
  'email-yourprovider',
  '{
    "provider": "yourprovider",
    "fromEmail": "noreply@yourdomain.com",
    "fromName": "Your Company"
  }',
  0x...,  -- Encrypted API key
  1,
  2,
  1
);

Step 10: Test Your Provider

// Resolve credential name from TenantSettings (defaults to "email-primary")
var credentialName = await _tenantSettingsService.GetAsync<string>(
    tenantID: 1,
    key: "EmailCredentialName",
    defaultValue: "email-primary",
    ct: cancellationToken);

var result = await _emailService.SendAsync(
    tenantID: 1,
    to: "test@example.com",
    subject: "Test Email",
    html: "<p>Test from YourProvider</p>",
    credentialName: credentialName,
    ct: cancellationToken);

Assert.IsTrue(result.Success);
Assert.IsNotNull(result.MessageID);

Important Design Points

Isolation Your provider code is completely isolated. The core email system doesn't need to know your provider exists.
Credential Types Use the right credential type: PasswordRecord (1) for SMTP, ApiKeyRecord (2) for API keys, OAuth2Record (3) for OAuth2.
Error Handling Always return EmailDispatchResult with proper error codes. Never throw exceptions from dispatchers.

Next Steps