Documentation/Message Processing

Message Processing

Receiving, processing, validating and storing SMTP messages.

Message Events

SMTP server triggers various events:

MessageEvents.cs
using Zetian;

var server = new SmtpServerBuilder()
    .Port(25)
    .Build();

// When message is received
server.MessageReceived += async (sender, e) =>
{
    var message = e.Message;
    
    // Message information
    Console.WriteLine($"Message ID: {message.Id}");
    Console.WriteLine($"From: {message.From}");
    Console.WriteLine($"To: {string.Join(", ", message.Recipients)}");
    Console.WriteLine($"Size: {message.Size} bytes");
    Console.WriteLine($"Subject: {message.Subject}");
    
    // Session information
    Console.WriteLine($"Session: {e.Session.Id}");
    Console.WriteLine($"From IP: {e.Session.RemoteEndPoint}");
    Console.WriteLine($"Is authenticated: {e.Session.IsAuthenticated}");
    
    // Save the raw message
    var fileName = $"messages/{message.Id}.eml";
    Directory.CreateDirectory("messages");
    await message.SaveToFileAsync(fileName);
    
    // Reject message if needed
    if (message.From?.Address?.Contains("spam") == true)
    {
        e.Cancel = true;
        e.Response = new SmtpResponse(554, "Message rejected as spam");
    }
};

// When session is created
server.SessionCreated += (sender, e) =>
{
    Console.WriteLine($"New session from {e.Session.RemoteEndPoint}");
    Console.WriteLine($"Session ID: {e.Session.Id}");
};

// When session is completed
server.SessionCompleted += (sender, e) =>
{
    Console.WriteLine($"Session completed: {e.Session.Id}");
    Console.WriteLine($"Messages received: {e.Session.MessageCount}");
    
    // Calculate duration
    var duration = DateTime.UtcNow - e.Session.StartTime;
    Console.WriteLine($"Duration: {duration.TotalSeconds:F2} seconds");
    
    if (e.Session.IsAuthenticated)
    {
        Console.WriteLine($"Authenticated as: {e.Session.AuthenticatedIdentity}");
    }
};

Message Events

  • MessageReceived - Message received
  • MessageRejected - Message rejected
  • MessageStored - Message stored
  • MessageForwarded - Message forwarded

Session Events

  • SessionCreated - Session started
  • SessionCompleted - Session completed
  • Authentication - Authenticated
  • ErrorOccurred - Error occurred

Message Validation and Filtering

Validate messages and filter unwanted content:

MessageValidation.cs
// Message validation and filtering
server.MessageReceived += (sender, e) =>
{
    var message = e.Message;
    
    // Size check
    if (message.Size > 10_000_000) // 10MB
    {
        e.Cancel = true;
        e.Response = new SmtpResponse(552, "Message too large");
        return;
    }
    
    // Check sender domain
    if (message.From?.Address?.Contains("@spammer.com") == true)
    {
        e.Cancel = true;
        e.Response = new SmtpResponse(550, "Sender domain blocked");
        return;
    }
    
    // SPF/DKIM validation
    if (!ValidateSPF(e.Session.RemoteEndPoint, message.From?.Address))
    {
        e.Cancel = true;
        e.Response = new SmtpResponse(550, "SPF validation failed");
        return;
    }
    
    // Parse with MimeKit for content filtering
    using var stream = new MemoryStream(message.GetRawData());
    var mimeMessage = MimeMessage.Load(stream);
    
    // Content filtering
    var blockedWords = new[] { "viagra", "lottery", "winner" };
    if (blockedWords.Any(word => 
        mimeMessage.Subject?.Contains(word, StringComparison.OrdinalIgnoreCase) ?? false))
    {
        e.Cancel = true;
        e.Response = new SmtpResponse(550, "Content policy violation");
        return;
    }
    
    // Virus scan (example with external service)
    if (await ScanForVirus(message.GetRawData()))
    {
        e.Cancel = true;
        e.Response = new SmtpResponse(550, "Message rejected: Virus detected");
        return;
    }
};

// Dynamic rejection
server.MessageReceived += async (sender, e) =>
{
    var blacklist = await GetBlacklistAsync();
    
    if (blacklist.Contains(e.Message.From))
    {
        e.Cancel = true;
        e.Response = new SmtpResponse(550, "Sender blacklisted");
    }
};

