zulip/scripts/lib/email-mirror-postfix

170 lines
5.6 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, e.g. "https://example.com". Default value: "https://127.0.0.1".
-u URL Destination relative for email uploading. Default value: "/api/internal/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 typing import NoReturn
from urllib.error import HTTPError
from urllib.parse import urlencode, urljoin, urlsplit
from urllib.request import Request, urlopen
sys.path.append(os.path.join(os.path.dirname(__file__), "..", ".."))
from scripts.lib.zulip_tools import get_config, get_config_file
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="127.0.0.1",
help="Destination server address for uploading email from email mirror. "
"Address must contain an HTTP protocol. Otherwise, default value is assumed "
"based on the http_only setting.",
)
parser.add_argument(
"-u",
"--dst-url",
dest="url",
default="/api/internal/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) -> NoReturn:
if e.code == 400:
response_content = e.read()
response_data = json.loads(response_content)
print(response_data["msg"])
sys.exit(posix.EX_NOUSER)
else:
print("4.4.2 Connection dropped: Internal server error.")
sys.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.")
sys.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")
sys.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:
return
if not urlsplit(host).scheme:
config_file = get_config_file()
http_only = get_config(config_file, "application_server", "http_only", False)
scheme = "http://" if http_only else "https://"
host = scheme + host
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())
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
)