Notably, we try to reduce disk usage by standardizind on common
base Docker images. We also improve node_modules by reducing
duplicate dependencies and install speed.
Store review cycles make shipping web-layer fixes through the stores
too slow, so the apps update their JS bundle over the air. The chain is
fully self-hosted to keep sovereignty: bundles and channel manifests
live on an anonymous-read S3 bucket (create_bucket --public / the
create-ota-bucket script) and the Capgo plugin is driven entirely from
JS against that manifest (autoUpdate off — no Capgo server involved).
Bundles are RSA-signed at publish time and verified against the
per-instance public key baked in at cap sync, so a tampered zip on the
public bucket is rejected.
Versions use a git-derived <count>-<sha> id stamped into the builtin
bundle so a fresh install does not re-download its own commit, and
channels (dev/staging/prod) are fully independent because
NEXT_PUBLIC_* vars are inlined at build time.
Also ships docs/mobile.md.
Ship the existing SPA as native iOS/Android apps without forking the
codebase: Capacitor wraps the web build, and every mobile-specific
behavior is gated behind isNativePlatform() so the web app is
untouched. The native shells route fetch/cookies through the native
HTTP layer (CapacitorHttp) — the WebView cookie jar is unreliable for
cross-origin sessions — which is why login runs in the system browser
(cross-app SSO via the shared IdP cookie) and finishes through the
backend session handoff, with the deep-link scheme pinned by
sso-invariants tests. Downloads/share go through the Filesystem/Share
plugins since WebView navigation would lose the session.
The backend sends acr_values=eidas1 on every authorization request
(OIDC_AUTH_REQUEST_EXTRA_PARAMS) but the dev realm had an empty
acr.loa.map, so Keycloak treated the value as an unknown essential acr
claim. Web logins survived it, but the mobile system-browser flow
(ASWebAuthenticationSession / Custom Tabs) failed the login round-trip,
breaking cross-app SSO in dev. Mapping eidas1 to LoA 1 mirrors what the
production IdP declares.
Capacitor apps must run the OIDC flow in the system browser (the IdP
cookie has to live there to provide cross-app SSO), but the browser's
cookies never reach the app's native HTTP layer, so the Django session
created by the callback would be stranded. The callback now redirects
to an allowlisted app deep link with a one-time token that the app
exchanges for its session cookie and CSRF token. The token is bound to
the initiating app instance with a PKCE S256 verifier, single-use,
short-lived (MOBILE_AUTH_TOKEN_TTL) and the anonymous exchange endpoint
is throttled per IP to cap brute-force guessing. An empty
MOBILE_AUTH_CALLBACK_SCHEMES (the default) keeps the whole handoff
disabled.
The upcoming Capacitor mobile shell replays the Django session cookie
through its native HTTP layer but not the `csrftoken` cookie, so
cookie-based CSRF would break every mutation on mobile. Enabling
CSRF_USE_SESSIONS moves the secret server-side and removes the need for
a JS-readable cookie: the token is now delivered on the authenticated
/users/me/ response, cached in memory by the SPA and echoed as
X-CSRFToken. On web this is equivalent or safer — the secret is no
longer readable by scripts nor overridable via cross-subdomain cookie
tossing.
Some of our users has reported issues using the application
with Chrome 109. Instead of polyfilling one by one each
method, we setup vite legacy plugin and configured a
browserlist. It adds a 23.5Kb Gzipped module but the
polyfill strategy management is more standard and robust.
Close#741
About the unquote-message logic, we encount a bug with a thread
implying Outlook Desktop quotes. Actually, for Outlook web we were looking for
a hr tag as quote separtor element. But sometimes this one can be wrap into a div
and we missed it.
Parse sanitized html before rendering to detect links that are just raw text then
transform them into anchor.
Furthermore, for security purpore, we catch event when a user clicks on a link then
display a confirmation modal displaying the real link.
Co-authored-by: Valentin Regnault <valentinregnault22@gmail.com>
When the user switches to anoter mailbox while it is on a search view,
we reset search params and go to to the inbox as it does not make
sens to keep search params on the new mailbox
Postfix was already removed as a mta-out dependency, this is the second step so we have a pure python, more auditable path for incoming emails. We plan to keep postfix as a compatible option for a while but it won't be the default once this is battle tested.
We are currently using react-email to generate html bodies. This
library aims to generate marketing email consistent in all mail
providers. For personal message, it generates too much custom styles
that can increase spam score of those messages.
Safari 26 changed `text-wrap: pretty` to re-optimize line breaks over
the whole paragraph (webkit.org/blog/16547). The global CSS reset
applies it to every paragraph, so BlockNote editor paragraphs get
re-balanced on every keystroke, wrapping lines well before the editor
width. Restore `text-wrap: wrap` inside .bn-editor, which covers the
message, signature and template composers. Received messages render in
an iframe and are not affected by the reset.
Fixes#735
Signed-off-by: Nicolas Aunai <nicolas.aunai@lpp.polytechnique.fr>
Co-authored-by: Jean-Baptiste Penrath <jbpenrath@gmail.com>
Display the count of unread messages next to the mailbox name
to help to quickly identify mailbox with new unread messages.
Resolve#738
Co-authored-by: Nicolas Aunai <nicolas.aunai@lpp.polytechnique.fr>
The relay section of main.cf.j2 is indented by one space. Jinja keeps that
leading whitespace in the rendered /etc/postfix/main.cf, and Postfix treats any
line starting with whitespace as a continuation of the previous parameter. So
the smtp_sasl_* directives get folded into the relayhost value instead of being
set as parameters, leaving smtp_sasl_auth_enable at its default (no) and the
relayhost value malformed.
Outbound mail through an authenticated relay then fails: the relay rejects it
with "502 5.7.0 Please authenticate first", and Postfix logs "fatal: valid
hostname or network address required in server description: =". De-indenting the
block to column 0 makes each directive a real parameter. Verified against
Scaleway TEM: postconf then shows smtp_sasl_auth_enable = yes and delivery
succeeds (250 queued).
Signed-off-by: Corentin Giraud <corentin@giraud.dev>
In message form, we prevent the user to press "Enter" to submit
the form by error when composing message. But this logic breaks
the line-break on chrome for android.
The openapi schema specifies that an attachment has always
a name but we have some case where this attribute is None.
To guarantee this contract, we use a fallback value `unnamed`
when name is None during serialization.
Also fix other issues of the same kind identified.
When a user clicks on the checkbox of the thread-item to select it,
the preventDefault call prevent the checkbox to update its state so
under the hood the thread was well added to the selection but the
user has a wrong state. Now we make the checkbox fully controlled
and non-interactive, in this way, user always interact with the
thread-item link and the selection state is used to set the
checkbox state
Mails sent without a To recipient (Bcc- or Cc-only) ship with no To
header, which anti-spam filters treat as a negative signal. Inject an
RFC 4356 empty-group "To: undisclosed-recipients:;" before DKIM signing
so it's covered by the signature.
Drafts had no deletion path: the only "delete" affordance was the soft
trash flag, which makes no sense for a draft that was never sent. Users
accumulated drafts they could not get rid of.
Add a generic bulk hard-delete endpoint (POST /threads/bulk-delete/)
scoped by message flag (draft/trashed), mirroring the flag endpoint's
batch + editable_by authorization pattern. It deletes only the
scope-matching messages so reply-draft threads keep their real messages,
and removes the thread only once it is emptied.
Furthermore, the draft auto save feature was too agressive and can result
to too many blank draft persisted (signature, quote insertion
triggered a save as soon as user clicks on new message / reply
or forward. Now 30s autosave is only enabled when the draft
is created and logic to trigger auto save on form change has been
improved.
Finally, the ui has been revamp to improve draft display.
Currently draft are always wrapped into parent message component
that was make sens for reply/forward but now for new message.
Now that jmap-email 0.1.0 is available on pypi we install it from
this registry and remove all tweaks to install the deps from local
folder. We keep the volume override for backend services in order
to be able to work on jmap-email and test it with ease in local
development environment.
Declare the thread list as a listbox with multiselectable elements.
Now when multiselect is enable, clicking on a thread add it to the
current selection, it does not reset the selection.
Furthermore, the keyboard navigation has been improved.
Since we disabled backend i18n, template placeholders were not
translated according to the active frontend language.
We revamp the logic to delegate translation of those variables
to the frontend. Furthermore, we add a new builtin variable
that allows to bind the username into template.
Last but not least we add support of style to template
variable inline elements and we render name instead of value
into the editor.
Some strings did not use `t()`. We know use that everywhere.
In order to prevent the use of literal string as text, we enable
the eslint rule `i18next/no-literal-string`. As this rule triggers
warning for all material-icons span elements we replace all of them
by Icon component provided by UI-Kit (which was technical debt)
UI-Kit expose LaGaufreV2 component we can now remove our custom
implementation and use this component instead.
Note: There is currently a bug that prevent to close lagaufre when
we open a Dropdown menu. We implement a quick fix here that must be
removed once https://github.com/suitenumerique/integration/pull/55
will be merged
Gather all mailbox settings (accesses, templates, auto-replies,
signatures and integrations) into a setting dialog. This one is
only accessible to mailbox admin users. Furthermore a general tab
has been added to allow user to edit the mailbox sender name.
Renaming a mailbox silently no-oped whenever its Contact was missing
(`contact` is nullable and not always created): the update filtered on a
null pk and changed zero rows while still returning 200. Route the rename
through a new `Mailbox.set_display_name()` helper that creates and links a
Contact when absent, so the name is always persisted.