pleroma.debian.social

pleroma.debian.social

Continuing where I left off yesterday, today will likely be about rspamd.

The first question I immediately asked myself: since rspamd has slop, and I managed to replace another sloppy part of the stack1, can I replace rspamd too?


  1. dovecot, with aerogramme ↩︎

And immediately, my mind goes: what if rspamd, but rust and #roto? Iocaine for email.

So. Very. Tempting.

@doctor but, but, but,... this is just a very tiny part, essentially just filtering! We can do that! We're good at that!

It also allows experimenting with current Roto, which would be great help with iocaine 4!

@algernon ironically, identifying spam might be one of the few not-terrible applications of an LLM?

@cinebox there are no non-terrible uses of an llm. I can do better spam classification with clear, understandable rules, at a tiny fraction of the resource cost, and without relying on tech that keeps trying to DDoS me.

@algernon You mean I am good at that. I said no.

@doctor Please? For science! Lets just try! No commitment, a quick experiment!

@algernon Quick hack is how you end up maintaining shit for decades.

@doctor but we like majntaining good shit.

@algernon they're still shit, and we're stupid.

@doctor so that's a yes...

I guess we'll rake the road less traveled, and go on an rspamd-shaped side quest. Our party will include the great wizard, Rust, a trusty cleric, Roto, and an unhinged bard that enchanted them, and his song leads them into a maze of email, all alike, yet so different and unique.

While still in bed and planning, another side quest: how shall we name this experiment?

It's something that came up in a whim, was not premeditated, so I'm currently coming up blank.

Gonna read some more RYOMS, perhaps I will gain some inspiration.

Routinely Yacking Old Mail Selector

Roboustly Yacking Our Milter System

Yeah, I think RYOMS will be a suitable acronym. Plenty of good ways to expand it.

Today is gonna be a good day, for we are not only avoiding slop and fighting spam, we are also experimenting, and blowing things up in a lab, and that's absolutely delightful.

Boom! Heeheheheee.

Also: that typo. Rake the roads less traveled. An accidental phrase I will be using more, going forward, because it is beautiful.

I'm gonna put the repo under the iocaine org on my forge. Will probably move OJF there, too, because they're all the same family, really.

So very excited! Almost ready to jump out of bed, and not wait until the rest if the family wakes up.

I wanna haaaaaack!

Since I use a relay for outgoing mail, I only need to care about the verification parts of SPF and DKIM, for incoming mail.

This makes life a whole lot simpler.

Heh. What if... ryoms explain mail.eml?

It'd run the checks, and explain each result, and the score given. More useful than a list of checks & scores in a header.

Could even run the milter in verbose mode, where it saves the explanation to S3, to a file based on msgid or something.

Could then add an x-ryoms-info header, with an url to the explanation.

I'm gonna have so much fun with this.

Btw, this too will be Filthy Human Hands licensed. Technically not FLOSS. 0 fucks given.

Another benefit of this side quest is that I can experiment more freely with building a Roto library / module collection. A set of reusable parts.

Oooh oooh oooh. Fuuuck. I am a genius.

Ryoms web ui: log in with imap credentials, see all the reports stored on s3. Allows deleting / archiving, and I might even make it possible to fetch the raw email via imap too.

Should also have an api, so I can teach my mail cluent to fetch the report on demand, and display it in emacs.

Bayesian filtering? Mmm, not yet. I have trained rspamd on 10 years of spam and ham, and the bayesian score is still pretty useless.

The spam I catch, I usually catch without training. So I win't bother until I have to.

Why is it useless, though? Because I receive a lot if legit mail, from banks, utilities companies, forges, etc, that are all very html, very tracking pixels and shit.

@algernon

Please tell me S3 will not be mandatory, because hell no I'm not giving freaking Amazon access to my email.

@argv_minus_one s3 as in object storage. I'm using Garage, that is what I will test with, and it will be optional anyway.

@algernon Corporate email senders are doing their utmost to make spam filtering difficult.

@algernon

Ok, sounds good. 👍

@algernon

I've had good results with rspamd training. No LLM usage, but I think it is using an old-fashioned neural network. No custom rules, at any rate.

@cinebox

@algernon

More spam filtering software would be good! Right now it's just SpamAssassin (which seems exceedingly bad at assassinating spam) and rspamd (which is a lot more corporate and selling-you-something than I'm comfortable with, not to mention the vibe code) (and both of them are difficult to configure).

FML.

I can reuse half of iocaine for ryoms. All the matchers (aho-corasick, regexpset, asn lookup, ipset, etc) are useful here too. So I'll factor those out into their own crate too.

Rebuild Your Own Milter System

Rig Your Own Mail Suspicions

So many opportunities! I decided not to bless an official expansion. It'll be just ryoms, you're free to expand it however you wish.

I might end up using particularly entertaining expansions as release code names past 1.0 though.

Okay. I'm at the PC now. Lets get down to business.

I'll move OJF to the iocaine org first, will tweak my postfix config a little, then open up the port forwarding on Eru, so I can receive mail from outside - so I have something I can test with under production circumstances.

