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