@aidalinfo/email-mcp 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,135 @@
1
+ # Exploitation du MCP email central
2
+
3
+ Serveur `@aidalinfo/email-mcp` en mode HTTP (`EMAIL_MCP_TRANSPORT=http`), partagé par les agents
4
+ Hermes. Ce document couvre le déploiement, les clés clientes, le coupe-circuit, le webhook Scaleway
5
+ TEM et la recette **sans envoi réel**.
6
+
7
+ > Chaque étape qui touche l'infrastructure (identité IAM Scaleway, application Coolify, DNS,
8
+ > Traefik, premier envoi réel) demande une **validation humaine séparée** avant exécution
9
+ > (issue #67, « hors périmètre »). Ce document décrit le « comment », il n'autorise rien.
10
+
11
+ ## 1. Prérequis Scaleway
12
+
13
+ 1. Domaine `pulsemyit.fr` en statut **`checked`** dans Transactional Email (projet et région choisis).
14
+ 2. **Application IAM dédiée** `email-mcp`, politique `TransactionalEmailFullAccess` limitée **à ce
15
+ seul projet**, une clé API. Elle n'est déposée que dans l'environnement du serveur (Coolify,
16
+ variable marquée secrète) — jamais dans la configuration d'un agent.
17
+
18
+ ## 2. Déploiement (Coolify)
19
+
20
+ - **Serveur** : un worker Coolify, **jamais** l'hôte de management / routage.
21
+ - **Image** : `docker build -f packages/email-mcp/Dockerfile -t email-mcp:<version> .` depuis la
22
+ racine du dépôt. Rien n'est téléchargé au démarrage ; l'image tourne en utilisateur `node`.
23
+ - **Compose** : [`deploy/compose.yaml`](../deploy/compose.yaml) — système de fichiers en lecture
24
+ seule, `cap_drop: ALL`, `no-new-privileges`, volume `/data` (registre des envois, audit,
25
+ coupe-circuit), `/config` en lecture seule (`clients.json`, `templates/`).
26
+ - **Variables obligatoires** : `SCW_SECRET_KEY`, `EMAIL_PROJECT_ID`, `EMAIL_ALLOWED_RECIPIENTS`, et
27
+ un fichier clients valide. Il en manque une → le conteneur s'arrête avec un message explicite
28
+ (`Missing required env var: …`, `Missing client keys: …`) et ne répond pas. L'expéditeur est fixé
29
+ à `noreply@pulsemyit.fr` dans l'image.
30
+ - **Santé** : `GET /health` → `{"status":"ok","service":"email-mcp","version":"…","sending":"enabled|disabled"}`.
31
+ Aucun secret, aucun identifiant de projet, aucune région.
32
+
33
+ ### Exposition publique
34
+
35
+ Tant que l'enregistrement DNS `email.mcp.pulsemyit.fr` n'est **pas créé et validé**, ne déclarez
36
+ **aucun domaine** sur l'application Coolify : le service reste joignable seulement sur le réseau
37
+ interne (`expose: 8080`, pas de `ports:`). Une fois le DNS validé (décision séparée) : domaine
38
+ `https://email.mcp.pulsemyit.fr` dans Coolify, certificat TLS par Traefik, puis recette (§ 6).
39
+
40
+ ## 3. Clés clientes
41
+
42
+ Une clé par agent (`emk_` + 256 bits aléatoires). Le serveur ne garde que son **empreinte SHA-256**.
43
+
44
+ ```bash
45
+ npx -y @aidalinfo/email-mcp keygen hermes-ops
46
+ # client key (give it to "hermes-ops" only, it is not stored anywhere):
47
+ # emk_…
48
+ # clients file entry:
49
+ # {"id":"hermes-ops","keys":["sha256:…"]}
50
+ ```
51
+
52
+ 1. Ajoutez l'entrée dans `/config/clients.json` (voir [`deploy/clients.example.json`](../deploy/clients.example.json)) :
53
+ `maxPerHour`, `maxPerDay` (ne peuvent que baisser les limites du serveur), `allowedRecipients`
54
+ (restreint l'allowlist du serveur), `templatesOnly` (routines), `disabled`.
55
+ 2. Déposez la clé dans le coffre de l'agent (secret Hermes), jamais dans un dépôt, un ticket ou un
56
+ message.
57
+ 3. Le fichier est relu à chaque modification : **pas de redémarrage**. S'il devient invalide (JSON
58
+ cassé, clé brute au lieu d'une empreinte…), **toutes** les requêtes sont refusées jusqu'à
59
+ correction, et un avertissement part sur stderr.
60
+
61
+ ### Rotation sans coupure
62
+
63
+ 1. `keygen` pour le même client → ajoutez la nouvelle empreinte **à côté** de l'ancienne (`"keys": [ancienne, nouvelle]`).
64
+ 2. Mettez la nouvelle clé dans le coffre de l'agent, vérifiez un appel (`email_config`).
65
+ 3. Retirez l'ancienne empreinte du fichier.
66
+
67
+ ### Révocation
68
+
69
+ Retirez l'empreinte (ou passez `"disabled": true`) : effet à la requête suivante. Une clé
70
+ compromise se révoque **avant** d'en émettre une nouvelle.
71
+
72
+ ## 4. Coupe-circuit
73
+
74
+ ```bash
75
+ docker exec <conteneur> touch /data/STOP # tout envoi est refusé immédiatement (aperçu et envoi)
76
+ docker exec <conteneur> rm /data/STOP # reprise
77
+ ```
78
+
79
+ `/health` passe à `"sending":"disabled"`. Pour un seul agent : `"disabled": true` dans `clients.json`.
80
+
81
+ ## 5. Webhook Scaleway TEM (suivi des livraisons)
82
+
83
+ TEM publie ses événements dans un **topic Topics and Events (SNS)** ; le serveur s'y abonne en HTTPS.
84
+
85
+ 1. `EMAIL_WEBHOOK_SECRET=$(openssl rand -hex 32)` dans l'environnement du serveur.
86
+ 2. Topics and Events : créer un topic dans la région du domaine, puis un **abonnement HTTPS** vers
87
+ `https://email.mcp.pulsemyit.fr/webhooks/tem/<EMAIL_WEBHOOK_SECRET>`. Le serveur confirme lui-même
88
+ l'abonnement, **uniquement** si l'URL de confirmation est `https://sns.mnq.<région>.scaleway.com/…`.
89
+ 3. Transactional Email → Webhooks : créer un webhook sur le domaine, avec l'ARN du topic et les
90
+ types `email_delivered`, `email_dropped`, `email_deferred`, `email_spam`,
91
+ `email_mailbox_not_found`, `email_blocklisted`.
92
+
93
+ Ce qui est gardé : le **type** et la **date** de l'événement, et seulement pour les e-mails envoyés
94
+ par ce serveur (connus du registre). Les événements d'un autre projet ou d'un autre e-mail sont
95
+ ignorés (202). Mauvais secret → 404. Les agents les lisent dans `email_status` (`deliveryEvents`).
96
+ Les suppressions (rebond dur, plainte) se lisent avec `email_suppressions` (adresses autorisées seulement).
97
+
98
+ Sans webhook, `email_status` l'indique (`"deliveryEvents": "webhook not configured"`) au lieu de
99
+ renvoyer une liste vide ambiguë.
100
+
101
+ ## 6. Recette sans envoi réel
102
+
103
+ 1. **Démarrage en échec** : lancer l'image sans `SCW_SECRET_KEY` → sortie immédiate avec
104
+ `Missing required env var: SCW_SECRET_KEY`. Sans fichier clients → `Missing client keys`.
105
+ 2. **Contrôle de déploiement** (aucun envoi : seul un aperçu est demandé, son jeton est jeté) :
106
+
107
+ ```bash
108
+ EMAIL_MCP_URL=https://email.mcp.pulsemyit.fr \
109
+ EMAIL_MCP_KEY=emk_… EMAIL_MCP_CHECK_TO=vous@pulsemyit.fr \
110
+ node packages/email-mcp/scripts/deploy-check.mjs
111
+ ```
112
+
113
+ Vérifie `/health` (et qu'il ne révèle rien), le refus sans clé et avec une mauvaise clé (401),
114
+ `initialize`, `tools/list`, `email_config` et un `email_preview` dont l'expéditeur effectif est
115
+ `noreply@pulsemyit.fr`.
116
+ 3. **Deux clients** : refaire 2. avec la clé d'un second agent ; dans le journal d'audit, chaque
117
+ entrée porte son `client`.
118
+ 4. **Coupe-circuit** : `touch /data/STOP`, refaire 2. → l'aperçu est refusé (`kill switch`) ; `rm`.
119
+ 5. Le **premier envoi réel** (allowlist réduite à une adresse de l'équipe) est une décision séparée.
120
+
121
+ ## 7. Journal d'audit
122
+
123
+ Une ligne JSON par aperçu, envoi, rejeu, refus ou échec, sur stderr et dans `EMAIL_AUDIT_LOG_FILE`
124
+ (`/data/audit.jsonl`, mode 600) : `timestamp`, `tool`, `client`, `result`, `from`, `to`, `cc`,
125
+ `subject`, `messageDigest`, `template`, `attachments` (type, taille, sha256), `temEmailIds`,
126
+ `idempotencyKey`, `errorCode`, `approvalRef`. **Jamais** de corps, de contenu ni de nom de pièce
127
+ jointe, de clé cliente ou de secret ; chaque ligne passe par le masquage central. Les refus
128
+ d'authentification sont journalisés (`tool: "http"`, `errorCode: "unauthorized"`) sans la clé reçue.
129
+
130
+ ## 8. Limites connues
131
+
132
+ - Quotas en mémoire : un redémarrage les remet à zéro (le registre empêche toujours les doublons).
133
+ - Une seule instance : le registre est un fichier local, pas de réplication.
134
+ - Pièces jointes : jointes au message (pas de lien de téléchargement temporaire), 2 Mo au total
135
+ par défaut, à ajuster à la limite de taille de TEM.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aidalinfo/email-mcp",
3
- "version": "0.1.1",
4
- "description": "Outbound email MCP server over Scaleway Transactional Email (TEM) — fixed sender from config, recipient allowlist, two-step confirmed, rate-limited and audited sends",
3
+ "version": "0.2.0",
4
+ "description": "Outbound email MCP server over Scaleway Transactional Email (TEM) — stdio or central Streamable HTTP with per-client Bearer keys, fixed sender, recipient allowlist, preview then confirmed idempotent sends, quotas, kill switch, versioned templates, controlled attachments, delivery webhook and audit",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -9,7 +9,9 @@
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
- "README.md"
12
+ "README.md",
13
+ "docs",
14
+ "deploy"
13
15
  ],
14
16
  "publishConfig": {
15
17
  "access": "public"