systeme:smtp:relais
Différences
Ci-dessous, les différences entre deux révisions de la page.
| Les deux révisions précédentesRévision précédenteProchaine révision | Révision précédente | ||
| systeme:smtp:relais [2026/03/27 12:25] – [utiliser LPIPE pour appeler le script] techer.charles_educ-valadon-limoges.fr | systeme: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 | + | Application (exemple |
| + | ===== Préparation Azure AD (OAuth2) ===== | ||
| - | Autre solution | + | * création d'une **Inscription d' |
| - | Postfix | + | * Portail Azure => Entra ID |
| + | * Inscription d' | ||
| + | * Nom : smtp2graph-relay | ||
| + | * Locataire unique seulement | ||
| + | * S' | ||
| - | ===== Prérequis | + | * Récupérer : |
| + | * Tenant ID | ||
| + | * Client ID | ||
| + | * Ajouter un secret Client (dans Certificates & Secrets) | ||
| + | |||
| + | * Ajouter la permission Microsoft Graph : | ||
| + | * Autorisations d' | ||
| + | |||
| + | * Grant admin consent. | ||
| + | |||
| + | * Adresse email 0365 utilisée pour l’envoi | ||
| + | |||
| + | ===== Installation des prérequis | ||
| + | * conteneur LXC : 2 Gio RAM ; 2 coeurs ; DD de 20 Gio | ||
| + | * modifier le fichier **/ | ||
| < | < | ||
| - | apt install mailutils libsasl2-modules | + | Types: deb |
| + | URIs: http:// | ||
| + | Suites: trixie-security | ||
| + | Components: contrib main | ||
| + | Signed-By: / | ||
| + | |||
| + | Types: deb | ||
| + | URIs: http:// | ||
| + | Suites: trixie trixie-updates | ||
| + | Components: contrib main | ||
| + | Signed-By: / | ||
| </ | </ | ||
| - | visualiser logs : | + | *. installer les paquets pour le script |
| < | < | ||
| - | journalctl | + | apt install mailutils libsasl2-modules |
| + | apt install python3-pip | ||
| </ | </ | ||
| + | |||
| + | |||
| ===== 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 = " | TENANT = " | ||
| CLIENT_ID = " | CLIENT_ID = " | ||
| Ligne 34: | Ligne 73: | ||
| FROM_ADDR = " | FROM_ADDR = " | ||
| - | # Lire message depuis stdin | + | DEBUG_LOG |
| - | raw = sys.stdin.read() | + | |
| - | msg = email.message_from_string(raw) | + | |
| - | subject = msg.get(' | + | def debug(msg): |
| - | to = msg.get(' | + | with open(DEBUG_LOG, |
| + | f.write(msg + " | ||
| - | # Extraire contenu brut (texte uniquement pour l' | ||
| - | 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( | + | "" |
| - | | + | |
| - | | + | même si Documenso génère des listes complexes. |
| - | " | + | """ |
| - | "scope": "https:// | + | if not field_value: |
| - | " | + | |
| - | | + | |
| - | } | + | |
| - | ) | + | |
| - | token = token_req.json()[" | + | |
| - | # Préparer JSON Graph API | + | parsed |
| - | mail = { | + | |
| - | | + | |
| - | " | + | |
| - | " | + | |
| - | " | + | |
| - | }, | + | |
| - | " | + | |
| - | } | + | |
| - | # Appel Graph API | + | for name, addr in parsed: |
| - | send = requests.post( | + | |
| - | f"https:// | + | |
| - | headers={ | + | |
| - | | + | |
| - | | + | |
| - | }, | + | |
| - | data=json.dumps(mail) | + | |
| - | ) | + | |
| - | print(" | + | return cleaned |
| - | </ | + | |
| - | * Rendre le script exécutable : | ||
| - | < | + | def flatten_body(part, |
| - | chmod +x / | + | """ |
| - | chmod 755 / | + | |
| - | chown root: | + | """ |
| - | </ | + | ctype = part.get_content_type() |
| + | disp = str(part.get(" | ||
| - | ===== Configurer Postfix ===== | + | # 1. Si multipart → parcourir les sous-parties |
| + | if part.is_multipart(): | ||
| + | for subpart in part.get_payload(): | ||
| + | flatten_body(subpart, | ||
| + | return | ||
| - | ==== utiliser LPIPE pour appeler le script ==== | + | # 2. Corps texte |
| - | * Créer le fichier **/ | + | if ctype == " |
| + | payload | ||
| + | if payload: | ||
| + | text_body.append(payload.decode(" | ||
| + | return | ||
| - | < | + | |
| - | * | + | if ctype == " |
| - | * graph: | + | |
| - | </ | + | if payload: |
| + | | ||
| + | return | ||
| - | * compiler | + | # 4. Images inline (multipart/ |
| + | if " | ||
| + | cid = part.get(" | ||
| + | payload = part.get_payload(decode=True) | ||
| + | if payload and cid: | ||
| + | inline_images.append({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }) | ||
| + | return | ||
| - | < | + | # 5. Attachments |
| - | postmap / | + | if " |
| - | </ | + | |
| + | payload = part.get_payload(decode=True) | ||
| + | if filename and payload: | ||
| + | attachments.append({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }) | ||
| + | return | ||
| - | * vérifier | ||
| - | < | + | # --------------------------------------------------- |
| - | postmap | + | # MAIN |
| - | </ | + | # --------------------------------------------------- |
| + | try: | ||
| + | raw = sys.stdin.read() | ||
| + | | ||
| - | * vérifier que le transport existe | + | debug(" |
| - | < | + | subject = msg.get(" |
| - | #postconf -M | + | |
| - | ... | + | |
| - | graph unix - n n - - pipe | + | |
| - | </ | + | |
| - | * Configurer Postfix en modifiant le fichier / | + | # Extraction des adresses MIME (Documenso friendly) |
| + | to_list = clean_address_list(msg.get(" | ||
| + | cc_list = clean_address_list(msg.get(" | ||
| + | bcc_list = clean_address_list(msg.get(" | ||
| + | reply_to_list = clean_address_list(msg.get(" | ||
| - | < | + | debug(" |
| - | myhostname = postfix-relay.lan | + | |
| - | mydomain = lan | + | |
| - | mydestination = | + | |
| - | relayhost = | + | |
| - | transport_maps | + | # Corps du message |
| + | text_body | ||
| + | html_body = [] | ||
| + | attachments = [] | ||
| + | inline_images = [] | ||
| - | lpipe_destination_recipient_limit = 1 | + | flatten_body(msg, |
| - | </ | + | # Déterminer le corps principal HTML |
| + | final_html = "" | ||
| - | * Config / | + | if html_body: |
| + | final_html = " | ||
| + | elif text_body: | ||
| + | final_html = "< | ||
| + | else: | ||
| + | final_html = "< | ||
| - | < | + | debug(" |
| - | graph | + | |
| - | | + | |
| - | user=postfix | + | |
| - | argv=/ | + | |
| - | </ | + | |
| + | # --------------------------------------------------- | ||
| + | # TOKEN OAUTH2 | ||
| + | # --------------------------------------------------- | ||
| + | debug(" | ||
| - | ===== Préparation Azure AD (OAuth2) ===== | + | token_res |
| + | f" | ||
| + | data={ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | | ||
| - | * création d'une **Inscription d' | + | debug(" |
| - | * Portail Azure => Entra ID | + | |
| - | * Inscription d' | + | |
| - | * Nom : smtp2graph-relay | + | |
| - | * Locataire unique seulement | + | |
| - | * S' | + | |
| - | * Récupérer : | + | token_json = token_res.json() |
| - | | + | |
| - | * Client ID | + | raise Exception(" |
| - | * Ajouter un secret Client | + | |
| - | * Ajouter la permission Microsoft Graph : | + | token = token_json[" |
| - | * Application permission :Mail.Send | + | |
| - | * Grant admin consent. | + | # --------------------------------------------------- |
| + | # CONSTRUCTION JSON GRAPH | ||
| + | # --------------------------------------------------- | ||
| + | def addr_obj(addr): | ||
| + | return {" | ||
| - | * Adresse email 0365 utilisée pour l’envoi | + | message = { |
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| - | ===== Installation des prérequis ===== | + | if reply_to_list: |
| - | * conteneur LXC : 2 Gio RAM ; 2 coeurs ; DD de 20 Gio | + | |
| - | * modifier le fichier **/ | + | |
| - | < | + | mail_json = { |
| - | Types: deb | + | " |
| - | URIs: http:// | + | " |
| - | Suites: trixie-security | + | } |
| - | Components: contrib main | + | |
| - | Signed-By: / | + | |
| - | Types: deb | + | debug(" |
| - | URIs: http:// | + | |
| - | Suites: trixie trixie-updates | + | |
| - | Components: contrib main | + | |
| - | Signed-By: / | + | |
| - | </ | + | |
| - | * ajouter les dépôts | + | # --------------------------------------------------- |
| + | # ENVOI VIA GRAPH API | ||
| + | # --------------------------------------------------- | ||
| + | graph_res = requests.post( | ||
| + | f" | ||
| + | headers={ | ||
| + | " | ||
| + | " | ||
| + | }, | ||
| + | data=json.dumps(mail_json) | ||
| + | ) | ||
| - | < | + | debug(" |
| - | # Add Docker' | + | |
| - | apt update | + | |
| - | apt install ca-certificates curl | + | |
| - | install -m 0755 -d / | + | |
| - | curl -fsSL https:// | + | |
| - | chmod a+r / | + | |
| - | + | ||
| - | # Add the repository to Apt sources: | + | |
| - | tee / | + | |
| - | Types: deb | + | |
| - | URIs: https:// | + | |
| - | Suites: $(. / | + | |
| - | Components: stable | + | |
| - | Signed-By: / | + | |
| - | EOF | + | |
| + | except Exception as e: | ||
| + | debug(" | ||
| + | debug(traceback.format_exc()) | ||
| + | | ||
| </ | </ | ||
| - | * mettre à jour | + | |
| < | < | ||
| - | apt update && apt upgrade -y | + | chmod 755 / |
| + | chown root:root / | ||
| </ | </ | ||
| + | ===== Configurer Postfix ===== | ||
| - | | + | ==== utiliser LPIPE pour appeler le script ==== |
| + | | ||
| < | < | ||
| - | apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin | + | * |
| </ | </ | ||
| - | * Vérifier l' | + | * compiler |
| < | < | ||
| - | docker --version | + | postmap / |
| - | docker compose version | + | |
| </ | </ | ||
| - | ===== Postfix & 1smtp2graph ===== | + | * vérifier |
| - | * utilisation d'un Docker compose | + | < |
| + | postmap -s / | ||
| + | </ | ||
| - | <code file docker-compose.yml> | + | * vérifier que le transport |
| - | services: | + | |
| - | postfix: | + | |
| - | image: boky/ | + | |
| - | container_name: | + | |
| - | restart: unless-stopped | + | |
| - | environment: | + | |
| - | - ALLOW_EMPTY_SENDER=true | + | |
| - | volumes: | + | |
| - | - ./ | + | |
| - | - ./ | + | |
| - | network_mode: | + | |
| - | + | ||
| - | smtp2graph: | + | |
| - | image: ghcr.io/ | + | |
| - | container_name: | + | |
| - | restart: unless-stopped | + | |
| - | environment: | + | |
| - | PROXY_LISTEN_ADDRESS: | + | |
| - | OAUTH2_TENANT_ID: | + | |
| - | OAUTH2_CLIENT_ID: | + | |
| - | OAUTH2_CLIENT_SECRET: | + | |
| - | OAUTH2_SENDER: | + | |
| - | ports: | + | |
| - | - " | + | |
| - | + | ||
| - | + | ||
| - | </ | + | |
| - | + | ||
| - | + | ||
| - | ==== postfix/ | + | |
| < | < | ||
| - | # Postfix minimal relay to smtp2graph | + | #postconf |
| - | myhostname = postfix-relay.lan | + | ... |
| - | mydomain = lan | + | graph unix - n n - - pipe |
| - | myorigin = / | + | |
| - | 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:/ | + | |
| </ | </ | ||
| - | ==== / | + | * Configurer Postfix en modifiant le fichier |
| < | < | ||
| - | root@postfix-relay.lan | + | #myhostname = postfix-relay.lan |
| - | </ | + | #mydomain = lan |
| + | # | ||
| + | #relayhost = | ||
| + | # Postfix doit écouter sur toutes les interfaces | ||
| + | inet_interfaces = all | ||
| - | Puis : | + | # Autoriser l’IP des serveurs client |
| - | < | + | mynetworks = 127.0.0.0/8 9.9.9.0/24 |
| - | postmap | + | |
| - | </code> | + | |
| - | ==== postfix/ | + | # Postfix doit accepter les mails : |
| + | smtpd_recipient_restrictions | ||
| - | < | ||
| - | smtp inet n | ||
| - | pickup | ||
| - | cleanup | ||
| - | qmgr unix n | ||
| - | rewrite | ||
| - | bounce | ||
| - | defer | ||
| - | trace | ||
| - | verify | ||
| - | flush | ||
| - | proxymap | ||
| - | proxywrite unix - | ||
| - | smtp unix - | ||
| - | relay | ||
| - | discard | ||
| - | </ | ||
| - | On laisse Postfix en mode simple (no chroot) pour éviter les soucis SASL/TLS. | + | transport_maps = hash:/etc/ |
| - | ==== Configuration Documenso ==== | + | lpipe_destination_recipient_limit |
| - | Dans .env : | ||
| - | < | ||
| - | NEXT_PRIVATE_SMTP_TRANSPORT=" | ||
| - | NEXT_PRIVATE_SMTP_HOST=" | ||
| - | NEXT_PRIVATE_SMTP_PORT=" | ||
| - | NEXT_PRIVATE_SMTP_SECURE=" | ||
| - | NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS=" | ||
| - | NEXT_PRIVATE_SMTP_FROM_ADDRESS=" | ||
| - | NEXT_PRIVATE_SMTP_FROM_NAME=" | ||
| </ | </ | ||
| - | ===== Créer un compte Resend ===== | ||
| - | Lien : https:// | ||
| - | + | | |
| - | | + | |
| - | + | ||
| - | ===== Créer un compte SendGrid ===== | + | |
| - | Lien : https://login.sendgrid.com/ | + | |
| - | + | ||
| - | + | ||
| - | * générer une clé d' | + | |
| - | + | ||
| - | ===== Installer et confoigurer Postfix ===== | + | |
| < | < | ||
| - | apt update | + | graph |
| - | apt install postfix mailutils libsasl2-modules | + | flags=Fq. |
| + | user=nobody | ||
| + | argv=/ | ||
| </ | </ | ||
| - | * Créer le fichier / | + | * relancer Postfix : |
| < | < | ||
| - | [smtp.sendgrid.net]: | + | systemctl restart postfix |
| </ | </ | ||
| - | < | + | ==== Tester ==== |
| - | postmap / | + | |
| - | chmod 600 / | + | |
| - | </ | + | |
| - | * copier les modules SASL dans le CHROOT | + | * envoyer un message |
| < | < | ||
| - | mkdir -p / | + | echo " |
| - | cp -a / | + | |
| </ | </ | ||
| - | ===== Configurer Postfix en “SMTP relay” vers Microsoft 365 ===== | ||
| - | * éditer **/ | + | * vérifier dans les logs générés par le script qu'il n'y a pas d' |
| < | < | ||
| - | nano /etc/postfix/ | + | cat /tmp/graph_debug.log |
| </ | </ | ||
| - | * Ajoute / remplace | + | * vérifier dans les logs de Potsfix qu'il n'y a pas d' |
| < | < | ||
| - | relayhost = [smtp.sendgrid.net]: | + | journalctl -u postfix -n 50 |
| - | + | ||
| - | smtp_sasl_auth_enable = yes | + | |
| - | smtp_sasl_password_maps = hash:/etc/postfix/ | + | |
| - | smtp_sasl_security_options = noanonymous | + | |
| - | smtp_use_tls = yes | + | |
| - | smtp_tls_security_level = encrypt | + | |
| - | smtp_tls_CAfile = / | + | |
| - | + | ||
| - | + | ||
| - | inet_interfaces = all | + | |
| - | inet_protocols = ipv4 | + | |
| </ | </ | ||
| - | < | + | |
| - | systemctl restart postfix | + | |
| - | </ | + | |
systeme/smtp/relais.1774610759.txt.gz · Dernière modification : 2026/03/27 12:25 de techer.charles_educ-valadon-limoges.fr
