diff mbox

[09/11] parsemail: Convert to a management command

Message ID 1468402860-3409-10-git-send-email-stephen.finucane@intel.com
State Superseded
Headers show

Commit Message

Stephen Finucane July 13, 2016, 9:40 a.m. UTC
Management comands allow applications to register their own actions with
'manage.py'. This provides some advantages, like automatically
configuring Django (removing the need for 'django.setup' calls) and
removing the need to set the PYTHON_PATH. The 'parsemail' script is a
natural fit for this type of application. Migrate 'parsemail' to a
management command.

This includes some extensive work on logging configuration, as logging
is moved from code into settings. In addition, it removes a lot of the
customizable logging previously introduced in the parsemail command, in
favour of modifications to the settings files.

Signed-off-by: Stephen Finucane <stephen.finucane@intel.com>
Partial-bug: #17
Closes-bug: #15
---
 patchwork/bin/parsemail.py                 |  113 ----------------------------
 patchwork/bin/parsemail.sh                 |    2 +-
 patchwork/management/commands/parsemail.py |   63 +++++++++++++++
 patchwork/parser.py                        |   12 ++--
 patchwork/settings/base.py                 |   52 +++++++++++++
 5 files changed, 122 insertions(+), 120 deletions(-)
 delete mode 100755 patchwork/bin/parsemail.py
 create mode 100644 patchwork/management/commands/parsemail.py

Comments

Andy Doan July 19, 2016, 9:33 p.m. UTC | #1
On 07/13/2016 04:40 AM, Stephen Finucane wrote:
> Management comands allow applications to register their own actions with
> 'manage.py'. This provides some advantages, like automatically
> configuring Django (removing the need for 'django.setup' calls) and
> removing the need to set the PYTHON_PATH. The 'parsemail' script is a
> natural fit for this type of application. Migrate 'parsemail' to a
> management command.
>
> This includes some extensive work on logging configuration, as logging
> is moved from code into settings. In addition, it removes a lot of the
> customizable logging previously introduced in the parsemail command, in
> favour of modifications to the settings files.
>
> Signed-off-by: Stephen Finucane <stephen.finucane@intel.com>

Couple of questions:

> Partial-bug: #17
> Closes-bug: #15
> ---
>  patchwork/bin/parsemail.py                 |  113 ----------------------------
>  patchwork/bin/parsemail.sh                 |    2 +-
>  patchwork/management/commands/parsemail.py |   63 +++++++++++++++
>  patchwork/parser.py                        |   12 ++--
>  patchwork/settings/base.py                 |   52 +++++++++++++
>  5 files changed, 122 insertions(+), 120 deletions(-)
>  delete mode 100755 patchwork/bin/parsemail.py
>  create mode 100644 patchwork/management/commands/parsemail.py
>
> diff --git a/patchwork/bin/parsemail.py b/patchwork/bin/parsemail.py
> deleted file mode 100755
> index abcee04..0000000
> --- a/patchwork/bin/parsemail.py
> +++ /dev/null
> @@ -1,113 +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))
> diff --git a/patchwork/bin/parsemail.sh b/patchwork/bin/parsemail.sh
> index 9973392..5a8ca58 100755
> --- a/patchwork/bin/parsemail.sh
> +++ b/patchwork/bin/parsemail.sh
> @@ -24,6 +24,6 @@ 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/patchwork/manage.py parsemail"
>
>  exit 0

I've never been a consumer of this script, but it looks like you can't 
pass options to it, so it will only read patches from stdin. Wouldn't be 
a little cleaner if it was like:

PYTHONPATH="$PATCHWORK_BASE":"$PATCHWORK_BASE/lib/python:$PYTHONPATH" \
            DJANGO_SETTINGS_MODULE=patchwork.settings.production \
            exec "$PATCHWORK_BASE/patchwork/manage.py parsemail" $*

(NOTE i also added "exec" to be a little cleaner and exit with proper code)

