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:22] – [Script Python] 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 
 +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 28: 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 34: 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 +x /usr/local/bin/graph_sendmail.py +    """ 
-chmod 755 /usr/local/bin/graph_sendmail.py +    Parcourt récursivement l'arbre MIME
-chown root:postfix /usr/local/bin/graph_sendmail.py +    """ 
-</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
-  * compiler+            html_body.append(payload.decode("utf-8", errors="ignore")) 
 +        return
  
-<code> +    # 4. Images inline (multipart/related) 
-postmap /etc/postfix/transport +    if "inline" in disp and ctype.startswith("image/"): 
-</code>+        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
  
-   * vérifier +    # 5. Attachments 
-<code> +    if "attachment" in disp or part.get_filename(): 
-postmap -s /etc/postfix/transport +        filename = part.get_filename() 
-</code>    +        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
  
-  * Configurer Postfix en modifiant le fichier /etc/postfix/main.cf : 
  
-<code> +# --------------------------------------------------- 
-myhostname = postfix-relay.lan +# MAIN 
-mydomain = lan +# --------------------------------------------------
-mydestination +try: 
-relayhost =+    raw sys.stdin.read() 
 +    msg email.message_from_string(raw)
  
-transport_maps hash:/etc/postfix/transport+    debug("=== Nouveau mail reçu ===")
  
-lpipe_destination_recipient_limit 1+    subject msg.get("Subject", "(no subject)"
 +    debug("Sujet: " + subject)
  
-</code>+    # 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"))
  
-  * Config /etc/postfix/master.cf pour appeler le script Python en ajoutant :+    debug("To" + str(to_list)) 
 +    debug("Cc: " + str(cc_list)) 
 +    debug("Bcc: " + str(bcc_list)) 
 +    debug("Reply-To: " + str(reply_to_list))
  
-<code> +    # Corps du message 
-graph   unix  -           pipe +    text_body [] 
-  flags=Fq. +    html_body [] 
-  user=postfix +    attachments [] 
-  argv=/scripts/graph_sendmail.py +    inline_images = []
-</code>+
  
 +    flatten_body(msg, text_body, html_body, attachments, inline_images)
  
-===== Préparation Azure AD (OAuth2) =====+    # Déterminer le corps principal HTML 
 +    final_html ""
  
-  * création d'une **Inscription d'applications** Entra ID +    if html_body
-    * Portail Azure => Entra ID +        final_html "\n".join(html_body) 
-    * Inscription d'applications => Nouvelle inscription +    elif text_body: 
-    * Nom smtp2graph-relay +        final_html "<pre>" + "\n".join(text_body) + "</pre>" 
-    * Locataire unique seulement +    else
-    * S'incrire+        final_html = "<p>(vide)</p>"
  
-  * Récupérer +    debug("Corps HTML (200 chars)" + final_html[:200]) 
-    * Tenant ID +    debug("Attachments: " + str(len(attachments))) 
-    * Client ID +    debug("Inline images: " + str(len(inline_images)))
-    * Ajouter un secret Client (dans Certificates & Secrets)+
  
-  * Ajouter la permission Microsoft Graph : +    # --------------------------------------------------- 
-    * Application permission :Mail.Send+    # TOKEN OAUTH2 
 +    # --------------------------------------------------- 
 +    debug("Before OAuth token request")
  
-  * Grant admin consent.+    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" 
 +        } 
 +    )
  
-  * Adresse email 0365 utilisée pour l’envoi  afin que l'app puisse avoir le droit d’envoyer au nom de ce compte.+    debug("Token response: " + token_res.text)
  
-===== Installation des prérequis ===== +    token_json token_res.json() 
-    * conteneur LXC 2 Gio RAM ; 2 coeurs ; DD de 20 Gio +    if "access_token" not in token_json
-    * 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+        raise Exception("OAuth2 failed" + token_res.text)
  
-<code> +    token = token_json["access_token"]
-Types: deb +
-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 +    # CONSTRUCTION JSON GRAPH 
-Suites: trixie trixie-updates +    # --------------------------------------------------- 
-Components: contrib main +    def addr_obj(addr): 
-Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg +        return {"emailAddress": {"address": addr}}
-</code>+
  
-   * ajouter les dépôts+    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 
 +    }
  
