Build Your Own Email Provider
Implement a custom email provider in 10 steps. Complete guide with code examples.
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
- Sample JSON Configurations — See credential JSON examples
- Credentials Design — Understand encryption and vault
- Request New Provider — Submit your provider for inclusion