> diff --git a/patchwork/management/commands/parsemail.py b/patchwork/management/commands/parsemail.py
> new file mode 100644
> index 0000000..ce6a63e
> --- /dev/null
> +++ b/patchwork/management/commands/parsemail.py
> @@ -0,0 +1,63 @@
> +# 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
> +
> +from email import message_from_file
> +import logging
> +from optparse import make_option
> +import sys
> +
> +from django.core.management import base
> +from django.utils import log

"log" looks like an unused import?

> +
> +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'
> +    args = '<infile>'  # Django < 1.8 compatibility
> +    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.'
> +        ),
> +    )
> +
> +    def handle(self, *args, **options):
> +        # Attempt to parse the path if provided, and fallback to stdin if not
> +        if args:
> +            logger.info('Parsing mail loaded by filename')
> +            with open(args[0]) as file_:
> +                mail = message_from_file(file_)
> +        else:
> +            logger.info('Parsing mail loaded from stdin')
> +            mail = message_from_file(sys.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 as exc:
> +            logger.exception('Error when parsing incoming email',
> +                             extra={'mail': mail.as_string()})
> +            raise exc

Do you really want to raise an exception since logger.exception will 
show the stack trace? Maybe just a sys.exit(1)?
Stephen Finucane Aug. 13, 2016, 11:49 p.m. UTC | #2
On 19 Jul 16:33, Andy Doan wrote:
> On 07/13/2016 04:40 AM, Stephen Finucane wrote:
> > Management comands allow applications to register their own actions with
> > 'manage.py'. This provides some advantages, like automatically
> > configuring Django (removing the need for 'django.setup' calls) and
> > removing the need to set the PYTHON_PATH. The 'parsemail' script is a
> > natural fit for this type of application. Migrate 'parsemail' to a
> > management command.
> > 
> > This includes some extensive work on logging configuration, as logging
> > is moved from code into settings. In addition, it removes a lot of the
> > customizable logging previously introduced in the parsemail command, in
> > favour of modifications to the settings files.
> > 
> > Signed-off-by: Stephen Finucane <stephen.finucane@intel.com>
> 
> Couple of questions:
> 
> > Partial-bug: #17
> > Closes-bug: #15
> > ---
> >  patchwork/bin/parsemail.py                 |  113 ----------------------------
> >  patchwork/bin/parsemail.sh                 |    2 +-
> >  patchwork/management/commands/parsemail.py |   63 +++++++++++++++
> >  patchwork/parser.py                        |   12 ++--
> >  patchwork/settings/base.py                 |   52 +++++++++++++
> >  5 files changed, 122 insertions(+), 120 deletions(-)
> >  delete mode 100755 patchwork/bin/parsemail.py
> >  create mode 100644 patchwork/management/commands/parsemail.py
> > 
> > diff --git a/patchwork/bin/parsemail.py b/patchwork/bin/parsemail.py
> > deleted file mode 100755
> > index abcee04..0000000
> > --- a/patchwork/bin/parsemail.py
> > +++ /dev/null
> > @@ -1,113 +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))
> > diff --git a/patchwork/bin/parsemail.sh b/patchwork/bin/parsemail.sh
> > index 9973392..5a8ca58 100755
> > --- a/patchwork/bin/parsemail.sh
> > +++ b/patchwork/bin/parsemail.sh
> > @@ -24,6 +24,6 @@ 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/patchwork/manage.py parsemail"
> > 
> >  exit 0
> 
> I've never been a consumer of this script, but it looks like you can't pass
> options to it, so it will only read patches from stdin. Wouldn't be a little
> cleaner if it was like:
> 
> PYTHONPATH="$PATCHWORK_BASE":"$PATCHWORK_BASE/lib/python:$PYTHONPATH" \
>            DJANGO_SETTINGS_MODULE=patchwork.settings.production \
>            exec "$PATCHWORK_BASE/patchwork/manage.py parsemail" $*
> 
> (NOTE i also added "exec" to be a little cleaner and exit with proper code)

This script is meant to be used by procmail or other MDAs. It's kept
simple because everything else should use the actual Python script.  In
addition, it must always return 0 because if you don't then procmail
does something funny. I can't remember what the funny thing, but Jeremy
reverted a previous patch, which should be in the archives somewhere,
for this very reason. I'll add a note here in the next version.

