Documentation/Extensions

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