Repository navigation
UwUMail Server 0.22.0
Webmail 0.22.0 (bundled): base labels with an overlap warning and an earlier description you can
forget, the facts and reasons behind the AI spam check, time ranges kept in found dates, and domain
signatures with a domain picker, "Gilt für" including all domains, placeholders and a note on the
company footer.
Added
- Eight base labels for everyone: Rechnung, Versand, Termin, Newsletter, Konto & Sicherheit,
Persönlich, Arbeit/Geschäftlich and Werbung (Invoice, Shipping, Appointment, Newsletter,
Account & security, Personal, Work & business, Promotions in English), each with a fixed,
non-overlapping definition and examples, switched on or off one by one (auto). A label of the
same meaning someone had already (Rechnungen,Termine…) becomes the base label; own labels
stay. - At most a main label and a second one, and none when in doubt. Every way that puts labels on
now says how sure it is; a main label needs 0.8, a second one 0.88, and base labels that exclude
each other (personal and newsletter, say) never go on together. Four new detectors (account,
personal, work, advertising) and stricter old ones; facts read from every mail (sender type,
List-Unsubscribe, amounts, invoice and tracking numbers, codes, greetings …) decide with them. - The model only in doubt, held to the facts. The label worker decides with rules, detectors,
learned senders, similar mails and the classifier first and asks the model only about the labels
they leave open, with the facts, hints and the person's corrections; it answers yes, no or
unsure. A lone yes is never a second label, a mass mail is never personal, and a model saying
yes to everything is not believed. On a corpus of 234 invented mails, labels without a model
went from 74 % to 98 % precision (recall 33 % → 67 %), and with gemma-3-4b from 57 % to 95 %
(recall 84 % → 81 %); on 508 real mails from 24 % to 96 % (recall 89 % → 64 %).
Notifications of apps and social networks (followers, recaps, mentions) are never newsletters or
promotions, a test mail gets no label, an invoice needs an invoice word, number, amount or PDF and
a shipment a tracking number, carrier or shipping word. A label taken off a sender's mail by hand is not put on that sender's
mail by itself again, by the model neither. - Similar mails decide labels: with the new admin provider kinds OpenAI embeddings, Ollama
embeddings and OpenAI-compatible embeddings by vectors of the person's labeled mails (one byte
per dimension, deleted with the mail), without one by their words. - Corrections teach more: taking a label off a sender's mail by hand keeps it off that sender's
mail, and hand-labelings are shown to the model as examples (sender domain, subject, start of
the text; the newest few per label). - Overlap warning:
AssistLabel/checkOverlaptells which labels a new or changed label would
overlap with (same name, the meaning of a base label, largely the same words); the webmail warns
while a label is written. - Small-model hint: the portal warns for chat models below 7 billion parameters and recommends
Qwen3-8B, Qwen3-14B or gemma-3-12b-it; docs/llm.md has a llama.cpp setup for chat and embeddings. - Moving a whole domain, by the admin (Admin → People → Moves): a wizard makes the domain
(with DKIM) and the mailboxes if they are missing, takes the people as a table or a CSV list
(;or,, with or without header, errors by line) with old login, password, name, address
here, quota and aliases, and copies mail over IMAP (folders and special-use folders merged,
nothing twice by Message-ID, flags and dates kept) and contacts and calendars over
CalDAV/CardDAV (autodiscovery, presets for mailcow/SOGo, Nextcloud, iCloud, GMX and WEB.DE,
.vcf/.icsupload per mailbox, Kolab-style IMAP contact and calendar folders). Progress per
mailbox and overall, readable errors, retry with a new login, pause/continue, a small
configurable number of mailboxes at once, and moves go on after a restart. - Delta sync until the MX switch: mailboxes keep syncing on a schedule until the admin
finishes the move (with an MX check); the last round runs, then the old passwords (sealed until
then) are wiped. - Password links for a moved domain: one invite link per new mailbox (7 days), with copy
buttons, CSV download and a printable overview; mailboxes that already have a password get
none, and the list says so. - Single mailbox moves by the admin in the same wizard, into an existing mailbox or a new one;
personal moves under My account → Moving share the copying code and the progress. - Signatures per domain. Pick a domain and write one signature for all your addresses there,
apply it to several or all domains, and give single addresses their own only where needed
(portal and webmail). Placeholders{name},{adresse}/{address}and{domain}are filled per
address. JMAPIdentity/getreturns the effective signature, so every mail program keeps
working; new extensionurn:uwumail:jmap:signatures(docs/signatures.md,
docs/jmap-signatures.md). Where all addresses of a domain had the same signature, the update
makes it the domain's. - Company signature per domain for admins: as a template people without their own get, or as
a mandatory footer the server appends on sending, through JMAP and SMTP submission alike, before
DKIM signing. Only the body text parts are rewritten; signed or encrypted mail (S/MIME, PGP) is
left alone and a footer already there is not added twice. - AI spam check: facts decide, the AI explains. The server first weighs what it knows
(authentication, its spam filter, your history with the sender, the new phishing checks) into a
score; the score sets which verdicts are possible, and the model may only choose among them.
Every reason must quote the mail or cite one of the facts; reasons it makes up are dropped and
counted. The confidence now comes mostly from the facts.Assist/spamCheckreturns the weighing
(facts),reasonDetailsanddroppedReasons; webmail 0.22.0 shows them. Against a small local
model (gemma-3-4b) on half the corpus, phishing called phishing rose from 54 % to 71 % and spam
from 50 % to 70 %, with no wanted mail called spam (before: 2 phishing mails called
legitimate); on real wanted mail of strangers, 1 of 40 was called spam instead of 3. - Phishing checks without the network. Look-alike, homoglyph and punycode domains of about 50
brands (and, in the AI spam check, of your contacts' domains), a display name showing another
address or domain, a brand's name in the display name or subject of mail from elsewhere, link
text naming a brand while the link leads elsewhere, a request for login data with links off the
sender's site, and a Reply-To to another site. New rules in docs/spam-filter.md. - A spam corpus for CI. 195 made-up German and English mails (wanted mail, spam, phishing, all
with reserved domains) and a test that fails when more than 1 % of the wanted mail would go to
Junk or less than 70 % of the phishing would.
Changed
- Fewer false positives. On the corpus, phishing that goes to Junk by the rules alone rose
from 26 % to 77 % while wanted mail stays out of Junk and is no longer greylisted (7 before).
User names with dots in link texts are no longer taken for domains, which greylisted many
social-network notifications. The Bayes filter adds at most +3.5 to mail DMARC vouches for
that trips nothing else (it alone put authenticated receipts and newsletters into Junk), unless
your own marks make it 99 % sure.
Security
- Company footer: no text can become a MIME boundary (review SIG-1). A rewritten part whose
text has a line starting with--is written as quoted-printable with every leading-of an
encoded line (soft line breaks too) as=2D(R2-SIG-1), and a message that would not keep its
MIME structure and the content of every other part goes out without the footer, so no part
appears after the virus scan. With severalFromaddresses, the first footer of their domains
applies. - The size limit counts the footer (SIG-4): a message that only fits without the company
footer is refused at submission (tooLarge), also for undo send and send later. - Submission refuses a malformed header block (SIG-6), as inbound mail already did: a
Bcc
below a line that is no header field went out to every recipient. - SignatureSettings/set writes each domain and identity once (SIG-2): duplicate domain keys
(*/*,Example.ORG/example.org) count once, the last wins; an account has at most 2000
identities. - Moves keep mail filed in "Kalender" or "Contacts" folders (review MOV-1): only messages that
are nothing but a contact or calendar object, of a kind the move takes, become contacts or
events; everything else, and every object whose import fails, is copied as mail. - Password links CSV without formulas (MOV-2): cells starting with
=,+,-,@, tab or
CR get a leading'. - A refused move leaves nothing behind (MOV-3): limits and busy mailboxes are checked before a
domain, mailbox or alias is made, and what was made is taken back when the move fails. - One move per mailbox at a time (MOV-4): an admin's move and the person's own move exclude
each other; a trashed account's move entries stop and lose their old passwords; the move page
reads only its own people and polls every 5 s instead of 3 s. - docs/signatures.md: the footer is best effort, not a compliance guarantee, the Sent copy has no
footer, and clients must sanitise signature HTML (SIG-3, SIG-5). - Imports cannot fill the memory (review M-1): moves, personal moves and fetched mailboxes ask
forRFC822.SIZEfirst and fetch small messages in portions of about 16 MiB and large ones
alone; a fetch may take only a little more than the sizes given; all imports share a 512 MiB
budget of fetched mail; message bodies are moved out of the answer instead of copied. A message
larger thansmtp.max_message_sizeis skipped and counted with the skipped ones. A move the
server went down during at three starts in a row is paused asinterruptedinstead of crashing
it again (migration 0075). - An understated size skips one message, not the whole import (review MFIX-M1): a body larger
than itsRFC822.SIZEallows (Exchange only estimates it) is read past instead of failing the
portion, fetched again on its own with room forsmtp.max_message_size, and skipped (UID in the
log) if it still does not fit, so fetched mailboxes and moves keep going past it. - One import holds no more than the room it counted (review MFIX2-H1): a portion ends at the
first message it read past, which comes again alone and the rest after it; a body larger than
smtp.max_message_sizeis let go at once; answers for UIDs that were not asked for, or twice,
are left out; only literals of at most 64 MiB are kept (larger ones are only read past); a
request above the 512 MiB budget is an error instead of a smaller permit. - Submission removes
X-UwUMail-Labela sender wrote, as inbound delivery already did. - docs/moving.md and docs/signatures.md list the known limitations the review left as they are.
- Regional bank names only under their own endings (SPAM-1).
sparkasse-…andvolksbank-…
count as the bank's own domain only under.deand.at;sparkasse-login.comand the like are
imitations again, and such a domain never vouches for a link that showssparkasse.de. - Tracking links only for authenticated senders (SPAM-3). A link whose text shows the sender's
own site and leads elsewhere is a harmless tracking link only when DMARC vouches for the From
domain; from a spoofable domain it counts asPHISHING_LINK_TEXT(+3) again, in the filter and
in the AI spam check. - Phishing checks stay cheap on hostile headers (SPAM-4). The display name (256 characters),
subject and text are capped inside the checks, host names longer than DNS allows are skipped,
brand names are split into words once, at most 2,000 contact domains are compared, and the AI
spam check parses the mail and runs the checks off the async runtime. - Word statistics held back only for proven senders (SPAM-2). The +3.5 limit for authenticated,
otherwise spotless mail now holds only for a sender with a good history or while the server's
Bayes is less than 99 % sure, and never for a sender whose mail mostly went to Junk; a throwaway
domain with its own DMARC no longer slips into the inbox on clean content. - Waiting messages cannot be taken over by a guessed Message-ID (SPAM-5). A retry only joins a
waiting entry with the same Message-ID when envelope sender and From match too, an arriving
message only removes waiting entries by Message-ID from the same envelope sender, and retries
keep an entry at most twice the waiting time from its first attempt. - AI spam check reasons must really cite (SPAM-6). A fact counts only when the reason's
evidenceis just that fact's number and the reason is about the fact's topic; a fact number in
the reason text proves nothing, and reasons naming a phone number or address that is neither in
the mail nor in the facts are dropped. - No verdicts from the sender (client review C-1, server side).
X-Spam-Status/X-Spam-Score
a message brings are removed even when the filter did not look, and submitted mail loses
Authentication-Resultsin this server's name and spam verdicts before local delivery. The AI
spam check and the label facts read only the block this server wrote on top (its ownReceived
up to the next one); for another account's mail only what stands above the firstReceived,
without counting a good filter score, and "suspicious" is always allowed there. - Embeddings only with consent (LABELS22-M1). Mail goes to the admin's embeddings provider only
while AI labels are on on the server and for the person, the person may use the assistant, and
their chosen labels model is one of the server's (not a personal or local one). Mail in Junk or
Trash is never embedded and no similar mail. - Known senders by an index, exactly, and only when authenticated (LABELS22-M2, -L1). Delivery no
longer reads every vCard of the recipient's address books for every message: a new index of card
addresses (migration 0072, filled once at start) is kept with every card write. A sender counts
as known only by exactly a card's address (not a prefix of one) and only when authentication
backs the From address. - An adopted label keeps your own description (LABELS22-L2). A label of yours that becomes a
base label because of its name keeps the description you had written aspreviousDescription
(migration 0073, shown inAssistLabel/get); the model gets it as a hint next to the definition. - Correction examples without codes, and gone with AI labels (LABELS22-L3). Examples kept from
hand-labelings mask runs of four digits or more and web addresses, are only sent while AI labels
are on, and are deleted when AI labels are switched off. - Neighbour search off the async runtime (LABELS22-L4). Comparing a mail with the person's
examples (words or vectors) runs on a blocking thread, the mail's words are made a set once, and
AssistLabel/applyruns one call at a time per person. - Authenticated means aligned, in the AI paths too (R2-M1). The AI spam check and the AI labels
call a From authenticated only when DMARC passed, or — without a DMARC policy — a DKIM signature
or SPF pass belongs to the From domain, a parent or a subdomain of it, as the SMTP checks decide
it. A pass for the sender's other domain, or a DMARC failure, no longer makes a contact "known"
or a lookalike link a tracking link.Authentication-Resultsis read with comments and quoted
strings handled, so a quoted envelope sender cannot inject a result. - Regional bank prefixes hide nothing (R2-L1). A
sparkasse-…/volksbank-…domain under
.de/.atcounts as the bank's own only when the From is authenticated, and never vouches for a
request for a login or data:CREDENTIAL_REQUESTfires even for a freshly registered domain with
its own DMARC. - Verdicts only in mail delivered here (R2-L2). Mail stored by IMAP APPEND or JMAP import can
carry a forged copy of this server's header block; the AI spam check and the AI labels now read
that block only in mail this server's SMTP delivery stored (migration 0074smtp_delivered;
copies keep the mark, mail stored before the update counts as delivered). - Reasons and examples, tightened (R2 info). A number an AI spam reason names must stand within
one number of the mail, and a reason calling the sender verified needs authentication behind it.
Correction examples also mask codes of letters and digits (AB7-K2X), and a mail with a one-time
code keeps no example. - An extreme date from the model can no longer stop the server (final review DATES-H1). Event
extraction ignores start and end dates outside 1970–2200 before doing any date arithmetic, so a
model answer (or a personal provider) naming the year 262142 no longer overflows and aborts. Authentication-Resultsread whole and one way (R3-L1, R3-L2, client C4-1). The parser is
shared by the strip of forged results and the assistant (uwumail_smtp::auth_results_parts,
authserv_id), reads every part however many DKIM results come first, and never keeps a word or
part a limit cut: a signervictim.example.<own domain>can no longer read as the victim's. A
stored message cut for parsing inside its header block leaves the cut header out.- Warnings stay warnings, praise stays praise (R3-L3, R4-L1). "Unverified", "not trustworthy",
"isn’t trustworthy", "doesn't seem genuine", "kein verifizierter Absender" and other negated
praise no longer count as calling the sender verified, so the AI spam check keeps such reasons. A
negation counts only right before the praise in its own clause: "No red flags: verified sender",
"no doubt trustworthy" or "nicht nur ein verifizierter Absender" are still praise and dropped when
the From is not authenticated. - Trace headers read strictly everywhere (R4 I-2, I-4, I-6).
Authentication-Resultswords end
only at ASCII whitespace, fetched mail reads the provider's results with the same parser, and
Assist/spamChecktakesAuthentication-Results,ReceivedandX-Spam-Statusvalues up to
16,000 characters (apps leave out longer ones, never cut them). - Another server's results believed only when well formed (R5 L-1, client C6-2, C6-3). For
fetched mail and the AI spam check of another account, anAuthentication-Resultswith an
unclosed comment or quote or a stray)counts as no result: a provider that echoes a sender's
parenthesis into a comment could otherwise let the sender adddmarc=passor aclient-ip. A
method named twice keeps its worse result, a failed DKIM result that names no signer outweighs a
pass, and a client address named twice counts as none. Only ASCII whitespace is trimmed from
domains and at header folds, and a domain with a Unicode space in it is none. The AI spam check
also reads "Phishing? No… verified sender" and "no less trustworthy" as praise. - No direction tricks on the spam card (webmail review WF-2). Phishing details, the facts' details,
the model's reasons and their quotes come without Unicode bidi controls. - An adopted label's earlier description can be forgotten (webmail review WF-1).
AssistLabel/settakespreviousDescription: null; the model gets no hint from it from then on. - A forged colleague is no known sender (final client review). The AI spam check counts the
reader's history with an address in full only when authentication backs the From, not at all when
DMARC failed or the results leave the From unaligned, and at half weight, at most 1.5 in all, when
the mail carries no results. Gift-card and urgent-transfer cues stay on unless the sender is both
known and authenticated, so CEO fraud from a forged address can come out as spam or phishing. - Amounts found in one pass (final client review X-1). Thousands of rejected copies of an amount
before the real one no longer make label facts take seconds; facts are made once per mail and off
the async runtime. - Known limitations documented. Three low-impact remainders are written up in
docs/spam-filter.md, "Known
limitations (security review)": sender-balanced comment echoes, client addresses written only in comments, and the
non-atomicifInStatecheck on domain signatures. - No startup rewrite of every mail (R3 I-2). Migration 0074 now adds
smtp_deliveredwith a
default instead of updating every row. Correction examples mask digits of any script. - Low findings of the review that were left as they are are listed under "Known limitations
(security review)" indocs/spam-filter.md,docs/labels.md,docs/moving.mdanddocs/signatures.md.
Fixed
- Appointments keep their times. "Samstag 03.10.26, zwischen 10:00 und 12:00" becomes
10:00–12:00 instead of an all-day event. The webmail's date finder knows "zwischen … und",
"10.00–12.00", "halb drei", "nachmittags", "c.t.", parcel time windows and date ranges with
"zwischen", and offers fewer false ones (login and pickup timestamps, billing periods, phone
numbers, "Auftrag … vom"); titles and places no longer read "Betrag", "Datum" or "Dorf". "Check
with AI" keeps a time range, is told to set all-day only when the mail gives no time, and its
answer never replaces a found time with a whole day (or the other way round). The server turns
an all-day answer with a time of day into a timed event and counts an all-day end as the last day. - Wartende Nachrichten: retries of one message are one entry. A greylisted sender that comes
back several times no longer fills the list with copies; the entry shows how often it tried and
when it last did. Once the message arrives, every waiting copy disappears, and a retry of a
message already delivered or discarded by hand no longer comes back onto the list. - Push no longer reports a change with an older state. A change notification handled late
read changes written meanwhile and sent them with the older state (JMAP push and web push); it now
reads only up to its own state.