Outils pour utilisateurs

Outils du site


systeme:smtp:relais

Différences

Ci-dessous, les différences entre deux révisions de la page.

Lien vers cette vue comparative

Les deux révisions précédentesRévision précédente
Prochaine révision
Révision précédente
systeme:smtp:relais [2026/03/27 12:49] – [Prérequis] techer.charles_educ-valadon-limoges.frsysteme:smtp:relais [2026/03/28 19:23] (Version actuelle) – [utiliser LPIPE pour appeler le script] techer.charles_educ-valadon-limoges.fr
Ligne 2: Ligne 2:
 ===== Principe ===== ===== Principe =====
  
-Documenso  => Postfix local (port 25) => SMTP2Graph (localhost:2525) => Microsoft Graph API (OAuth2 Client Credentials) =>  sendMail+Application (exemple Documenso=> Postfix (port 25) => Script Python (Communication via LPIPE) => Microsoft Graph API
  
 +===== Préparation Azure AD (OAuth2) =====
  
-Autre solution :  +  * création d'une **Inscription d'applications** Entra ID 
-Postfix => Script Python (Communication via LPIPE) => Microsoft Graph API+    * Portail Azure => Entra ID 
 +    * Inscription d'applications => Nouvelle inscription 
 +    * Nom : smtp2graph-relay 
 +    * Locataire unique seulement 
 +    * S'incrire
  
-===== Prérequis =====+  * Récupérer : 
 +    * Tenant ID 
 +    * Client ID 
 +    * Ajouter un secret Client (dans Certificates & Secrets) 
 + 
 +  * Ajouter la permission Microsoft Graph : 
 +    * Autorisations d'application (et non Autorisations déléguées) :Mail.Send 
 + 
 +  * Grant admin consent. 
 + 
 +  * Adresse email 0365 utilisée pour l’envoi  afin que l'app puisse avoir le droit d’envoyer au nom de ce compte. 
 + 
 +===== Installation des prérequis ===== 
 +    * conteneur LXC : 2 Gio RAM ; 2 coeurs ; DD de 20 Gio 
 +    * modifier le fichier **/etc/apt/sources.list.d/debian.sources** pour avoir ce contenu (http://security.debian.org trixie-security remplacé par http://deb.debian.org/debian-security) 
  
 <code> <code>
-apt install mailutils libsasl2-modules +Types: deb 
-apt install python3-pip+URIs: http://deb.debian.org/debian-security 
 +Suites: trixie-security 
 +Components: contrib main 
 +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg 
 + 
 +Types: deb 
 +URIs: http://deb.debian.org/debian 
 +Suites: trixie trixie-updates 
 +Components: contrib main 
 +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
 </code> </code>
  
-visualiser logs : +  *. installer les paquets pour le script
 <code> <code>
-journalctl -u postfix -n 50+apt install mailutils libsasl2-modules 
 +apt install python3-pip
 </code> </code>
 +
 +
 ===== Script Python ===== ===== Script Python =====
  
Ligne 29: Ligne 60:
 import requests import requests
 import json import json
 +import base64
 +import traceback
 +from email.utils import getaddresses
  
 +
 +# ---------------------------------------------------
 +# CONFIG MICROSOFT GRAPH
 +# ---------------------------------------------------
 TENANT = "TON_TENANT_ID" TENANT = "TON_TENANT_ID"
 CLIENT_ID = "TON_CLIENT_ID" CLIENT_ID = "TON_CLIENT_ID"
Ligne 35: Ligne 73:
 FROM_ADDR = "ton.adresse@tondomaine.com" FROM_ADDR = "ton.adresse@tondomaine.com"
  
-# Lire message depuis stdin +DEBUG_LOG "/tmp/graph_debug.log"
-raw sys.stdin.read() +
-msg = email.message_from_string(raw)+
  
-subject = msg.get('Subject'+def debug(msg): 
-to msg.get('To')+    with open(DEBUG_LOG, "a", encoding="utf-8") as f: 
 +        f.write(msg + "\n")
  
-# Extraire contenu brut (texte uniquement pour l'instant) 
-if msg.is_multipart(): 
-    body = msg.get_payload()[0].get_payload() 
-else: 
-    body = msg.get_payload() 
  
-# Obtenir token OAuth2 +def clean_address_list(field_value): 
-token_req = requests.post+    """ 
-    f"https://login.microsoftonline.com/{TENANT}/oauth2/v2.0/token", +    Extrait correctement toutes les adresses 
-    data={ +    même si Documenso génère des listes complexes. 
-        "client_id": CLIENT_ID, +    """ 
-        "scope""https://graph.microsoft.com/.default", +    if not field_value
-        "client_secret"CLIENT_SECRET, +        return []
-        "grant_type": "client_credentials" +
-    } +
-+
-token = token_req.json()["access_token"]+
  
-# Préparer JSON Graph API +    parsed getaddresses([field_value]) 
-mail { +    cleaned = []
-    "message":+
-        "subject": subject, +
-        "body": {"contentType": "Text", "content": body}, +
-        "toRecipients": [{"emailAddress": {"address": to}}]+
-    }, +
-    "saveToSentItems": True +
-}+
  
-# Appel Graph API +    for name, addr in parsed: 
-send requests.post( +        addr addr.replace("<", "").replace(">", "").strip() 
-    f"https://graph.microsoft.com/v1.0/users/{FROM_ADDR}/sendMail", +        if addr
-    headers={ +            cleaned.append(addr)
-        "Authorization": f"Bearer {token}", +
-        "Content-Type""application/json" +
-    }, +
-    data=json.dumps(mail) +
-)+
  
-print("Graph Response:", send.status_code, send.text) +    return cleaned
-</code>+
  
-  * Rendre le script exécutable : 
  
-<code> +def flatten_body(part, text_body, html_body, attachments, inline_images): 
-chmod 755 /usr/local/bin/graph_sendmail.py +    """ 
-chown root:root /usr/local/bin/graph_sendmail.py +    Parcourt récursivement l'arbre MIME
-</code>+    """ 
 +    ctype = part.get_content_type() 
 +    disp = str(part.get("Content-Disposition", "")).lower()
  
-===== Configurer Postfix =====+    # 1. Si multipart → parcourir les sous-parties 
 +    if part.is_multipart(): 
 +        for subpart in part.get_payload(): 
 +            flatten_body(subpart, text_body, html_body, attachments, inline_images) 
 +        return
  
-==== utiliser LPIPE pour appeler le script ==== +    # 2. Corps texte 
-  * Créer le fichier **/etc/postfix/transport** :+    if ctype == "text/plain" and "attachment" not in disp: 
 +        payload part.get_payload(decode=True) 
 +        if payload: 
 +            text_body.append(payload.decode("utf-8", errors="ignore")) 
 +        return
  
-<code> +    # 3. Corps HTML 
-    lpipe:dummy +    if ctype == "text/html" and "attachment" not in disp
-*     graph+        payload = part.get_payload(decode=True) 
-</code>+        if payload
 +            html_body.append(payload.decode("utf-8", errors="ignore")) 
 +        return
  
-  * compiler+    # 4. Images inline (multipart/related) 
 +    if "inline" in disp and ctype.startswith("image/"): 
 +        cid = part.get("Content-ID", "").strip("<>"
 +        payload = part.get_payload(decode=True) 
 +        if payload and cid: 
 +            inline_images.append({ 
 +                "@odata.type": "#microsoft.graph.fileAttachment", 
 +                "name": cid, 
 +                "contentId": cid, 
 +                "isInline": True, 
 +                "contentBytes": base64.b64encode(payload).decode() 
 +            }) 
 +        return
  
-<code> +    # 5. Attachments 
-postmap /etc/postfix/transport +    if "attachment" in disp or part.get_filename(): 
-</code>+        filename = part.get_filename() 
 +        payload = part.get_payload(decode=True) 
 +        if filename and payload: 
 +            attachments.append({ 
 +                "@odata.type": "#microsoft.graph.fileAttachment", 
 +                "name": filename, 
 +                "contentBytes": base64.b64encode(payload).decode() 
 +            }) 
 +        return
  
-   * vérifier 
  
-<code> +# --------------------------------------------------- 
-postmap -s /etc/postfix/transport +# MAIN 
-</code>    +--------------------------------------------------- 
 +try: 
 +    raw = sys.stdin.read() 
 +    msg = email.message_from_string(raw)
  
-  * vérifier que le transport existe+    debug("=== Nouveau mail reçu ===")
  
-<code> +    subject = msg.get("Subject", "(no subject)") 
-#postconf -M +    debug("Sujet: " + subject)
-... +
-graph unix - n n - - pipe +
-</code>+
  
-  * Configurer Postfix en modifiant le fichier /etc/postfix/main.cf :+    # Extraction des adresses MIME (Documenso friendly) 
 +    to_list = clean_address_list(msg.get("To")) 
 +    cc_list = clean_address_list(msg.get("Cc")) 
 +    bcc_list = clean_address_list(msg.get("Bcc")) 
 +    reply_to_list = clean_address_list(msg.get("Reply-To"))
  
-<code> +    debug("To: " + str(to_list)) 
-myhostname = postfix-relay.lan +    debug("Cc: " + str(cc_list)) 
-mydomain = lan +    debug("Bcc: " + str(bcc_list)) 
-mydestination = +    debug("Reply-To: " + str(reply_to_list))
-relayhost =+
  
-transport_maps hash:/etc/postfix/transport+    # Corps du message 
 +    text_body [] 
 +    html_body = [] 
 +    attachments = [] 
 +    inline_images = []
  
-lpipe_destination_recipient_limit = 1+    flatten_body(msg, text_body, html_body, attachments, inline_images)
  
-</code>+    # Déterminer le corps principal HTML 
 +    final_html = ""
  
-  * Config /etc/postfix/master.cf pour appeler le script Python en ajoutant :+    if html_body: 
 +        final_html = "\n".join(html_body) 
 +    elif text_body: 
 +        final_html = "<pre>" + "\n".join(text_body) + "</pre>" 
 +    else: 
 +        final_html = "<p>(vide)</p>"
  
-<code> +    debug("Corps HTML (200 chars): " + final_html[:200]) 
-graph   unix  -           pipe +    debug("Attachments: " + str(len(attachments))) 
-  flags=Fq. +    debug("Inline images: " + str(len(inline_images)))
-  user=nobody +
-  argv=/scripts/graph_sendmail.py +
-</code>+
  
 +    # ---------------------------------------------------
 +    # TOKEN OAUTH2
 +    # ---------------------------------------------------
 +    debug("Before OAuth token request")
  
-===== Préparation Azure AD (OAuth2=====+    token_res requests.post( 
 +        f"https://login.microsoftonline.com/{TENANT}/oauth2/v2.0/token", 
 +        data=
 +            "client_id": CLIENT_ID, 
 +            "scope": "https://graph.microsoft.com/.default", 
 +            "client_secret": CLIENT_SECRET, 
 +            "grant_type": "client_credentials" 
 +        } 
 +    )
  
-  * création d'une **Inscription d'applications** Entra ID : +    debug("Token response" + token_res.text)
-    * Portail Azure => Entra ID +
-    * Inscription d'applications => Nouvelle inscription +
-    * Nom : smtp2graph-relay +
-    * Locataire unique seulement +
-    * S'incrire+
  
-  * Récupérer : +    token_json = token_res.json() 
-    * Tenant ID +    if "access_token" not in token_json: 
-    * Client ID +        raise Exception("OAuth2 failed: " + token_res.text)
-    * Ajouter un secret Client (dans Certificates & Secrets)+
  
-  * Ajouter la permission Microsoft Graph : +    token = token_json["access_token"]
-    * Application permission :Mail.Send+
  
-  * Grant admin consent.+    # --------------------------------------------------- 
 +    # CONSTRUCTION JSON GRAPH 
 +    # --------------------------------------------------- 
 +    def addr_obj(addr): 
 +        return {"emailAddress": {"address": addr}}
  
-  * Adresse email 0365 utilisée pour l’envoi  afin que l'app puisse avoir le droit d’envoyer au nom de ce compte.+    message = { 
 +        "subject": subject, 
 +        "body":
 +            "contentType": "HTML", 
 +            "content": final_html 
 +        }, 
 +        "toRecipients": [addr_obj(a) for a in to_list], 
 +        "ccRecipients": [addr_obj(a) for a in cc_list], 
 +        "bccRecipients": [addr_obj(a) for a in bcc_list], 
 +        "attachments": attachments + inline_images 
 +    }
  
