@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.
- package/README.md +135 -65
- package/deploy/clients.example.json +4 -0
- package/deploy/compose.yaml +47 -0
- package/deploy/templates/rapport-hebdo@1.json +13 -0
- package/dist/attachments.js +203 -0
- package/dist/audit.js +4 -31
- package/dist/clients.js +144 -0
- package/dist/config.js +30 -20
- package/dist/errors.js +3 -1
- package/dist/http.js +131 -0
- package/dist/index.js +63 -14
- package/dist/ledger.js +128 -0
- package/dist/quota.js +77 -0
- package/dist/redact.js +23 -0
- package/dist/tem-client.js +5 -0
- package/dist/templates.js +118 -0
- package/dist/tools.js +349 -90
- package/dist/webhook.js +67 -0
- package/docs/exploitation.md +135 -0
- package/package.json +5 -3
|
@@ -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.
|
|
4
|
-
"description": "Outbound email MCP server over Scaleway Transactional Email (TEM) —
|
|
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"
|