mirror of https://github.com/zulip/zulip.git
Re-implement email-mirror using imaplib.
Switch from twisted to imaplib to gain python 3 compatibility and make code easier to understand.
This commit is contained in:
parent
af28d026e3
commit
6e9bc44123
|
@ -37,35 +37,25 @@ from __future__ import absolute_import
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
import six
|
import six
|
||||||
from typing import Any
|
from typing import Any, List, Generator
|
||||||
|
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
import email
|
|
||||||
import os
|
import os
|
||||||
from email.header import decode_header
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
import sys
|
import sys
|
||||||
import posix
|
import posix
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
from zerver.lib.actions import decode_email_address
|
|
||||||
from zerver.lib.notifications import convert_html_to_markdown
|
|
||||||
from zerver.lib.upload import upload_message_image
|
|
||||||
from zerver.lib.queue import queue_json_publish
|
from zerver.lib.queue import queue_json_publish
|
||||||
from zerver.models import Stream, get_user_profile_by_email, UserProfile
|
|
||||||
from zerver.lib.email_mirror import logger, process_message, \
|
from zerver.lib.email_mirror import logger, process_message, \
|
||||||
extract_and_validate, ZulipEmailForwardError, \
|
extract_and_validate, ZulipEmailForwardError, \
|
||||||
mark_missed_message_address_as_used, is_missed_message_address
|
mark_missed_message_address_as_used, is_missed_message_address
|
||||||
|
|
||||||
from twisted.internet import protocol, reactor, ssl
|
import email
|
||||||
if six.PY2:
|
from email.message import Message
|
||||||
from twisted.mail import imap4
|
from imaplib import IMAP4_SSL
|
||||||
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../../api"))
|
|
||||||
import zulip
|
|
||||||
|
|
||||||
## Setup ##
|
## Setup ##
|
||||||
|
|
||||||
|
@ -78,71 +68,28 @@ file_handler.setFormatter(formatter)
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
logger.addHandler(file_handler)
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
## IMAP callbacks ##
|
def get_imap_messages():
|
||||||
|
# type: () -> Generator[Message, None, None]
|
||||||
def logout(result, proto):
|
mbox = IMAP4_SSL(settings.EMAIL_GATEWAY_IMAP_SERVER, settings.EMAIL_GATEWAY_IMAP_PORT)
|
||||||
# Log out.
|
mbox.login(settings.EMAIL_GATEWAY_LOGIN, settings.EMAIL_GATEWAY_PASSWORD)
|
||||||
return proto.logout()
|
try:
|
||||||
|
mbox.select(settings.EMAIL_GATEWAY_IMAP_FOLDER)
|
||||||
def delete(result, proto):
|
try:
|
||||||
# Close the connection, which also processes any flags that were
|
status, num_ids_data = mbox.search(None, 'ALL') # type: bytes, List[bytes]
|
||||||
# set on messages.
|
for msgid in num_ids_data[0].split():
|
||||||
return proto.close().addCallback(logout, proto)
|
status, msg_data = mbox.fetch(msgid, '(RFC822)')
|
||||||
|
msg_as_bytes = msg_data[0][1]
|
||||||
def fetch(result, proto, mailboxes):
|
if six.PY2:
|
||||||
if not result:
|
message = email.message_from_string(msg_as_bytes)
|
||||||
return proto.logout()
|
else:
|
||||||
|
message = email.message_from_bytes(msg_as_bytes)
|
||||||
# Make sure we forward the messages in time-order.
|
yield message
|
||||||
message_uids = sorted(result.keys())
|
mbox.store(msgid, '+FLAGS', '\\Deleted')
|
||||||
for uid in message_uids:
|
mbox.expunge()
|
||||||
message = email.message_from_string(result[uid]["RFC822"])
|
finally:
|
||||||
process_message(message)
|
mbox.close()
|
||||||
# Delete the processed messages from the Inbox.
|
finally:
|
||||||
message_set = ",".join([result[key]["UID"] for key in message_uids])
|
mbox.logout()
|
||||||
d = proto.addFlags(message_set, ["\\Deleted"], uid=True, silent=False)
|
|
||||||
d.addCallback(delete, proto)
|
|
||||||
|
|
||||||
return d
|
|
||||||
|
|
||||||
def examine_mailbox(result, proto, mailbox):
|
|
||||||
# Fetch messages from a particular mailbox.
|
|
||||||
return proto.fetchMessage("1:*", uid=True).addCallback(fetch, proto, mailbox)
|
|
||||||
|
|
||||||
def select_mailbox(result, proto):
|
|
||||||
# Select which mailbox we care about.
|
|
||||||
mbox = [x for x in result if settings.EMAIL_GATEWAY_IMAP_FOLDER in x[2]][0][2]
|
|
||||||
return proto.select(mbox).addCallback(examine_mailbox, proto, result)
|
|
||||||
|
|
||||||
def list_mailboxes(res, proto):
|
|
||||||
# List all of the mailboxes for this account.
|
|
||||||
return proto.list("", "*").addCallback(select_mailbox, proto)
|
|
||||||
|
|
||||||
def connected(proto):
|
|
||||||
d = proto.login(settings.EMAIL_GATEWAY_LOGIN, settings.EMAIL_GATEWAY_PASSWORD)
|
|
||||||
d.addCallback(list_mailboxes, proto)
|
|
||||||
d.addErrback(login_failed)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def login_failed(failure):
|
|
||||||
return failure
|
|
||||||
|
|
||||||
def done(_):
|
|
||||||
# type: (Any) -> None
|
|
||||||
reactor.callLater(0, reactor.stop)
|
|
||||||
|
|
||||||
py3_warning = "email-mirror is not available on python 3."
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# type: () -> None
|
|
||||||
if six.PY2:
|
|
||||||
imap_client = protocol.ClientCreator(reactor, imap4.IMAP4Client)
|
|
||||||
d = imap_client.connectSSL(settings.EMAIL_GATEWAY_IMAP_SERVER, settings.EMAIL_GATEWAY_IMAP_PORT,
|
|
||||||
ssl.ClientContextFactory())
|
|
||||||
d.addCallbacks(connected, login_failed)
|
|
||||||
d.addBoth(done)
|
|
||||||
else:
|
|
||||||
print(py3_warning)
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = __doc__
|
help = __doc__
|
||||||
|
@ -154,9 +101,6 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
# type: (*Any, **str) -> None
|
# type: (*Any, **str) -> None
|
||||||
if six.PY3:
|
|
||||||
print(py3_warning)
|
|
||||||
return
|
|
||||||
rcpt_to = os.environ.get("ORIGINAL_RECIPIENT", options['recipient'])
|
rcpt_to = os.environ.get("ORIGINAL_RECIPIENT", options['recipient'])
|
||||||
if rcpt_to is not None:
|
if rcpt_to is not None:
|
||||||
if is_missed_message_address(rcpt_to):
|
if is_missed_message_address(rcpt_to):
|
||||||
|
@ -175,7 +119,7 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
# Read in the message, at most 25MiB. This is the limit enforced by
|
# Read in the message, at most 25MiB. This is the limit enforced by
|
||||||
# Gmail, which we use here as a decent metric.
|
# Gmail, which we use here as a decent metric.
|
||||||
message = sys.stdin.read(25*1024*1024)
|
msg_text = sys.stdin.read(25*1024*1024)
|
||||||
|
|
||||||
if len(sys.stdin.read(1)) != 0:
|
if len(sys.stdin.read(1)) != 0:
|
||||||
# We're not at EOF, reject large mail.
|
# We're not at EOF, reject large mail.
|
||||||
|
@ -185,7 +129,7 @@ class Command(BaseCommand):
|
||||||
queue_json_publish(
|
queue_json_publish(
|
||||||
"email_mirror",
|
"email_mirror",
|
||||||
{
|
{
|
||||||
"message": message,
|
"message": msg_text,
|
||||||
"rcpt_to": rcpt_to
|
"rcpt_to": rcpt_to
|
||||||
},
|
},
|
||||||
lambda x: None
|
lambda x: None
|
||||||
|
@ -198,5 +142,5 @@ class Command(BaseCommand):
|
||||||
print("Please configure the Email Mirror Gateway in /etc/zulip/, "
|
print("Please configure the Email Mirror Gateway in /etc/zulip/, "
|
||||||
"or specify $ORIGINAL_RECIPIENT if piping a single mail.")
|
"or specify $ORIGINAL_RECIPIENT if piping a single mail.")
|
||||||
exit(1)
|
exit(1)
|
||||||
reactor.callLater(0, main)
|
for message in get_imap_messages():
|
||||||
reactor.run()
|
process_message(message)
|
||||||
|
|
Loading…
Reference in New Issue