Performance Tip

Protocol-level filtering (with SmtpServerBuilder) is more performant as messages are rejected before being fully received. Event-based filtering is more flexible but runs after the entire message is received.

Message Storage

Save messages to file system or database:

MessageStorage.cs
// Protocol-Level Storage (with SmtpServerBuilder)
var server = new SmtpServerBuilder()
    .Port(25)
    .WithFileMessageStore(@"C:\smtp_messages", createDateFolders: true)
    .Build();

// Event-Based Storage (with Event handler)
server.MessageReceived += async (sender, e) =>
{
    var message = e.Message;
    
    // Save to file
    var directory = $"messages/{DateTime.Now:yyyy-MM-dd}";
    Directory.CreateDirectory(directory);
    
    var fileName = $"{directory}/{message.Id}.eml";
    await message.SaveToFileAsync(fileName);
    
    // Save message metadata as JSON (no external dependencies needed)
    var messageInfo = new
    {
        Id = message.Id,
        From = message.From?.Address,
        To = message.Recipients.Select(r => r.Address).ToArray(),
        Size = message.Size,
        Subject = message.Subject,
        ReceivedDate = DateTime.UtcNow,
        RemoteIp = e.Session.RemoteEndPoint?.ToString(),
        HasAttachments = message.HasAttachments,
        AttachmentCount = message.AttachmentCount
    };
    
    var jsonFile = $"{directory}/{message.Id}.json";
    var json = JsonSerializer.Serialize(messageInfo, new JsonSerializerOptions 
    { 
        WriteIndented = true 
    });
    await File.WriteAllTextAsync(jsonFile, json);
};

// Custom Message Store Implementation
public class JsonMessageStore : IMessageStore
{
    private readonly string _directory;
    
    public JsonMessageStore(string directory)
    {
        _directory = directory;
        Directory.CreateDirectory(directory);
    }
    
    public async Task<bool> SaveAsync(
        ISmtpSession session, 
        ISmtpMessage message,
        CancellationToken cancellationToken)
    {
        try
        {
            // Save raw message
            var emlFile = Path.Combine(_directory, $"{message.Id}.eml");
            await message.SaveToFileAsync(emlFile);
            
            // Save metadata as JSON
            var metadata = new
            {
                Id = message.Id,
                From = message.From?.Address,
                Recipients = message.Recipients.Select(r => r.Address).ToArray(),
                Subject = message.Subject,
                Size = message.Size,
                ReceivedAt = DateTime.UtcNow,
                SessionId = session.Id,
                RemoteEndPoint = session.RemoteEndPoint?.ToString(),
                IsAuthenticated = session.IsAuthenticated,
                AuthenticatedUser = session.AuthenticatedIdentity
            };
            
            var jsonFile = Path.Combine(_directory, $"{message.Id}.json");
            var json = JsonSerializer.Serialize(metadata, new JsonSerializerOptions 
            { 
                WriteIndented = true 
            });
            await File.WriteAllTextAsync(jsonFile, json, cancellationToken);
            
            return true;
        }
        catch
        {
            return false;
        }
    }
}

File System

Save as EML format files

SQL/NoSQL

Entity Framework, MongoDB, etc.

Cloud Storage

Azure Blob, AWS S3, etc.

Message Forwarding

Forward messages to other servers or systems:

MessageForwarding.cs
// Simple message forwarding
server.MessageReceived += async (sender, e) =>
{
    var message = e.Message;
    
    try
    {
        // Forward to another SMTP server
        using var client = new SmtpClient("relay.example.com", 587);
        client.EnableSsl = true;
        client.Credentials = new NetworkCredential("relay_user", "relay_password");
        
        var mailMessage = new MailMessage
        {
            From = new MailAddress(message.From?.Address ?? "[email protected]"),
            Subject = message.Subject ?? "(No Subject)",
            Body = message.TextBody ?? string.Empty,
            IsBodyHtml = false
        };
        
        foreach (var recipient in message.Recipients)
        {
            mailMessage.To.Add(recipient.Address);
        }
        
        // Note: To handle attachments, parse the raw message with MimeKit:
        // var mimeMessage = MimeMessage.Load(new MemoryStream(message.GetRawData()));
        // foreach (var attachment in mimeMessage.Attachments) { ... }
        
        await client.SendMailAsync(mailMessage);
        Console.WriteLine($"Message {message.Id} forwarded to relay server");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Failed to forward message: {ex.Message}");
    }
};

