mirror of
https://github.com/zulip/zulip.git
synced 2025-11-03 13:33:24 +00:00
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.
89 lines
3.1 KiB
Python
89 lines
3.1 KiB
Python
"""Cron job implementation of Zulip's 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 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
|
|
connect to your IMAP server and batch-process all messages.
|
|
|
|
We extract and validate the target stream from information in the
|
|
recipient address and retrieve, forward, and archive the message.
|
|
|
|
"""
|
|
|
|
import email.parser
|
|
import email.policy
|
|
import logging
|
|
from collections.abc import Generator
|
|
from email.message import EmailMessage
|
|
from imaplib import IMAP4_SSL
|
|
from typing import Any
|
|
|
|
from django.conf import settings
|
|
from django.core.management.base import CommandError
|
|
from typing_extensions import override
|
|
|
|
from zerver.lib.email_mirror import logger, process_message
|
|
from zerver.lib.management import ZulipBaseCommand
|
|
|
|
## Setup ##
|
|
|
|
log_format = "%(asctime)s: %(message)s"
|
|
logging.basicConfig(format=log_format)
|
|
|
|
formatter = logging.Formatter(log_format)
|
|
file_handler = logging.FileHandler(settings.EMAIL_MIRROR_LOG_PATH)
|
|
file_handler.setFormatter(formatter)
|
|
logger.setLevel(logging.DEBUG)
|
|
logger.addHandler(file_handler)
|
|
|
|
|
|
def get_imap_messages() -> Generator[EmailMessage, None, None]:
|
|
# We're probably running from cron, try to batch-process mail
|
|
if (
|
|
not settings.EMAIL_GATEWAY_BOT
|
|
or not settings.EMAIL_GATEWAY_LOGIN
|
|
or not settings.EMAIL_GATEWAY_PASSWORD
|
|
or not settings.EMAIL_GATEWAY_IMAP_SERVER
|
|
or not settings.EMAIL_GATEWAY_IMAP_PORT
|
|
or not settings.EMAIL_GATEWAY_IMAP_FOLDER
|
|
):
|
|
raise CommandError(
|
|
"Please configure the email mirror gateway in /etc/zulip/, "
|
|
"or specify $ORIGINAL_RECIPIENT if piping a single mail."
|
|
)
|
|
mbox = IMAP4_SSL(settings.EMAIL_GATEWAY_IMAP_SERVER, settings.EMAIL_GATEWAY_IMAP_PORT)
|
|
mbox.login(settings.EMAIL_GATEWAY_LOGIN, settings.EMAIL_GATEWAY_PASSWORD)
|
|
try:
|
|
mbox.select(settings.EMAIL_GATEWAY_IMAP_FOLDER)
|
|
try:
|
|
status, num_ids_data = mbox.search(None, "ALL")
|
|
for message_id in num_ids_data[0].split():
|
|
status, msg_data = mbox.fetch(message_id, "(RFC822)")
|
|
assert isinstance(msg_data[0], tuple)
|
|
msg_as_bytes = msg_data[0][1]
|
|
yield email.parser.BytesParser(
|
|
_class=EmailMessage, policy=email.policy.default
|
|
).parsebytes(msg_as_bytes)
|
|
mbox.store(message_id, "+FLAGS", "\\Deleted")
|
|
mbox.expunge()
|
|
finally:
|
|
mbox.close()
|
|
finally:
|
|
mbox.logout()
|
|
|
|
|
|
class Command(ZulipBaseCommand):
|
|
help = __doc__
|
|
|
|
@override
|
|
def handle(self, *args: Any, **options: str) -> None:
|
|
for message in get_imap_messages():
|
|
process_message(message)
|