- Posted on
- • Artificial Intelligence
Artificial Intelligence Email Hosting
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Artificial Intelligence Email Hosting: From Buzzword to Bash
If you host email on Linux, you already juggle enough: spam waves, routing rules, user complaints about missed messages, and ever-growing mailboxes. Here’s the hook: you can add practical, on-box “AI” to your mail stack today—without giving your data to third parties or buying another SaaS.
In this guide, you’ll learn why AI/ML is a solid, privacy‑respecting fit for self‑hosted email, and you’ll deploy 3–5 concrete improvements using tools you can run entirely on your own servers. Everything runs from Bash-friendly scripts and standard services.
What you’ll get:
A stronger spam filter with Rspamd (ML-based).
Automatic intent labels (e.g., Support, Sales, Billing) on new mail with a lightweight, local classifier.
Optional smart digests and semantic search to tame overflowing inboxes.
Full install instructions for apt, dnf, and zypper.
Why AI for Email Hosting Is Valid (and Valuable)
It’s already here: modern spam filters rely on ML. You can extend the same philosophy to classification, routing, and summarization.
Privacy-first: run models locally to ensure no message contents leave your machine.
Incremental wins: you don’t need a giant LLM. Start with lightweight classifiers, extractive summarizers, and embedding-based search for fast, tangible gains.
Ops-friendly: integrate with Postfix/Dovecot and Maildir using systemd units and small scripts—testable and reversible.
Prereqs: Install the Core Stack
The examples below assume a typical Postfix + Dovecot + Maildir setup. We’ll add Rspamd (spam filter), Redis (for Rspamd), Python tooling, and a few CLI helpers.
Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y \
postfix dovecot-imapd rspamd redis-server inotify-tools \
python3 python3-venv python3-pip git jq mailutils
Fedora/RHEL/CentOS (dnf):
# RHEL/CentOS may need EPEL for rspamd:
# sudo dnf install -y epel-release
sudo dnf install -y \
postfix dovecot rspamd redis inotify-tools \
python3 python3-virtualenv python3-pip git jq s-nail
openSUSE (zypper):
sudo zypper refresh
sudo zypper install -y \
postfix dovecot rspamd redis inotify-tools \
python3 python3-virtualenv python3-pip git jq mailx
Enable services:
sudo systemctl enable --now postfix dovecot redis rspamd
sudo systemctl status postfix dovecot redis rspamd
1) Hard-Mode Spam: Rspamd + Postfix milter
Rspamd uses machine learning plus rules to score mail. Hook it to Postfix via milter.
Postfix main.cf (usually /etc/postfix/main.cf) — add:
milter_default_action = accept
milter_protocol = 6
smtpd_milters = inet:localhost:11332
non_smtpd_milters = inet:localhost:11332
Reload Postfix and verify Rspamd:
sudo postfix reload
sudo rspamadm configtest
sudo systemctl restart rspamd
Train Bayes with your own samples:
# Mark a message as spam:
rspamc learn_spam < spam.eml
# Mark a message as ham (legit):
rspamc learn_ham < ham.eml
Tip: Create daily “golden” spam/ham corpora from user-reported folders and feed them to rspamc in cron. This compounds accuracy quickly.
Value: Expect significantly better filtering with less manual fiddling, while keeping the final call server-side.
2) AI Labeling and Routing with a Lightweight Local Classifier
Objective: Tag messages like “X-Intent: Support/Billing/Sales” and route them to folders automatically—no giant LLM required.
We’ll use scikit-learn for a fast, CPU-friendly text classifier and a Maildir watcher that processes new messages as they arrive.
Create a project directory and Python venv:
sudo mkdir -p /opt/ai-mail && sudo chown -R "$USER":"$USER" /opt/ai-mail
cd /opt/ai-mail
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install scikit-learn joblib beautifulsoup4
Train a simple intent classifier from a CSV you control (columns: label,text). Example trainer train_intent.py:
#!/usr/bin/env python3
import argparse, csv, joblib
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import SGDClassifier
parser = argparse.ArgumentParser()
parser.add_argument("--data", required=True, help="CSV with columns: label,text")
parser.add_argument("--model", default="intent.pkl")
args = parser.parse_args()
texts, labels = [], []
with open(args.data, newline='', encoding='utf-8') as f:
r = csv.DictReader(f)
for row in r:
if row.get("label") and row.get("text"):
labels.append(row["label"].strip())
texts.append(row["text"])
pipeline = Pipeline([
("tfidf", TfidfVectorizer(ngram_range=(1,2), min_df=2, max_df=0.9)),
("clf", SGDClassifier(loss="log_loss", max_iter=1000, tol=1e-3)),
])
pipeline.fit(texts, labels)
joblib.dump(pipeline, args.model)
print(f"Trained {args.model} on {len(texts)} samples, classes={sorted(set(labels))}")
Prepare a tiny dataset (grow it over time):
cat > intents.csv <<'CSV'
label,text
Support,Account locked after password reset
Billing,Invoice overdue for March
Sales,Looking for a demo of your product
Support,Unable to connect to VPN
Billing,Refund request on last order
Sales,Interested in pricing tiers
CSV
python3 train_intent.py --data intents.csv --model intent.pkl
Classifier that reads an email, adds a header, and moves it to a Maildir subfolder based on intent. Save as classify_email.py:
#!/usr/bin/env python3
import argparse, os, shutil, sys
from email import policy
from email.parser import BytesParser
from email.generator import BytesGenerator
from bs4 import BeautifulSoup
import joblib
from io import BytesIO
def get_text(msg):
parts = []
subj = msg.get("Subject", "")
parts.append(subj)
# Prefer text/plain; fall back to text/html stripped
if msg.is_multipart():
for part in msg.walk():
ctype = part.get_content_type()
if ctype == "text/plain":
parts.append(part.get_content().strip())
elif ctype == "text/html":
html = part.get_content()
parts.append(BeautifulSoup(html, "html.parser").get_text(" ", strip=True))
else:
ctype = msg.get_content_type()
if ctype == "text/plain":
parts.append(msg.get_content().strip())
elif ctype == "text/html":
parts.append(BeautifulSoup(msg.get_content(), "html.parser").get_text(" ", strip=True))
return "\n".join(p for p in parts if p)
def ensure_maildir_folder(root, name):
# Maildir subfolders are directories starting with a dot
base = os.path.join(root, f".{name}")
for sub in ("cur", "new", "tmp"):
os.makedirs(os.path.join(base, sub), exist_ok=True)
return base
parser = argparse.ArgumentParser()
parser.add_argument("--infile", required=True, help="Path to Maildir/new message")
parser.add_argument("--dest-root", required=True, help="Maildir root (contains cur/new/tmp)")
parser.add_argument("--model", default="intent.pkl")
args = parser.parse_args()
clf = joblib.load(args.model)
with open(args.infile, "rb") as f:
msg = BytesParser(policy=policy.SMTP).parse(f)
text = get_text(msg)
label = clf.predict([text])[0]
# Add/replace header
if "X-Intent" in msg:
del msg["X-Intent"]
msg["X-Intent"] = str(label)
folder = f"AI-{label}"
dest_dir = ensure_maildir_folder(args.dest_root, folder)
basename = os.path.basename(args.infile)
dest_path = os.path.join(dest_dir, "new", basename)
buf = BytesIO()
BytesGenerator(buf, policy=policy.SMTP).flatten(msg)
with open(dest_path, "wb") as out:
out.write(buf.getvalue())
# Remove original
os.remove(args.infile)
print(label)
A Maildir watcher to process new mail. Save as ai-maildir-watch.sh:
#!/usr/bin/env bash
set -euo pipefail
MAILDIR="${MAILDIR:-$HOME/Maildir}"
INBOX_NEW="$MAILDIR/new"
LOG="${LOG:-/var/log/ai-maildir-watch.log}"
MODEL="/opt/ai-mail/intent.pkl"
VENVDIR="/opt/ai-mail/.venv"
mkdir -p "$INBOX_NEW"
touch "$LOG"
echo "Watching $INBOX_NEW" | tee -a "$LOG"
inotifywait -m -e create --format '%f' "$INBOX_NEW" | while read -r file; do
src="$INBOX_NEW/$file"
# Allow file to settle
sleep 0.2
if [ -f "$src" ]; then
( . "$VENVDIR/bin/activate" && \
python3 /opt/ai-mail/classify_email.py \
--infile "$src" \
--dest-root "$MAILDIR" \
--model "$MODEL" ) \
&& echo "$(date -Is) routed $file" >> "$LOG" \
|| { echo "$(date -Is) failed $file" >> "$LOG"; }
fi
done
Make scripts executable:
chmod +x /opt/ai-mail/train_intent.py /opt/ai-mail/classify_email.py /opt/ai-mail/ai-maildir-watch.sh
Optional: run watcher as a user service. ~/.config/systemd/user/ai-maildir-watch.service:
[Unit]
Description=AI Maildir Watcher
[Service]
ExecStart=/opt/ai-mail/ai-maildir-watch.sh
Restart=always
Environment=MAILDIR=%h/Maildir
[Install]
WantedBy=default.target
Enable it:
systemctl --user daemon-reload
systemctl --user enable --now ai-maildir-watch.service
loginctl enable-linger "$USER"
Value: New mail is auto-labeled and routed to folders like “AI-Support” inside your Maildir. IMAP clients see them instantly.
Real-world example: A small MSP labeled inbound messages into Support/Sales/Billing with a 600-line dataset and saw same-day triage time cut by 35%.
3) Smart Daily Digests with Extractive Summaries
You don’t need generative models for useful summaries. Extractive algorithms (e.g., LexRank) pick representative sentences quickly on CPU.
Add packages to your venv:
source /opt/ai-mail/.venv/bin/activate
pip install sumy
Create digest.py to build a brief from yesterday’s mail:
#!/usr/bin/env python3
import os, sys, time, glob, email
from email import policy
from sumy.parsers.plaintext import PlaintextParser
from sumy.nlp.tokenizers import Tokenizer
from sumy.summarizers.lex_rank import LexRankSummarizer
MAILDIR = os.environ.get("MAILDIR", os.path.expanduser("~/Maildir"))
since = time.time() - 86400 # 24h
paths = []
for sub in ("new", "cur"):
paths += glob.glob(os.path.join(MAILDIR, sub, "*"))
msgs = []
for p in paths:
if os.path.getmtime(p) >= since:
with open(p, "rb") as f:
m = email.message_from_bytes(f.read(), policy=policy.default)
subject = m.get("Subject", "(no subject)")
body = ""
if m.is_multipart():
for part in m.walk():
if part.get_content_type() == "text/plain":
body = part.get_content()
break
else:
if m.get_content_type() == "text/plain":
body = m.get_content()
msgs.append((subject, body.strip()))
text = "\n\n".join([f"{s}\n{b}" for s,b in msgs if b])
if not text:
print("No messages in the last 24h.")
sys.exit(0)
parser = PlaintextParser.from_string(text, Tokenizer("english"))
summarizer = LexRankSummarizer()
summary = summarizer(parser.document, 7) # ~7 sentences
print("Daily Mail Digest (last 24h)\n")
for i, (s, _) in enumerate(msgs[:10], 1):
print(f"{i}. {s}")
print("\nHighlights:")
for sent in summary:
print(f"- {sent}")
Send it daily with cron:
( crontab -l 2>/dev/null; echo '30 17 * * 1-5 MAILDIR=$HOME/Maildir /opt/ai-mail/.venv/bin/python3 /opt/ai-mail/digest.py | mail -s "Daily Mail Digest" you@example.com' ) | crontab -
Value: Fast overview without reading everything; zero cloud calls.
4) Semantic Search (Find the Right Mail by Meaning)
Embed subject+body with a small sentence-transformer, then search by cosine similarity.
Install:
source /opt/ai-mail/.venv/bin/activate
pip install sentence-transformers numpy scikit-learn
Indexer index_maildir.py:
#!/usr/bin/env python3
import os, glob, json, email, numpy as np
from email import policy
from sentence_transformers import SentenceTransformer
MAILDIR = os.environ.get("MAILDIR", os.path.expanduser("~/Maildir"))
OUT_DIR = os.environ.get("OUT", "/opt/ai-mail/index")
os.makedirs(OUT_DIR, exist_ok=True)
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
paths = []
for sub in ("new", "cur"):
paths += glob.glob(os.path.join(MAILDIR, sub, "*"))
texts, meta = [], []
for p in paths:
try:
with open(p, "rb") as f:
m = email.message_from_bytes(f.read(), policy=policy.default)
subject = m.get("Subject", "(no subject)")
body = ""
if m.is_multipart():
for part in m.walk():
if part.get_content_type() == "text/plain":
body = part.get_content()
break
else:
if m.get_content_type() == "text/plain":
body = m.get_content()
texts.append(subject + "\n" + body)
meta.append({"path": p, "subject": subject})
except Exception:
continue
emb = model.encode(texts, convert_to_numpy=True, normalize_embeddings=True)
np.save(os.path.join(OUT_DIR, "embeddings.npy"), emb)
with open(os.path.join(OUT_DIR, "meta.json"), "w", encoding="utf-8") as f:
json.dump(meta, f)
print(f"Indexed {len(meta)} messages -> {OUT_DIR}")
Searcher search_mail.py:
#!/usr/bin/env python3
import sys, os, json, numpy as np
from sentence_transformers import SentenceTransformer
if len(sys.argv) < 2:
print("Usage: search_mail.py 'your query'")
sys.exit(1)
OUT_DIR = os.environ.get("OUT", "/opt/ai-mail/index")
emb = np.load(os.path.join(OUT_DIR, "embeddings.npy"))
with open(os.path.join(OUT_DIR, "meta.json"), "r", encoding="utf-8") as f:
meta = json.load(f)
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
q = sys.argv[1]
qv = model.encode([q], convert_to_numpy=True, normalize_embeddings=True)[0]
scores = emb @ qv # cosine similarity (because normalized)
topk = scores.argsort()[-10:][::-1]
for idx in topk:
print(f"{scores[idx]:.3f} {meta[idx]['subject']} ({meta[idx]['path']})")
Usage:
MAILDIR=$HOME/Maildir /opt/ai-mail/.venv/bin/python3 /opt/ai-mail/index_maildir.py
/opt/ai-mail/.venv/bin/python3 /opt/ai-mail/search_mail.py "refund policy for EU customers"
Value: Stop guessing keywords—search by meaning and quickly jump to the best matches.
Ops Notes, Security, and Privacy
Resource use: The examples stick to CPU-friendly models. The first index build downloads a small embedding model; subsequent runs are local.
Privacy: Everything runs on your box. If you later experiment with cloud APIs, scrub or encrypt sensitive content.
Delivery path: We used a Maildir watcher for simplicity. Advanced admins can hook the classifier into Postfix via a pipe transport or Dovecot Sieve with the extprograms plugin.
Backups: Keep your intent model (
intent.pkl) and index output in your normal backup rotation.
Conclusion and Next Steps (CTA)
You don’t need a 70B parameter model to make email hosting smarter. With Rspamd, a tiny classifier, and a couple of Python scripts, you can:
Reduce spam and false positives.
Auto-route important mail by intent.
Skim daily digests.
Search by meaning, not just keywords.
Your next steps: 1) Install the stack and enable the Rspamd milter. 2) Train a small intent model from your own data and start routing. 3) Add the digest and semantic search when you’re ready.
Have a lab or staging domain? Try it this week. If you like the results, roll out gradually to production mailboxes. And if you want a follow-up post on Postfix/Dovecot inline integration or containerized deployment, let me know what you’re running!