Stalwart's LLM spam classifier answered by TypeSafe Jev https://github.com/mikluko/stalwart-jev
  • Go 95.6%
  • Go Template 3%
  • Dockerfile 1.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
agent 27d317132e
All checks were successful
ci / image (push) Successful in 1m9s
ci / chart (push) Successful in 11s
ci / test (push) Successful in 2m1s
chat: confidence_level, not a second level key in the verdict log
2026-10-22 22:47:01 +02:00
.forgejo/workflows chart: helm.sh/chart label survives a +build version 2026-10-22 17:27:32 +02:00
charts/stalwart-jev chart: helm.sh/chart label survives a +build version 2026-10-22 17:27:32 +02:00
internal chat: confidence_level, not a second level key in the verdict log 2026-10-22 22:47:01 +02:00
.gitignore stalwart-jev: Stalwart SpamLlm answered by TypeSafe Jev 2026-10-22 16:50:58 +02:00
Dockerfile stalwart-jev: Stalwart SpamLlm answered by TypeSafe Jev 2026-10-22 16:50:58 +02:00
go.mod stalwart-jev: MIT, a Helm chart, published under mikluko 2026-10-22 17:02:47 +02:00
go.sum stalwart-jev: Stalwart SpamLlm answered by TypeSafe Jev 2026-10-22 16:50:58 +02:00
LICENSE stalwart-jev: MIT, a Helm chart, published under mikluko 2026-10-22 17:02:47 +02:00
main.go stalwart-jev: MIT, a Helm chart, published under mikluko 2026-10-22 17:02:47 +02:00
main_test.go stalwart-jev: Stalwart SpamLlm answered by TypeSafe Jev 2026-10-22 16:50:58 +02:00
README.md stalwart-jev: MIT, a Helm chart, published under mikluko 2026-10-22 17:02:47 +02:00

stalwart-jev

Answers Stalwart's LLM spam classifier (SpamLlm) from TypeSafe Jev. It serves POST /v1/chat/completions in the shape Stalwart calls, asks Jev one Choice question over the message's subject and body, and replies <Category>,<High|Medium|Low>,p=<probability>. Categories are Stalwart's defaults: Unsolicited, Commercial, Harmful, Legitimate.

Stalwart builds its request as <prompt>\n\nSubject: <subject>\n\n<body>; the prompt configured in SpamLlm must not itself contain \n\nSubject: . SpamLlm is a Stalwart Enterprise feature: without a license Stalwart never calls the classifier.

Install

The chart is published as oci://sarak.sh/mikluko/charts/stalwart-jev, the image as sarak.sh/mikluko/stalwart-jev, both versioned X.Y.Z from the vX.Y.Z tag.

helm install stalwart-jev oci://sarak.sh/mikluko/charts/stalwart-jev \
  --namespace mail --set apiKey=<TypeSafe API key>

The chart's NetworkPolicy admits only pods labelled app.kubernetes.io/name: stalwart; set networkPolicy.allowFrom to match your Stalwart pods.

Stalwart

Point an AiModel at the service and enable SpamLlm with it, as stalwart-cli apply plan lines:

{"@type":"upsert","object":"AiModel","matchOn":["name"],"value":{"jev":{"name":"jev","url":"http://stalwart-jev:8080/v1/chat/completions","model":"jev-latest","modelType":"Chat","timeout":30000}}}
{"@type":"update","object":"SpamLlm","value":{"@type":"Enable","modelId":"#jev","prompt":"Classify this email.","categories":{"Unsolicited":true,"Commercial":true,"Harmful":true,"Legitimate":true},"confidence":{"High":true,"Medium":true,"Low":true},"separator":",","responsePosCategory":0,"responsePosConfidence":1,"responsePosExplanation":2,"temperature":0.0}}

Stalwart tags each message LLM_<CATEGORY>_<CONFIDENCE>, for example LLM_HARMFUL_HIGH. Its shipped rules give these tags no score, so the verdict changes nothing until SpamTag entries weigh them:

{"@type":"upsert","object":"SpamTag","matchOn":["tag"],"value":{"llm-harmful-high":{"@type":"Score","tag":"LLM_HARMFUL_HIGH","score":7.0},"llm-legitimate-high":{"@type":"Score","tag":"LLM_LEGITIMATE_HIGH","score":-3.0}}}

When the service fails or times out, Stalwart delivers the message untagged.

Configuration

Variable Default
TYPESAFE_API_KEY required
TYPESAFE_MODEL jev-latest
TYPESAFE_URL https://api.typesafe.ai/v1/systemone
TYPESAFE_TIMEOUT 20s per request to TypeSafe
MAX_BODY_BYTES 32768 body sent to Jev is cut to this
CONFIDENCE_HIGH 0.8 lowest Choice confidence reported as High
CONFIDENCE_MEDIUM 0.5 lowest reported as Medium; below is Low
LISTEN_ADDR :8080

Telemetry is OpenTelemetry over OTLP, set by the standard OTEL_* variables. Metrics: stalwart_jev.verdicts by category and confidence, stalwart_jev.failures by reason, stalwart_jev.confidence, stalwart_jev.input_tokens. Logs are JSON on stdout with trace_id.