py3: Switch almost all shebang lines to use `python3`.
This causes `upgrade-zulip-from-git`, as well as a no-option run of
`tools/build-release-tarball`, to produce a Zulip install running
Python 3, rather than Python 2. In particular this means that the
virtualenv we create, in which all application code runs, is Python 3.
One shebang line, on `zulip-ec2-configure-interfaces`, explicitly
keeps Python 2, and at least one external ops script, `wal-e`, also
still runs on Python 2. See discussion on the respective previous
commits that made those explicit. There may also be some other
third-party scripts we use, outside of this source tree and running
outside our virtualenv, that still run on Python 2.
2017-08-02 23:15:16 +02:00
|
|
|
#!/usr/bin/env python3
|
2017-04-18 17:28:55 +02:00
|
|
|
|
2020-01-30 21:32:09 +01:00
|
|
|
"""Postfix implementation of the incoming email gateway's helper for
|
|
|
|
forwarding emails into Zulip.
|
2017-04-18 17:28:55 +02:00
|
|
|
|
2020-01-30 21:32:09 +01:00
|
|
|
https://zulip.readthedocs.io/en/latest/production/settings.html#email-gateway
|
2017-04-18 17:28:55 +02:00
|
|
|
|
2020-01-30 21:32:09 +01:00
|
|
|
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.
|
2017-04-18 17:28:55 +02:00
|
|
|
|
2020-01-30 21:32:09 +01:00
|
|
|
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).
|
2017-04-18 17:28:55 +02:00
|
|
|
|
|
|
|
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}
|
|
|
|
|
2020-01-30 21:32:09 +01:00
|
|
|
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.
|
|
|
|
|
2017-04-18 17:28:55 +02:00
|
|
|
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
|
2020-01-30 21:32:09 +01:00
|
|
|
"/etc/zulip/zulip-secrets.conf". This key is used to authenticate
|
|
|
|
the HTTP requests made by this tool.
|
2017-04-18 17:28:55 +02:00
|
|
|
|
|
|
|
-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.
|
2020-01-30 21:32:09 +01:00
|
|
|
|
2017-04-18 17:28:55 +02:00
|
|
|
"""
|
2020-06-11 00:54:34 +02:00
|
|
|
import argparse
|
2020-06-05 23:35:52 +02:00
|
|
|
import base64
|
2020-06-11 00:54:34 +02:00
|
|
|
import json
|
2017-04-18 17:28:55 +02:00
|
|
|
import os
|
2020-06-11 00:54:34 +02:00
|
|
|
import posix
|
2017-04-30 19:05:12 +02:00
|
|
|
import ssl
|
2017-04-18 17:28:55 +02:00
|
|
|
import sys
|
2017-11-06 03:10:47 +01:00
|
|
|
from configparser import RawConfigParser
|
2020-06-11 00:54:34 +02:00
|
|
|
from urllib.error import HTTPError
|
|
|
|
from urllib.parse import urlencode, urljoin
|
|
|
|
from urllib.request import Request, urlopen
|
2017-04-18 17:28:55 +02:00
|
|
|
|
2017-11-10 01:57:41 +01:00
|
|
|
parser = argparse.ArgumentParser()
|
2017-04-18 17:28:55 +02:00
|
|
|
|
2020-09-02 21:24:05 +02:00
|
|
|
parser.add_argument('-r', '--recipient', default='',
|
2017-11-10 01:57:41 +01:00
|
|
|
help="Original recipient.")
|
2017-04-18 17:28:55 +02:00
|
|
|
|
2020-09-02 21:24:05 +02:00
|
|
|
parser.add_argument('-s', '--shared-secret', default='',
|
2017-11-10 01:57:41 +01:00
|
|
|
help="Secret access key.")
|
2017-04-18 17:28:55 +02:00
|
|
|
|
2020-09-02 21:24:05 +02:00
|
|
|
parser.add_argument('-d', '--dst-host', dest="host", default='https://127.0.0.1',
|
2017-11-10 01:57:41 +01:00
|
|
|
help="Destination server address for uploading email from email mirror. "
|
|
|
|
"Address must contain a HTTP protocol.")
|
2017-04-18 17:28:55 +02:00
|
|
|
|
2020-09-02 21:24:05 +02:00
|
|
|
parser.add_argument('-u', '--dst-url', dest="url", default='/email_mirror_message',
|
2017-11-10 01:57:41 +01:00
|
|
|
help="Destination relative url for uploading email from email mirror.")
|
2017-04-18 17:28:55 +02:00
|
|
|
|
2020-09-02 20:45:48 +02:00
|
|
|
parser.add_argument('-n', '--not-verify-ssl', dest="verify_ssl", action='store_false',
|
2017-11-10 01:57:41 +01:00
|
|
|
help="Disable ssl certificate verifying for self-signed certificates")
|
2017-04-18 17:28:55 +02:00
|
|
|
|
2020-09-02 20:45:48 +02:00
|
|
|
parser.add_argument('-t', '--test', action='store_true',
|
2017-11-10 01:57:41 +01:00
|
|
|
help="Test mode.")
|
2017-04-18 17:28:55 +02:00
|
|
|
|
2017-11-10 01:57:41 +01:00
|
|
|
options = parser.parse_args()
|
2017-04-18 17:28:55 +02:00
|
|
|
|
|
|
|
MAX_ALLOWED_PAYLOAD = 25 * 1024 * 1024
|
|
|
|
|
2017-04-30 19:05:12 +02:00
|
|
|
|
python: Convert function type annotations to Python 3 style.
Generated by com2ann (slightly patched to avoid also converting
assignment type annotations, which require Python 3.6), followed by
some manual whitespace adjustment, and six fixes for runtime issues:
- def __init__(self, token: Token, parent: Optional[Node]) -> None:
+ def __init__(self, token: Token, parent: "Optional[Node]") -> None:
-def main(options: argparse.Namespace) -> NoReturn:
+def main(options: argparse.Namespace) -> "NoReturn":
-def fetch_request(url: str, callback: Any, **kwargs: Any) -> Generator[Callable[..., Any], Any, None]:
+def fetch_request(url: str, callback: Any, **kwargs: Any) -> "Generator[Callable[..., Any], Any, None]":
-def assert_server_running(server: subprocess.Popen[bytes], log_file: Optional[str]) -> None:
+def assert_server_running(server: "subprocess.Popen[bytes]", log_file: Optional[str]) -> None:
-def server_is_up(server: subprocess.Popen[bytes], log_file: Optional[str]) -> bool:
+def server_is_up(server: "subprocess.Popen[bytes]", log_file: Optional[str]) -> bool:
- method_kwarg_pairs: List[FuncKwargPair],
+ method_kwarg_pairs: "List[FuncKwargPair]",
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-19 03:48:37 +02:00
|
|
|
def process_response_error(e: HTTPError) -> None:
|
2017-04-30 19:05:12 +02:00
|
|
|
if e.code == 400:
|
|
|
|
response_content = e.read()
|
|
|
|
response_data = json.loads(response_content.decode('utf8'))
|
|
|
|
print(response_data['msg'])
|
2019-08-02 22:39:46 +02:00
|
|
|
exit(posix.EX_NOUSER)
|
2017-04-30 19:05:12 +02:00
|
|
|
else:
|
|
|
|
print("4.4.2 Connection dropped: Internal server error.")
|
|
|
|
exit(1)
|
|
|
|
|
|
|
|
|
python: Convert function type annotations to Python 3 style.
Generated by com2ann (slightly patched to avoid also converting
assignment type annotations, which require Python 3.6), followed by
some manual whitespace adjustment, and six fixes for runtime issues:
- def __init__(self, token: Token, parent: Optional[Node]) -> None:
+ def __init__(self, token: Token, parent: "Optional[Node]") -> None:
-def main(options: argparse.Namespace) -> NoReturn:
+def main(options: argparse.Namespace) -> "NoReturn":
-def fetch_request(url: str, callback: Any, **kwargs: Any) -> Generator[Callable[..., Any], Any, None]:
+def fetch_request(url: str, callback: Any, **kwargs: Any) -> "Generator[Callable[..., Any], Any, None]":
-def assert_server_running(server: subprocess.Popen[bytes], log_file: Optional[str]) -> None:
+def assert_server_running(server: "subprocess.Popen[bytes]", log_file: Optional[str]) -> None:
-def server_is_up(server: subprocess.Popen[bytes], log_file: Optional[str]) -> bool:
+def server_is_up(server: "subprocess.Popen[bytes]", log_file: Optional[str]) -> bool:
- method_kwarg_pairs: List[FuncKwargPair],
+ method_kwarg_pairs: "List[FuncKwargPair]",
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-19 03:48:37 +02:00
|
|
|
def send_email_mirror(
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
rcpt_to: str, shared_secret: str, host: str, url: str, test: bool, verify_ssl: bool,
|
python: Convert function type annotations to Python 3 style.
Generated by com2ann (slightly patched to avoid also converting
assignment type annotations, which require Python 3.6), followed by
some manual whitespace adjustment, and six fixes for runtime issues:
- def __init__(self, token: Token, parent: Optional[Node]) -> None:
+ def __init__(self, token: Token, parent: "Optional[Node]") -> None:
-def main(options: argparse.Namespace) -> NoReturn:
+def main(options: argparse.Namespace) -> "NoReturn":
-def fetch_request(url: str, callback: Any, **kwargs: Any) -> Generator[Callable[..., Any], Any, None]:
+def fetch_request(url: str, callback: Any, **kwargs: Any) -> "Generator[Callable[..., Any], Any, None]":
-def assert_server_running(server: subprocess.Popen[bytes], log_file: Optional[str]) -> None:
+def assert_server_running(server: "subprocess.Popen[bytes]", log_file: Optional[str]) -> None:
-def server_is_up(server: subprocess.Popen[bytes], log_file: Optional[str]) -> bool:
+def server_is_up(server: "subprocess.Popen[bytes]", log_file: Optional[str]) -> bool:
- method_kwarg_pairs: List[FuncKwargPair],
+ method_kwarg_pairs: "List[FuncKwargPair]",
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-19 03:48:37 +02:00
|
|
|
) -> None:
|
2017-04-18 17:28:55 +02:00
|
|
|
if not rcpt_to:
|
|
|
|
print("5.1.1 Bad destination mailbox address: No missed message email address.")
|
2019-08-02 22:39:46 +02:00
|
|
|
exit(posix.EX_NOUSER)
|
2020-06-05 23:35:52 +02:00
|
|
|
msg_bytes = sys.stdin.buffer.read(MAX_ALLOWED_PAYLOAD + 1)
|
|
|
|
if len(msg_bytes) > MAX_ALLOWED_PAYLOAD:
|
2017-04-18 17:28:55 +02:00
|
|
|
# We're not at EOF, reject large mail.
|
|
|
|
print("5.3.4 Message too big for system: Max size is 25MiB")
|
2019-08-02 22:39:46 +02:00
|
|
|
exit(posix.EX_DATAERR)
|
2017-04-18 17:28:55 +02:00
|
|
|
|
2017-04-30 19:05:12 +02:00
|
|
|
secrets_file = RawConfigParser()
|
2017-04-18 17:28:55 +02:00
|
|
|
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
|
|
|
|
|
2019-08-10 00:30:33 +02:00
|
|
|
context = None
|
|
|
|
if not verify_ssl:
|
|
|
|
context = ssl.create_default_context()
|
|
|
|
context.check_hostname = False
|
|
|
|
context.verify_mode = ssl.CERT_NONE
|
2020-06-05 23:35:52 +02:00
|
|
|
data = {
|
|
|
|
"rcpt_to": rcpt_to,
|
|
|
|
"msg_base64": base64.b64encode(msg_bytes).decode(),
|
|
|
|
"secret": shared_secret,
|
|
|
|
}
|
2017-04-30 19:05:12 +02:00
|
|
|
req = Request(url=urljoin(host, url), data=urlencode(data).encode('utf8'))
|
|
|
|
try:
|
2019-08-10 00:30:33 +02:00
|
|
|
urlopen(req, context=context)
|
2017-04-30 19:05:12 +02:00
|
|
|
except HTTPError as err:
|
|
|
|
process_response_error(err)
|
|
|
|
|
|
|
|
|
|
|
|
recipient = str(os.environ.get("ORIGINAL_RECIPIENT", options.recipient))
|
2017-04-18 17:28:55 +02:00
|
|
|
send_email_mirror(recipient, options.shared_secret, options.host, options.url, options.test,
|
|
|
|
options.verify_ssl)
|