deleted file mode 100755
@@ -1,114 +0,0 @@
-#!/usr/bin/env python
-#
-# Patchwork - automated patch tracking system
-# Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
-#
-# This file is part of the Patchwork package.
-#
-# Patchwork is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# Patchwork is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Patchwork; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-
-from __future__ import absolute_import
-
-import argparse
-from email import message_from_file
-import logging
-import sys
-
-import django
-from django.conf import settings
-from django.utils.log import AdminEmailHandler
-
-from patchwork.parser import parse_mail
-
-LOGGER = logging.getLogger(__name__)
-
-VERBOSITY_LEVELS = {
- 'debug': logging.DEBUG,
- 'info': logging.INFO,
- 'warning': logging.WARNING,
- 'error': logging.ERROR,
- 'critical': logging.CRITICAL
-}
-
-
-extra_error_message = '''
-== Mail
-
-%(mail)s
-
-
-== Traceback
-
-'''
-
-
-def setup_error_handler():
- """Configure error handler.
-
- Ensure emails are send to settings.ADMINS when errors are
- encountered.
- """
- if settings.DEBUG:
- return
-
- mail_handler = AdminEmailHandler()
- mail_handler.setLevel(logging.ERROR)
- mail_handler.setFormatter(logging.Formatter(extra_error_message))
-
- logger = logging.getLogger('patchwork')
- logger.addHandler(mail_handler)
-
- return logger
-
-
-def main(args):
- django.setup()
- logger = setup_error_handler()
- parser = argparse.ArgumentParser()
-
- def list_logging_levels():
- """Give a summary of all available logging levels."""
- return sorted(list(VERBOSITY_LEVELS.keys()),
- key=lambda x: VERBOSITY_LEVELS[x])
-
- parser.add_argument('infile', nargs='?', type=argparse.FileType('r'),
- default=sys.stdin, help='input mbox file (a filename '
- 'or stdin)')
-
- group = parser.add_argument_group('Mail parsing configuration')
- group.add_argument('--list-id', help='mailing list ID. If not supplied '
- 'this will be extracted from the mail headers.')
- group.add_argument('--verbosity', choices=list_logging_levels(),
- help='debug level', default='info')
-
- args = vars(parser.parse_args())
-
- logging.basicConfig(level=VERBOSITY_LEVELS[args['verbosity']])
-
- mail = message_from_file(args['infile'])
- try:
- result = parse_mail(mail, args['list_id'])
- if result:
- return 0
- return 1
- except:
- if logger:
- logger.exception('Error when parsing incoming email', extra={
- 'mail': mail.as_string(),
- })
- raise
-
-if __name__ == '__main__':
- sys.exit(main(sys.argv))
@@ -24,6 +24,12 @@ PATCHWORK_BASE=`readlink -e $BIN_DIR/../..`
PYTHONPATH="$PATCHWORK_BASE":"$PATCHWORK_BASE/lib/python:$PYTHONPATH" \
DJANGO_SETTINGS_MODULE=patchwork.settings.production \
- "$PATCHWORK_BASE/patchwork/bin/parsemail.py"
+ "$PATCHWORK_BASE/manage.py" parsemail $@
+# NOTE(stephenfin): We must return 0 here. When parsemail is used as a
+# delivery command from a mail server like postfix (as it is intended
+# to be), a non-zero exit code will cause a bounce message to be
+# returned to the user. We don't want to do that for a parse error, so
+# always return 0. For more information, refer to
+# https://patchwork.ozlabs.org/patch/602248/
exit 0
new file mode 100644
@@ -0,0 +1,81 @@
+# Patchwork - automated patch tracking system
+# Copyright (C) 2016 Intel Corporation
+#
+# This file is part of the Patchwork package.
+#
+# Patchwork is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# Patchwork is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Patchwork; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import argparse
+from email import message_from_file
+import logging
+from optparse import make_option
+import sys
+
+import django
+from django.core.management import base
+
+from patchwork.parser import parse_mail
+
+logger = logging.getLogger(__name__)
+
+
+class Command(base.BaseCommand):
+ help = 'Parse an mbox file and store any patch/comment found.'
+
+ if django.VERSION < (1, 8):
+ args = '<infile>'
+ option_list = base.BaseCommand.option_list + (
+ make_option(
+ '--list-id',
+ help='mailing list ID. If not supplied, this will be '
+ 'extracted from the mail headers.'),
+ )
+ else:
+ def add_arguments(self, parser):
+ parser.add_argument(
+ 'infile',
+ nargs='?',
+ type=argparse.FileType('r'),
+ default=sys.stdin,
+ help='input mbox file (a filename or stdin)')
+ parser.add_argument(
+ '--list-id',
+ action='store_true',
+ help='mailing list ID. If not supplied, this will be '
+ 'extracted from the mail headers.')
+
+ def handle(self, *args, **options):
+ path = (args[0] if args else
+ options['infile'] if 'infile' in options else None)
+ stdin = options.get('stdin', sys.stdin)
+
+ # Attempt to parse the path if provided, and fallback to stdin if not
+ if path and not isinstance(path, file):
+ logger.info('Parsing mail loaded by filename')
+ with open(path, 'r+') as file_:
+ mail = message_from_file(file_)
+ else:
+ logger.info('Parsing mail loaded from stdin')
+ mail = message_from_file(stdin)
+
+ try:
+ result = parse_mail(mail, options['list_id'])
+ if result:
+ sys.exit(0)
+ logger.warning('Failed to parse mail')
+ sys.exit(1)
+ except Exception:
+ logger.exception('Error when parsing incoming email',
+ extra={'mail': mail.as_string()})
@@ -42,7 +42,7 @@ _hunk_re = re.compile(r'^\@\@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? \@\@')
_filename_re = re.compile(r'^(---|\+\+\+) (\S+)')
list_id_headers = ['List-ID', 'X-Mailing-List', 'X-list']
-LOGGER = logging.getLogger(__name__)
+logger = logging.getLogger(__name__)
def normalise_space(str):
@@ -599,7 +599,7 @@ def parse_mail(mail, list_id=None):
hint = mail.get('X-Patchwork-Hint', '').lower()
if hint == 'ignore':
- LOGGER.debug("Ignoring email due to 'ignore' hint")
+ logger.debug("Ignoring email due to 'ignore' hint")
return
if list_id:
@@ -608,7 +608,7 @@ def parse_mail(mail, list_id=None):
project = find_project_by_header(mail)
if project is None:
- LOGGER.error('Failed to find a project for email')
+ logger.error('Failed to find a project for email')
return
# parse content
@@ -651,7 +651,7 @@ def parse_mail(mail, list_id=None):
delegate=delegate,
state=find_state(mail))
patch.save()
- LOGGER.debug('Patch saved')
+ logger.debug('Patch saved')
return patch
elif x == 0: # (potential) cover letters
@@ -683,7 +683,7 @@ def parse_mail(mail, list_id=None):
submitter=author,
content=message)
cover_letter.save()
- LOGGER.debug('Cover letter saved')
+ logger.debug('Cover letter saved')
return cover_letter
@@ -704,7 +704,7 @@ def parse_mail(mail, list_id=None):
submitter=author,
content=message)
comment.save()
- LOGGER.debug('Comment saved')
+ logger.debug('Comment saved')
return comment
@@ -125,6 +125,8 @@ STATICFILES_DIRS = [
# Third-party application settings
#
+# rest_framework
+
try:
# django rest framework isn't a standard package in most distros, so
# don't make it compulsory
@@ -136,17 +138,65 @@ try:
except ImportError:
pass
-#
-# Third-party application settings
-#
-
-# rest_framework
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning'
}
#
+# Logging settings
+#
+
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'formatters': {
+ 'email': {
+ 'format': '== Mail\n\n%(mail)s\n\n== Traceback\n',
+ },
+ },
+ 'filters': {
+ 'require_debug_false': {
+ '()': 'django.utils.log.RequireDebugFalse',
+ },
+ 'require_debug_true': {
+ '()': 'django.utils.log.RequireDebugTrue',
+ },
+ },
+ 'handlers': {
+ 'console': {
+ 'level': 'DEBUG',
+ 'filters': ['require_debug_true'],
+ 'class': 'logging.StreamHandler',
+ },
+ 'mail_admins': {
+ 'level': 'ERROR',
+ 'filters': ['require_debug_false'],
+ 'class': 'django.utils.log.AdminEmailHandler',
+ 'formatter': 'email',
+ 'include_html': True,
+ },
+ },
+ 'loggers': {
+ 'django': {
+ 'handlers': ['console'],
+ 'level': 'INFO',
+ 'propagate': True,
+ },
+ 'patchwork.parser': {
+ 'handlers': ['console'],
+ 'level': 'DEBUG',
+ 'propagate': False,
+ },
+ 'patchwork.management.commands': {
+ 'handlers': ['console', 'mail_admins'],
+ 'level': 'INFO',
+ 'propagate': True,
+ },
+ },
+}
+
+#
# Patchwork settings
#
@@ -0,0 +1,23 @@
+# Patchwork - automated patch tracking system
+# Copyright (C) 2016 Stephen Finucane <stephenfinucane@hotmail.com>
+#
+# This file is part of the Patchwork package.
+#
+# Patchwork is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# Patchwork is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Patchwork; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import os
+
+TEST_MAIL_DIR = os.path.join(os.path.dirname(__file__), 'mail')
+TEST_PATCH_DIR = os.path.join(os.path.dirname(__file__), 'patches')
new file mode 100644
@@ -0,0 +1,51 @@
+# Patchwork - automated patch tracking system
+# Copyright (C) 2016 Stephen Finucane <stephenfinucane@hotmail.com>
+#
+# This file is part of the Patchwork package.
+#
+# Patchwork is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# Patchwork is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Patchwork; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+import os
+
+from django.core.management import call_command
+from django.test import TestCase
+from django.utils.six import StringIO
+
+from patchwork.tests import TEST_MAIL_DIR
+
+
+class ParsemailTest(TestCase):
+ def test_invalid_path(self):
+ with self.assertRaises(IOError):
+ call_command('parsemail', 'xyz123random')
+
+ def test_path_failure(self):
+ # we haven't created a project yet, so this will fail
+ with self.assertRaises(SystemExit) as exc:
+ call_command('parsemail',
+ os.path.join(TEST_MAIL_DIR,
+ '0001-git-pull-request.mbox'))
+
+ self.assertEqual(exc.exception.code, 1)
+
+ def test_stdin_failure(self):
+ # we haven't created a project yet, so this will fail
+ with open(os.path.join(TEST_MAIL_DIR,
+ '0001-git-pull-request.mbox')) as file_:
+ with self.assertRaises(SystemExit) as exc:
+ call_command('parsemail',
+ stdin=file_)
+
+ self.assertEqual(exc.exception.code, 1)
@@ -38,6 +38,7 @@ from patchwork.parser import parse_mail as _parse_mail
from patchwork.parser import parse_pull_request
from patchwork.parser import parse_series_marker
from patchwork.parser import split_prefixes
+from patchwork.tests import TEST_MAIL_DIR
from patchwork.tests.utils import create_project
from patchwork.tests.utils import create_state
from patchwork.tests.utils import create_user
@@ -45,9 +46,6 @@ from patchwork.tests.utils import read_patch
from patchwork.tests.utils import SAMPLE_DIFF
-TEST_MAIL_DIR = os.path.join(os.path.dirname(__file__), 'mail')
-
-
def read_mail(filename, project=None):
"""Read a mail from a file."""
file_path = os.path.join(TEST_MAIL_DIR, filename)
@@ -32,6 +32,7 @@ from patchwork.models import Patch
from patchwork.models import Person
from patchwork.models import Project
from patchwork.models import State
+from patchwork.tests import TEST_PATCH_DIR
SAMPLE_DIFF = """--- /dev/null 2011-01-01 00:00:00.000000000 +0800
+++ a 2011-01-01 00:00:00.000000000 +0800
@@ -39,7 +40,6 @@ SAMPLE_DIFF = """--- /dev/null 2011-01-01 00:00:00.000000000 +0800
+a
"""
SAMPLE_CONTENT = 'Hello, world.'
-TEST_PATCH_DIR = os.path.join(os.path.dirname(__file__), 'patches')
def read_patch(filename, encoding=None):