mirror of https://github.com/zulip/zulip.git
170 lines
5.6 KiB
Python
Executable File
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
|
|
)
|