Skip to content

v0.1.7

Choose a tag to compare

@github-actions github-actions released this 18 Sep 10:04
· 56 commits to main since this release

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 @mar is 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 a mailto: 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 the mailto:.

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-compliant invite.ics attachment
    (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 # comments for 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=REPLY with 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 on to:. "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:subscription now parses as intended: AND is 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 (so from:a OR from:b is
    never silently split into a plain "From" field).

  • A new search operator, is:subscription. It matches messages that carry
    a List-Unsubscribe header — 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 style outright, 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.exe no longer paints itself annexe_exe.jpg. Punycode
    domains are flagged, with their Unicode form in the tooltip.

  • Links in rich cards are restricted to http(s). The url, checkinUrl and
    trackingUrl fields 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.com displayed 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 served inline with the Content-Type the SENDER wrote, so
    an HTML or SVG part opened as a page with the reader's session — and a second
    part declared application/javascript loaded from that page same-origin,
    defeating script-src 'self'. Only a short list of previewable types (images,
    PDF, audio, video) keeps its type and stays inline; everything else is
    served as application/octet-stream with Content-Disposition: attachment.
    Both cases now carry nosniff and 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: inline data:/cid: accepted only as an image source, e-mail
    CSS scrubbed of @import and the script-in-CSS hooks, and svg/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: and cid: 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 hooks expression(),
    behavior: and -moz-binding, in <style> blocks as well as style=
    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 declared title where the manifest requires
    label, so the whole module.toml failed 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 .kbpkg per platform (Linux, Windows,
    macOS) that the Kubuno server installs itself — from the admin console, or
    offline with kubuno modules:install <file>.kbpkg.

  • Internal refactor: handlers/messages.rs (≈1230 lines, a grab-bag of unrelated concerns) was split into a messages/ 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 the List-Unsubscribe, matching the familiar
    mail-client layout.

Fixed

  • A withdrawn dependency is no longer used. A crate deep in the tree
    (spin 0.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 zip is absent. The Windows job of
    the continuous integration has no zip, 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
    SHA256SUMS so a copy carried offline can be checked without the catalogue.
    Nothing changes for existing installations: the .deb, .rpm, .exe and
    .pkg are 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 .deb takes
    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.md documents how to
    report vulnerabilities, and a CI workflow enforces clippy -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 adresses settings — 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, shared parts). No visible or behavioural change.