Repository navigation
v0.1.7
Changed
- Typing
@in a recipient field reaches for a person. One letter after it
is now enough to be offered a list, instead of the usual two: the@has
already said you are naming somebody rather than typing an address. What
follows the@is what gets searched, and a half-typed@maris no longer
turned into a recipient of its own when you leave the field.
Added
-
A recipient pasted as a
Name <address>string is read as a name and an
address. It used to become a single unusable recipient carrying the whole
string. -
Writing to someone opens the composer, from anywhere in the instance. The
"write to them" action other modules offer beside a person — a guest on a
calendar event today — used to be amailto:handed to whatever the operating
system opens, which on an instance that has a mailbox is the wrong answer: it
loses the signature, the drafts and the sent copy. The composer already
floated above every module; it is now reachable by them, pre-filled with the
recipient, without leaving the page you were on. An instance without this
module keeps themailto:.
Added
-
"Directions" on an invitation. When the event carries a place and the Maps module is installed, the invitation card offers Directions beside the answers. It opens Maps in a new tab with the destination already filled in, so the invitation stays open behind it.
-
Guests can answer an invitation straight from the e-mail. The invitation now ends with a Répondre block offering Yes / No / Maybe; each button carries that guest's own link, so anyone can answer from any mail client without signing in to Kubuno. The buttons appear once an administrator has set the instance's public address in Calendar; without it the message explains that the answer is given from the recipient's own calendar, through the attached file.
-
Meeting invitation e-mails. When Calendar publishes an
invitation, Mail now sends the invitation e-mail on behalf of the organizer's
default account: a sober HTML body (event title, when, where, organizer, guest
list, description) plus a standards-compliantinvite.icsattachment
(text/calendar; method=REQUEST). A cancellation goes out the same way
(method=CANCEL), and a rescheduled event is flagged "Invitation mise à jour".
Recipients using any calendar client can accept, decline or answer "maybe"
straight from the message. -
RSVPs update the organizer's calendar automatically. When a guest replies
to an invitation (METHOD:REPLY) and the reply lands in the organizer's local
mailbox, Mail forwards the response to Calendar so the attendee's status
(accepted / declined / tentative) is reflected on the event without anyone
re-entering it. The same happens for a reply that syncs into an organizer's
external IMAP account (e.g. Gmail). -
Remote images from an unknown sender are held back, Gmail-style. A message
whose images would be fetched from outside the instance now shows a notice —
"External images are not displayed" — with Display images (this message) and
Always display images from (remembered). Nothing leaves the browser
until you ask: fetching a remote image tells its host the message was opened.
Inline (cid:), embedded (data:) and same-origin images are never touched,
and CSS backgrounds are neutralised too, not just<img>. -
Administrators define an instance-wide image allowlist (Mail → Filtering)
applying to every account on top of each user's own list, the way a Workspace
allowlist set at the top level applies to all organisations. It ships stocked
with major legitimate senders, accepts# commentsfor grouping, and an entry
only takes effect on a DMARC-authenticated message — nobody opted into it
individually, so a spoofed From must not be able to ride it. -
Notifications for calendar invitations. Receiving an invitation, a reply
to an invitation you sent (accepted / declined / maybe) or a cancellation now
raises its own notification, on top of the plain "new mail" one. Each is a
separate activity in Settings → Notifications. -
Declining an invitation can carry a reason, or propose another time.
Choosing "No" opens an optional panel: a note for the organizer (sent as the
iCalendar COMMENT) and an alternative slot. Filling the slot turns the answer
into an iTIP counter-proposal (METHOD:COUNTER) the organizer can accept. -
RSVP to calendar invitations, Gmail-style. An invitation card now offers
Yes / Maybe / No. Answering emails the organizer a proper iMIP reply
(text/calendar; method=REPLYwith the attendee's PARTSTAT), mirrors the
event into the calendar for Yes/Maybe (confirmed/tentative), and remembers the
answer so the chosen button stays selected on reload. -
Gmail-style rich cards for structured mail. Emails carrying schema.org
annotations (JSON-LD) or a calendar invitation (ICS) now show a card above the
body: calendar events/invitations, flight, hotel, train/bus, restaurant, car
rental, and order/parcel — with the key details laid out and, where relevant,
a check-in or tracking link. Event and invitation cards offer "Add to
calendar" (creates the event in the Calendar module) and a ".ics" download.
Extraction runs at delivery/sync time, so cards appear on mail received from
then on. -
Multiple sources and destinations in the search criteria. The "From"
field accepts several addresses, OR-combined (any of these senders); the "To"
field is a full AND/OR rule tree with nested groups — the same builder as the
"Additional filters" tab, its conditions locked onto:. "To" starts
compact like "From" — one input and a [+] — and switches to the multi-line
builder when a second condition is added, collapsing back at one. Both
round-trip with the bar's query text ((from:a OR from:b), parenthesized pure-to:
groups). -
Gmail-style contact suggestions in address fields of the search panel.
From/To criteria and builder rows whose operator takes an address (cc:,
bcc:,deliveredto:) suggest contacts as you type — avatar, display name
and address, merged from the contacts module and the mail address index. -
Logical repetitions are detected and removed when the search is
validated. Clicking "Search" simplifies the rule trees: duplicate
conditions collapse (label:x OR label:x→label:x), same-combinator
nesting flattens, single-child groups unwrap, and boolean absorption applies
(A OR (A AND B)→A); duplicate sources are deduplicated too. -
Search results always carry the filter-chip row — date, "Has attachment",
"To", "Exclude promotional offers" (new), "Unread" and the "Advanced search"
link. On a search view the chips refine the committed query (they append to
it and lift off it); folder views keep their scoped chip bar. -
Searching on senders features them above the results — avatar, display
name and the address as a mail link that opens the composer prefilled with
them. Multi-source queries ((from:a OR from:b)) show one card per sender,
flowing side by side on a single row; each card highlights on hover with a
plain rounded grey rectangle. -
The committed search lives in the URL —
#search/<encoded query>, Gmail
style: a search survives reloads, can be deep-linked and follows the browser's
back/forward. Clearing the search returns the hash to the folder. -
Condition rows can be reordered by drag and drop. Every condition and
group carries a grip; dropping between siblings shows a crisp insertion bar
(no translucent browser ghost), and dropping onto a group's header — which
highlights — moves the node into that group. Conditions and whole groups
move freely between root and any group; a group can never be dropped into its
own descendants. -
Uniform field typography in the advanced panel. The free-value inputs
render at the same 14px as every dropdown (the coarse-pointer 16px anti-zoom
guard was out-specifying the primitives' size on some devices). -
The search language accepts an explicit
AND.(-in:spam AND in:trash) OR is:subscriptionnow parses as intended:ANDis a combinator (juxtaposition
already meant AND), no longer a literal word to search for. -
The "Additional filters" tab is now a full recursive condition tree — the
standard query-builder model: every group carries one combinator ("all
conditions (AND)" / "any condition (OR)") and holds conditions, raw
expressions and nested groups, with "Condition" / "Group" buttons and per-node
deletion at every level. Any parenthesized AND/OR mix decomposes into editable
rows, and every edit rewrites the search bar live with correct parentheses.
Top-level OR chains are kept whole when pre-filling (sofrom:a OR from:bis
never silently split into a plain "From" field). -
A new search operator,
is:subscription. It matches messages that carry
aList-Unsubscribeheader — the same population the "Manage subscriptions"
view is built on. It combines with every other operator (from:,in:,
OR, negation, parentheses), and is suggested in the search bar.
Fixed
-
Sent messages keep their formatting. Every outgoing message was cleaned with a policy that strips
styleoutright, so bold colours, spacing and alignment were lost the moment a message left — invitations arrived at their guests as an unstyled table. Sending now applies the module's own vetted policy, the same one the reader uses. -
A conversation carrying an attachment now opens on a single click, wherever you click it. The attachment strip stretches across the whole row, and it swallowed clicks: the entire lower half of such a row opened nothing, so it often took a second click higher up. Clicking an attachment chip still previews that file instead of opening the conversation.
-
Sender, subject and date stay on the first line of a row. When an attachment chip added a second line, they drifted to the vertical middle of the taller row; the chips now sit on the line below, with everything else aligned on the first.
-
Clicking a folder or label in the sidebar now leaves the search, the way it does in any mail client: the search box empties and the folder shows its own content. The committed query used to stay applied on top of every folder you clicked, so the list never changed — only the highlighted row did. Back/forward out of a search and the inbox categories behave the same way. The URL is now the single source of truth for being in a search (
#search/<query>). -
Searching from a page without a message list now shows the results. A search committed from Settings, Manage subscriptions, Drafts or Scheduled — by typing in the bar or by clicking a sender on the subscriptions page — opens the mailbox on that search instead of leaving you on the page you came from.
-
An iMIP reply (RSVP) that omits
DTSTART— allowed by RFC 5546 — is now recognised and forwarded to the organizer's calendar instead of being silently ignored. -
A sender can no longer impersonate someone in the interface. A display
name carrying somebody else's address (a spoofed display name sent from
elsewhere) now shows the real address next to it — on mobile too, where it
used to be hidden — with a warning chip. Bidi and invisible characters are
stripped from every attacker-authored text, so an attachment named
annexe_<RLO>gpj.exeno longer paints itselfannexe_exe.jpg. Punycode
domains are flagged, with their Unicode form in the tooltip. -
Links in rich cards are restricted to http(s). The
url,checkinUrland
trackingUrlfields come from the sender's own JSON-LD or.ics, and were
stored and rendered unchecked — protection rested on React refusing
javascript:hrefs, i.e. on a framework version. They are now validated at
extraction (the key is dropped from the stored node), again when parsed, and
once more where the link is built. -
IMAP ingestion is bounded. A hostile or misbehaving IMAP server could make
the module load an unbounded batch into memory: message sizes are now read
first (RFC822.SIZE) so oversized messages are skipped without downloading,
fetches are grouped under a byte budget, and attachments are capped per
message in count and in total size — the message is kept, only the excess
parts are dropped. Attachment bytes are borrowed rather than copied, halving
the resident cost of an ingest. -
Trust indicators no longer read as guarantees when nothing was verified.
"Sent by", "signed by" and "security" are read from headers the message
carries. Our own SMTP server checks SPF/DKIM/DMARC on delivery, but a message
synced from an external mailbox arrives with none of that — so a forged
DKIM-Signature: d=paypal.comdisplayed as "signed by paypal.com". Unless the
message actually passed DMARC, these values are now marked "(non vérifié)". -
An attachment can no longer be rendered as a document in Kubuno's origin.
Attachments were servedinlinewith theContent-Typethe SENDER wrote, so
an HTML or SVG part opened as a page with the reader's session — and a second
part declaredapplication/javascriptloaded from that page same-origin,
defeatingscript-src 'self'. Only a short list of previewable types (images,
PDF, audio, video) keeps its type and staysinline; everything else is
served asapplication/octet-streamwithContent-Disposition: attachment.
Both cases now carrynosniffand a sandbox CSP, and control characters —
bidi overrides included — are stripped from the filename. -
The reader enforces the same policy as the server. A message stored before
the server rules were tightened still carries whatever was allowed the day it
arrived, so the last pass before the markup reaches a live document now blocks
the same things: inlinedata:/cid:accepted only as an image source, e-mail
CSS scrubbed of@importand the script-in-CSS hooks, andsvg/math/
template/form controls refused outright. -
Nothing in an e-mail can execute. The body sanitiser was audited against
Gmail's policy and hardened:data:andcid:URLs are now accepted ONLY as
an image source, so a link can no longer carry an inline
data:text/html;base64,<script>document; and e-mail CSS — which the
sanitiser passed through untouched — is stripped of@import(which pulls a
remote stylesheet) and of the legacy script-in-CSS hooksexpression(),
behavior:and-moz-binding, in<style>blocks as well asstyle=
attributes. Scripts, event handlers, iframes, objects, embeds, forms,<meta refresh>,<base>,<link>and SVG were already removed; 21 attack shapes
are now pinned by tests. -
The Mail module's settings never reached the admin console. One
[[setting_groups]]entry declaredtitlewhere the manifest requires
label, so the wholemodule.tomlfailed to load and NOT ONE of the module's
68 instance settings was registered. -
The condition builder's "Condition" and "Group" text buttons are now icon
buttons with tooltips — a lighter group header, same actions. -
One insertion slot per boundary in the condition builder. "After row N"
and "before row N+1" were shown as two distinct drop positions when they are
the same place; drop targets are now normalized to a single slot per sibling
boundary, so hovering either side of a boundary marks the exact same bar. -
Drag-and-drop in the condition builder no longer shivers. The insertion
bar is drawn as an absolute overlay (an in-flow bar shifted the row under the
pointer and made the before/after test oscillate), the drop mark only updates
when the target boundary actually changes (a state write per dragover event
re-rendered in a loop), and leaving a child no longer clears the mark (the
dragleave storm made it flicker).
Changed
-
This module now installs as a Kubuno package (
.kbpkg) only. Its system
packages (Debian/RPM and the Windows and macOS installers) are no longer
built: the module is distributed as one.kbpkgper platform (Linux, Windows,
macOS) that the Kubuno server installs itself — from the admin console, or
offline withkubuno modules:install <file>.kbpkg. -
Internal refactor:
handlers/messages.rs(≈1230 lines, a grab-bag of unrelated concerns) was split into amessages/module — one file per responsibility (reading, sending, attachments, flags, address suggestions, OpenPGP, calendar-invitation RSVP). No visible or behavioural change; every import path is unchanged. -
An invitation is presented like a calendar entry. Its card is now shown above the whole conversation, ahead of the sender's header, rather than inside the message under it — an invitation precedes the mail that carries it. The card leads with the day said plainly ("Tomorrow · 07:30 – 08:30"), then the event name in full size, then where it is held and who is organising it side by side, and finally Yes / No / Maybe. The three answers carry equal weight until one is given, after which it stays marked and the other two step back. The invitation e-mail itself is laid out the same way: the organiser's own words first, then Date, Place and Guests as titled blocks with room to breathe, instead of a cramped label-value table.
-
"Reply all" is hidden when there is no one else to reply to. On a message
whose only correspondent is the sender, it would send the exact same mail as
"Reply", so only "Reply" and "Forward" are offered. -
The README now opens with the module's logo. The public README on
GitHub now shows the module's designer logo (the same PNG shown as the
browser tab icon and in the applications menu) at the top of the page — the
repository landing now matches the icon a signed-in user sees inside the
platform. The image ships in-repo, under.github/logo.png, so it renders
even when the repo is browsed offline. -
New Mail logo. The old inline SVG mark is replaced by a raster PNG (blue
hexagon with an envelope), used in the waffle menu, icon slots and as the
browser-tab favicon. -
Square corners on the composer. The floating composer, its docked
minimized bar and its full-screen panel now have square corners, matching
the platform's flat floating-window look. -
Composer caption buttons match the close button. Minimize and
maximize/restore use the same size, hover veil and outer spacing as the
close button. -
Classic window-caption glyphs on the composer. Maximize is now a plain
square and restore two overlapping squares, instead of diagonal double
arrows. -
The advanced search panel gained an "Additional filters" tab — a real
condition builder. Operator fragments from the bar's query (parenthesized
groups, OR chains, any supported operator) are decomposed into editable rows —
the classic filter-builder layout: an AND/OR connector from the second row, an
operator picker over the full supported list, an enumerated or free value, an
exclusion toggle and a per-row delete, plus "Add a condition". Consecutive OR
rows serialize back as a parenthesized group, and every edit rewrites the
search bar live. "Has the words" only ever holds free words. The panel sits in
two tabs (Criteria / Additional filters), uses the shared UI primitives
throughout, with uniform typography and no separator rules between fields. -
The advanced search panel mirrors the search bar — both ways. Opening it
decomposes the bar's query into the fields (From, To, Subject, "Has the
words", size, date, scope…), unknown fragments landing verbatim in "Has the
words"; editing any field rewrites the bar's text live. Fields and query text
can no longer disagree. -
Manage subscriptions, redesigned. The subscription list now shows each
sender with a coloured avatar, its address on its own column and a
plain-language frequency ("More than 20 emails recently", "10-20 emails
recently", …). Clicking a sender opens the mailbox on the full Gmail-style
query —from:X (-in:spam OR in:trash) is:subscription— everywhere but
spam, trash included, subscription messages only. "Unsubscribe" is a discreet
inline action that
first asks for confirmation — "Stop receiving messages from all of X's mailing
lists?" — before triggering theList-Unsubscribe, matching the familiar
mail-client layout.
Fixed
- A withdrawn dependency is no longer used. A crate deep in the tree
(spin0.9.8, pulled in through the HTTP stack) was yanked by its authors.
No vulnerability was announced, but a withdrawn crate has no business in a
release; the lockfile now takes the version that replaced it. - The package could not be built where
zipis absent. The Windows job of
the continuous integration has nozip, so the Windows package was simply lost
the first time it was attempted — a script failure, not a build failure. The
builder now falls back to 7-Zip, then to PowerShell.
Added
- This module now ships a
.kbpkg— the single package format a Kubuno
server installs by itself, the same file on Linux, Windows and macOS. It
carries the same binary, interface and manifest as the system packages,
arranged the way the server expects to find a module on disk, plus a
SHA256SUMSso a copy carried offline can be checked without the catalogue.
Nothing changes for existing installations: the.deb,.rpm,.exeand
.pkgare still published, and a catalogue that sees both simply prefers the
new one. It is also the only format the server can unpack without an external
tool, which is what makes one-click installation possible away from
Debian-like systems.
Fixed
- A built package could be thrown away instead of published. The job that
attaches a package to the release waited ten minutes for another workflow to
create that release, then gave up with "release never appeared — build.yml
likely failed". The diagnosis was wrong: on a repository whose.debtakes
longer than ten minutes to build, the release simply did not exist yet, and a
package that had built perfectly was discarded. Four modules reached v0.1.6
with packages missing for some systems because of it. The job now creates the
release itself when it is missing, so it no longer depends on another workflow
finishing first.
Added
-
Security policy and CI quality gate. A
SECURITY.mddocuments how to
report vulnerabilities, and a CI workflow enforcesclippy -D warnings, a
dependency-vulnerability audit (cargo audit) and the frontend typecheck/tests. -
Automatic address attribution. Once a primary domain is verified (DNS
proof and MX), every account without a mailbox on it receives one, and so
does every new account thereafter. The local part is built from an
administrator's rule (Attribution des adressessettings — e.g.
{prenom}.{nom}, tokens{prenom} {nom} {p} {n} {username}), accents folded
and clashes resolved with a numeric suffix. It never overwrites: an address
set by hand, or from a previous run, is left untouched, and the whole
behaviour can be turned off. Reconciled on a timer, so nothing is lost to a
missed event or a restart. -
Mail now publishes, on the platform's event bus, that a recipient exchanged
with a correspondent. An address book can collect those into its "Other
contacts" list; mail itself knows nothing about who listens, and publishing
never affects delivery.
Changed
-
Pill-shaped buttons are gone from the interface. Filter chips, view
segments, tab selectors and action buttons that were drawn as pills now use the
same 4 px corner radius as every other button — the shape set them apart for no
reason other than habit. Round buttons that hold a lone icon, avatars, status
dots and non-clickable badges keep their shape: a circle around a single glyph
is not a pill. -
The folder filter chips are no longer pill-shaped. « De », « Indifférente »,
« Contient une pièce jointe », « À » and « Non lu » now use the same 4 px corner
radius as every other button in the product; the pill set them apart for no
reason other than habit. Round icon-only buttons (zoom, print) and status
badges keep their shape — a circle around a lone icon is not the same thing. -
The monospace fallback for message bodies no longer names an external font that
was never shipped; it uses DM Mono, which the instance actually serves. -
Internal refactor: the message composer (
ComposeWindow) was split into
focused files (mail-app/compose/:SendButton,ComposeFormatToolbar,
ComposeActionBar, sharedparts). No visible or behavioural change.