SnappyMail CalDAV : notre fork conforme aux standards, testé sur Cyrus IMAP
Nous exploitons des messageries Cyrus IMAP avec le webmail SnappyMail. Il manquait l’agenda dans le navigateur. Un plugin existait — écrit pour un hébergeur précis, inutilisable ailleurs. Nous l’avons forké, remis d’aplomb sur les standards, et publié.
Le point de départ
Le plugin CalDAV Auto publié par Mailbux fait ce qu’il annonce… sur l’infrastructure de son auteur. Ailleurs, le symptôme est déroutant : l’agenda s’affiche, mais reste définitivement vide, sans le moindre message d’erreur.
La cause n’est pas un bug isolé, c’est une série d’hypothèses codées en dur :
- le nom d’hôte du fournisseur est écrit dans le code ;
- l’URL d’agenda est déduite en réécrivant
/dav/card/en/dav/cal/— le schéma d’URL de ce fournisseur, pas celui des serveurs DAV en général ; - tous les chemins d’erreur retournent en silence, ce qui rend un agenda cassé indiscernable d’un agenda vide.
Cyrus, lui, sert /dav/calendars/user/<user>/<collection>, découvrable par la propriété standard calendar-home-set (RFC 4791). Rien d’exotique : c’est la disposition de la plupart des serveurs conformes.
L’URL est une configuration, pas du code
Le changement structurant. L’URL vient désormais de la page de réglages du plugin, sous forme de gabarit avec les substitutions {user}, {email}, {login} et {domain} :
| Réglage | Exemple |
|---|---|
| Gabarit d’URL CalDAV | https://dav.example.com/dav/calendars/user/{user}/Default/ |
| Domaine DAV par défaut | example.com — les adresses de ce domaine utilisent la partie locale seule ; vide = toujours l’adresse complète |
| Compléter les participants depuis l’annuaire | voir plus bas |
Le domaine par défaut correspond au virtdomains: userid de Cyrus : une boîte du domaine par défaut s’adresse par sa partie locale, les autres par l’adresse complète.
Point de conception délibéré : il n’y a aucune valeur par défaut. Sans gabarit configuré, le plugin l’écrit dans le journal et s’abstient, plutôt que d’inventer une URL qui ne peut pas fonctionner. Les noms de collection ne sont plus mis en minuscules non plus — dans une URL, ils sont sensibles à la casse.
Laisser le gabarit vide fait dériver l’URL d’agenda des réglages du plugin CardDAV, pour les déploiements où les deux partagent la même racine DAV.
Ce qui était cassé
Les ressources statiques ne se chargeaient pas. Le plugin chargeait sa copie de FullCalendar depuis ?/Plugins/<plugin>/<fichier>. Ce n’est pas une route pour les fichiers d’un plugin : ServiceActions::ServicePlugins() ignore tout ce qui suit /?/Plugins/ et renvoie le bundle JS compilé des plugins. Le navigateur ré-exécutait donc le plugin au lieu de charger la bibliothèque, et window.FullCalendar n’était jamais défini. Le fichier est maintenant servi par un part hook dédié, avec liste blanche de noms de fichiers.
Le repli CDN ne pouvait pas fonctionner. En cas d’échec, le code amont allait chercher FullCalendar sur un CDN public — ce qu’une politique script-src 'self' bloque par construction. Ce repli ne pouvait jamais aboutir et ne faisait que masquer l’erreur réelle : il a été supprimé. Dans la même veine, les attributs onclick en ligne, qui violent script-src-attr sous une CSP à nonce, ont été remplacés par les écouteurs d’événements que le code attachait déjà.
Les événements récurrents ne se répétaient pas. RRULE n’était jamais développée et les lignes pliées jamais dépliées : une série récurrente n’apparaissait qu’à sa date de début d’origine. L’analyse passe désormais par la bibliothèque Sabre VObject déjà livrée avec SnappyMail — pas de dépendance supplémentaire — et les occurrences sont développées sur une fenêtre autour de la date courante. Les événements non récurrents contournent le développement, pour ne pas tronquer l’historique.
Les rappels ne rappelaient rien. VALARM était purement ignorée, et le contrôle « rappel » de la boîte de dialogue se contentait d’ajouter un marqueur textuel à la description — sans jamais transmettre la valeur au serveur. Les alarmes sont désormais résolues en heures absolues (déclencheurs DATE-TIME, et durées relatives à START ou END) et présentées via l’API Notification, avec une bannière dans la page en repli. Les rappels posés ici sont écrits comme de vrais composants VALARM : les autres clients CalDAV les respectent aussi.
Et surtout : les échecs sont désormais rapportés. C’est ce qui transforme un diagnostic d’une demi-journée en une ligne de journal.
Inviter des participants — sans construire d’iTIP
La boîte de dialogue d’événement comporte un champ Inviter, acceptant plusieurs adresses séparées par virgules ou points-virgules, sous la forme nom@example.com ou Nom <nom@example.com>. L’enregistrement ajoute un ORGANIZER et un ATTENDEE par adresse.
Les invitations ne sont ni construites ni envoyées par le plugin. Sous RFC 6638, l’ordonnancement appartient au serveur CalDAV : il voit les participants sur l’événement stocké, leur écrit, et les marque d’un SCHEDULE-STATUS. Vérifié contre Cyrus IMAP, qui renvoie SCHEDULE-STATUS=1.1 pour une invitation effectivement dépêchée.
C’est un choix d’architecture autant que de sécurité : le plugin n’a aucun chemin SMTP, ne compose aucun message, n’en met aucun en file. Côté serveur, cela suppose l’ordonnancement activé — sur Cyrus, caldav_allowscheduling et imipnotifier pour la remise par courriel.
Modifier un événement enregistré renvoie la liste des invités : ajouter ou retirer quelqu’un l’invite ou le désinvite. Le SEQUENCE est incrémenté dès que l’horaire ou la liste change — c’est ce qui indique aux clients des participants d’accepter la mise à jour. Les réponses mettent à jour la liste au fil des retours.
Le pendant côté destinataire — accepter ou refuser une invitation reçue — fait l’objet d’un plugin distinct : Meeting Invitations.
Complétion des participants : le réglage à comprendre avant de déployer
Le champ Inviter complète à la saisie, depuis les mêmes sources que SnappyMail utilise pour adresser un message. Jusqu’où il va est une décision de déploiement, exposée en réglage :
- Activé (par défaut) — toutes les sources disponibles, y compris un annuaire LDAP d’entreprise si un plugin de suggestions en fournit un. Un organisateur invite n’importe quel collègue en tapant une partie de son nom.
- Désactivé — le carnet d’adresses personnel de l’organisateur, rien d’autre.
Le défaut suppose ce qu’un déploiement correct fournit : l’annuaire configuré appartient à une seule organisation. Un hébergeur doit donner à chaque client sa propre racine d’annuaire plutôt que de pointer tout le monde vers une racine partagée.
À désactiver là où cette hypothèse ne tient pas — un annuaire réellement partagé entre clients sans lien entre eux — car la chaîne de suggestions est globale : la complétion permettrait alors à un utilisateur d’énumérer les adresses des autres.
À noter : le plugin de suggestions LDAP de SnappyMail se configure par instance, pas par domaine ; un seul base_dn sert tous les comptes de l’instance. Donner des racines séparées aux clients implique donc des instances SnappyMail séparées, ou un plugin de suggestions qui restreint la recherche au domaine du compte.
Le réglage ne change que ce qui est proposé : un organisateur peut toujours taper une adresse à la main, et la remise reste la décision du serveur CalDAV.
Licence et attribution
Le fork conserve la licence du plugin d’origine (© 2025 Mailbux) et garde le README amont intact, en dessous du nôtre. Un fork correctif ne réécrit pas l’histoire du travail qu’il reprend : il documente ce qu’il change et pourquoi.
→ https://github.com/FathiBenNasr/SnappyMail-CalDAV-Plugin
Vous exploitez une messagerie Cyrus, Postfix ou Sendmail et vous voulez un webmail avec agenda et contacts, hébergé chez vous ? Découvrez nos services ou parlons-en. Et suivez-nous sur GitHub.