-<code> +    if reply_to_list
-# Add Docker's official GPG key+        message["replyTo"] = [addr_obj(a) for in reply_to_list]
-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: +    mail_json = { 
-tee /etc/apt/sources.list.d/docker.sources <<EOF +        "message"message, 
-Typesdeb +        "saveToSentItems": True 
-URIs: https://download.docker.com/linux/debian +    }
-Suites: $(. /etc/os-release && echo "$VERSION_CODENAME"+
-Components: stable +
-Signed-By/etc/apt/keyrings/docker.asc +
-EOF+
  
-</code>+    debug("Mail JSON ready")
  
-   * mettre à jour+    # --------------------------------------------------- 
 +    # 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 response: " + graph_res.text) 
-apt update && apt upgrade -y +    debug("Status: " + str(graph_res.status_code))
-</code>+
  
- +except Exception as e: 
-  * installer Docker +    debug("SCRIPT ERROR: " + str(e)) 
- +    debug(traceback.format_exc()) 
-<code> +    
-apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin+
 </code> </code>
  
-  * Vérifier l'installation:+  * Rendre le script exécutable :
  
 <code> <code>
-docker --version +chmod 755 /usr/local/bin/graph_sendmail.py 
-docker compose version+chown root:root /usr/local/bin/graph_sendmail.py
 </code> </code>
  
-===== Postfix & 1smtp2graph =====+===== Configurer Postfix =====
  
-  * utilisation d'un Docker compose +==== utiliser LPIPE pour appeler le script ==== 
- +  * Créer le fichier **/etc/postfix/transport** :
-<code file docker-compose.yml> +
-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 +*     graph:
-myhostname = postfix-relay.lan +
-mydomain = lan +
-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====+  * compiler
  
 <code> <code>
-root@postfix-relay.lan  ton.adresse@tondomaine.com+postmap /etc/postfix/transport
 </code> </code>
  
 +   * vérifier
  
-Puis : 
 <code> <code>
-postmap /etc/postfix/generic +postmap -s /etc/postfix/transport 
-</code>+</code>    
  
-==== postfix/master.cf ====+  * vérifier que le transport existe
  
 <code> <code>
-smtp      inet  n             n                   smtpd +#postconf -M 
-pickup    unix  n                         60      pickup +... 
-cleanup   unix              y                   cleanup +graph unix - n n - - pipe
-qmgr      unix        -             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> </code>
  
-On laisse Postfix en mode simple (no chroot) pour éviter les soucis SASL/TLS.+  * Configurer Postfix en modifiant le fichier /etc/postfix/main.cf pour ajouter :
  
-==== Configuration Documenso ==== 
- 
-Dans .env : 
 <code> <code>
-NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth" +#myhostname postfix-relay.lan 
-NEXT_PRIVATE_SMTP_HOST="127.0.0.1" +#mydomain lan 
-NEXT_PRIVATE_SMTP_PORT="25" +#mydestination 
-NEXT_PRIVATE_SMTP_SECURE="false" +#relayhost =
-NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS="true" +
-NEXT_PRIVATE_SMTP_FROM_ADDRESS="ton.adresse@tondomaine.com" +
-NEXT_PRIVATE_SMTP_FROM_NAME="Documenso" +
-</code> +
-===== Créer un compte Resend ===== +
-Lien : https://resend.com/+
  
 +# Postfix doit écouter sur toutes les interfaces
 +inet_interfaces = all
  
-  * générer une clé d'API+# Autoriser l’IP des serveurs client  
 +mynetworks = 127.0.0.0/8 9.9.9.0/24
  
-===== Créer un compte SendGrid ===== +# Postfix doit accepter les mails : 
-Lien : https://login.sendgrid.com/+smtpd_recipient_restrictions permit_mynetworks, reject_unauth_destination
  
  
-  * générer une clé d'API+transport_maps = hash:/etc/postfix/transport
  
-===== Installer et confoigurer Postfix =====+lpipe_destination_recipient_limit 1
  
-<code> 
-apt update 
-apt install postfix mailutils libsasl2-modules 
 </code> </code>
  
-  * Créer le fichier /etc/postfix/sasl_passwd +  * Config /etc/postfix/master.cf pour appeler le script Python en ajoutant à la fin du fichier :
  
 <code> <code>
-[smtp.sendgrid.net]:587 apikey:re_123456789abcdef+graph   unix  -           pipe 
 +  flags=Fq. 
 +  user=nobody 
 +  argv=/scripts/graph_sendmail.py
 </code> </code>
 +
 +  * relancer Postfix :
  
 <code> <code>
-postmap /etc/postfix/sasl_passwd +systemctl restart postfix
-chmod 600 /etc/postfix/sasl_passwd*+
 </code> </code>
  
-  copier les modules SASL dans le CHROOT+==== Tester ==== 
 + 
 +  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.1774610524.txt.gz · Dernière modification : 2026/03/27 12:22 de techer.charles_educ-valadon-limoges.fr