// Conditional forwarding based on recipient domains
server.MessageReceived += async (sender, e) =>
{
    var message = e.Message;
    
    // Forward messages to specific domains
    var forwardDomains = new[] { "external.com", "partner.org" };
    
    var recipientsToForward = message.Recipients
        .Where(r => forwardDomains.Any(d => r.Address.EndsWith($"@{d}", StringComparison.OrdinalIgnoreCase)))
        .ToList();
    
    if (recipientsToForward.Any())
    {
        await ForwardToExternalServer(message, recipientsToForward);
    }
    
    // Process the rest locally
    var localRecipients = message.Recipients
        .Except(recipientsToForward)
        .ToList();
    
    if (localRecipients.Any())
    {
        await ProcessLocally(message, localRecipients);
    }
};

// Helper method to forward messages to external server
private static async Task ForwardToExternalServer(ISmtpMessage message, List<MailAddress> recipients)
{
    using var client = new SmtpClient("external-relay.example.com", 587);
    client.EnableSsl = true;
    client.Credentials = new NetworkCredential("external_user", "external_password");

    var mailMessage = new MailMessage
    {
        From = new MailAddress(message.From?.Address ?? "[email protected]"),
        Subject = message.Subject ?? "(No Subject)",
        Body = message.TextBody ?? string.Empty,
        IsBodyHtml = false
    };

    foreach (var recipient in recipients)
    {
        mailMessage.To.Add(recipient.Address);
    }

    await client.SendMailAsync(mailMessage);
    Console.WriteLine($"Forwarded to external server for {recipients.Count} recipient(s)");
}

// Helper method to process messages locally  
private static async Task ProcessLocally(ISmtpMessage message, List<MailAddress> recipients)
{
    foreach (var recipient in recipients)
    {
        var mailboxDir = $"mailboxes/{recipient.Address.Replace("@", "_at_")}";
        Directory.CreateDirectory(mailboxDir);
        
        var fileName = Path.Combine(mailboxDir, $"{message.Id}.eml");
        await message.SaveToFileAsync(fileName);
        
        Console.WriteLine($"Message saved to local mailbox: {recipient.Address}");
    }
}

Parsing Message Content

Process MIME parts, headers and attachments:

MessageParsing.cs
// Parsing message content with MimeKit
// Install-Package MimeKit
using MimeKit;

server.MessageReceived += (sender, e) =>
{
    var message = e.Message;
    
    // Parse raw message with MimeKit
    using var stream = new MemoryStream(message.GetRawData());
    var mimeMessage = MimeMessage.Load(stream);
    
    // Headers
    foreach (var header in mimeMessage.Headers)
    {
        Console.WriteLine($"{header.Field}: {header.Value}");
    }
    
    // Basic properties
    Console.WriteLine($"Subject: {mimeMessage.Subject}");
    Console.WriteLine($"From: {mimeMessage.From}");
    Console.WriteLine($"To: {mimeMessage.To}");
    Console.WriteLine($"Date: {mimeMessage.Date}");
    
    // Text and HTML body
    var textBody = mimeMessage.TextBody;
    var htmlBody = mimeMessage.HtmlBody;
    
    // Attachments
    foreach (var attachment in mimeMessage.Attachments)
    {
        if (attachment is MimePart part)
        {
            Console.WriteLine($"Attachment: {part.FileName} ({part.ContentType})");
            
            // Save attachment
            using var attachmentStream = File.Create($"attachments/{part.FileName}");
            part.Content.DecodeTo(attachmentStream);
        }
    }
    
    // Priority header
    if (mimeMessage.Headers.Contains(HeaderId.XPriority))
    {
        var priority = mimeMessage.Headers[HeaderId.XPriority];
        Console.WriteLine($"Priority: {priority}");
    }
};

Headers

From, To, Subject, Date, Message-ID, Custom headers

MIME Parts

Text/plain, text/html, multipart, attachments