Josupeit.Mail.SpamAssassin.Client 0.1.6

Prefix Reserved
dotnet add package Josupeit.Mail.SpamAssassin.Client --version 0.1.6
                    
NuGet\Install-Package Josupeit.Mail.SpamAssassin.Client -Version 0.1.6
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Josupeit.Mail.SpamAssassin.Client" Version="0.1.6" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Josupeit.Mail.SpamAssassin.Client" Version="0.1.6" />
                    
Directory.Packages.props
<PackageReference Include="Josupeit.Mail.SpamAssassin.Client" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Josupeit.Mail.SpamAssassin.Client --version 0.1.6
                    
#r "nuget: Josupeit.Mail.SpamAssassin.Client, 0.1.6"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Josupeit.Mail.SpamAssassin.Client@0.1.6
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Josupeit.Mail.SpamAssassin.Client&version=0.1.6
                    
Install as a Cake Addin
#tool nuget:?package=Josupeit.Mail.SpamAssassin.Client&version=0.1.6
                    
Install as a Cake Tool

Josupeit.Mail.SpamAssassin.Client

A client for the spamc/spamd protocol - the HTTP-like request/response protocol that SpamAssassin's scanning daemon speaks - for .NET.

dotnet add package Josupeit.Mail.SpamAssassin.Client

Targets net8.0, announces protocol version 1.5, and covers the full command set: CHECK, SYMBOLS, REPORT, REPORT_IFSPAM, PROCESS, HEADERS, TELL, PING and SKIP. Every operation comes in two colours - an async one and a genuinely blocking one - and the package depends on nothing outside the framework.

The transport is yours

The library never opens a socket, resolves a host, or knows whether spamd runs beside it or across a data centre. It is handed a pair of streams - one it writes the request to, one it reads the response from - and speaks the protocol over them. TCP, a Unix domain socket, TLS through stunnel, or a pair of MemoryStreams in a test are all the same to it.

spamd serves exactly one command per connection and then closes it, so the client is constructed with a factory that produces a fresh connection per call rather than with a connection of its own. One client instance therefore serves as many scans as you like, without pretending the daemon supports something it does not.

Using it

Teach the library how to reach the daemon by implementing one small interface - two streams and a way to be disposed:

internal sealed class TcpSpamdConnection(TcpClient socket) : ISpamAssassinConnection
{
    private readonly NetworkStream _stream = socket.GetStream();

    public Stream RequestStream => _stream;

    public Stream ResponseStream => _stream;

    // The default only flushes. On a socket there is more to say: shutting down the sending half is how the
    // protocol expects a client to announce that its request is finished.
    public void CompleteRequest() => socket.Client.Shutdown(SocketShutdown.Send);

    public Task CompleteRequestAsync(CancellationToken cancellationToken = default)
    {
        CompleteRequest();

        return Task.CompletedTask;
    }

    public void Dispose() => socket.Dispose();

    public ValueTask DisposeAsync()
    {
        socket.Dispose();

        return default;
    }
}

Then ask it things. One client instance serves any number of scans; it takes a fresh connection for each, because that is all the daemon allows:

var client = new SpamAssassinClient(
    connect: () => new TcpSpamdConnection(new TcpClient("spamd.example.com", 783)),
    connectAsync: async cancellationToken =>
    {
        var socket = new TcpClient();

        try
        {
            await socket.ConnectAsync("spamd.example.com", 783, cancellationToken);

            return new TcpSpamdConnection(socket);
        }
        catch
        {
            // A socket that failed to connect is still a socket, and nothing else is going to close it: the client
            // only ever disposes a connection it was handed.
            socket.Dispose();

            throw;
        }
    });

var verdict = await client.ScanAsync(message);

if (verdict.IsSpam)
    logger.LogInformation("Scored {Score} against a threshold of {Threshold}.", verdict.Score, verdict.Threshold);

// The rewritten message goes straight into a stream you supply, so a large one never sits in memory.
await using var marked = File.Create("marked.eml");

await client.ProcessAsync(message, marked);

// And the write side of the protocol, for a message a user moved into their spam folder.
await client.TellAsync(TellInstruction.LearnAsSpam, message);

A daemon that refuses a request throws ErrorResponseSpamAssassinException with the code it sent; a daemon that answers something unreadable, stops mid-answer, or exceeds the limits you configured throws one of the other three SpamAssassinException types. Failures of your own streams are not wrapped.

Licence

EUPL-1.2.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.6 0 8/20/2026

- build: adds the package icon and the repository logo
- feat: adds the spamc client
- docs: adds the repository and package readmes
- build: adds the solution and the library project
- build: adds the packaging props and targets for src
- build: adds the repository-wide build props