Extensions and Plugin Development
Develop custom filters, storage providers and plugins.
Extensible Architecture
Zetian is easily extensible thanks to its interface-based architecture. You can develop your own filters, storage providers and plugins.
Custom Filters
Create custom filters by implementing the IMailboxFilter interface:
CustomSpamFilter.cs
using System.Net;
using Zetian.Core;
using Zetian.Storage;
// Custom mailbox filter
public class CustomSpamFilter : IMailboxFilter
{
private readonly ISpamDatabase _spamDb;
public CustomSpamFilter(ISpamDatabase spamDb)
{
_spamDb = spamDb;
}
public async Task<bool> CanAcceptFromAsync(
ISmtpSession session,
string from,
long size,
CancellationToken cancellationToken)
{
// Spam database check
if (await _spamDb.IsSpammerAsync(from))
{
return false;
}
// IP reputation check
var ipScore = await GetIpReputationAsync(session.RemoteEndPoint);
if (ipScore < 0.5)
{
return false;
}
// Size check
if (size > 50_000_000) // 50MB
{
return false;
}
return true;
}
public async Task<bool> CanDeliverToAsync(
ISmtpSession session,
string to,
string from,
CancellationToken cancellationToken)
{
// Check if user exists
if (!await UserExistsAsync(to))
{
return false;
}
// User's blacklist
var userBlacklist = await GetUserBlacklistAsync(to);
if (userBlacklist.Contains(from))
{
return false;
}
// Quota check
if (await IsQuotaExceededAsync(to))
{
return false;
}
return true;
}
// IP reputation check helper
private async Task<double> GetIpReputationAsync(EndPoint remoteEndPoint)
{
// Simulate IP reputation check
var ipAddress = (remoteEndPoint as IPEndPoint)?.Address?.ToString();
// In real implementation, check against IP reputation services
// For demo, allow localhost and private IPs
if (ipAddress == "127.0.0.1" || ipAddress?.StartsWith("192.168.") == true)
{
return 1.0; // Perfect reputation
}
// Simulate async operation
await Task.Delay(10);
// Return random reputation score for demo
return 0.7; // Default good reputation
}
// User existence check helper
private async Task<bool> UserExistsAsync(string email)
{
// Simulate user database check
await Task.Delay(10);
// For demo, accept common email patterns
var validUsers = new[] { "admin", "user", "test", "info", "support" };
var localPart = email.Split('@')[0].ToLower();
return validUsers.Contains(localPart);
}
// User blacklist check helper
private async Task<HashSet<string>> GetUserBlacklistAsync(string userEmail)
{
// Simulate fetching user's personal blacklist
await Task.Delay(10);
// For demo, return a sample blacklist
return new HashSet<string>
{
"[email protected]",
"[email protected]",
"[email protected]"
};
}
// Quota check helper
private async Task<bool> IsQuotaExceededAsync(string userEmail)
{
// Simulate quota check
await Task.Delay(10);
// For demo, quota is never exceeded
return false;
}
}
// Spam database interface
public interface ISpamDatabase
{
Task<bool> IsSpammerAsync(string email);
Task AddSpammerAsync(string email);
Task RemoveSpammerAsync(string email);
}
// Simple in-memory spam database implementation
public class SimpleSpamDatabase : ISpamDatabase
{
private readonly HashSet<string> _spammerEmails;
private readonly HashSet<string> _spammerDomains;
public SimpleSpamDatabase()
{
// Initialize with known spam emails and domains
_spammerEmails = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"[email protected]",
"[email protected]",
"[email protected]"
};
_spammerDomains = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"spam.com",
"malicious.com",
"spammy-domain.net",
"10minutemail.com",
"tempmail.com"
};
}
public Task<bool> IsSpammerAsync(string email)
{
if (string.IsNullOrEmpty(email))
return Task.FromResult(true); // No email = spam
// Check if exact email is in spam list
if (_spammerEmails.Contains(email))
return Task.FromResult(true);
// Check if domain is in spam list
var atIndex = email.IndexOf('@');
if (atIndex > 0 && atIndex < email.Length - 1)
{
var domain = email.Substring(atIndex + 1);
if (_spammerDomains.Contains(domain))
return Task.FromResult(true);
}
return Task.FromResult(false);
}
public Task AddSpammerAsync(string email)
{
_spammerEmails.Add(email);
// Also add domain to spam list
var atIndex = email.IndexOf('@');
if (atIndex > 0 && atIndex < email.Length - 1)
{
var domain = email.Substring(atIndex + 1);
_spammerDomains.Add(domain);
}
return Task.CompletedTask;
}
public Task RemoveSpammerAsync(string email)
{
_spammerEmails.Remove(email);
return Task.CompletedTask;
}
}
// Create spam database instance
var spamDatabase = new SimpleSpamDatabase();
// Create custom spam filter
var spamFilter = new CustomSpamFilter(spamDatabase);
// Build server with custom filter
var server = new SmtpServerBuilder()
.Port(25)
.MailboxFilter(spamFilter)
.Build();
// Start server
await server.StartAsync();
Spam Control
Database and AI-based spam detection
IP Reputation
IP address reputation check
Quota Management
Per-user quota control
Custom Storage Providers
Create custom storage solutions by implementing the IMessageStore interface:
CustomMessageStore.cs
using Zetian.Storage;
using Azure.Storage.Blobs;
// Azure Blob Storage message store
public class AzureBlobMessageStore : IMessageStore
{
private readonly BlobContainerClient _containerClient;
public AzureBlobMessageStore(string connectionString, string containerName)
{
var blobServiceClient = new BlobServiceClient(connectionString);
_containerClient = blobServiceClient.GetBlobContainerClient(containerName);
_containerClient.CreateIfNotExists();
}
public async Task<bool> SaveAsync(
ISmtpSession session,
ISmtpMessage message,
CancellationToken cancellationToken)
{
try
{
// Blob name: year/month/day/messageId.eml
var blobName = $"{DateTime.UtcNow:yyyy/MM/dd}/{message.Id}.eml";
var blobClient = _containerClient.GetBlobClient(blobName);
// Metadata
var metadata = new Dictionary<string, string>
{
["From"] = message.From?.Address ?? "unknown",
["To"] = string.Join(";", message.Recipients),
["Subject"] = message.Subject ?? "",
["RemoteIp"] = (session.RemoteEndPoint as IPEndPoint)?.Address.ToString() ?? "",
["ReceivedAt"] = DateTime.UtcNow.ToString("O")
};
// Upload raw message to blob
using var stream = new MemoryStream(message.GetRawMessage());
await blobClient.UploadAsync(
stream,
new BlobUploadOptions
{
Metadata = metadata
},
cancellationToken);
// Add to Azure Search for indexing
await IndexMessageAsync(message, blobName);
return true;
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to save message to Azure Blob Storage");
return false;
}
}
private async Task IndexMessageAsync(ISmtpMessage message, string blobPath)
{
// Azure Cognitive Search integration
var document = new
{
Id = message.Id,
From = message.From?.Address,
Recipients = message.Recipients,
Subject = message.Subject,
TextBody = message.TextBody,
Date = DateTime.UtcNow,
BlobPath = blobPath
};
await _searchClient.IndexDocumentAsync(document);
}
}
// Usage
var azureStore = new AzureBlobMessageStore(
"DefaultEndpointsProtocol=https;AccountName=...",
"smtp-messages");
var server = new SmtpServerBuilder()
.Port(25)
.MessageStore(azureStore)
.Build();
Cloud Storage
- • Azure Blob Storage
- • AWS S3
- • Google Cloud Storage
- • MinIO
Database Storage
- • SQL Server / PostgreSQL
- • MongoDB / CosmosDB
- • Elasticsearch
- • Redis
Composite Filters
Combine multiple filters with AND/OR logic:
CompositeFilter.cs
using Zetian.Storage;
// Combining multiple filters
var compositeFilter = new CompositeMailboxFilter(CompositeMode.All) // All must pass
.AddFilter(new DomainMailboxFilter()
.AllowFromDomains("trusted.com", "partner.org")
.BlockFromDomains("spam.com"))
.AddFilter(new RateLimitFilter(100, TimeSpan.FromHour(1)))
.AddFilter(new GeoLocationFilter(allowedCountries: new[] { "TR", "US", "GB" }))
.AddFilter(new CustomSpamFilter(spamDatabase));
var server = new SmtpServerBuilder()
.Port(25)
.MailboxFilter(compositeFilter)
.Build();
// Combining with OR logic
var orFilter = new CompositeMailboxFilter(CompositeMode.Any) // At least one must pass
.AddFilter(new WhitelistFilter(trustedSenders))
.AddFilter(new AuthenticatedUserFilter()) // Authenticated users always pass
.AddFilter(new InternalNetworkFilter("192.168.0.0/16"));
// Nested composite filters
var mainFilter = new CompositeMailboxFilter(CompositeMode.All)
.AddFilter(orFilter) // Whitelist OR authenticated OR internal
.AddFilter(new AntiVirusFilter()) // AND must pass virus check
.AddFilter(new SizeFilter(25_000_000)); // AND must be smaller than 25MB
Extension Methods
Extension methods that add new features to SmtpServer:
ExtensionMethods.cs
using Nest;
using System.Net;
using Zetian.Core;
using System.Net.Http.Json;
using Metrics = Prometheus.Metrics;
// Extending server with extension methods
public static class SmtpServerExtensions
{
// Add webhook support
public static ISmtpServer AddWebhook(
this ISmtpServer server,
string webhookUrl)
{
server.MessageReceived += async (sender, e) =>
{
var payload = new
{
messageId = e.Message.Id,
from = e.Message.From?.Address,
to = e.Message.Recipients,
subject = e.Message.Subject,
size = e.Message.Size,
timestamp = DateTime.UtcNow
};
using var client = new HttpClient();
await client.PostAsJsonAsync(webhookUrl, payload);
};
return server;
}
// Add Prometheus metrics
public static ISmtpServer AddPrometheusMetrics(
this ISmtpServer server)
{
var messagesReceived = Metrics.CreateCounter(
"smtp_messages_received_total",
"Total number of messages received");
var messageSize = Metrics.CreateHistogram(
"smtp_message_size_bytes",
"Message size in bytes");
var sessionDuration = Metrics.CreateHistogram(
"smtp_session_duration_seconds",
"Session duration in seconds");
server.MessageReceived += (sender, e) =>
{
messagesReceived.Inc();
messageSize.Observe(e.Message.Size);
};
server.SessionCompleted += (sender, e) =>
{
var duration = DateTime.UtcNow - e.Session.StartTime;
sessionDuration.Observe(duration.TotalSeconds);
};
return server;
}
// Add Elasticsearch logging
public static ISmtpServer AddElasticsearchLogging(
this ISmtpServer server,
string elasticUrl)
{
var elasticClient = new ElasticClient(new Uri(elasticUrl));
server.MessageReceived += async (sender, e) =>
{
var logEntry = new
{
Timestamp = DateTime.UtcNow,
MessageId = e.Message.Id,
From = e.Message.From?.Address,
Recipients = e.Message.Recipients,
Subject = e.Message.Subject,
Size = e.Message.Size,
RemoteIp = (session.RemoteEndPoint as IPEndPoint)?.Address.ToString() ?? ""
};
await elasticClient.IndexDocumentAsync(logEntry);
};
return server;
}
}
// Usage
var server = new SmtpServerBuilder()
.Port(25)
.Build()
.AddWebhook("https://api.example.com/smtp-webhook")
.AddPrometheusMetrics()
.AddElasticsearchLogging("http://localhost:9200");
// Initialize plugins
await pluginManager.InitializePluginsAsync(server);
// Start server
await server.StartAsync();
Webhook
HTTP webhook integration
Metrics
Prometheus metrics
Logging
Elasticsearch logging
Plugin System
Create a modular plugin system:
PluginSystem.cs
using Zetian.Core;
using Zetian.Protocol;
// Spam checker interface
public interface ISpamChecker
{
Task<double> CheckAsync(ISmtpMessage message);
}
// Simple spam checker implementation
public class SimpleSpamChecker : ISpamChecker
{
private readonly string[] _spamKeywords = new[]
{
"viagra", "casino", "lottery", "winner", "prize",
"free money", "click here", "limited offer", "act now"
};
public Task<double> CheckAsync(ISmtpMessage message)
{
double score = 0.0;
// Check subject
if (!string.IsNullOrEmpty(message.Subject))
{
var subjectLower = message.Subject.ToLowerInvariant();
foreach (var keyword in _spamKeywords)
{
if (subjectLower.Contains(keyword))
{
score += 0.2; // Each keyword adds to spam score
}
}
}
// Check for missing headers
if (string.IsNullOrEmpty(message.Subject))
score += 0.3;
if (message.From == null || string.IsNullOrEmpty(message.From.Address))
score += 0.4;
// Normalize score between 0 and 1
score = Math.Min(1.0, score);
return Task.FromResult(score);
}
}
// Plugin system
public interface ISmtpPlugin
{
string Name { get; }
string Version { get; }
Task InitializeAsync(ISmtpServer server);
}
// Anti-spam plugin
public class AntiSpamPlugin : ISmtpPlugin
{
public string Name => "Anti-Spam Plugin";
public string Version => "1.0.0";
private readonly ISpamChecker _spamChecker;
public AntiSpamPlugin(ISpamChecker spamChecker)
{
_spamChecker = spamChecker;
}
public async Task InitializeAsync(ISmtpServer server)
{
server.MessageReceived += async (sender, e) =>
{
var spamScore = await _spamChecker.CheckAsync(e.Message);
if (spamScore > 0.8)
{
e.Cancel = true;
e.Response = new SmtpResponse(550, $"Spam detected (score: {spamScore:F2})");
}
else if (spamScore > 0.5)
{
// Redirect to spam folder
e.Message.Headers["X-Spam-Score"] = spamScore.ToString("F2");
e.Message.Headers["X-Spam-Flag"] = "YES";
}
};
}
}
// Plugin manager
public class PluginManager
{
private readonly List<ISmtpPlugin> _plugins = new();
public void RegisterPlugin(ISmtpPlugin plugin)
{
_plugins.Add(plugin);
Console.WriteLine($"Plugin registered: {plugin.Name} v{plugin.Version}");
}
public async Task InitializePluginsAsync(ISmtpServer server)
{
foreach (var plugin in _plugins)
{
await plugin.InitializeAsync(server);
Console.WriteLine($"Plugin initialized: {plugin.Name}");
}
}
}
// Usage
var spamChecker = new SimpleSpamChecker();
var pluginManager = new PluginManager();
pluginManager.RegisterPlugin(new AntiSpamPlugin(spamChecker));
// You can add more plugins:
// pluginManager.RegisterPlugin(new AntiVirusPlugin(virusScanner));
// pluginManager.RegisterPlugin(new GreylistingPlugin(database));
// pluginManager.RegisterPlugin(new DkimValidationPlugin());
var server = new SmtpServerBuilder()
.Port(25)
.Build();
// Initialize plugins
await pluginManager.InitializePluginsAsync(server);
// Start server
await server.StartAsync();
Plugin Examples
- • Anti-Spam: SpamAssassin integration
- • Anti-Virus: ClamAV scanning
- • Greylisting: Temporary rejection
- • DKIM/SPF: Email validation
- • Rate Limiting: Dynamic rate limiting
Best Practices
- • Write extensions with async/await
- • Properly handle and log errors
- • Use caching for performance
- • Write thread-safe code
- • Use dependency injection
- • Write unit tests