Files
upstream-zulip/zerver/management/commands/email_mirror.py
upstream-mirror 64898b2c79
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
2026-09-15 15:05:30 +05:00

90 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, skip_unless_locked
## 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
@skip_unless_locked
def handle(self, *args: Any, **options: str) -> None:
for message in get_imap_messages():
process_message(message)