Then I can get down and start writing the milter. There's an indiemilter crate I've been eyeing that looks like a good foundation to build on.

I think my postfix config is fine. Lets see what ports I need to open. Looks like I need 25 for SMTP, 465 for submission over tls. That's the only two ports postfix has open in the current config anyway.

My old server has 587 open too, but I don't think I need that.

@algernon From memory 587 is plain text or STARTTLS and 465 is TLS only

@az Yep! In my scenario, 25 is for receiving mail from external servers to local, 465 is for local users to send email to $wherever. Since all my local users will use TLS, 587 is unnecessary.

testssl.sh reports A+, I get some lower score on cipher strength, because the server doesn't present an order. I wonder if I can fix that.

Eh, it doesn't matter.

Looks like smtp2go can deliver to it fine too. Cool, cool.

We can start working on the milter then!

There's one annoying problem with the current setup: I'm using haproxy to proxy the connections to Luggage. So postfix sees the external IP as Eru's IP.

I suppose... I suppose I should run postfix (and ryoms) on Eru, much like I run Caddy and iocaine there too. Thus, only Aerogramme (and Garage) would be in the homelab.

The downside of this is that postfix's queue will take space on Eru, but... there's ~13GiB free. A mail queue will fit there fine.

...I'll deal with that later. My mail on the experimental domain is kinda disfunctional at the moment, but... noone should be sending mail there to begin with.

Also, with lunch out of the way, it's time to put on some fine techno, and hack away on my milter.

Some Agnis will do fine.

Youtube link

Agnis: Baba Jaga

Good stuff. See? I listen to all kinds of music, not just metal!

Received: from telchar.local (unknown [10.69.0.1])
        (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
         key-exchange x25519 server-signature ECDSA (prime256v1) server-digest SHA256)
        (No client certificate requested)
        by fax-machine.174070135.xyz (Postfix) with ESMTPSA id E5FE610E7C65
        for <algernon@174070135.xyz>; Sun, 19 Jul 2026 13:58:09 +0200 (CEST)
From: Gergely Nagy <algernon@174070135.xyz>
To: Gergely Nagy <algernon@174070135.xyz>
Subject: Test message, whee!
x-ryoms-been-here: yes

Hello world.

Baby steps! Can't do anything yet, but wired up all the callbacks, printing all the info they receive, and I can inject a header.

Still only a vague idea about how I want the actual Roto program to look, mind you. There's plenty to figure out there.

Like, I want to have a library of convenience functions, which are not in Rust, but in Roto. And I'd like to let the system load any number of arbitrary modules before the main script, so the main script can then make use of them.

Funny historical tidbit: way back in the early 2000s, I wrote a POP3 server, and was halfway through implementing an IMAP server too before I got disctracted1. When testing Aerogramme, I telnet'd to the imap port, and all of the protocol knowledge came right back. 25+ years later, I still remember how to speak it.

But I can't remember the bedtime story I told last night.


  1. I changed jobs, and no longer needed an IMAP server. ↩︎

Dear $deity, that logo.

@algernon
IMAP is one of those protocols I can't speak using /bin/nc, and I guess that's for the better.

(Read: you monster!)

Mkay. So I have a number of callbacks, each receive a mutable context, the context is bound to the message being processed, isn't global.

As a first approximation, I can get away with providing a few accessors, and... go from there?

Uhh-ohh. How do I shove down the roto runtime into the callbacks?

Ok, well, that was easy.

I mean, it looks horrible. But it's fine, actually. A little cleanup later will fix it.

Nevertheless, I can now call into Roto. I don't pass arguments yet, nor do I return anything. But I can call into roto, and only compile the script once, on startup.

@zhenech
. LOGIN username password

Is about the extent of my ability to speak IMAP
@algernon

...and I just wrapped the Status type, so my Roto functions can return it.

@wouter @zhenech

a01 LOGIN username password
a02 SELECT "INBOX"
a03 FETCH 1 RFC822
a04 LOGOUT

:)

Feels like the harder part here will be writing a bunch of helpers to expose to the runtime, and writing a "standard library" of Roto functions that bind common patterns together.

@algernon
Yes, now that you mention it I remember doing that too at some point; but usually what I need to do is make sure things talk to me and don't fall over at the first sight of a client, for which a login command is enough and I don't need to do more.
@zhenech
replies
0
announces
0
likes
1

fn on_negotiate() -> Status {
	print("Hello from Roto");
	Status.all_opts()
}

Not much yet, but its Roto, and it gets called by the milter, and the return value gets used.

Hmm hmm. async stuff will be a bit of a challenge, I guess.

Okay, I think I will need a little break here, to figure out how I want to structure the Roto parts, and what kind of stuff I need to make available for the runtime, and stuff like that.

Will also look into the spf & dkim validation milters based on indymilter, to get some ideas and insight into what I will also require.

Mind you... maybe I don't need spf & dkim validation, specifically. If I can use a chain of milters in Postfix, I don't need ryoms  to implement everything.

I probably will, eventually, because running one milter is much more efficient than running 3+.

Made a bit of progress, the Roto side is slowly shaping up.

Will continue some other time, now its time for dreams.