> > diff --git a/patchwork/management/commands/parsemail.py b/patchwork/management/commands/parsemail.py
> > new file mode 100644
> > index 0000000..ce6a63e
> > --- /dev/null
> > +++ b/patchwork/management/commands/parsemail.py
> > @@ -0,0 +1,63 @@
> > +# 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
> > +
> > +from email import message_from_file
> > +import logging
> > +from optparse import make_option
> > +import sys
> > +
> > +from django.core.management import base
> > +from django.utils import log
> 
> "log" looks like an unused import?

Yup, I forgot 'tox -e pep8'. Fixed in next version.

> > +
> > +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'
> > +    args = '<infile>'  # Django < 1.8 compatibility
> > +    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.'
> > +        ),
> > +    )
> > +
> > +    def handle(self, *args, **options):
> > +        # Attempt to parse the path if provided, and fallback to stdin if not
> > +        if args:
> > +            logger.info('Parsing mail loaded by filename')
> > +            with open(args[0]) as file_:
> > +                mail = message_from_file(file_)
> > +        else:
> > +            logger.info('Parsing mail loaded from stdin')
> > +            mail = message_from_file(sys.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 as exc:
> > +            logger.exception('Error when parsing incoming email',
> > +                             extra={'mail': mail.as_string()})
> > +            raise exc
> 
> Do you really want to raise an exception since logger.exception will show
> the stack trace? Maybe just a sys.exit(1)?

Good call. I'll do that.
diff mbox

Patch

diff --git a/patchwork/bin/parsemail.py b/patchwork/bin/parsemail.py
deleted file mode 100755
index abcee04..0000000
--- a/patchwork/bin/parsemail.py
+++ /dev/null
@@ -1,113 +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))
diff --git a/patchwork/bin/parsemail.sh b/patchwork/bin/parsemail.sh
index 9973392..5a8ca58 100755
--- a/patchwork/bin/parsemail.sh
+++ b/patchwork/bin/parsemail.sh
@@ -24,6 +24,6 @@  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/patchwork/manage.py parsemail"
 
 exit 0
diff --git a/patchwork/management/commands/parsemail.py b/patchwork/management/commands/parsemail.py
new file mode 100644
index 0000000..ce6a63e
--- /dev/null
+++ b/patchwork/management/commands/parsemail.py
@@ -0,0 +1,63 @@ 
+# 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
+
+from email import message_from_file
+import logging
+from optparse import make_option
+import sys
+
+from django.core.management import base
+from django.utils import log
+
+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'
+    args = '<infile>'  # Django < 1.8 compatibility
+    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.'
+        ),
+    )
+
+    def handle(self, *args, **options):
+        # Attempt to parse the path if provided, and fallback to stdin if not
+        if args:
+            logger.info('Parsing mail loaded by filename')
+            with open(args[0]) as file_:
+                mail = message_from_file(file_)
+        else:
+            logger.info('Parsing mail loaded from stdin')
+            mail = message_from_file(sys.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 as exc:
+            logger.exception('Error when parsing incoming email',
+                             extra={'mail': mail.as_string()})
+            raise exc
diff --git a/patchwork/parser.py b/patchwork/parser.py
index ef919c1..331641b 100644
--- a/patchwork/parser.py
+++ b/patchwork/parser.py
@@ -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
@@ -679,7 +679,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
 
@@ -700,7 +700,7 @@  def parse_mail(mail, list_id=None):
         submitter=author,
         content=message)
     comment.save()
-    LOGGER.debug('Comment saved')
+    logger.debug('Comment saved')
 
     return comment
 
diff --git a/patchwork/settings/base.py b/patchwork/settings/base.py
index 735c67a..94eb001 100644
--- a/patchwork/settings/base.py
+++ b/patchwork/settings/base.py
@@ -141,6 +141,58 @@  STATICFILES_DIRS = [
     os.path.join(ROOT_DIR, 'htdocs'),
 ]
 
+#
+# 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