-===== Installation des prérequis ===== +    if reply_to_list
-    * conteneur LXC 2 Gio RAM ; 2 coeurs ; DD de 20 Gio +        message["replyTo"] = [addr_obj(afor a in reply_to_list]
-    * modifier le fichier **/etc/apt/sources.list.d/debian.sources** pour avoir ce contenu (http://security.debian.org trixie-security remplacé par http://deb.debian.org/debian-security+
  
-<code> +    mail_json = { 
-Typesdeb +        "message"message, 
-URIshttp://deb.debian.org/debian-security +        "saveToSentItems"True 
-Suites: trixie-security +    }
-Components: contrib main +
-Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg+
  
-Types: deb +    debug("Mail JSON ready")
-URIs: http://deb.debian.org/debian +
-Suites: trixie trixie-updates +
-Components: contrib main +
-Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg +
-</code>+
  
-   * ajouter les dépôts+    # --------------------------------------------------- 
 +    # ENVOI VIA GRAPH API 
 +    # --------------------------------------------------- 
 +    graph_res = requests.post( 
 +        f"https://graph.microsoft.com/v1.0/users/{FROM_ADDR}/sendMail", 
 +        headers={ 
 +            "Authorization": f"Bearer {token}", 
 +            "Content-Type": "application/json" 
 +        }, 
 +        data=json.dumps(mail_json) 
 +    )
  
-<code> +    debug("Graph sendMail responsegraph_res.text) 
-# Add Docker's official GPG key: +    debug("Status" + str(graph_res.status_code))
-apt update +
-apt install ca-certificates curl +
-install -m 0755 -d /etc/apt/keyrings +
-curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc +
-chmod a+r /etc/apt/keyrings/docker.asc +
- +
-# Add the repository to Apt sources: +
-tee /etc/apt/sources.list.d/docker.sources <<EOF +
-Types: deb +
-URIs: https://download.docker.com/linux/debian +
-Suites: $(. /etc/os-release && echo "$VERSION_CODENAME") +
-Components: stable +
-Signed-By: /etc/apt/keyrings/docker.asc +
-EOF+
  
 +except Exception as e:
 +    debug("SCRIPT ERROR: " + str(e))
 +    debug(traceback.format_exc())
 +    
 </code> </code>
  
-   mettre à jour+  Rendre le script exécutable :
  
 <code> <code>
-apt update && apt upgrade -y+chmod 755 /usr/local/bin/graph_sendmail.py 
 +chown root:root /usr/local/bin/graph_sendmail.py
 </code> </code>
  
 +===== Configurer Postfix =====
  
-  installer Docker+==== utiliser LPIPE pour appeler le script ==== 
 +  Créer le fichier **/etc/postfix/transport** :
  
 <code> <code>
-apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin+*     graph:
 </code> </code>
  
-  * Vérifier l'installation:+  * compiler
  
 <code> <code>
-docker --version +postmap /etc/postfix/transport
-docker compose version+
 </code> </code>
  
-===== Postfix & 1smtp2graph =====+   * vérifier
  
-  * utilisation d'un Docker compose+<code> 
 +postmap -s /etc/postfix/transport 
 +</code>    
  
-<code file docker-compose.yml> +  * vérifier que le transport existe
-services: +
-  postfix: +
-    image: boky/postfix +
-    container_name: postfix-relay +
-    restart: unless-stopped +
-    environment: +
-      - ALLOW_EMPTY_SENDER=true +
-    volumes: +
-      - ./postfix/main.cf:/etc/postfix/main.cf +
-      - ./postfix/master.cf:/etc/postfix/master.cf +
-    network_mode: "host" +
- +
-  smtp2graph: +
-    image: ghcr.io/microsoft/smtp-oauth2-proxy:latest +
-    container_name: smtp2graph +
-    restart: unless-stopped +
-    environment: +
-      PROXY_LISTEN_ADDRESS: "0.0.0.0:2525" +
-      OAUTH2_TENANT_ID: "TON_TENANT_ID" +
-      OAUTH2_CLIENT_ID: "TON_CLIENT_ID" +
-      OAUTH2_CLIENT_SECRET: "TON_CLIENT_SECRET" +
-      OAUTH2_SENDER: "ton.adresse@tondomaine.com" +
-    ports: +
-      - "2525:2525" +
- +
- +
-</code> +
- +
- +
-==== postfix/main.cf (spécial "transport smtp2graph") ====+
  
 <code> <code>
-Postfix minimal relay to smtp2graph +#postconf -M 
-myhostname = postfix-relay.lan +... 
-mydomain = lan +graph unix - n n - - pipe
-myorigin = /etc/mailname +
-mydestination = +
-relayhost = [127.0.0.1]:2525 +
- +
-smtp_tls_security_level = may +
-smtp_sasl_auth_enable = no +
- +
-# Generic mapping (optionnel pour réécrire root@...) +
-smtp_generic_maps = hash:/etc/postfix/generic+
 </code> </code>
  
-==== /etc/postfix/generic====+  * Configurer Postfix en modifiant le fichier /etc/postfix/main.cf pour ajouter :
  
 <code> <code>
-root@postfix-relay.lan  ton.adresse@tondomaine.com +#myhostname = postfix-relay.lan 
-</code>+#mydomain = lan 
 +#mydestination = 
 +#relayhost =
  
 +# Postfix doit écouter sur toutes les interfaces
 +inet_interfaces = all
  
-Puis : +# Autoriser l’IP des serveurs client  
-<code> +mynetworks = 127.0.0.0/8 9.9.9.0/24
-postmap /etc/postfix/generic +
-</code>+
  
-==== postfix/master.cf ====+# Postfix doit accepter les mails : 
 +smtpd_recipient_restrictions permit_mynetworks, reject_unauth_destination
  
-<code> 
-smtp      inet  n                               smtpd 
-pickup    unix  n                         60      pickup 
-cleanup   unix  n                               cleanup 
-qmgr      unix  n                   300           qmgr 
-rewrite   unix  -                               trivial-rewrite 
-bounce    unix  -                               bounce 
-defer     unix  -                               bounce 
-trace     unix  -                               bounce 
-verify    unix  -                               verify 
-flush     unix  n                               flush 
-proxymap  unix  -                               proxymap 
-proxywrite unix -                               proxymap 
-smtp      unix  -                               smtp 
-relay     unix  -                               smtp 
-discard   unix  -                               discard 
-</code> 
  
-On laisse Postfix en mode simple (no chroot) pour éviter les soucis SASL/TLS.+transport_maps = hash:/etc/postfix/transport
  
-==== Configuration Documenso ====+lpipe_destination_recipient_limit 1
  
-Dans .env : 
-<code> 
-NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth" 
-NEXT_PRIVATE_SMTP_HOST="127.0.0.1" 
-NEXT_PRIVATE_SMTP_PORT="25" 
-NEXT_PRIVATE_SMTP_SECURE="false" 
-NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS="true" 
-NEXT_PRIVATE_SMTP_FROM_ADDRESS="ton.adresse@tondomaine.com" 
-NEXT_PRIVATE_SMTP_FROM_NAME="Documenso" 
 </code> </code>
-===== Créer un compte Resend ===== 
-Lien : https://resend.com/ 
  
- +  Config /etc/postfix/master.cf pour appeler le script Python en ajoutant à la fin du fichier :
-  générer une clé d'API +
- +
-===== Créer un compte SendGrid ===== +
-Lien : https://login.sendgrid.com/ +
- +
- +
-  * générer une clé d'API +
- +
-===== Installer et confoigurer Postfix =====+
  
 <code> <code>
-apt update +graph   unix    n         pipe 
-apt install postfix mailutils libsasl2-modules+  flags=Fq. 
 +  user=nobody 
 +  argv=/scripts/graph_sendmail.py
 </code> </code>
  
-  * Créer le fichier /etc/postfix/sasl_passwd +  * relancer Postfix :
  
 <code> <code>
-[smtp.sendgrid.net]:587 apikey:re_123456789abcdef+systemctl restart postfix
 </code> </code>
  
-<code> +==== Tester ====
-postmap /etc/postfix/sasl_passwd +
-chmod 600 /etc/postfix/sasl_passwd* +
-</code>+
  
-  * copier les modules SASL dans le CHROOT+  * envoyer un message
  
 <code> <code>
-mkdir -p /var/spool/postfix/usr/lib/x86_64-linux-gnu/sasl2/ +echo "hello" | mail -s "test mime" -A document.pdf charles@gmail.xxx
-cp -a /usr/lib/x86_64-linux-gnu/sasl2/* /var/spool/postfix/usr/lib/x86_64-linux-gnu/sasl2/+
 </code> </code>
-===== Configurer Postfix en “SMTP relay” vers Microsoft 365 ===== 
  
-  * éditer **/etc/postfix/main.cf** :+  * vérifier dans les logs générés par le script qu'il n'y a pas d'erreur (le statut doit être 202) 
  
 <code> <code>
-nano /etc/postfix/main.cf+cat /tmp/graph_debug.log
 </code> </code>
  
-  * Ajoute / remplace :+  * vérifier dans les logs de Potsfix qu'il n'y a pas d'erreur 
  
 <code> <code>
-relayhost = [smtp.sendgrid.net]:587 +journalctl -u postfix -n 50
- +
-smtp_sasl_auth_enable = yes +
-smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd +
-smtp_sasl_security_options = noanonymous +
-smtp_use_tls = yes +
-smtp_tls_security_level = encrypt +
-smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt +
- +
- +
-inet_interfaces = all +
-inet_protocols = ipv4+
 </code> </code>
  
-<code> + 
-systemctl restart postfix +
-</code>+
systeme/smtp/relais.1774612150.txt.gz · Dernière modification : 2026/03/27 12:49 de techer.charles_educ-valadon-limoges.fr