mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-31 20:13:46 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			159 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			159 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env python3
 | |
| 
 | |
| """Postfix implementation of the incoming email gateway's helper for
 | |
| forwarding emails into Zulip.
 | |
| 
 | |
| https://zulip.readthedocs.io/en/latest/production/email-gateway.html
 | |
| 
 | |
| The email gateway supports two major modes of operation: An email
 | |
| server (using Postfix) where the email address configured in
 | |
| EMAIL_GATEWAY_PATTERN delivers emails directly to Zulip (this) or a
 | |
| cron job that connects to an IMAP inbox (which receives the emails)
 | |
| periodically.
 | |
| 
 | |
| Zulip's Puppet configuration takes care of configuring Postfix to
 | |
| execute this script when emails are received by Postfix, piping the
 | |
| email content via standard input (and the destination email address in
 | |
| the ORIGINAL_RECIPIENT environment variable).
 | |
| 
 | |
| In Postfix, you can express that via an /etc/aliases entry like this:
 | |
|  |/home/zulip/deployments/current/scripts/lib/email-mirror-postfix -r ${original_recipient}
 | |
| 
 | |
| To manage DoS issues, this script does very little work (just sending
 | |
| an HTTP request to queue the message for processing) to avoid
 | |
| importing expensive libraries.
 | |
| 
 | |
| Also you can use optional keys to configure the script and change default values:
 | |
| 
 | |
| -s SHARED_SECRET    For adding shared secret key if it is not contained in
 | |
|                     "/etc/zulip/zulip-secrets.conf".  This key is used to authenticate
 | |
|                     the HTTP requests made by this tool.
 | |
| 
 | |
| -d HOST             Destination Zulip host for email uploading. Address must contain type of
 | |
|                     HTTP protocol, i.e "https://example.com". Default value: "https://127.0.0.1".
 | |
| 
 | |
| -u URL             Destination relative for email uploading. Default value: "/email_mirror_message".
 | |
| 
 | |
| -n                  Disable checking ssl certificate. This option is used for
 | |
|                     self-signed certificates. Default value: False.
 | |
| 
 | |
| -t                  Disable sending request to the Zulip server. Default value: False.
 | |
| 
 | |
| """
 | |
| import argparse
 | |
| import base64
 | |
| import json
 | |
| import os
 | |
| import posix
 | |
| import ssl
 | |
| import sys
 | |
| from configparser import RawConfigParser
 | |
| from urllib.error import HTTPError
 | |
| from urllib.parse import urlencode, urljoin
 | |
| from urllib.request import Request, urlopen
 | |
| 
 | |
| parser = argparse.ArgumentParser()
 | |
| 
 | |
| parser.add_argument("-r", "--recipient", default="", help="Original recipient.")
 | |
| 
 | |
| parser.add_argument("-s", "--shared-secret", default="", help="Secret access key.")
 | |
| 
 | |
| parser.add_argument(
 | |
|     "-d",
 | |
|     "--dst-host",
 | |
|     dest="host",
 | |
|     default="https://127.0.0.1",
 | |
|     help="Destination server address for uploading email from email mirror. "
 | |
|     "Address must contain a HTTP protocol.",
 | |
| )
 | |
| 
 | |
| parser.add_argument(
 | |
|     "-u",
 | |
|     "--dst-url",
 | |
|     dest="url",
 | |
|     default="/email_mirror_message",
 | |
|     help="Destination relative URL for uploading email from email mirror.",
 | |
| )
 | |
| 
 | |
| parser.add_argument(
 | |
|     "-n",
 | |
|     "--not-verify-ssl",
 | |
|     dest="verify_ssl",
 | |
|     action="store_false",
 | |
|     help="Disable ssl certificate verifying for self-signed certificates",
 | |
| )
 | |
| 
 | |
| parser.add_argument("-t", "--test", action="store_true", help="Test mode.")
 | |
| 
 | |
| options = parser.parse_args()
 | |
| 
 | |
| MAX_ALLOWED_PAYLOAD = 25 * 1024 * 1024
 | |
| 
 | |
| 
 | |
| def process_response_error(e: HTTPError) -> None:
 | |
|     if e.code == 400:
 | |
|         response_content = e.read()
 | |
|         response_data = json.loads(response_content)
 | |
|         print(response_data["msg"])
 | |
|         exit(posix.EX_NOUSER)
 | |
|     else:
 | |
|         print("4.4.2 Connection dropped: Internal server error.")
 | |
|         exit(1)
 | |
| 
 | |
| 
 | |
| def send_email_mirror(
 | |
|     rcpt_to: str,
 | |
|     shared_secret: str,
 | |
|     host: str,
 | |
|     url: str,
 | |
|     test: bool,
 | |
|     verify_ssl: bool,
 | |
| ) -> None:
 | |
|     if not rcpt_to:
 | |
|         print("5.1.1 Bad destination mailbox address: No missed message email address.")
 | |
|         exit(posix.EX_NOUSER)
 | |
|     msg_bytes = sys.stdin.buffer.read(MAX_ALLOWED_PAYLOAD + 1)
 | |
|     if len(msg_bytes) > MAX_ALLOWED_PAYLOAD:
 | |
|         # We're not at EOF, reject large mail.
 | |
|         print("5.3.4 Message too big for system: Max size is 25MiB")
 | |
|         exit(posix.EX_DATAERR)
 | |
| 
 | |
|     secrets_file = RawConfigParser()
 | |
|     secrets_file.read("/etc/zulip/zulip-secrets.conf")
 | |
|     if not shared_secret:
 | |
|         shared_secret = secrets_file.get("secrets", "shared_secret")
 | |
| 
 | |
|     if test:
 | |
|         exit(0)
 | |
| 
 | |
|     if host == "https://127.0.0.1":
 | |
|         # Don't try to verify SSL when posting to 127.0.0.1; it won't
 | |
|         # work, and connections to 127.0.0.1 are secure without SSL.
 | |
|         verify_ssl = False
 | |
| 
 | |
|     # Because this script is run from postfix, it does not have any
 | |
|     # http proxy environment variables set which might interfere with
 | |
|     # access to localhost.
 | |
| 
 | |
|     context = None
 | |
|     if not verify_ssl:
 | |
|         context = ssl.create_default_context()
 | |
|         context.check_hostname = False
 | |
|         context.verify_mode = ssl.CERT_NONE
 | |
|     data = {
 | |
|         "rcpt_to": rcpt_to,
 | |
|         "msg_base64": base64.b64encode(msg_bytes).decode(),
 | |
|         "secret": shared_secret,
 | |
|     }
 | |
|     req = Request(url=urljoin(host, url), data=urlencode(data).encode("utf8"))
 | |
|     try:
 | |
|         urlopen(req, context=context)
 | |
|     except HTTPError as err:
 | |
|         process_response_error(err)
 | |
| 
 | |
| 
 | |
| recipient = str(os.environ.get("ORIGINAL_RECIPIENT", options.recipient))
 | |
| send_email_mirror(
 | |
|     recipient, options.shared_secret, options.host, options.url, options.test, options.verify_ssl
 | |
| )
 |