Continuing where I left off yesterday, today will likely be about rspamd.
And immediately, my mind goes: what if rspamd, but rust and #roto? Iocaine for email.
So. Very. Tempting.
@algernon No.
@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.
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.
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.
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.
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.
Ok, sounds good. 👍
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.
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.
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.
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.
Youtube link
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.
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?
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.
- replies
- 1
- announces
- 0
- likes
- 0
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.
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.
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+.
AZ Guy in 🇨🇦
