mirror of
https://github.com/zulip/zulip.git
synced 2025-11-02 04:53:36 +00:00
email-mirror: Add a standalone server that processes incoming email.
Using postfix to handle the incoming email gateway complicates things a great deal: - It cannot verify that incoming email addresses exist in Zulip before accepting them; it thus accepts mail at the `RCPT TO` stage which it cannot handle, and thus must reject after the `DATA`. - It is built to handle both incoming and outgoing email, which results in subtle errors (1c17583ad5,79931051bd,a53092687e, #18600). - Rate-limiting happens much too late to avoid denial of service (#12501). - Mis-configurations of the HTTP endpoint can break incoming mail (#18105). Provide a replacement SMTP server which accepts incoming email on port 25, verifies that Zulip can accept the address, and that no rate-limits are being broken, and then adds it directly to the relevant queue. Removes an incorrect comment which implied that missed-message addresses were only usable once. We leave rate-limiting to only channel email addresses, since missed-message addresses are unlikely to be placed into automated systems, as channel email addresses are. Also simplifies #7814 somewhat.
This commit is contained in:
committed by
Tim Abbott
parent
e6bcde00e3
commit
1f0cfd4662
@@ -4,10 +4,9 @@ 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, and this, a
|
||||
cron job that connects to an IMAP inbox (which receives the emails)
|
||||
periodically.
|
||||
server where the email address configured in EMAIL_GATEWAY_PATTERN
|
||||
delivers emails directly to Zulip, and this, a cron job that connects
|
||||
to an IMAP inbox (which receives the emails) periodically.
|
||||
|
||||
Run this in a cron job every N minutes if you have configured Zulip to
|
||||
poll an external IMAP mailbox for messages. The script will then
|
||||
|
||||
73
zerver/management/commands/email_server.py
Normal file
73
zerver/management/commands/email_server.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import os
|
||||
import ssl
|
||||
from typing import Any
|
||||
from urllib.parse import SplitResult
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandParser
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.lib.email_mirror_server import run_smtp_server
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = """SMTP server to ingest incoming emails"""
|
||||
|
||||
@override
|
||||
def add_arguments(self, parser: CommandParser) -> None:
|
||||
parser.add_argument(
|
||||
"--listen", help="[Port, or address:port, to bind HTTP server to]", default="0.0.0.0:25"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--user",
|
||||
help="User to drop privileges to, if started as root.",
|
||||
type=str,
|
||||
required=(os.geteuid() == 0),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--group",
|
||||
help="Group to drop privileges to, if started as root.",
|
||||
type=str,
|
||||
required=(os.geteuid() == 0),
|
||||
)
|
||||
tls_cert: str | None = None
|
||||
tls_key: str | None = None
|
||||
if os.access("/etc/ssl/certs/zulip.combined-chain.crt", os.R_OK) and os.access(
|
||||
"/etc/ssl/private/zulip.key", os.R_OK
|
||||
):
|
||||
tls_cert = "/etc/ssl/certs/zulip.combined-chain.crt"
|
||||
tls_key = "/etc/ssl/private/zulip.key"
|
||||
elif os.access("/etc/ssl/certs/ssl-cert-snakeoil.pem", os.R_OK) and os.access(
|
||||
"/etc/ssl/private/ssl-cert-snakeoil.key", os.R_OK
|
||||
):
|
||||
tls_cert = "/etc/ssl/certs/ssl-cert-snakeoil.pem"
|
||||
tls_key = "/etc/ssl/private/ssl-cert-snakeoil.key"
|
||||
parser.add_argument(
|
||||
"--tls-cert",
|
||||
help="Path to TLS certificate chain file",
|
||||
type=str,
|
||||
default=tls_cert,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tls-key",
|
||||
help="Path to TLS private key file",
|
||||
type=str,
|
||||
default=tls_key,
|
||||
)
|
||||
|
||||
@override
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
listen = options["listen"]
|
||||
if listen.isdigit():
|
||||
host, port = "0.0.0.0", int(listen) # noqa: S104
|
||||
else:
|
||||
r = SplitResult("", listen, "", "", "")
|
||||
if r.port is None:
|
||||
raise RuntimeError(f"{listen!r} does not have a valid port number.")
|
||||
host, port = r.hostname or "0.0.0.0", r.port # noqa: S104
|
||||
if options["tls_cert"] and options["tls_key"]:
|
||||
tls_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
tls_context.load_cert_chain(options["tls_cert"], options["tls_key"])
|
||||
else:
|
||||
tls_context = None
|
||||
|
||||
run_smtp_server(options["user"], options["group"], host, port, tls_context)
|
||||
Reference in New Issue
Block a user