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_text_email as _construct_text_email
from ..email import construct_email as _construct_email
-from ..email import _construct_email as _raw_construct_email
from ..storage import assignment_path as _assignment_path
from ..tabulate import tabulate as _tabulate
from ..template import _student_email as _student_email
-from . import respond as _respond
+from . import InvalidMessage as _InvalidMessage
+from . import InvalidSubjectMessage as _InvalidSubjectMessage
+from . import Response as _Response
+from . import UnsignedMessage as _UnsignedMessage
+
+
+class InvalidStudent (_InvalidSubjectMessage):
+ def __init__(self, students=None, **kwargs):
+ if 'error' not in kwargs:
+ kwargs['error'] = 'Subject matches multiple students'
+ super(InvalidStudent, self).__init__(kwargs)
+ self.students = students
def run(basedir, course, original, message, person, subject,
- trust_email_infrastructure=False, respond=None,
+ trust_email_infrastructure=False,
use_color=None, dry_run=False, **kwargs):
"""
>>> from pgp_mime.email import encodedMIMEText
- >>> from pygrader.model.grade import Grade
- >>> from pygrader.test.course import StubCourse
+ >>> from ..model.grade import Grade
+ >>> from ..test.course import StubCourse
+ >>> from . import InvalidMessage, Response
>>> course = StubCourse()
>>> person = list(
... course.course.find_people(email='bb@greyhavens.net'))[0]
>>> message = encodedMIMEText('This text is not important.')
>>> message['Message-ID'] = '<123.456@home.net>'
- >>> def respond(message):
- ... print('respond with:\\n{}'.format(
- ... message.as_string().replace('\\t', ' ')))
+ >>> def process(**kwargs):
+ ... try:
+ ... run(**kwargs)
+ ... except Response as response:
+ ... print('respond with:')
+ ... print(response.message.as_string().replace('\\t', ' '))
+ ... except InvalidMessage as error:
+ ... print('{} error:'.format(type(error).__name__))
+ ... print(error)
Unauthenticated messages are refused by default.
- >>> run(basedir=course.basedir, course=course.course, original=message,
- ... message=message, person=person, subject='[get]',
- ... max_late=0, respond=respond)
- Traceback (most recent call last):
- ...
- ValueError: must request information in a signed email
+ >>> process(
+ ... basedir=course.basedir, course=course.course, original=message,
+ ... message=message, person=person, subject='[get]', max_late=0)
+ UnsignedMessage error:
+ unsigned message
Although you can process them by setting the
``trust_email_infrastructure`` option. This might not be too
sysadmins is considered unacceptable, you've can only email users
who have registered PGP keys.
- >>> run(basedir=course.basedir, course=course.course, original=message,
- ... message=message, person=person, subject='[get]',
- ... max_late=0, trust_email_infrastructure=True, respond=respond)
- ... # doctest: +ELLIPSIS, +REPORT_UDIFF
- Traceback (most recent call last):
- ...
- ValueError: no grades for <Person Bilbo Baggins>
-
Students without grades get a reasonable response.
- >>> message.authenticated = True
- >>> try:
- ... run(basedir=course.basedir, course=course.course, original=message,
- ... message=message, person=person, subject='[get]',
- ... max_late=0, respond=respond)
- ... except ValueError as error:
- ... print('\\ngot error: {}'.format(error))
+ >>> process(
+ ... basedir=course.basedir, course=course.course, original=message,
+ ... message=message, person=person, subject='[get]', max_late=0,
+ ... trust_email_infrastructure=True)
... # doctest: +ELLIPSIS, +REPORT_UDIFF
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: no grades for Billy
- <BLANKLINE>
- --===============...==
- Content-Type: multipart/mixed; boundary="===============...=="
- MIME-Version: 1.0
- <BLANKLINE>
- --===============...==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
- <BLANKLINE>
- Billy,
+ Subject: No grades for Billy
<BLANKLINE>
We don't have any of your grades on file for this course.
- <BLANKLINE>
- Yours,
- phys-101 robot
- --===============...==
- Content-Type: message/rfc822
- MIME-Version: 1.0
- <BLANKLINE>
+
+ >>> message.authenticated = True
+ >>> process(
+ ... basedir=course.basedir, course=course.course, original=message,
+ ... message=message, person=person, subject='[get]', max_late=0)
+ ... # doctest: +ELLIPSIS, +REPORT_UDIFF
+ respond with:
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
- Message-ID: <123.456@home.net>
- <BLANKLINE>
- This text is not important.
- --===============...==--
- --===============...==
- MIME-Version: 1.0
- Content-Transfer-Encoding: 7bit
- Content-Description: OpenPGP digital signature
- Content-Type: application/pgp-signature; name="signature.asc"; charset="us-ascii"
+ Subject: No grades for Billy
<BLANKLINE>
- -----BEGIN PGP SIGNATURE-----
- Version: GnuPG v2.0.19 (GNU/Linux)
- <BLANKLINE>
- ...
- -----END PGP SIGNATURE-----
- <BLANKLINE>
- --===============...==--
- <BLANKLINE>
- got error: no grades for <Person Bilbo Baggins>
+ We don't have any of your grades on file for this course.
Once we add a grade, they get details on all their grades for the
course.
... assignment=course.course.assignment('Assignment 1'),
... points=10, comment='Looks good.')
>>> course.course.grades.append(grade)
- >>> run(basedir=course.basedir, course=course.course, original=message,
+ >>> process(
+ ... basedir=course.basedir, course=course.course, original=message,
... message=message, person=person, subject='[get]',
- ... max_late=0, respond=respond)
+ ... max_late=0)
... # doctest: +ELLIPSIS, +REPORT_UDIFF
respond with:
Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
>>> person = list(
... course.course.find_people(email='eye@tower.edu'))[0]
>>> person.pgp_key = None
- >>> run(basedir=course.basedir, course=course.course, original=message,
+ >>> process(
+ ... basedir=course.basedir, course=course.course, original=message,
... message=message, person=person, subject='[get]',
- ... max_late=0, respond=respond)
+ ... max_late=0)
... # doctest: +ELLIPSIS, +REPORT_UDIFF
respond with:
Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
They can also request grades for a particular student.
- >>> run(basedir=course.basedir, course=course.course, original=message,
+ >>> process(
+ ... basedir=course.basedir, course=course.course, original=message,
... message=message, person=person,
... subject='[get] {}'.format(student.name),
- ... max_late=0, respond=respond)
+ ... max_late=0)
... # doctest: +ELLIPSIS, +REPORT_UDIFF
respond with:
Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
... 'from smtp.home.net (smtp.home.net [123.456.123.456]) '
... 'by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF '
... 'for <wking@tremily.us>; Sun, 09 Oct 2011 11:50:46 -0400 (EDT)')
- >>> _handle_submission(
- ... basedir=course.basedir, course=course.course, original=submission,
- ... message=submission, person=student,
- ... subject='[submit] Assignment 1')
+ >>> try:
+ ... _handle_submission(
+ ... basedir=course.basedir, course=course.course,
+ ... original=submission, message=submission, person=student,
+ ... subject='[submit] Assignment 1')
+ ... except _Response:
+ ... pass
Now lets request the submissions.
- >>> run(basedir=course.basedir, course=course.course, original=message,
+ >>> process(
+ ... basedir=course.basedir, course=course.course, original=message,
... message=message, person=person,
... subject='[get] {}, {}'.format(student.name, 'Assignment 1'),
- ... max_late=0, respond=respond)
+ ... max_late=0)
... # doctest: +ELLIPSIS, +REPORT_UDIFF
respond with:
Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
else:
authenticated = hasattr(message, 'authenticated') and message.authenticated
if not authenticated:
- response_subject = 'must request information in a signed email'
- if respond:
- if person.pgp_key:
- hint = (
- 'Please resubmit your request in an OpenPGP-signed email\n'
- 'using your PGP key {}.').format(persion.pgp_key)
- else:
- hint = (
- "We don't even have a PGP key on file for you. Please talk\n"
- 'to your professor or TA about getting one set up.')
- _respond(
- course=course, person=person, original=original,
- subject=response_subject, text=(
- 'We got an email from you with the following subject:\n'
- ' {!r}\n'
- 'but we cannot provide the information unless we know it\n'
- 'really was you who asked for it.\n\n'
- '{}').format(subject, hint),
- respond=respond)
- raise ValueError(response_subject)
+ raise _UnsignedMessage()
if 'assistants' in person.groups or 'professors' in person.groups:
email = _get_admin_email(
basedir=basedir, course=course, original=original,
- person=person, subject=subject, respond=respond,
- use_color=None)
+ person=person, subject=subject, use_color=use_color)
elif 'students' in person.groups:
email = _get_student_email(
basedir=basedir, course=course, original=original,
- person=person, respond=respond, use_color=None)
+ person=person, use_color=use_color)
else:
raise NotImplementedError(
'strange groups {} for {}'.format(person.groups, person))
- if respond:
- respond(email)
+ raise _Response(message=email)
def _get_student_email(basedir, course, original, person, student=None,
- respond=None, use_color=None):
+ use_color=None):
if student is None:
student = person
targets = None
basedir=basedir, author=course.robot, course=course,
student=student, targets=targets, old=True))
if len(emails) == 0:
- if respond:
- if targets:
- text = (
- "We don't have any grades for {} on file for this course."
- ).format(student.name)
- else:
- text = (
- "We don't have any of your grades on file for this course.")
- _respond(
- course=course, person=person, original=original,
- subject='no grades for {}'.format(student.alias()), text=text,
- respond=respond)
- raise ValueError('no grades for {}'.format(student))
+ if targets is None:
+ text = (
+ "We don't have any of your grades on file for this course."
+ )
+ else:
+ text = (
+ "We don't have any grades for {} on file for this course."
+ ).format(student.name)
+ message = _pgp_mime.encodedMIMEText(text)
+ message['Subject'] = 'No grades for {}'.format(student.alias())
+ raise _Response(message=message)
elif len(emails) > 1:
raise NotImplementedError(emails)
email,callback = emails[0]
return email
def _get_student_submission_email(
- basedir, course, original, person, assignments, student,
- respond=None, use_color=None):
+ basedir, course, original, person, assignments, student, use_color=None):
subject = '{} assignment submissions for {}'.format(
course.name, student.name)
text = '{}:\n * {}\n'.format(
else:
for msg in mbox:
message.attach(_MIMEMessage(msg))
- return _raw_construct_email(
- author=course.robot, targets=[person], subject=subject, message=message)
+ return _construct_email(
+ author=course.robot, targets=[person], subject=subject,
+ message=message)
-def _get_admin_email(basedir, course, original, person, subject, respond=None,
+def _get_admin_email(basedir, course, original, person, subject,
use_color=None):
lsubject = subject.lower()
students = [p for p in course.find_people()
stream = _io.StringIO()
_tabulate(course=course, statistics=True, stream=stream)
text = stream.getvalue()
- email = _construct_email(
+ email = _construct_text_email(
author=course.robot, targets=[person],
subject='All grades for {}'.format(course.name),
text=text)
if len(assignments) == 0:
email = _get_student_email(
basedir=basedir, course=course, original=original,
- person=person, student=student, respond=respond,
- use_color=None)
+ person=person, student=student, use_color=use_color)
else:
email = _get_student_submission_email(
basedir=basedir, course=course, original=original,
person=person, student=student, assignments=assignments,
- use_color=None)
+ use_color=use_color)
else:
- if respond:
- _respond(
- course=course, person=person, original=original,
- subject='subject matches multiple students',
- text=(
- 'We got an email from you with the following subject:\n'
- ' {!r}\n'
- 'but it matches several students:\n'
- ' * {}').format(
- subject, '\n * '.join(s.name for s in students)),
- respond=respond)
- raise ValueError(
- 'subject {!r} matches multiple students {}'.format(
- subject, students))
+ raise InvalidStudent(students=students)
return email
import os as _os
import os.path as _os_path
+import pgp_mime as _pgp_mime
+
from .. import LOG as _LOG
from ..color import color_string as _color_string
from ..color import standard_colors as _standard_colors
from ..extract_mime import message_time as _message_time
from ..storage import assignment_path as _assignment_path
from ..storage import set_late as _set_late
-from . import respond as _respond
+from . import InvalidMessage as _InvalidMessage
+from . import Response as _Response
+
+
+class InvalidAssignment (_InvalidMessage):
+ def __init__(self, assignment, **kwargs):
+ if 'error' not in kwargs:
+ kwargs['error'] = 'Received invalid {} submission'.format(
+ assignment.name)
+ super(InvalidAssignment, self).__init__(**kwargs)
+ self.assignment = assignment
def run(basedir, course, original, message, person, subject,
- max_late=0, respond=None, use_color=None,
- dry_run=None, **kwargs):
+ max_late=0, use_color=None, dry_run=None, **kwargs):
"""
>>> from pgp_mime.email import encodedMIMEText
- >>> from pygrader.test.course import StubCourse
+ >>> from ..test.course import StubCourse
+ >>> from . import Response
>>> course = StubCourse()
>>> person = list(
... course.course.find_people(email='bb@greyhavens.net'))[0]
... 'by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF '
... 'for <wking@tremily.us>; Sun, 09 Oct 2011 11:50:46 -0400 (EDT)')
>>> subject = '[submit] assignment 1'
- >>> def respond(message):
- ... print('respond with:\\n{}'.format(message.as_string()))
- >>> run(basedir=course.basedir, course=course.course, original=message,
- ... message=message, person=person, subject=subject,
- ... max_late=0, respond=respond)
+ >>> try:
+ ... run(basedir=course.basedir, course=course.course, original=message,
+ ... message=message, person=person, subject=subject,
+ ... max_late=0)
+ ... except Response as e:
+ ... print('respond with:')
+ ... print(e.message.as_string())
... # doctest: +ELLIPSIS, +REPORT_UDIFF
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: multipart/mixed; boundary="===============...=="
- MIME-Version: 1.0
- <BLANKLINE>
- --===============...==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
- <BLANKLINE>
- Billy,
+ Subject: Received Assignment 1 submission
<BLANKLINE>
We received your submission for Assignment 1 on ....
- <BLANKLINE>
- Yours,
- phys-101 robot
- --===============...==
- Content-Type: message/rfc822
- MIME-Version: 1.0
- <BLANKLINE>
- Content-Type: text/plain; charset="us-ascii"
- MIME-Version: 1.0
- Content-Transfer-Encoding: 7bit
- Content-Disposition: inline
- Message-ID: <123.456@home.net>
- Received: from smtp.home.net (smtp.home.net [123.456.123.456]) by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF for <wking@tremily.us>; Sun, 09 Oct 2011 11:50:46 -0400 (EDT)
- <BLANKLINE>
- The answer is 42.
- --===============...==--
- --===============...==
- 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.cleanup()
"""
time = _message_time(message=message, use_color=use_color)
-
- for assignment in course.assignments:
- if _match_assignment(assignment, subject):
- break
- if not _match_assignment(assignment, subject):
- response_subject = 'no assignment found in {!r}'.format(subject)
- if respond:
- submittable_assignments = [
- a for a in course.assignments if a.submittable]
- if not submittable_assignments:
- hint = (
- 'In fact, there are no submittable assignments for\n'
- 'this course!')
- else:
- hint = (
- 'Remember to use the full name for the assignment in the\n'
- 'subject. For example:\n'
- ' {} submission').format(
- submittable_assignments[0].name)
- _respond(
- course=course, person=person, original=original,
- subject=response_subject, text=(
- 'We got an email from you with the following subject:\n'
- ' {!r}\n'
- 'which does not match any submittable assignment name\n'
- 'for {}.\n'
- '{}').format(subject, course.name, hint),
- respond=respond)
- raise ValueError(response_subject)
-
- if not assignment.submittable:
- response_subject = 'received invalid {} submission'.format(
- assignment.name)
- if respond:
- _respond(
- course=course, person=person, original=original,
- subject=response_subject, text=(
- 'We received your submission for {}, but you are not\n'
- 'allowed to submit that assignment via email.'
- ).format(assignment.name),
- respond=respond)
- raise ValueError(response_subject)
-
- if respond:
- response_subject = 'received {} submission'.format(assignment.name)
- if time:
- time_str = 'on {}'.format(_formatdate(time))
- else:
- time_str = 'at an unknown time'
- _respond(
- course=course, person=person, original=original,
- subject=response_subject, text=(
- 'We received your submission for {} {}.'
- ).format(assignment.name, time_str),
- respond=respond)
-
+ assignment = _get_assignment(
+ course=course, subject=subject, use_color=use_color)
assignment_path = _assignment_path(basedir, assignment, person)
_save_local_message_copy(
msg=message, person=person, assignment_path=assignment_path,
_check_late(
basedir=basedir, assignment=assignment, person=person, time=time,
max_late=max_late, use_color=use_color, dry_run=dry_run)
+ if time:
+ time_str = 'on {}'.format(_formatdate(time))
+ else:
+ time_str = 'at an unknown time'
+ message = _pgp_mime.encodedMIMEText((
+ 'We received your submission for {} {}.'
+ ).format(
+ assignment.name, time_str))
+ message['Subject'] = 'Received {} submission'.format(assignment.name)
+ raise _Response(message=message)
def _match_assignment(assignment, subject):
return assignment.name.lower() in subject.lower()
+def _get_assignment(course, subject, use_color):
+ assignments = [a for a in course.assignments
+ if _match_assignment(a, subject)]
+ if len(assignments) != 1:
+ if len(assignments) == 0:
+ response_subject = 'no assignment found in {!r}'.format(subject)
+ error = (
+ 'does not match any submittable assignment name\n'
+ 'for {}.\n').format(course.name)
+ else:
+ response_subject = 'several assignments found in {!r}'.format(
+ subject)
+ error = (
+ 'matches several submittable assignment names\n'
+ 'for {}: * {}\n').format(
+ course.name,
+ '\n * '.join(a.name for a in assignments))
+ submittable_assignments = [
+ a for a in course.assignments if a.submittable]
+ if not submittable_assignments:
+ hint = (
+ 'In fact, there are no submittable assignments for\n'
+ 'this course!')
+ else:
+ hint = (
+ 'Remember to use the full name for the assignment in the\n'
+ 'subject. For example:\n'
+ ' {} submission').format(
+ submittable_assignments[0].name)
+ message = _pgp_mime.encodedMIMEText((
+ 'We got an email from you with the following subject:\n'
+ ' {!r}\n'
+ 'which {}.\n\n'
+ '{}\n').format(subject, course.name, hint))
+ message['Subject'] = response_subject
+ raise _Response(
+ message=message, exception=ValueError(response_subject))
+ assignment = assignments[0]
+
+ if not assignment.submittable:
+ raise InvalidAssignment(assignment)
+ return assignments[0]
+
def _save_local_message_copy(msg, person, assignment_path, use_color=None,
dry_run=False):
highlight,lowlight,good,bad = _standard_colors(use_color=use_color)
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
import mailbox as _mailbox
import re as _re
import sys as _sys
-from pgp_mime import verify as _verify
+import pgp_mime as _pgp_mime
from lxml import etree as _etree
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 .model.person import Person as _Person
-from .handler import respond as _respond
+from .handler import InvalidMessage as _InvalidMessage
+from .handler import InvalidSubjectMessage as _InvalidSubjectMessage
+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.submission import run as _handle_submission
_TAG_REGEXP = _re.compile('^.*\[([^]]*)\].*$')
+class NoReturnPath (_InvalidMessage):
+ def __init__(self, address, **kwargs):
+ if 'error' not in kwargs:
+ kwargs['error'] = 'no Return-Path'
+ super(NoReturnPath, self).__init__(**kwargs)
+
+
+class UnregisteredAddress (_InvalidMessage):
+ def __init__(self, address, **kwargs):
+ if 'error' not in kwargs:
+ kwargs['error'] = 'unregistered address {}'.format(address)
+ super(UnregisteredAddress, self).__init__(**kwargs)
+ self.address = address
+
+
+class AmbiguousAddress (_InvalidMessage):
+ def __init__(self, address, people, **kwargs):
+ if 'error' not in kwargs:
+ kwargs['error'] = 'ambiguous address {}'.format(address)
+ super(AmbiguousAddress, self).__init__(**kwargs)
+ self.address = address
+ self.people = people
+
+
+class SubjectlessMessage (_InvalidSubjectMessage):
+ def __init__(self, **kwargs):
+ if 'error' not in kwargs:
+ kwargs['error'] = 'no subject'
+ super(SubjectlessMessage, self).__init__(**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))
+ super(InvalidHandlerMessage, self).__init__(**kwargs)
+ self.target = target
+ self.handlers = handlers
+
+
def mailpipe(basedir, course, stream=None, mailbox=None, input_=None,
- output=None, max_late=0, handlers={
+ output=None, continue_after_invalid_message=False, max_late=0,
+ handlers={
'get': _handle_get,
'submit': _handle_submission,
- }, respond=None, use_color=None,
- dry_run=False, **kwargs):
+ }, respond=None, use_color=None, dry_run=False, **kwargs):
"""Run from procmail to sort incomming submissions
For example, you can setup your ``.procmailrc`` like this::
If you don't want procmail to eat the message, you can use the
``c`` flag (carbon copy) by starting your rule off with ``:0 c``.
- >>> from asyncore import loop
>>> from io import StringIO
>>> from pgp_mime.email import encodedMIMEText
- >>> from pygrader.test.course import StubCourse
- >>> from pygrader.test.client import MessageSender
- >>> from pygrader.test.server import SMTPServer
-
- Messages with unrecognized ``Return-Path``\s are silently dropped:
+ >>> from .handler import InvalidMessage, Response
+ >>> from .test.course import StubCourse
>>> course = StubCourse()
- >>> def process(peer, mailfrom, rcpttos, data):
+ >>> def respond(message):
+ ... print('respond with:\\n{}'.format(message.as_string()))
+ >>> def process(message):
... mailpipe(
... basedir=course.basedir, course=course.course,
- ... stream=StringIO(data), output=course.mailbox)
+ ... stream=StringIO(message.as_string()),
+ ... output=course.mailbox,
+ ... continue_after_invalid_message=True,
+ ... respond=respond)
>>> message = encodedMIMEText('The answer is 42.')
>>> message['Message-ID'] = '<123.456@home.net>'
- >>> message['Return-Path'] = '<invalid.return.path@home.net>'
>>> message['Received'] = (
... 'from smtp.home.net (smtp.home.net [123.456.123.456]) '
... 'by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF '
>>> message['From'] = 'Billy B <bb@greyhavens.net>'
>>> message['To'] = 'phys101 <phys101@tower.edu>'
>>> message['Subject'] = '[submit] assignment 1'
- >>> messages = [message]
- >>> ms = MessageSender(address=('localhost', 1025), messages=messages)
- >>> loop()
- >>> course.print_tree() # doctest: +REPORT_UDIFF
+
+ Messages with unrecognized ``Return-Path``\s are silently dropped:
+
+ >>> process(message) # doctest: +REPORT_UDIFF, +ELLIPSIS
+ >>> course.print_tree() # doctest: +REPORT_UDIFF, +ELLIPSIS
course.conf
+ mail
+ mail/cur
+ mail/new
+ mail/tmp
+
+ Response to a message from an unregistered person:
+
+ >>> message['Return-Path'] = '<invalid.return.path@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: "invalid.return.path@home.net" <invalid.return.path@home.net>
+ Subject: unregistered address invalid.return.path@home.net
+ <BLANKLINE>
+ --===============...==
+ Content-Type: multipart/mixed; boundary="===============...=="
+ MIME-Version: 1.0
+ <BLANKLINE>
+ --===============...==
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Disposition: inline
+ <BLANKLINE>
+ invalid.return.path@home.net,
+ <BLANKLINE>
+ Your email address is not registered with pygrader for
+ Physics 101. If you feel it should be, contact your professor
+ or TA.
+ <BLANKLINE>
+ Yours,
+ phys-101 robot
+ <BLANKLINE>
+ --===============...==
+ Content-Type: message/rfc822
+ MIME-Version: 1.0
+ <BLANKLINE>
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Disposition: inline
+ Message-ID: <123.456@home.net>
+ Received: from smtp.home.net (smtp.home.net [123.456.123.456]) by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF for <wking@tremily.us>; Sun, 09 Oct 2011 11:50:46 -0400 (EDT)
+ From: Billy B <bb@greyhavens.net>
+ To: phys101 <phys101@tower.edu>
+ Subject: [submit] assignment 1
+ Return-Path: <invalid.return.path@home.net>
+ <BLANKLINE>
+ The answer is 42.
+ --===============...==--
+ --===============...==
+ 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>
+ --===============...==--
If we add a valid ``Return-Path``, we get the expected delivery:
- >>> server = SMTPServer(
- ... ('localhost', 1025), None, process=process, count=1)
>>> del message['Return-Path']
>>> message['Return-Path'] = '<bb@greyhavens.net>'
- >>> ms = MessageSender(address=('localhost', 1025), messages=messages)
- >>> loop()
+ >>> process(message) # doctest: +REPORT_UDIFF, +ELLIPSIS
+ respond with:
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Disposition: inline
+ Content-Transfer-Encoding: 7bit
+ <BLANKLINE>
+ Billy,
+ <BLANKLINE>
+ We received your submission for Assignment 1 on Sun, 09 Oct 2011 15:50:46 -0000.
+ <BLANKLINE>
+ Yours,
+ phys-101 robot
+ <BLANKLINE>
+
>>> course.print_tree() # doctest: +REPORT_UDIFF, +ELLIPSIS
Bilbo_Baggins
Bilbo_Baggins/Assignment_1
The last ``Received`` is used to timestamp the message:
- >>> server = SMTPServer(
- ... ('localhost', 1025), None, process=process, count=1)
>>> del message['Message-ID']
>>> message['Message-ID'] = '<abc.def@home.net>'
>>> del message['Received']
... 'from smtp.home.net (smtp.home.net [123.456.123.456]) '
... 'by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF '
... 'for <wking@tremily.us>; Mon, 09 Oct 2011 11:50:46 -0400 (EDT)')
- >>> messages = [message]
- >>> ms = MessageSender(address=('localhost', 1025), messages=messages)
- >>> loop()
+ >>> process(message) # doctest: +REPORT_UDIFF, +ELLIPSIS
+ respond with:
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Disposition: inline
+ Content-Transfer-Encoding: 7bit
+ <BLANKLINE>
+ Billy,
+ <BLANKLINE>
+ We received your submission for Assignment 1 on Mon, 10 Oct 2011 16:50:46 -0000.
+ <BLANKLINE>
+ Yours,
+ phys-101 robot
+ <BLANKLINE>
>>> course.print_tree() # doctest: +REPORT_UDIFF, +ELLIPSIS
Bilbo_Baggins
Bilbo_Baggins/Assignment_1
Response to a successful submission:
- >>> def respond(message):
- ... print('respond with:\\n{}'.format(message.as_string()))
- >>> def process(peer, mailfrom, rcpttos, data):
- ... mailpipe(
- ... basedir=course.basedir, course=course.course,
- ... stream=StringIO(data), output=course.mailbox,
- ... respond=respond)
- >>> server = SMTPServer(
- ... ('localhost', 1025), None, process=process, count=1)
>>> del message['Message-ID']
>>> message['Message-ID'] = '<hgi.jlk@home.net>'
- >>> messages = [message]
- >>> ms = MessageSender(address=('localhost', 1025), messages=messages)
- >>> loop() # doctest: +REPORT_UDIFF, +ELLIPSIS
+ >>> 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: multipart/mixed; boundary="===============...=="
- MIME-Version: 1.0
- <BLANKLINE>
- --===============...==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
- Content-Transfer-Encoding: 7bit
Content-Disposition: inline
+ Content-Transfer-Encoding: 7bit
<BLANKLINE>
Billy,
<BLANKLINE>
<BLANKLINE>
Yours,
phys-101 robot
- --===============...==
- Content-Type: message/rfc822
- MIME-Version: 1.0
<BLANKLINE>
- Content-Type: text/plain; charset="us-ascii"
- MIME-Version: 1.0
- Content-Transfer-Encoding: 7bit
- Content-Disposition: inline
- From: Billy B <bb@greyhavens.net>
- To: phys101 <phys101@tower.edu>
- Subject: [submit] assignment 1
- Return-Path: <bb@greyhavens.net>
- Received: from smtp.mail.uu.edu (localhost.localdomain [127.0.0.1]) by smtp.mail.uu.edu (Postfix) with SMTP id 68CB45C8453 for <wking@tremily.us>; Mon, 10 Oct 2011 12:50:46 -0400 (EDT)
- Received: from smtp.home.net (smtp.home.net [123.456.123.456]) by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF for <wking@tremily.us>; Mon, 09 Oct 2011 11:50:46 -0400 (EDT)
- Message-ID: <hgi.jlk@home.net>
- <BLANKLINE>
- The answer is 42.
- --===============...==--
- --===============...==
- 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:
- >>> server = SMTPServer(
- ... ('localhost', 1025), None, process=process, count=1)
>>> del message['Subject']
>>> message['Subject'] = '[submit] attendance 1'
- >>> messages = [message]
- >>> ms = MessageSender(address=('localhost', 1025), messages=messages)
- >>> loop() # doctest: +REPORT_UDIFF, +ELLIPSIS
+ >>> process(message) # doctest: +REPORT_UDIFF, +ELLIPSIS
respond with:
Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
MIME-Version: 1.0
From: Robot101 <phys101@tower.edu>
Reply-to: Robot101 <phys101@tower.edu>
To: Bilbo Baggins <bb@shire.org>
- Subject: received invalid Attendance 1 submission
+ Subject: Received invalid Attendance 1 submission
<BLANKLINE>
--===============...==
Content-Type: multipart/mixed; boundary="===============...=="
<BLANKLINE>
Yours,
phys-101 robot
+ <BLANKLINE>
--===============...==
Content-Type: message/rfc822
MIME-Version: 1.0
Response to a bad subject:
- >>> server = SMTPServer(
- ... ('localhost', 1025), None, process=process, count=1)
>>> del message['Subject']
>>> message['Subject'] = 'need help for the first homework'
- >>> messages = [message]
- >>> ms = MessageSender(address=('localhost', 1025), messages=messages)
- >>> loop() # doctest: +REPORT_UDIFF, +ELLIPSIS
+ >>> process(message) # doctest: +REPORT_UDIFF, +ELLIPSIS
respond with:
Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
MIME-Version: 1.0
<BLANKLINE>
Billy,
<BLANKLINE>
- We received an email message from you without
- subject tags.
+ We received an email message from you with an invalid
+ subject.
<BLANKLINE>
Yours,
phys-101 robot
+ <BLANKLINE>
--===============...==
Content-Type: message/rfc822
MIME-Version: 1.0
Response to a missing subject:
- >>> server = SMTPServer(
- ... ('localhost', 1025), None, process=process, count=1)
>>> del message['Subject']
- >>> messages = [message]
- >>> ms = MessageSender(address=('localhost', 1025), messages=messages)
- >>> loop() # doctest: +REPORT_UDIFF, +ELLIPSIS
+ >>> process(message) # doctest: +REPORT_UDIFF, +ELLIPSIS
respond with:
Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg="pgp-sha1"; boundary="===============...=="
MIME-Version: 1.0
<BLANKLINE>
Yours,
phys-101 robot
+ <BLANKLINE>
--===============...==
Content-Type: message/rfc822
MIME-Version: 1.0
>>> student = course.course.person(email='bb@greyhavens.net')
>>> student.pgp_key = '4332B6E3'
- >>> server = SMTPServer(
- ... ('localhost', 1025), None, process=process, count=1)
>>> del message['Subject']
- >>> messages = [message]
- >>> ms = MessageSender(address=('localhost', 1025), messages=messages)
- >>> loop() # doctest: +REPORT_UDIFF, +ELLIPSIS
+ >>> process(message) # doctest: +REPORT_UDIFF, +ELLIPSIS
respond with:
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; micalg="pgp-sha1"; boundary="===============...=="
MIME-Version: 1.0
<BLANKLINE>
--===============...==--
- Response to a message from an unregistered person:
-
- >>> server = SMTPServer(
- ... ('localhost', 1025), None, process=process, count=1)
- >>> del message['Return-Path']
- >>> message['Return-Path'] = '<invalid.return.path@home.net>'
- >>> messages = [message]
- >>> ms = MessageSender(address=('localhost', 1025), messages=messages)
- >>> loop() # 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: "invalid.return.path@home.net" <invalid.return.path@home.net>
- Subject: unregistered address invalid.return.path@home.net
- <BLANKLINE>
- --===============...==
- Content-Type: multipart/mixed; boundary="===============...=="
- MIME-Version: 1.0
- <BLANKLINE>
- --===============...==
- Content-Type: text/plain; charset="us-ascii"
- MIME-Version: 1.0
- Content-Transfer-Encoding: 7bit
- Content-Disposition: inline
- <BLANKLINE>
- invalid.return.path@home.net,
- <BLANKLINE>
- Your email address is not registered with pygrader for
- Physics 101. If you feel it should be, contact your professor
- or TA.
- <BLANKLINE>
- Yours,
- phys-101 robot
- --===============...==
- Content-Type: message/rfc822
- MIME-Version: 1.0
- <BLANKLINE>
- Content-Type: text/plain; charset="us-ascii"
- MIME-Version: 1.0
- Content-Transfer-Encoding: 7bit
- Content-Disposition: inline
- From: Billy B <bb@greyhavens.net>
- To: phys101 <phys101@tower.edu>
- Received: from smtp.mail.uu.edu (localhost.localdomain [127.0.0.1]) by smtp.mail.uu.edu (Postfix) with SMTP id 68CB45C8453 for <wking@tremily.us>; Mon, 10 Oct 2011 12:50:46 -0400 (EDT)
- Received: from smtp.home.net (smtp.home.net [123.456.123.456]) by smtp.mail.uu.edu (Postfix) with ESMTP id 5BA225C83EF for <wking@tremily.us>; Mon, 09 Oct 2011 11:50:46 -0400 (EDT)
- Message-ID: <hgi.jlk@home.net>
- Return-Path: <invalid.return.path@home.net>
- <BLANKLINE>
- The answer is 42.
- --===============...==--
- --===============...==
- 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.cleanup()
"""
highlight,lowlight,good,bad = _standard_colors(use_color=use_color)
stream = _sys.stdin
for original,message,person,subject,target in _load_messages(
course=course, stream=stream, mailbox=mailbox, input_=input_,
- output=output, respond=respond, use_color=use_color, dry_run=dry_run):
- handler = _get_handler(
- course=course, handlers=handlers, message=message, person=person,
- subject=subject, target=target)
+ output=output, use_color=use_color, dry_run=dry_run,
+ continue_after_invalid_message=continue_after_invalid_message,
+ respond=respond):
try:
+ handler = _get_handler(handlers=handlers, target=target)
handler(
basedir=basedir, course=course, original=original,
message=message, person=person, subject=subject,
- max_late=max_late, respond=respond,
- use_color=use_color, dry_run=dry_run)
- except ValueError as error:
- _LOG.warn(_color_string(string=str(error), color=bad))
+ max_late=max_late, use_color=use_color, dry_run=dry_run)
+ except _InvalidMessage as error:
+ if not continue_after_invalid_message:
+ raise
+ 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)
+
def _load_messages(course, stream, mailbox=None, input_=None, output=None,
- respond=None, use_color=None, dry_run=False):
+ continue_after_invalid_message=False, respond=None,
+ use_color=None, dry_run=False):
if mailbox is None:
mbox = None
messages = [(None,_message_from_file(stream))]
else:
raise ValueError(mailbox)
for key,msg in messages:
- ret = _parse_message(
- course=course, message=msg, respond=respond, use_color=use_color)
- if ret:
- if output is not None and dry_run is False:
- # move message from input mailbox to output mailbox
- ombox.add(msg)
- if mbox is not None:
- del mbox[key]
- yield ret
-
-def _parse_message(course, message, respond=None, use_color=None):
+ try:
+ ret = _parse_message(
+ course=course, message=msg, use_color=use_color)
+ except _InvalidMessage as error:
+ if not continue_after_invalid_message:
+ raise
+ if respond:
+ response = _get_error_response(error)
+ if response is not None:
+ respond(response)
+ continue
+ if output is not None and dry_run is False:
+ # move message from input mailbox to output mailbox
+ ombox.add(msg)
+ if mbox is not None:
+ del mbox[key]
+ yield ret
+
+def _parse_message(course, message, use_color=None):
"""Parse an incoming email and respond if neccessary.
Return ``(msg, person, assignment, time)`` on successful parsing.
Return ``None`` on failure.
"""
- highlight,lowlight,good,bad = _standard_colors(use_color=use_color)
original = message
+ person = subject = target = None
try:
person = _get_message_person(
- course=course, message=message, original=original,
- respond=respond, use_color=use_color)
+ course=course, message=message, use_color=use_color)
if person.pgp_key:
message = _get_decoded_message(
- course=course, message=message, original=original, person=person,
- respond=respond, use_color=use_color)
- subject = _get_message_subject(
- course=course, message=message, original=original, person=person,
- respond=respond, use_color=use_color)
- target = _get_message_target(
- course=course, message=message, original=original, person=person,
- subject=subject, respond=respond, use_color=use_color)
- except ValueError as error:
- _LOG.debug(_color_string(string=str(error), color=bad))
- return None
+ 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)
+ 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
+ raise
return (original, message, person, subject, target)
-def _get_message_person(course, message, original, respond=None,
- use_color=None):
- mid = message['Message-ID']
+def _get_message_person(course, message, use_color=None):
sender = message['Return-Path'] # RFC 822
if sender is None:
- raise ValueError('no Return-Path in {}'.format(mid))
+ raise NoReturnPath(message)
sender = sender[1:-1] # strip wrapping '<' and '>'
people = list(course.find_people(email=sender))
if len(people) == 0:
- if respond:
- person = _Person(name=sender, emails=[sender])
- response_subject = 'unregistered address {}'.format(sender)
- _respond(
- course=course, person=person, original=original,
- subject=response_subject, text=(
- 'Your email address is not registered with pygrader for\n'
- '{}. If you feel it should be, contact your professor\n'
- 'or TA.').format(course.name),
- respond=respond)
- raise ValueError('no person found to match {}'.format(sender))
+ raise UnregisteredAddress(message=message, address=sender)
if len(people) > 1:
- raise ValueError('multiple people match {} ({})'.format(
- sender, ', '.join(str(p) for p in people)))
+ raise AmbiguousAddress(message=message, address=sender, people=people)
return people[0]
-def _get_decoded_message(course, message, original, person,
- respond=None, use_color=None):
- message = _get_verified_message(
+def _get_decoded_message(course, message, person, use_color=None):
+ msg = _get_verified_message(
message, person.pgp_key, use_color=use_color)
- if message is None:
- if respond:
- mid = original['Message-ID']
- response_subject = 'unsigned message {}'.format(mid)
- _respond(
- course=course, person=person, original=original,
- subject=response_subject, text=(
- 'We received an email message from you without a valid\n'
- 'PGP signature.'),
- respond=respond)
- raise ValueError('unsigned message from {}'.format(person.alias()))
- return message
-
-def _get_message_subject(course, message, original, person,
- respond=None, use_color=None):
+ if msg is None:
+ raise _UnsignedMessage(message=message)
+ return msg
+
+def _get_message_subject(message, use_color=None):
"""
>>> from email.header import Header
>>> from pgp_mime.email import encodedMIMEText
>>> message = encodedMIMEText('The answer is 42.')
>>> message['Message-ID'] = 'msg-id'
- >>> _get_message_subject(
- ... course=None, message=message, original=message, person=None)
+ >>> _get_message_subject(message=message)
Traceback (most recent call last):
...
- ValueError: no subject in msg-id
+ pygrader.mailpipe.SubjectlessMessage: no subject
>>> del message['Subject']
>>> subject = Header('unicode part', 'utf-8')
>>> subject.append('-ascii part', 'ascii')
>>> message['Subject'] = subject.encode()
- >>> _get_message_subject(
- ... course=None, message=message, original=message, person=None)
+ >>> _get_message_subject(message=message)
'unicode part-ascii part'
>>> del message['Subject']
>>> message['Subject'] = 'clean subject'
- >>> _get_message_subject(
- ... course=None, message=message, original=message, person=None)
+ >>> _get_message_subject(message=message)
'clean subject'
"""
if message['Subject'] is None:
- mid = message['Message-ID']
- response_subject = 'no subject in {}'.format(mid)
- if respond:
- _respond(
- course=course, person=person, original=original,
- subject=response_subject, text=(
- 'We received an email message from you without a subject.'
- ),
- respond=respond)
- raise ValueError(response_subject)
+ raise SubjectlessMessage(subject=None, message=message)
parts = _decode_header(message['Subject'])
part_strings = []
_LOG.debug('decoded header {} -> {}'.format(parts[0], subject))
return subject.lower().replace('#', '')
-def _get_message_target(course, message, original, person, subject,
- respond=None, use_color=None):
+def _get_message_target(subject, use_color=None):
"""
- >>> _get_message_target(course=None, message=None, original=None,
- ... person=None, subject='no tag')
+ >>> _get_message_target(subject='no tag')
Traceback (most recent call last):
...
- ValueError: no tag in 'no tag'
- >>> _get_message_target(course=None, message=None, original=None,
- ... person=None, subject='[] empty tag')
+ pygrader.handler.InvalidSubjectMessage: no tag in 'no tag'
+ >>> _get_message_target(subject='[] empty tag')
Traceback (most recent call last):
...
- ValueError: empty tag in '[] empty tag'
- >>> _get_message_target(course=None, message=None, original=None,
- ... person=None, subject='[abc] empty tag')
+ pygrader.handler.InvalidSubjectMessage: empty tag in '[] empty tag'
+ >>> _get_message_target(subject='[abc] empty tag')
'abc'
- >>> _get_message_target(course=None, message=None, original=None,
- ... person=None, subject='[phys160:abc] empty tag')
+ >>> _get_message_target(subject='[phys160:abc] empty tag')
'abc'
"""
match = _TAG_REGEXP.match(subject)
if match is None:
- response_subject = 'no tag in {!r}'.format(subject)
- if respond:
- _respond(
- course=course, person=person, original=original,
- subject=response_subject, text=(
- 'We received an email message from you without\n'
- 'subject tags.'),
- respond=respond)
- raise ValueError(response_subject)
+ raise _InvalidSubjectMessage(
+ subject=subject, error='no tag in {!r}'.format(subject))
tag = match.group(1)
if tag == '':
- response_subject = 'empty tag in {!r}'.format(subject)
- if respond:
- _respond(
- course=course, person=person, original=original,
- subject=response_subject, text=(
- 'We received an email message from you with empty\n'
- 'subject tags.'),
- respond=respond)
- raise ValueError(response_subject)
+ raise _InvalidSubjectMessage(
+ subject=subject, error='empty tag in {!r}'.format(subject))
target = tag.rsplit(':', 1)[-1]
_LOG.debug('extracted target {} -> {}'.format(subject, target))
return target
-def _get_handler(course, handlers, message, person, subject, target,
- respond=None, use_color=None):
+def _get_handler(handlers, target, use_color=None):
try:
handler = handlers[target]
- except KeyError:
+ 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))
- if respond:
- targets = sorted(handlers.keys())
- if not targets:
- hint = (
- 'In fact, there are no available handlers for this\n'
- 'course!\n')
- else:
- hint = (
- 'Perhaps you meant to use one of the following:\n'
- ' {}\n\n').format('\n '.join(targets))
- _respond(
- course=course, person=person, original=original,
- subject=response_subject, text=(
- 'We got an email from you with the following subject:\n'
- ' {!r}\n'
- 'which does not match any submittable handler name for\n'
- '{}.\n'
- '{}').format(repr(subject), course.name, hint),
- respond=respond)
- return None
+ raise InvalidHandlerMessage(target=target, handlers=handlers)
return handler
def _get_verified_message(message, pgp_key, use_color=None):
highlight,lowlight,good,bad = _standard_colors(use_color=use_color)
mid = message['message-id']
try:
- decrypted,verified,result = _verify(message=message)
+ decrypted,verified,result = _pgp_mime.verify(message=message)
except (ValueError, AssertionError):
_LOG.warn(_color_string(
string='could not verify {} (not signed?)'.format(mid),
decrypted[k] = v
decrypted.authenticated = True
return decrypted
+
+def _get_error_response(error):
+ author = error.course.robot
+ target = getattr(error, 'person', None)
+ subject = str(error)
+ if isinstance(error, InvalidHandlerMessage):
+ targets = sorted(error.handlers.keys())
+ if not targets:
+ hint = (
+ 'In fact, there are no available handlers for this\n'
+ 'course!')
+ else:
+ 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'
+ ' {!r}\n'
+ 'which does not match any submittable handler name for\n'
+ '{}.\n'
+ '{}').format(repr(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.'
+ elif isinstance(error, AmbiguousAddress):
+ text = (
+ 'Multiple people match {} ({})'.format(
+ error.address, ', '.join(p.name for p in error.people)))
+ elif isinstance(error, UnregisteredAddress):
+ target = _Person(name=error.address, emails=[error.address])
+ text = (
+ 'Your email address is not registered with pygrader for\n'
+ '{}. If you feel it should be, contact your professor\n'
+ 'or TA.').format(error.course.name)
+ elif isinstance(error, NoReturnPath):
+ return
+ elif isinstance(error, _InvalidSubjectMessage):
+ text = (
+ 'We received an email message from you with an invalid\n'
+ 'subject.')
+ 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):
+ 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):
+ text = (
+ 'We got 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, _InvalidMessage):
+ text = subject
+ else:
+ raise NotImplementedError((type(error), error))
+ if target is None:
+ raise NotImplementedError((type(error), error))
+ return _construct_response(
+ author=author,
+ targets=[target],
+ subject=subject,
+ text=(
+ '{},\n\n'
+ '{}\n\n'
+ 'Yours,\n'
+ '{}\n'.format(target.alias(), text, author.alias())),
+ original=error.message)