- Python 95.2%
- JavaScript 4.4%
- HTML 0.4%
Héron CRM
CRM mono-utilisateur intégrant IMAP, CalDAV, CardDAV et Todoist. Le front-end
est un vault Obsidian alimenté par projection depuis SQLite ; l'admin Django ne
sert plus que d'observabilité.
Stack : Django 5.2 + SQLite + gunicorn, Python 3.13, dépendances gérées avec uv.
Développement local
Prérequis : Python 3.13 + uv
cp .env.example .env
# éditer .env : HERON_USERNAME / HERON_PASSWORD / DJANGO_SECRET_KEY au minimum
uv sync
uv run pre-commit install
uv run python manage.py migrate
uv run python manage.py runserver
Ouvrir http://localhost:8000 → redirige vers l'admin Django (seule UI web ; le
front-end métier est un vault Obsidian, voir plus bas) → se connecter avec les
credentials de .env.
Tests
uv run pytest # tous les tests
uv run pytest contacts/tests/ # une seule app
uv run pytest -k carddav # un sous-ensemble par nom
Linting
uv run ruff check .
uv run ruff format .
Variables d'environnement
Voir .env.example. Les plus importantes :
HERON_USERNAME,HERON_PASSWORD— identifiants de l'utilisateur uniqueHERON_FIRSTNAME,HERON_LASTNAME,HERON_EMAIL— profilDJANGO_SECRET_KEY— clé Django (longue, aléatoire)BOOTSWATCH_THEME— thème Bootswatch de l'admin (exflatly,superhero)DATABASE_PATH— chemin SQLite (défaut :heron.sqlite3à la racine)HERON_MEDIA_ROOT— dossier des pièces jointes (défaut :media/)HERON_OBSIDIAN_VAULT_DIR— dossier du vault Obsidian, cible de la projectionHERON_SSO_ENABLED— activer le SSO YunoHost (défaut :false)
Modèle de synchronisation
- Ingestion → SQLite (sens unique, incrémental) :
imap_sync,caldav_sync,carddav_sync,todoist_sync. - Projection SQLite → vault Obsidian :
obsidian_sync(le vault est le front-end ; le corps des notes appartient à l'utilisateur, jamais écrasé). - Source de vérité des contacts/organismes : le carnet CardDAV. Les contacts ne sont plus auto-créés depuis les mails/évènements ; ces derniers les référencent par email (lien « mort » tant que le contact n'est pas dans le carnet, résolu automatiquement dès qu'il y arrive).
Ordre conseillé d'un cycle complet : carddav_sync → imap_sync → caldav_sync
→ todoist_sync → obsidian_sync.
Déploiement YunoHost
Voir le repo heron_ynh.
Déploiement manuel (uv + systemd)
Ces instructions ciblent Debian 12 / Ubuntu 24.04 avec nginx.
1. Prérequis
sudo apt install -y nginx python3-pip
# Installer uv
curl -LsSf https://astral.sh/uv/install.sh | sh
2. Utilisateur système et répertoires
sudo useradd --system --home /opt/heron --shell /usr/sbin/nologin heron
sudo mkdir -p /opt/heron /var/lib/heron/media /var/lib/heron/vault
sudo chown heron:heron /opt/heron /var/lib/heron
3. Code et dépendances
sudo -u heron git clone https://codeberg.org/ColinMaudry/heron /opt/heron
sudo -u heron /bin/bash -c "cd /opt/heron && uv python install 3.13 && uv sync --frozen --no-dev"
4. Configuration
sudo cp /opt/heron/.env.example /opt/heron/.env
sudo nano /opt/heron/.env # remplir SECRET_KEY, HERON_USERNAME, HERON_PASSWORD, etc.
# Chemins recommandés :
# DATABASE_PATH=/var/lib/heron/heron.sqlite3
# HERON_MEDIA_ROOT=/var/lib/heron/media
# HERON_OBSIDIAN_VAULT_DIR=/var/lib/heron/vault
# HERON_SSO_ENABLED=false # true uniquement si nginx injecte Remote-User
5. Migrations et statiques
sudo -u heron /bin/bash -c "cd /opt/heron && uv run python manage.py migrate --noinput"
sudo -u heron /bin/bash -c "cd /opt/heron && uv run python manage.py collectstatic --noinput"
6. Service gunicorn
Créer /etc/systemd/system/heron.service :
[Unit]
Description=Héron CRM — gunicorn
After=network.target
[Service]
User=heron
WorkingDirectory=/opt/heron
EnvironmentFile=/opt/heron/.env
ExecStart=/opt/heron/.venv/bin/python -m gunicorn heron.wsgi:application \
--bind 127.0.0.1:8000 \
--workers 2
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now heron
7. Timers de synchronisation
Créer /etc/systemd/system/heron-sync@.service :
[Unit]
Description=Héron sync — %i
After=heron.service
[Service]
User=heron
WorkingDirectory=/opt/heron
EnvironmentFile=/opt/heron/.env
ExecStart=/opt/heron/.venv/bin/python manage.py %i
Type=oneshot
Créer un timer par commande de sync (imap_sync, caldav_sync, carddav_sync,
todoist_sync, obsidian_sync) sur le modèle suivant — adapter Description,
Unit=heron-sync@<commande>.service et OnCalendar :
[Unit]
Description=Héron CardDAV sync (toutes les 15 min)
[Timer]
Unit=heron-sync@carddav_sync.service
OnCalendar=*:0/15
RandomizedDelaySec=60
Persistent=true
[Install]
WantedBy=timers.target
Cadences conseillées : imap_sync *:0/5, caldav_sync/carddav_sync/
todoist_sync/obsidian_sync *:0/15.
sudo systemctl daemon-reload
sudo systemctl enable --now \
heron-imap_sync.timer heron-caldav_sync.timer heron-carddav_sync.timer \
heron-todoist_sync.timer heron-obsidian_sync.timer
8. Nginx
Adapter nginx.conf.example (remplacer domaine et chemins) et l'activer :
sudo cp nginx.conf.example /etc/nginx/sites-available/heron
sudo nano /etc/nginx/sites-available/heron
# Remplacer heron.example.org, /chemin/vers/staticfiles, /chemin/vers/media
sudo ln -s /etc/nginx/sites-available/heron /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Chemins des statiques et media (avec la config recommandée ci-dessus) :
- Statiques :
/opt/heron/staticfiles/ - Media :
/var/lib/heron/media/
Synchronisation IMAP
Variables .env : HERON_IMAP_HOST, HERON_IMAP_PORT, HERON_IMAP_USERNAME,
HERON_IMAP_PASSWORD, HERON_IMAP_USE_SSL, HERON_IMAP_FOLDERS.
uv run python manage.py imap_sync
Idempotente : checkpoint par dossier (UIDVALIDITY + last UID). Ne crée plus de contacts — elle persiste les emails des participants et lie le mail aux contacts CardDAV déjà connus ; un mail n'est matérialisé que s'il a au moins un contact connu (non ignoré).
Synchronisation CalDAV
Variables .env : HERON_CALDAV_URL, HERON_CALDAV_USERNAME,
HERON_CALDAV_PASSWORD, HERON_CALDAV_PAST_DAYS, HERON_CALDAV_FUTURE_DAYS.
uv run python manage.py caldav_sync
Incrémentale via sync-token RFC 6578. Comme l'IMAP, elle ne crée pas de contacts : les évènements persistent les emails des participants et se lient aux contacts CardDAV connus.
Synchronisation CardDAV
Le carnet CardDAV est la source de vérité des contacts et organismes. Les
organismes sont dérivés du champ ORG des vCards ; un contact taggé de la
catégorie heron_ignore est ignoré par Héron (pas de note, ne déclenche pas la
matérialisation d'un mail).
Variables .env : HERON_CARDDAV_URL (URL de la collection carnet),
HERON_CARDDAV_USERNAME, HERON_CARDDAV_PASSWORD.
uv run python manage.py carddav_sync
Incrémentale via sync-collection (RFC 6578). Quand un contact arrive dans le carnet, les liens « morts » par email des mails/évènements existants se résolvent automatiquement.
Outil de diagnostic (lecture seule, sortie obfusquée) pour valider la connexion :
uv run python scripts/probe_carddav.py.
Bascule one-shot vers CardDAV (purge des contacts legacy auto-créés + reset
des checkpoints IMAP/CalDAV pour une re-ingestion complète) :
uv run python manage.py carddav_cutover --yes.
Projection Obsidian
Variable .env : HERON_OBSIDIAN_VAULT_DIR.
uv run python manage.py obsidian_sync
Projection à sens unique SQLite → vault (contacts, organismes, projets, évènements, mails). Le frontmatter possédé est réécrit à chaque run ; le corps des notes et les clés ajoutées à la main sont préservés. Écritures idempotentes (pas de réécriture si inchangé) et détection des fichiers de conflit Syncthing.
Synchronisation Todoist
Variables .env : HERON_TODOIST_API_TOKEN, HERON_TODOIST_PROJECT_WHITELIST.
Convention de taggage : tagger les tâches Todoist avec @<slug-organisme> (slug
visible dans l'admin Django).
uv run python manage.py todoist_sync
L'ingestion LinkedIn (extension Firefox → POST /api/linkedin/ingest) est
désactivée pour l'instant : les contacts proviennent du carnet CardDAV. La
ré-activation/adaptation est prévue ultérieurement.
Architecture
Voir docs/superpowers/specs/ pour les specs détaillées (dont le pivot Obsidian
et l'ingestion CardDAV).
Changelog
1.2.0
- Contacts et organismes sourcés depuis un carnet CardDAV (
carddav_sync) ; fin de l'auto-création depuis mails/évènements et LinkedIn (désactivée). - Mails/évènements : liens contacts par email, résolus quand le contact arrive dans le carnet ; matérialisation des mails conditionnée à un contact connu.
- Commande de bascule
carddav_cutover; détection des conflits Syncthing. - Retrait de Logseq (
logseq_sync), du statut/poubelle des contacts, de la projection photo, et deHERON_PERSONAL_DOMAINS.
1.1.0
- plus de Web UI, le contenu est projeté sur Obsidian
1.0.0
- version initiale, avec Web UI et sources des notes sur logseq