From 174e295bf9e3f97fd442d815f1a3bdb5781b88f7 Mon Sep 17 00:00:00 2001 From: Mathias Rav Date: Fri, 1 May 2015 17:02:10 +0200 Subject: [PATCH] Fix tutormail --- tutormail/__init__.py | 165 ---------------------------- tutormail/server.py | 188 +++++++++++++++++++++++++++++++ tutormail/test.py | 249 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 437 insertions(+), 165 deletions(-) create mode 100644 tutormail/server.py create mode 100644 tutormail/test.py diff --git a/tutormail/__init__.py b/tutormail/__init__.py index 1d4b93d..e69de29 100644 --- a/tutormail/__init__.py +++ b/tutormail/__init__.py @@ -1,165 +0,0 @@ -# encoding: utf8 -import os -import textwrap - -from emailtunnel import SMTPForwarder, Message, InvalidRecipient - -import django - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mftutor.settings") -django.setup() - -import mftutor.settings - -from mftutor.aliases.models import resolve_alias -from mftutor.tutor.models import Tutor, TutorGroup, RusClass, Rus - - -class TutorForwarder(SMTPForwarder): - ERROR_TEMPLATE = """ - Nedenstående email blev ikke leveret til nogen. - - {reason} - - {message} - """ - - def __init__(self, *args, **kwargs): - self.gf_year = mftutor.settings.YEAR - self.tutor_year = mftutor.settings.TUTORMAIL_YEAR - self.rus_year = mftutor.settings.RUSMAIL_YEAR - self.gf_groups = mftutor.settings.GF_GROUPS - self.rusclass_base = mftutor.settings.RUSCLASS_BASE - - def handle_envelope(self, envelope, peer): - try: - return super(TutorForwarder).handle_envelope(envelope, peer) - except ForwardToAdmin as e: - self.forward_to_admin(envelope, e.args[0]) - - def translate_recipient(self, rcptto): - name, domain = rcptto.split('@') - groups = self.get_groups(name) - if groups: - emails = self.get_group_emails(name, groups) - if not emails: - raise ForwardToAdmin('Grupper er tomme: %r' % (groups,)) - return emails - - tutors_only, rusclasses = self.get_rusclasses(name) - if rusclasses is not None: - emails = self.get_rusclass_emails(tutors_only, rusclasses) - if not emails: - raise ForwardToAdmin('Ingen tutor/rus-modtagere: %r' % - (groups,)) - return emails - - raise InvalidRecipient(name) - - def forward_to_admin(self, envelope, reason): - admin_emails = ['mathiasrav@gmail.com'] - sender = recipient = 'webfar@matfystutor.dk' - - subject = '[TutorForwarder] %s' % (reason[:50],) - body = textwrap.dedent(self.ERROR_TEMPLATE).format( - reason=reason, message=envelope.message) - admin_message = Message.compose( - sender, recipient, subject, body) - admin_message.add_header('Auto-Submitted', 'auto-replied') - self.deliver(admin_message, admin_emails, sender) - - def get_groups(self, recipient): - """Get all TutorGroups that an alias refers to.""" - group_names = resolve_alias(recipient) - groups = [] - for name in group_names: - group_and_year = self.get_group(name) - if group_and_year is not None: - groups.append(group_and_year) - return groups - - def get_group(self, group_name): - """Resolves a concrete group name to a (group, year)-tuple. - - Returns None if the group name is invalid, - or a tuple (group, year) where group is a TutorGroup - and year is the year to find the tutors in. - """ - - # Find the year - if group_name in self.gf_groups: - year = self.gf_year - elif group_name.startswith('g') and group_name[1:] in self.gf_groups: - year = self.gf_year - 1 - group_name = group_name[1:] - else: - year = self.tutor_year - - # Is name a tutorgroup? - try: - group = TutorGroup.objects.get(handle=group_name) - except TutorGroup.DoesNotExist: - return None - - # Disallow 'alle' - if group.handle == 'alle': - return None - - return (group, year) - - def get_group_emails(self, name, groups): - emails = [] - for group, year in groups: - group_tutors = Tutor.objects.filter( - groups=group, year=year, - early_termination__isnull=True) - group_emails = [tutor.profile.email for tutor in group_tutors] - emails += [email for email in group_emails - if email is not None] - - # Remove duplicate email addresses - return sorted(set(emails)) - - def get_rusclasses(self, recipient): - """(tutors_only, list of RusClass)""" - year = self.rus_year - - tutors_only_prefix = 'tutor+' - if recipient.startswith(tutors_only_prefix): - recipient = recipient[len(tutors_only_prefix):] - tutors_only = True - else: - tutors_only = False - - rusclasses = None - - for official, handle, internal in self.rusclass_base: - if recipient == handle: - rusclasses = list(RusClass.objects.filter( - year=year, - handle__startswith=recipient)) - - if rusclasses is None: - try: - rusclasses = [RusClass.objects.get(year=year, handle=recipient)] - except RusClass.DoesNotExist: - pass - - return (tutors_only, rusclasses) - - def get_rusclass_emails(self, tutors_only, rusclasses): - tutor_emails = [ - tutor.profile.email - for tutor in Tutor.objects.filter(rusclass__in=rusclasses) - ] - if tutors_only: - rus_emails = [] - else: - rus_emails = [ - rus.profile.email - for rus in Rus.objects.filter(rusclass__in=rusclasses) - ] - - emails = tutor_emails + rus_emails - - return sorted(set(email for email in emails if email)) diff --git a/tutormail/server.py b/tutormail/server.py new file mode 100644 index 0000000..8868ea8 --- /dev/null +++ b/tutormail/server.py @@ -0,0 +1,188 @@ +# encoding: utf8 +import os +import logging +import textwrap + +from emailtunnel import SMTPForwarder, Message, InvalidRecipient + +import django + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mftutor.settings") +django.setup() + +import mftutor.settings + +from mftutor.aliases.models import resolve_alias +from mftutor.tutor.models import Tutor, TutorGroup, RusClass, Rus + + +class ForwardToAdmin(Exception): + pass + + +class TutorForwarder(SMTPForwarder): + ERROR_TEMPLATE = """ + Nedenstående email blev ikke leveret til nogen. + + {reason} + + {message} + """ + + def __init__(self, *args, **kwargs): + self.gf_year = kwargs.pop('gf_year', None) + self.tutor_year = kwargs.pop('tutor_year', None) + self.rus_year = kwargs.pop('rus_year', None) + + years = (self.gf_year, self.tutor_year, self.rus_year) + if all(years): + logging.info("Year from kwargs: (%s, %s, %s)" % + (self.gf_year, self.tutor_year, self.rus_year)) + else: + if any(years): + logging.error("must specify all of gf_year, tutor_year, " + + "rus_year or none of them") + self.gf_year = mftutor.settings.YEAR + self.tutor_year = mftutor.settings.TUTORMAIL_YEAR + self.rus_year = mftutor.settings.RUSMAIL_YEAR + logging.info("Year from mftutor.settings: (%s, %s, %s)" % + (self.gf_year, self.tutor_year, self.rus_year)) + + self.gf_groups = kwargs.pop( + 'gf_groups', mftutor.settings.GF_GROUPS) + self.rusclass_base = kwargs.pop( + 'rusclass_base', mftutor.settings.RUSCLASS_BASE) + super(TutorForwarder, self).__init__(*args, **kwargs) + + def handle_envelope(self, envelope, peer): + try: + return super(TutorForwarder, self).handle_envelope(envelope, peer) + except ForwardToAdmin as e: + self.forward_to_admin(envelope, e.args[0]) + + def translate_recipient(self, rcptto): + name, domain = rcptto.split('@') + groups = self.get_groups(name) + if groups: + emails = self.get_group_emails(name, groups) + if not emails: + raise ForwardToAdmin('Grupper er tomme: %r' % (groups,)) + return emails + + tutors_only, rusclasses = self.get_rusclasses(name) + if rusclasses is not None: + emails = self.get_rusclass_emails(tutors_only, rusclasses) + if not emails: + raise ForwardToAdmin('Ingen tutor/rus-modtagere: %r' % + (groups,)) + return emails + + raise InvalidRecipient(name) + + def forward_to_admin(self, envelope, reason): + admin_emails = ['mathiasrav@gmail.com'] + sender = recipient = 'webfar@matfystutor.dk' + + subject = '[TutorForwarder] %s' % (reason[:50],) + body = textwrap.dedent(self.ERROR_TEMPLATE).format( + reason=reason, message=envelope.message) + admin_message = Message.compose( + sender, recipient, subject, body) + admin_message.add_header('Auto-Submitted', 'auto-replied') + self.deliver(admin_message, admin_emails, sender) + + def get_groups(self, recipient): + """Get all TutorGroups that an alias refers to.""" + group_names = resolve_alias(recipient) + groups = [] + for name in group_names: + group_and_year = self.get_group(name) + if group_and_year is not None: + groups.append(group_and_year) + return groups + + def get_group(self, group_name): + """Resolves a concrete group name to a (group, year)-tuple. + + Returns None if the group name is invalid, + or a tuple (group, year) where group is a TutorGroup + and year is the year to find the tutors in. + """ + + # Find the year + if group_name in self.gf_groups: + year = self.gf_year + elif group_name.startswith('g') and group_name[1:] in self.gf_groups: + year = self.gf_year - 1 + group_name = group_name[1:] + else: + year = self.tutor_year + + # Is name a tutorgroup? + try: + group = TutorGroup.objects.get(handle=group_name) + except TutorGroup.DoesNotExist: + return None + + # Disallow 'alle' + if group.handle == 'alle': + return None + + return (group, year) + + def get_group_emails(self, name, groups): + emails = [] + for group, year in groups: + group_tutors = Tutor.objects.filter( + groups=group, year=year, + early_termination__isnull=True) + group_emails = [tutor.profile.email for tutor in group_tutors] + emails += [email for email in group_emails + if email is not None] + + # Remove duplicate email addresses + return sorted(set(emails)) + + def get_rusclasses(self, recipient): + """(tutors_only, list of RusClass)""" + year = self.rus_year + + tutors_only_prefix = 'tutor+' + if recipient.startswith(tutors_only_prefix): + recipient = recipient[len(tutors_only_prefix):] + tutors_only = True + else: + tutors_only = False + + rusclasses = None + + for official, handle, internal in self.rusclass_base: + if recipient == handle: + rusclasses = list(RusClass.objects.filter( + year=year, + handle__startswith=recipient)) + + if rusclasses is None: + try: + rusclasses = [RusClass.objects.get(year=year, handle=recipient)] + except RusClass.DoesNotExist: + pass + + return (tutors_only, rusclasses) + + def get_rusclass_emails(self, tutors_only, rusclasses): + tutor_emails = [ + tutor.profile.email + for tutor in Tutor.objects.filter(rusclass__in=rusclasses) + ] + if tutors_only: + rus_emails = [] + else: + rus_emails = [ + rus.profile.email + for rus in Rus.objects.filter(rusclass__in=rusclasses) + ] + + emails = tutor_emails + rus_emails + + return sorted(set(email for email in emails if email)) diff --git a/tutormail/test.py b/tutormail/test.py new file mode 100644 index 0000000..168509c --- /dev/null +++ b/tutormail/test.py @@ -0,0 +1,249 @@ +import time +import logging +logging.basicConfig(level=logging.DEBUG) +import smtplib +import asyncore +import threading + +import email.header + +from emailtunnel import SMTPReceiver, Envelope +from tutormail.server import TutorForwarder +import emailtunnel.send + + +envelopes = [] + + +def deliver_local(message, recipients, sender): + logging.info("deliver_local: From: %r To: %r Subject: %r" + % (sender, recipients, str(message.subject))) + for recipient in recipients: + if '@' not in recipient: + raise smtplib.SMTPDataError(0, 'No @ in %r' % recipient) + envelope = Envelope(message, sender, recipients) + envelopes.append(envelope) + + +class DumpReceiver(SMTPReceiver): + def handle_envelope(self, envelope): + envelopes.append(envelope) + + +# class RecipientTest(object): +# _recipients = [] +# +# def get_envelopes(self): +# envelopes = [] +# for i, recipient in enumerate(self._recipients): +# envelopes.append( +# ('-F', 'recipient_test@localhost', +# '-T', '%s@TAAGEKAMMERET.dk' % recipient, +# '-s', '%s_%s' % (id(self), i), +# '-I', 'X-test-id', self.get_test_id())) +# return envelopes +# +# def get_test_id(self): +# return str(id(self)) +# +# def check_envelopes(self, envelopes): +# recipients = [] +# for i, envelope in enumerate(envelopes): +# recipients += envelope.rcpttos +# self.check_recipients(recipients) +# +# def check_recipients(self, recipients): +# raise NotImplementedError() +# +# +# class SameRecipientTest(RecipientTest): +# def __init__(self, *recipients): +# self._recipients = recipients +# +# def check_recipients(self, recipients): +# if len(recipients) != len(self._recipients): +# raise AssertionError( +# "Bad recipient count: %r vs %r" % +# (recipients, self._recipients)) +# if any(x != recipients[0] for x in recipients): +# raise AssertionError("Recipients not the same: %r" % recipients) +# +# +# class MultipleRecipientTest(RecipientTest): +# def __init__(self, recipient): +# self._recipients = [recipient] +# +# def check_recipients(self, recipients): +# if len(recipients) <= 1: +# raise AssertionError("Only %r recipients" % len(recipients)) +# +# +# class SubjectRewriteTest(object): +# def __init__(self, subject): +# self.subject = subject +# +# def get_envelopes(self): +# return [ +# ('-F', 'subject-test@localhost', +# '-T', 'FORM13@TAAGEKAMMERET.dk', +# '-s', self.subject, +# '-I', 'X-test-id', self.get_test_id()) +# ] +# +# def check_envelopes(self, envelopes): +# message = envelopes[0].message +# +# try: +# output_subject_raw = message.get_unique_header('Subject') +# except KeyError as e: +# raise AssertionError('No Subject in message') from e +# +# input_header = email.header.make_header( +# email.header.decode_header(self.subject)) +# +# input_subject = str(input_header) +# output_subject = str(message.subject) +# +# if '[TK' in input_subject: +# expected_subject = input_subject +# else: +# expected_subject = '[TK] %s' % input_subject +# +# if output_subject != expected_subject: +# raise AssertionError( +# 'Bad subject: %r == %r turned into %r == %r, ' +# 'expected %r' % (self.subject, input_subject, +# output_subject_raw, output_subject, +# expected_subject)) +# +# def get_test_id(self): +# return str(id(self)) +# +# +# class ErroneousSubjectTest(object): +# def __init__(self, subject): +# self.subject = subject +# +# def get_envelopes(self): +# return [ +# ('-F', 'subject-test@localhost', +# '-T', 'FORM13@TAAGEKAMMERET.dk', +# '-s', self.subject, +# '-I', 'X-test-id', self.get_test_id()) +# ] +# +# def check_envelopes(self, envelopes): +# # If the message did not throw an error, we are happy +# pass +# +# def get_test_id(self): +# return str(id(self)) +# +# +# class NoSubjectTest(object): +# def get_envelopes(self): +# return [ +# ('-F', 'no-subject-test@localhost', +# '-T', 'FORM13@TAAGEKAMMERET.dk', +# '-I', 'X-test-id', self.get_test_id()) +# ] +# +# def check_envelopes(self, envelopes): +# # If the message did not throw an error, we are happy +# pass +# +# def get_test_id(self): +# return str(id(self)) + + +def main(): + relayer_port = 11110 + dumper_port = 11111 + relayer = TutorForwarder('127.0.0.1', relayer_port, + '127.0.0.1', dumper_port, + gf_year=2015, tutor_year=2015, rus_year=2014) + # dumper = DumpReceiver('127.0.0.1', dumper_port) + relayer.deliver = deliver_local + + poller = threading.Thread( + target=asyncore.loop, + kwargs={'timeout': 0.1, 'use_poll': True}) + poller.start() + + # tests = [ + # SameRecipientTest('FORM13', 'FORM2013', 'FORM1314', 'gFORM14'), + # SameRecipientTest('FORM', 'BEST-CERM-INKA-KASS-nf-PR-SEKR-VC'), + # MultipleRecipientTest('BEST'), + # MultipleRecipientTest('BESTFU'), + # MultipleRecipientTest('FU'), + # MultipleRecipientTest('ADMIN'), + # MultipleRecipientTest('engineering'), + # MultipleRecipientTest('revy+revyteknik'), + # MultipleRecipientTest('tke'), + # SubjectRewriteTest('=?UTF-8?Q?Gl=C3=A6delig_jul?='), + # SubjectRewriteTest('=?UTF-8?Q?Re=3A_=5BTK=5D_Gl=C3=A6delig_jul?='), + # # Invalid encoding a; should be skipped by ecre in email.header + # ErroneousSubjectTest('=?UTF-8?a?hello_world?='), + # # Invalid base64 data; email.header raises an exception + # ErroneousSubjectTest('=?UTF-8?b?hello_world?='), + # NoSubjectTest(), + # ] + # test_envelopes = { + # test.get_test_id(): [] + # for test in tests + # } + + # for test in tests: + # for envelope in test.get_envelopes(): + # envelope = [str(x) for x in envelope] + # envelope += ['--relay', '127.0.0.1:%s' % relayer_port] + # print(repr(envelope)) + # emailtunnel.send.main(*envelope, body='Hej') + + addresses = 'web gwebfar best alle galle dat1 it tutor+mok tutor+nano2'.split() + for address in addresses: + sender = 'test@localhost' + recipient = '%s@matfystutor.dk' % address + subject = 'Hej %s' % address + emailtunnel.send.main( + '-F', sender, + '-T', recipient, + '-s', subject, + '--relay', '127.0.0.1:%s' % relayer_port, + body='Hej') + + logging.debug("Sleep for a bit...") + time.sleep(1) + logging.debug("%s envelopes" % len(envelopes)) + + print(envelopes) + + # for envelope in envelopes: + # try: + # header = envelope.message.get_unique_header('X-test-id') + # except KeyError: + # logging.error("Envelope without X-test-id") + # continue + # test_envelopes[header].append(envelope) + + # for i, test in enumerate(tests): + # for envelope in test_envelopes[test.get_test_id()]: + # received_objects = envelope.message.get_all_headers('Received') + # received = [str(o) for o in received_objects] + # print(repr(received)) + # try: + # test_id = test.get_test_id() + # e = test_envelopes[test_id] + # if not e: + # raise AssertionError("No envelopes for test id %r" % test_id) + # test.check_envelopes(e) + # except AssertionError as e: + # logging.error("Test %s failed: %s" % (i, e)) + # else: + # logging.info("Test %s succeeded" % i) + + logging.info("tutormail.test finished; you may Ctrl-C") + + +if __name__ == "__main__": + main()