24 Apr. 2025

SMTP Server mit C#

Intro

Ein Mail-Server kann als Bridge zwischen zwei Systemem funktionieren. Dabei kann mittels Mail-Client eine Email an eine Adresse verschickt werden und der Anhang und die Mail im Dateisystem gespeichert werden. Oder ein KI System sortiert die Dateien in die passenden Ordner.

Nach langer Suche habe ich eine kleine Bibliothek namens SmtpServer gefunden. Das Elegante hieran ist, dass man mittels Listener-Pattern einen oder mehrere Klassen zum Abarbeiten der Anfragen in eine Pipeline hängen kann.

Server starten

Es werden nur wenige Zeilen Code benötigt, um den Server z.B. in einer Konsolenapplikation zu starten.

static async Task Main(string[] args)
{
    Console.WriteLine("Starting SMTP Server");

    var options = new SmtpServerOptionsBuilder()
        .ServerName("localhost")
        .Port(25)
        .Build();

    //This is used to intercept the processing and inject hooks
    var serviceProvider = new ServiceProvider();

    //Let us start the server and wait
    var smtpServer = new SmtpServer.SmtpServer(options, serviceProvider);
    await smtpServer.StartAsync(CancellationToken.None);
}

Der Server ist jetzt gestartet und empfängt Emails. Testen kann man das mit einem einfach PowerShell Befehl.

Send-MailMessage -To foo@bar.de -Subject "Hello User" -Body "This is the message" -SmtpServer "localhost" -From "bar@foo.de"

Es erscheint nich nichts, weil wir keinen Listener implementiert haben.

Email lesen und abarbeiten

Für einen Listener muss eine Klasse von Typ MessageStore ableiten und die Methode SaveAsync() implementieren. In meinem Fall nehme ich eine kleine Bibliothek namens MimeKit zur Hilfe, um die Anhänge zu lesen.

internal class SampleMessageStore : MessageStore
{
    public override async Task<SmtpResponse> SaveAsync(
        ISessionContext context,
        IMessageTransaction transaction,
        ReadOnlySequence<byte> buffer,
        CancellationToken cancellationToken)
    {

        await using var stream = new MemoryStream();

        var position = buffer.GetPosition(0);
        while (buffer.TryGet(ref position, out var memory))
        {
            await stream.WriteAsync(memory, cancellationToken);
        }

        stream.Position = 0;

        var message = await MimeKit.MimeMessage.LoadAsync(stream, cancellationToken);
        Console.WriteLine("Mail received: " + message.Subject);
        foreach (var attachment in message.Attachments)
        {
            Console.WriteLine($"...Attachment found: {(attachment as MimePart)?.FileName} - {(attachment as MimePart)?.ContentType.MimeType}");
        }

        return SmtpResponse.Ok;
    }
}

Anschließend muss die Klasse im ServiceProvider registriert werden.

//This is used to intercept the processing and inject hooks
var serviceProvider = new ServiceProvider();
serviceProvider.Add(new SampleMessageStore());

Und nun erscheint eine kurze Ausgabe, wenn eine Email angekommen ist. Hierfür senden wir noch zwei Attachments mit.

Send-MailMessage -To foo@bar.de -Subject "Hello User" -Body "This is the message" -SmtpServer "localhost" \
-From "bar@foo.de" -Attachments .\README.md,.\SimpleSmtpServer\Program.cs