-# Copyright (C) 2012 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2012 W. Trevor King <wking@tremily.us>
#
# This file is part of pygrader.
#
from email import message_from_file as _message_from_file
from email.header import decode_header as _decode_header
from email.mime.text import MIMEText as _MIMEText
+from email.utils import parseaddr as _parseaddr
import mailbox as _mailbox
import re as _re
import sys as _sys
import pgp_mime as _pgp_mime
-from lxml import etree as _etree
+import pgp_mime.key as _pgp_mime_key
from . import LOG as _LOG
-from .color import color_string as _color_string
-from .color import standard_colors as _standard_colors
from .email import construct_email as _construct_email
from .email import construct_response as _construct_response
+from .extract_mime import message_time as _message_time
from .model.person import Person as _Person
+from .handler import InsecureMessage as _InsecureMessage
+from .handler import InvalidAssignmentSubject as _InvalidAssignmentSubject
from .handler import InvalidMessage as _InvalidMessage
+from .handler import InvalidStudentSubject as _InvalidStudentSubject
from .handler import InvalidSubjectMessage as _InvalidSubjectMessage
+from .handler import PermissionViolationMessage as _PermissionViolationMessage
from .handler import Response as _Response
from .handler import UnsignedMessage as _UnsignedMessage
-from .handler.get import InvalidStudent as _InvalidStudent
from .handler.get import run as _handle_get
-from .handler.submission import InvalidAssignment as _InvalidAssignment
+from .handler.grade import run as _handle_grade
+from .handler.grade import MissingGradeMessage as _MissingGradeMessage
from .handler.submission import run as _handle_submission
+from .handler.submission import InvalidSubmission as _InvalidSubmission
_TAG_REGEXP = _re.compile('^.*\[([^]]*)\].*$')
self.people = people
+class WrongSignatureMessage (_InsecureMessage):
+ def __init__(self, pgp_key=None, signatures=None, fingerprints=None,
+ decrypted=None, **kwargs):
+ if 'error' not in kwargs:
+ kwargs['error'] = 'not signed by the expected key'
+ super(WrongSignatureMessage, self).__init__(**kwargs)
+ self.pgp_key = pgp_key
+ self.signatures = signatures
+ self.fingerprints = fingerprints
+ self.decrypted = decrypted
+
+
+class UnverifiedSignatureMessage (_InsecureMessage):
+ def __init__(self, signature=None, decrypted=None, **kwargs):
+ if 'error' not in kwargs:
+ kwargs['error'] = 'unverified signature'
+ super(UnverifiedSignatureMessage, self).__init__(**kwargs)
+ self.signature = signature
+ self.decrypted = decrypted
+
+
class SubjectlessMessage (_InvalidSubjectMessage):
def __init__(self, **kwargs):
if 'error' not in kwargs:
class InvalidHandlerMessage (_InvalidSubjectMessage):
def __init__(self, target=None, handlers=None, **kwargs):
if 'error' not in kwargs:
- kwargs['error'] = 'no handler for {!r}'.format(
- kwargs.get('target', None))
+ kwargs['error'] = 'no handler for {!r}'.format(target)
super(InvalidHandlerMessage, self).__init__(**kwargs)
self.target = target
self.handlers = handlers
def mailpipe(basedir, course, stream=None, mailbox=None, input_=None,
output=None, continue_after_invalid_message=False, max_late=0,
+ trust_email_infrastructure=False,
handlers={
'get': _handle_get,
+ 'grade': _handle_grade,
'submit': _handle_submission,
- }, respond=None, use_color=None, dry_run=False, **kwargs):
+ }, respond=None, dry_run=False, **kwargs):
"""Run from procmail to sort incomming submissions
For example, you can setup your ``.procmailrc`` like this::
>>> message['Return-Path'] = '<bb@greyhavens.net>'
>>> process(message) # doctest: +REPORT_UDIFF, +ELLIPSIS
respond with:
+ Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
+ MIME-Version: 1.0
+ Content-Disposition: inline
+ Date: ...
+ From: Robot101 <phys101@tower.edu>
+ Reply-to: Robot101 <phys101@tower.edu>
+ To: Bilbo Baggins <bb@shire.org>
+ Subject: Received Assignment 1 submission
+ <BLANKLINE>
+ --===============...==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Disposition: inline
Yours,
phys-101 robot
<BLANKLINE>
+ --===============...==
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Description: OpenPGP digital signature
+ Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
+ <BLANKLINE>
+ -----BEGIN PGP SIGNATURE-----
+ Version: GnuPG v2.0.19 (GNU/Linux)
+ <BLANKLINE>
+ ...
+ -----END PGP SIGNATURE-----
+ <BLANKLINE>
+ --===============...==--
>>> course.print_tree() # doctest: +REPORT_UDIFF, +ELLIPSIS
Bilbo_Baggins
... 'for <wking@tremily.us>; Mon, 09 Oct 2011 11:50:46 -0400 (EDT)')
>>> process(message) # doctest: +REPORT_UDIFF, +ELLIPSIS
respond with:
+ Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
+ MIME-Version: 1.0
+ Content-Disposition: inline
+ Date: ...
+ From: Robot101 <phys101@tower.edu>
+ Reply-to: Robot101 <phys101@tower.edu>
+ To: Bilbo Baggins <bb@shire.org>
+ Subject: Received Assignment 1 submission
+ <BLANKLINE>
+ --===============...==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Disposition: inline
Yours,
phys-101 robot
<BLANKLINE>
+ --===============...==
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Description: OpenPGP digital signature
+ Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
+ <BLANKLINE>
+ -----BEGIN PGP SIGNATURE-----
+ Version: GnuPG v2.0.19 (GNU/Linux)
+ <BLANKLINE>
+ ...
+ -----END PGP SIGNATURE-----
+ <BLANKLINE>
+ --===============...==--
+
>>> course.print_tree() # doctest: +REPORT_UDIFF, +ELLIPSIS
Bilbo_Baggins
Bilbo_Baggins/Assignment_1
>>> message['Message-ID'] = '<hgi.jlk@home.net>'
>>> process(message) # doctest: +REPORT_UDIFF, +ELLIPSIS
respond with:
+ Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
+ MIME-Version: 1.0
+ Content-Disposition: inline
+ Date: ...
+ From: Robot101 <phys101@tower.edu>
+ Reply-to: Robot101 <phys101@tower.edu>
+ To: Bilbo Baggins <bb@shire.org>
+ Subject: Received Assignment 1 submission
+ <BLANKLINE>
+ --===============...==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Disposition: inline
Yours,
phys-101 robot
<BLANKLINE>
+ --===============...==
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Description: OpenPGP digital signature
+ Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
+ <BLANKLINE>
+ -----BEGIN PGP SIGNATURE-----
+ Version: GnuPG v2.0.19 (GNU/Linux)
+ <BLANKLINE>
+ ...
+ -----END PGP SIGNATURE-----
+ <BLANKLINE>
+ --===============...==--
Response to a submission on an unsubmittable assignment:
>>> course.cleanup()
"""
- highlight,lowlight,good,bad = _standard_colors(use_color=use_color)
if stream is None:
stream = _sys.stdin
for original,message,person,subject,target in _load_messages(
course=course, stream=stream, mailbox=mailbox, input_=input_,
- output=output, use_color=use_color, dry_run=dry_run,
+ output=output, dry_run=dry_run,
continue_after_invalid_message=continue_after_invalid_message,
+ trust_email_infrastructure=trust_email_infrastructure,
respond=respond):
try:
handler = _get_handler(handlers=handlers, target=target)
+ _LOG.debug('handling {}'.format(target))
handler(
basedir=basedir, course=course, message=message,
person=person, subject=subject,
- max_late=max_late, use_color=use_color, dry_run=dry_run)
+ max_late=max_late,
+ trust_email_infrastructure=trust_email_infrastructure,
+ dry_run=dry_run)
except _InvalidMessage as error:
+ error.course = course
+ error.message = original
+ for attribute,value in [('person', person),
+ ('subject', subject),
+ ('target', target)]:
+ if (value is not None and
+ getattr(error, attribute, None) is None):
+ setattr(error, attribute, value)
+ _LOG.warn('invalid message {}'.format(error.message_id()))
if not continue_after_invalid_message:
raise
+ _LOG.warn('{}'.format(error))
if respond:
- error.course = course
- error.message = original
- if person is not None and not hasattr(error, 'person'):
- error.person = person
- if subject is not None and not hasattr(error, 'subject'):
- error.subject = subject
- if target is not None and not hasattr(error, 'target'):
- error.target = target
response = _get_error_response(error)
respond(response)
except _Response as response:
if respond:
- author = course.robot
- target = person
msg = response.message
- if isinstance(response.message, _MIMEText):
- # Manipulate body (based on pgp_mime.append_text)
- original_encoding = msg.get_charset().input_charset
- original_payload = str(
- msg.get_payload(decode=True), original_encoding)
- new_payload = (
- '{},\n\n'
- '{}\n\n'
- 'Yours,\n'
- '{}\n').format(
- target.alias(), original_payload, author.alias())
- new_encoding = _pgp_mime.guess_encoding(new_payload)
- if msg.get('content-transfer-encoding', None):
- # clear CTE so set_payload will set it properly
- del msg['content-transfer-encoding']
- msg.set_payload(new_payload, new_encoding)
- subject = msg['Subject']
- del msg['Subject']
- assert subject is not None, msg
- msg = _construct_email(
- author=author, targets=[person], subject=subject,
- message=msg)
- respond(response.message)
-
+ if not response.complete:
+ author = course.robot
+ target = person
+ msg = response.message
+ if isinstance(response.message, _MIMEText):
+ # Manipulate body (based on pgp_mime.append_text)
+ original_encoding = msg.get_charset().input_charset
+ original_payload = str(
+ msg.get_payload(decode=True), original_encoding)
+ new_payload = (
+ '{},\n\n'
+ '{}\n\n'
+ 'Yours,\n'
+ '{}\n').format(
+ target.alias(), original_payload, author.alias())
+ new_encoding = _pgp_mime.guess_encoding(new_payload)
+ if msg.get('content-transfer-encoding', None):
+ # clear CTE so set_payload will set it properly
+ del msg['content-transfer-encoding']
+ msg.set_payload(new_payload, new_encoding)
+ subject = msg['Subject']
+ assert subject is not None, msg
+ del msg['Subject']
+ msg = _construct_email(
+ author=author, targets=[person], subject=subject,
+ message=msg)
+ respond(msg)
def _load_messages(course, stream, mailbox=None, input_=None, output=None,
- continue_after_invalid_message=False, respond=None,
- use_color=None, dry_run=False):
+ continue_after_invalid_message=False,
+ trust_email_infrastructure=False, respond=None,
+ dry_run=False):
if mailbox is None:
+ _LOG.debug('loading message from {}'.format(stream))
mbox = None
messages = [(None,_message_from_file(stream))]
if output is not None:
ombox = _mailbox.mbox(output, factory=None, create=True)
elif mailbox == 'maildir':
mbox = _mailbox.Maildir(input_, factory=None, create=False)
- messages = mbox.items()
+ messages = []
+ for key,msg in mbox.items():
+ subpath = mbox._lookup(key)
+ if subpath.endswith('.gitignore'):
+ _LOG.debug('skipping non-message {}'.format(subpath))
+ continue
+ messages.append((key, msg))
if output is not None:
ombox = _mailbox.Maildir(output, factory=None, create=True)
else:
raise ValueError(mailbox)
+ messages.sort(key=_get_message_time)
for key,msg in messages:
try:
ret = _parse_message(
- course=course, message=msg, use_color=use_color)
+ course=course, message=msg,
+ trust_email_infrastructure=trust_email_infrastructure)
except _InvalidMessage as error:
+ error.message = msg
+ _LOG.warn('invalid message {}'.format(error.message_id()))
if not continue_after_invalid_message:
raise
+ _LOG.warn('{}'.format(error))
if respond:
response = _get_error_response(error)
if response is not None:
del mbox[key]
yield ret
-def _parse_message(course, message, use_color=None):
+def _parse_message(course, message, trust_email_infrastructure=False):
"""Parse an incoming email and respond if neccessary.
Return ``(msg, person, assignment, time)`` on successful parsing.
original = message
person = subject = target = None
try:
- person = _get_message_person(
- course=course, message=message, use_color=use_color)
+ person = _get_message_person(course=course, message=message)
if person.pgp_key:
- message = _get_decoded_message(
- course=course, message=message, person=person,
- use_color=use_color)
- subject = _get_message_subject(message=message, use_color=use_color)
- target = _get_message_target(subject=subject, use_color=use_color)
+ _LOG.debug('verify message is from {}'.format(person))
+ try:
+ message = _get_verified_message(message, person.pgp_key)
+ except _UnsignedMessage as error:
+ if trust_email_infrastructure:
+ _LOG.warn('{}'.format(error))
+ else:
+ raise
+ subject = _get_message_subject(message=message)
+ target = _get_message_target(subject=subject)
except _InvalidMessage as error:
error.course = course
error.message = original
- if person is not None and not hasattr(error, 'person'):
- error.person = person
- if subject is not None and not hasattr(error, 'subject'):
- error.subject = subject
- if target is not None and not hasattr(error, 'target'):
- error.target = target
+ for attribute,value in [('person', person),
+ ('subject', subject),
+ ('target', target)]:
+ if (value is not None and
+ getattr(error, attribute, None) is None):
+ setattr(error, attribute, value)
raise
return (original, message, person, subject, target)
-def _get_message_person(course, message, use_color=None):
- sender = message['Return-Path'] # RFC 822
+def _get_message_person(course, message, trust_admin_from=True):
+ """Get the `Person` that sent the message.
+
+ We use 'Return-Path' (envelope from) instead of the message's From
+ header, because it's more consistent and harder to fake. However,
+ there may be times when you *want* to send a message in somebody
+ elses name.
+
+ For example, if a student submitted an assignment from an
+ unexpected address, you might add that address to their entry in
+ your course config, and then bounce the message back into
+ pygrader. In this case, the From header will still be the
+ student, but the 'Return-Path' will be you. With
+ `trust_admin_from` (on by default), messages who's 'Return-Path'
+ matches a professor or TA will have their 'From' line used to find
+ the final person responsible for the message.
+
+ >>> from pygrader.model.course import Course
+ >>> from pygrader.model.person import Person
+ >>> from pgp_mime import encodedMIMEText
+
+ >>> course = Course(people=[
+ ... Person(
+ ... name='Gandalf', emails=['g@grey.edu'], groups=['professors']),
+ ... Person(name='Bilbo', emails=['bb@shire.org']),
+ ... ])
+ >>> message = encodedMIMEText('testing')
+ >>> message['Return-Path'] = '<g@grey.edu>'
+ >>> message['From'] = 'Bill <bb@shire.org>'
+ >>> message['Message-ID'] = '<123.456@home.net>'
+
+ >>> person = _get_message_person(course=course, message=message)
+ >>> print(person)
+ <Person Bilbo>
+
+ >>> person = _get_message_person(
+ ... course=course, message=message, trust_admin_from=False)
+ >>> print(person)
+ <Person Gandalf>
+ """
+ sender = message['return-path'] # RFC 822
if sender is None:
raise NoReturnPath(message)
sender = sender[1:-1] # strip wrapping '<' and '>'
raise UnregisteredAddress(message=message, address=sender)
if len(people) > 1:
raise AmbiguousAddress(message=message, address=sender, people=people)
- return people[0]
-
-def _get_decoded_message(course, message, person, use_color=None):
- msg = _get_verified_message(
- message, person.pgp_key, use_color=use_color)
- if msg is None:
- raise _UnsignedMessage(message=message)
- return msg
-
-def _get_message_subject(message, use_color=None):
+ person = people[0]
+ if trust_admin_from and person.is_admin():
+ mid = message['message-id']
+ from_headers = message.get_all('from')
+ if len(from_headers) == 0:
+ _LOG.debug("no 'From' headers in {}".format(mid))
+ elif len(from_headers) > 1:
+ _LOG.debug("multiple 'From' headers in {}".format(mid))
+ else:
+ name,address = _parseaddr(from_headers[0])
+ people = list(course.find_people(email=address))
+ if len(people) == 0:
+ _LOG.debug("'From' address {} is unregistered".format(address))
+ if len(people) > 1:
+ _LOG.debug("'From' address {} is ambiguous".format(address))
+ _LOG.debug('message from {} treated as being from {}'.format(
+ person, people[0]))
+ person = people[0]
+ _LOG.debug('message from {}'.format(person))
+ return person
+
+def _get_message_subject(message):
"""
>>> from email.header import Header
>>> from pgp_mime.email import encodedMIMEText
_LOG.debug('decoded header {} -> {}'.format(parts[0], subject))
return subject.lower().replace('#', '')
-def _get_message_target(subject, use_color=None):
+def _get_message_target(subject):
"""
>>> _get_message_target(subject='no tag')
Traceback (most recent call last):
_LOG.debug('extracted target {} -> {}'.format(subject, target))
return target
-def _get_handler(handlers, target, use_color=None):
+def _get_handler(handlers, target):
try:
handler = handlers[target]
- except KeyError:
- response_subject = 'no handler for {}'.format(target)
- highlight,lowlight,good,bad = _standard_colors(use_color=use_color)
- _LOG.debug(_color_string(string=response_subject, color=bad))
- raise InvalidHandlerMessage(target=target, handlers=handlers)
+ except KeyError as error:
+ raise InvalidHandlerMessage(
+ target=target, handlers=handlers) from error
return handler
-def _get_verified_message(message, pgp_key, use_color=None):
+def _get_verified_message(message, pgp_key):
"""
>>> from pgp_mime import sign, encodedMIMEText
>>> our_message.authenticated
True
- If it is signed, but not by the right key, we get ``None``.
+ If it is signed, but not by the right key, we get an error.
>>> print(_get_verified_message(signed, pgp_key='01234567'))
- None
+ Traceback (most recent call last):
+ ...
+ pygrader.mailpipe.WrongSignatureMessage: not signed by the expected key
- If it is not signed at all, we get ``None``.
+ If it is not signed at all, we get another error.
>>> print(_get_verified_message(message, pgp_key='4332B6E3'))
- None
+ Traceback (most recent call last):
+ ...
+ pygrader.handler.UnsignedMessage: unsigned message
"""
- highlight,lowlight,good,bad = _standard_colors(use_color=use_color)
mid = message['message-id']
try:
- decrypted,verified,result = _pgp_mime.verify(message=message)
- except (ValueError, AssertionError):
- _LOG.warn(_color_string(
- string='could not verify {} (not signed?)'.format(mid),
- color=bad))
- return None
- _LOG.info(_color_string(str(result, 'utf-8'), color=lowlight))
- tree = _etree.fromstring(result.replace(b'\x00', b''))
+ decrypted,verified,signatures = _pgp_mime.verify(message=message)
+ except (ValueError, AssertionError) as error:
+ raise _UnsignedMessage(message=message) from error
+ for signature in signatures:
+ _LOG.debug(signature.dumps())
match = None
- for signature in tree.findall('.//signature'):
- for fingerprint in signature.iterchildren('fpr'):
- if fingerprint.text.endswith(pgp_key):
- match = signature
- break
- if match is None:
- _LOG.warn(_color_string(
- string='{} is not signed by the expected key'.format(mid),
- color=bad))
- return None
+ fingerprints = dict((s.fingerprint, s) for s in signatures)
+ for s in signatures:
+ for key in _pgp_mime_key.lookup_keys([s.fingerprint]):
+ if key.subkeys[0].fingerprint != s.fingerprint:
+ # the signature was made with a subkey. Add the primary.
+ fingerprints[key.subkeys[0].fingerprint] = s
+ if pgp_key.startswith('0x'):
+ key_tail = pgp_key[len('0x'):]
+ else:
+ key_tail = pgp_key
+ matches = [fingerprints[f] for f in fingerprints.keys()
+ if f.endswith(key_tail)]
+ if len(matches) == 0:
+ raise WrongSignatureMessage(
+ message=message, pgp_key=pgp_key, signatures=signatures,
+ fingerprints=fingerprints, decrypted=decrypted)
+ signature = matches[0]
if not verified:
- sumhex = list(signature.iterchildren('summary'))[0].get('value')
- summary = int(sumhex, 16)
- if summary != 0:
- _LOG.warn(_color_string(
- string='{} has an unverified signature'.format(mid),
- color=bad))
- return None
+ problems = [k for k,v in signature.summary.items() if v]
+ for good in ['green', 'valid']:
+ if good in problems:
+ problems.remove(good)
+ if problems:
+ raise UnverifiedSignatureMessage(
+ message=message, signature=signature, decrypted=decrypted)
# otherwise, we may have an untrusted key. We'll count that
# as verified here, because the caller is explicity looking
# for signatures by this fingerprint.
author = error.course.robot
target = getattr(error, 'person', None)
subject = str(error)
- if isinstance(error, InvalidHandlerMessage):
+ if isinstance(error, _InvalidSubmission):
+ subject = 'Received invalid {} submission'.format(
+ error.assignment.name)
+ text = (
+ 'We received your submission for {}, but you are not\n'
+ 'allowed to submit that assignment via email.'
+ ).format(error.assignment.name)
+ elif isinstance(error, _MissingGradeMessage):
+ subject = 'No grade in {!r}'.format(error.subject)
+ text = (
+ 'Your grade submission did not include a text/plain\n'
+ 'part containing the new grade and comment.'
+ )
+ elif isinstance(error, InvalidHandlerMessage):
targets = sorted(error.handlers.keys())
if not targets:
hint = (
'Perhaps you meant to use one of the following:\n'
' {}').format('\n '.join(targets))
text = (
- 'We got an email from you with the following subject:\n'
+ 'We received an email from you with the following subject:\n'
' {!r}\n'
'which does not match any submittable handler name for\n'
'{}.\n'
- '{}').format(repr(error.subject), error.course.name, hint)
+ '{}').format(error.subject, error.course.name, hint)
elif isinstance(error, SubjectlessMessage):
subject = 'no subject in {}'.format(error.message['Message-ID'])
text = 'We received an email message from you without a subject.'
'or TA.').format(error.course.name)
elif isinstance(error, NoReturnPath):
return
+ elif isinstance(error, _InvalidAssignmentSubject):
+ if error.assignments:
+ hint = (
+ 'but it matches several assignments:\n'
+ ' * {}').format('\n * '.join(
+ a.name for a in error.assignments))
+ else:
+ # prefer a submittable example assignment
+ assignments = [
+ a for a in error.course.assignments if a.submittable]
+ assignments += error.course.assignments # but fall back to any one
+ hint = (
+ 'Remember to use the full name for the assignment in the\n'
+ 'subject. For example:\n'
+ ' {} submission').format(assignments[0].name)
+ text = (
+ 'We received an email from you with the following subject:\n'
+ ' {!r}\n{}').format(error.subject, hint)
+ elif isinstance(error, _InvalidStudentSubject):
+ text = (
+ 'We received an email from you with the following subject:\n'
+ ' {!r}\n'
+ 'but it matches several students:\n'
+ ' * {}').format(
+ error.subject, '\n * '.join(s.name for s in error.students))
elif isinstance(error, _InvalidSubjectMessage):
text = (
'We received an email message from you with an invalid\n'
elif isinstance(error, _UnsignedMessage):
subject = 'unsigned message {}'.format(error.message['Message-ID'])
text = (
- 'We received an email message from you without a valid\n'
- 'PGP signature.')
- elif isinstance(error, _InvalidAssignment):
+ 'We received an email message from you without a PGP\n'
+ 'signature.'
+ )
+ elif isinstance(error, WrongSignatureMessage):
+ lines = [
+ 'We received an email message from you without a valid',
+ 'PGP signature. We were expecting a signature by',
+ '{}, but got signatures by:'.format(error.person.pgp_key),
+ ]
+ lines.extend([' {}'.format(s.fingerprint) for s in error.signatures])
+ text = '\n'.join(lines)
+ elif isinstance(error, UnverifiedSignatureMessage):
text = (
- 'We received your submission for {}, but you are not\n'
- 'allowed to submit that assignment via email.'
- ).format(error.assignment.name)
- elif isinstance(error, _InvalidStudent):
+ 'We received an email message from you with an unverified\n'
+ 'signature:\n\n'
+ '{}\n\n'
+ 'If this is the key you intended to use, contact your\n'
+ 'professor or TA.'
+ ).format(error.signature.dumps(prefix=' '))
+ elif isinstance(error, _PermissionViolationMessage):
text = (
- 'We got an email from you with the following subject:\n'
+ 'We received an email from you with the following subject:\n'
' {!r}\n'
- 'but it matches several students:\n'
+ "but you can't do that unless you belong to one of the\n"
+ 'following groups:\n'
' * {}').format(
- error.subject, '\n * '.join(s.name for s in error.students))
+ error.subject, '\n * '.join(error.allowed_groups))
elif isinstance(error, _InvalidMessage):
- text = subject
+ text = (
+ 'We received an email from you with the following subject:\n'
+ ' {!r}\n'
+ 'but the message was invalid:\n'
+ ' {}').format(error.subject, error)
else:
raise NotImplementedError((type(error), error))
if target is None:
'Yours,\n'
'{}\n'.format(target.alias(), text, author.alias())),
original=error.message)
+
+def _get_message_time(key_message):
+ "Key function for sorting mailbox (key,message) tuples."
+ key,message = key_message
+ return _message_time(message)