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:
Eklavya Sharma 2016-07-27 20:47:21 +05:30 committed by Tim Abbott
parent af28d026e3
commit 6e9bc44123
1 changed files with 30 additions and 86 deletions

View File

@ -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)