mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			137 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			137 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/settings.html#email-gateway
 | 
						|
 | 
						|
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', type=str, default='',
 | 
						|
                    help="Original recipient.")
 | 
						|
 | 
						|
parser.add_argument('-s', '--shared-secret', type=str, default='',
 | 
						|
                    help="Secret access key.")
 | 
						|
 | 
						|
parser.add_argument('-d', '--dst-host', dest="host", type=str, 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", type=str, 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.decode('utf8'))
 | 
						|
        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
 | 
						|
 | 
						|
    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)
 |