merge feature/refactor fb_develop
authorfrostbane <frostbane@programmer.net>
Fri, 04 Mar 2016 09:51:09 +0900
branchfb_develop
changeset 81 a2bd41e34aca
parent 78 875b47ec73b4 (current diff)
parent 80 56034fb9ec0d (diff)
child 82 b143f9dcfb8a
child 86 10ea34bcd570
merge feature/refactor
--- a/.gitignore	Tue Mar 01 09:05:26 2016 +0900
+++ b/.gitignore	Fri Mar 04 09:51:09 2016 +0900
@@ -2,4 +2,7 @@
 artemis.egg-info/
 build/
 dist/
-
+.idea/*
+*.pyo
+*.pyc
+pycharm-debug.egg
--- a/.hgignore	Tue Mar 01 09:05:26 2016 +0900
+++ b/.hgignore	Fri Mar 04 09:51:09 2016 +0900
@@ -3,3 +3,9 @@
 artemis.egg-info/
 build/
 dist/
+.idea/*
+*.pyo
+*.pyc
+pycharm-debug.egg
+syntax: glob
+artemis.iml
--- a/artemis/__init__.py	Tue Mar 01 09:05:26 2016 +0900
+++ b/artemis/__init__.py	Fri Mar 04 09:51:09 2016 +0900
@@ -1,3 +1,3 @@
-from artemis import *
+from main import *
 
-__version__ = '0.5.0'
+__version__ = '0.5.1'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artemis/add.py	Fri Mar 04 09:51:09 2016 +0900
@@ -0,0 +1,224 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+from mercurial import hg, util, commands
+from mercurial.i18n import _
+import sys, os, time, random, mailbox, glob, socket, ConfigParser
+import mimetypes
+from email import encoders
+from email.generator import Generator
+from email.mime.audio import MIMEAudio
+from email.mime.base import MIMEBase
+from email.mime.image import MIMEImage
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+from itertools import izip
+from artemis import Artemis
+
+__author__ = 'frostbane'
+__date__ = '2016/03/04'
+
+
+class ArtemisAdd:
+    commands = [
+        ('a', 'attach', [], 'Attach file(s) '
+                            '(e.g., -a filename1 -a filename2,'
+                            '-a ~/Desktop/file.zip)'),
+        ('p', 'property', [], 'Update properties '
+                              '(e.g., -p state=fixed)'),
+        ('n', 'no-property-comment', None, 'Do not add a comment '
+                                           'about changed properties'),
+        ('m', 'message', '', 'Use <text> as an issue subject'),
+        ('i', 'index', '0', 'Index of the message to comment.'),
+        ('c', 'commit', False, 'Perform a commit after the addition')
+    ]
+    usage = 'hg iadd [OPTIONS] [ID] [COMMENT]'
+
+    def __init__(self):
+        pass
+
+    def attach_files(self, msg, filenames):
+        outer = MIMEMultipart()
+        for k in msg.keys():
+            outer[k] = msg[k]
+
+        outer.attach(MIMEText(msg.get_payload()))
+
+        for filename in filenames:
+            ctype, encoding = mimetypes.guess_type(filename)
+            if ctype is None or encoding is not None:
+                # No guess could be made, or the file is encoded
+                # (compressed), so use a generic bag-of-bits type.
+                ctype = 'application/octet-stream'
+            maintype, subtype = ctype.split('/', 1)
+            if maintype == 'text':
+                fp = open(filename)
+                # Note: we should handle calculating the charset
+                attachment = MIMEText(fp.read(), _subtype=subtype)
+                fp.close()
+            elif maintype == 'image':
+                fp = open(filename, 'rb')
+                attachment = MIMEImage(fp.read(), _subtype=subtype)
+                fp.close()
+            elif maintype == 'audio':
+                fp = open(filename, 'rb')
+                attachment = MIMEAudio(fp.read(), _subtype=subtype)
+                fp.close()
+            else:
+                fp = open(filename, 'rb')
+                attachment = MIMEBase(maintype, subtype)
+                attachment.set_payload(fp.read())
+                fp.close()
+                # Encode the payload using Base64
+                encoders.encode_base64(attachment)
+            # Set the filename parameter
+            attachment.add_header('Content-Disposition', 'attachment',
+                                  filename=os.path.basename(filename))
+            outer.attach(attachment)
+        return outer
+
+    def random_id(self):
+        return "%x" % random.randint(2 ** 63, 2 ** 64 - 1)
+
+    def add(self, ui, repo, id=None, **opts):
+        """Adds a new issue, or comment to an existing issue ID or its
+        comment COMMENT"""
+
+        comment = int(opts["index"])
+
+        # First, make sure issues have a directory
+        issues_dir = ui.config('artemis', 'issues',
+                               default=Artemis.default_issues_dir)
+        issues_path = os.path.join(repo.root, issues_dir)
+        if not os.path.exists(issues_path):
+            os.mkdir(issues_path)
+
+        if id:
+            issue_fn, issue_id = Artemis.find_issue(ui, repo, id)
+            if not issue_fn:
+                ui.warn('No such issue\n')
+                return
+            Artemis.create_missing_dirs(issues_path, issue_id)
+            mbox = mailbox.Maildir(issue_fn,
+                                   factory=mailbox.MaildirMessage)
+            keys = Artemis.order_keys_date(mbox)
+            root = keys[0]
+
+        user = ui.username()
+
+        default_issue_text = "From: %s\nDate: %s\n" % (
+            user, util.datestr(format=Artemis.date_format))
+        if not id:
+            default_issue_text += "State: %s\n" % Artemis.default_state
+            default_issue_text += "Subject: brief description\n\n"
+        else:
+            subject = \
+                mbox[(comment < len(mbox) and keys[comment]) or root][
+                    'Subject']
+            if not subject.startswith('Re: '):
+                subject = 'Re: ' + subject
+            default_issue_text += "Subject: %s\n\n" % subject
+        default_issue_text += "Detailed description."
+
+        # Get properties, and figure out if we need
+        # an explicit comment
+        properties = Artemis.get_properties(opts['property'])
+        no_comment = id and properties and opts['no_property_comment']
+        message = opts['message']
+
+        # Create the text
+        if message:
+            if not id:
+                state_str = 'State: %s\n' % Artemis.default_state
+            else:
+                state_str = ''
+            issue = "From: %s\nDate: %s\nSubject: %s\n%s" % \
+                    (user, util.datestr(format=Artemis.date_format),
+                     message,
+                     state_str)
+        elif not no_comment:
+            issue = ui.edit(default_issue_text, user)
+
+            if issue.strip() == '':
+                ui.warn('Empty issue, ignoring\n')
+                return
+            if issue.strip() == default_issue_text:
+                ui.warn('Unchanged issue text, ignoring\n')
+                return
+        else:
+            # Write down a comment about updated properties
+            properties_subject = ', '.join(
+                    ['%s=%s' % (property, value) for (property, value)
+                     in
+                     properties])
+
+            issue = "From: %s\nDate: %s\nSubject: changed " \
+                    "properties (%s)\n" % \
+                    (user, util.datestr(format=Artemis.date_format),
+                     properties_subject)
+
+        # Create the message
+        msg = mailbox.MaildirMessage(issue)
+        if opts['attach']:
+            outer = self.attach_files(msg, opts['attach'])
+
+        else:
+            outer = msg
+
+        # Pick random filename
+        if not id:
+            issue_fn = issues_path
+            while os.path.exists(issue_fn):
+                issue_id = self.random_id()
+                issue_fn = os.path.join(issues_path, issue_id)
+            mbox = mailbox.Maildir(issue_fn,
+                                   factory=mailbox.MaildirMessage)
+            keys = Artemis.order_keys_date(mbox)
+        # else: issue_fn already set
+
+        # Add message to the mailbox
+        mbox.lock()
+        if id and comment >= len(mbox):
+            ui.warn(
+                    'No such comment number in mailbox, commenting on the '
+                    'issue itself\n')
+
+        if not id:
+            outer.add_header('Message-Id', "<%s-0-artemis@%s>" % (
+                issue_id, socket.gethostname()))
+
+        else:
+            root = keys[0]
+            outer.add_header('Message-Id', "<%s-%s-artemis@%s>" % (
+                issue_id, self.random_id(), socket.gethostname()))
+            outer.add_header('References', mbox[
+                (comment < len(mbox) and keys[comment]) or root][
+                'Message-Id'])
+            outer.add_header('In-Reply-To', mbox[
+                (comment < len(mbox) and keys[comment]) or root][
+                'Message-Id'])
+        new_bug_path = issue_fn + '/new/' + mbox.add(outer)
+        commands.add(ui, repo, new_bug_path)
+
+        # Fix properties in the root message
+        if properties:
+            root = Artemis.find_root_key(mbox)
+            msg = mbox[root]
+            for property, value in properties:
+                if property in msg:
+                    msg.replace_header(property, value)
+                else:
+                    msg.add_header(property, value)
+            mbox[root] = msg
+
+        mbox.close()
+
+        if opts['commit']:
+            commands.commit(ui, repo, issue_fn)
+
+        # If adding issue, add the new mailbox to the repository
+        if not id:
+            ui.status('Added new issue %s\n' % issue_id)
+        else:
+            Artemis.show_mbox(ui, mbox, 0)
--- a/artemis/artemis.py	Tue Mar 01 09:05:26 2016 +0900
+++ b/artemis/artemis.py	Fri Mar 04 09:51:09 2016 +0900
@@ -1,10 +1,14 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
 # Author: Dmitriy Morozov <hg@foxcub.org>, 2007 -- 2009
+__author__ = "Dmitriy Morozov"
 
 """A very simple and lightweight issue tracker for Mercurial."""
 
 from mercurial import hg, util, commands
 from mercurial.i18n import _
-import os, time, random, mailbox, glob, socket, ConfigParser
+import sys, os, time, random, mailbox, glob, socket, ConfigParser
 import mimetypes
 from email import encoders
 from email.generator import Generator
@@ -14,500 +18,299 @@
 from email.mime.multipart import MIMEMultipart
 from email.mime.text import MIMEText
 from itertools import izip
+from properties import ArtemisProperties
 
 
-state = { 'new':      ['new'],
-          'resolved': ['fixed', 'resolved'] }
-default_state = 'new'
-default_issues_dir = ".issues"
-filter_prefix = ".filter"
-date_format = '%a, %d %b %Y %H:%M:%S %1%2'
-maildir_dirs = ['new','cur','tmp']
-default_format = '%(id)s (%(len)3d) [%(state)s]: %(Subject)s'
-
-def ilist(ui, repo, **opts):
-    """List issues associated with the project"""
-
-    # Process options
-    show_all = opts['all']
-    properties = []
-    match_date, date_match = False, lambda x: True
-    if opts['date']:
-        match_date, date_match = True, util.matchdate(opts['date'])
-    order = 'new'
-    if opts['order']:
-        order = opts['order']
-
-    # Formats
-    formats = _read_formats(ui)
-
-    # Find issues
-    issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
-    issues_path = os.path.join(repo.root, issues_dir)
-    if not os.path.exists(issues_path): return
-
-    issues = glob.glob(os.path.join(issues_path, '*'))
-
-    _create_all_missing_dirs(issues_path, issues)
+def singleton(cls):
+    instances = {}
 
-    # Process filter
-    if opts['filter']:
-        filters = glob.glob(os.path.join(issues_path, filter_prefix + '*'))
-        config = ConfigParser.SafeConfigParser()
-        config.read(filters)
-        if not config.has_section(opts['filter']):
-            ui.write('No filter %s defined\n' % opts['filter'])
-        else:
-            properties += config.items(opts['filter'])
-
-    cmd_properties = _get_properties(opts['property'])
-    list_properties = [p[0] for p in cmd_properties if len(p) == 1]
-    list_properties_dict = {}
-    properties += filter(lambda p: len(p) > 1, cmd_properties)
+    def getinstance():
+        if cls not in instances:
+            instances[cls] = cls()
 
-    summaries = []
-    for issue in issues:
-        mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
-        root = _find_root_key(mbox)
-        if not root: continue
-        property_match = True
-        for property,value in properties:
-            if value:
-                property_match = property_match and (mbox[root][property] == value)
-            else:
-                property_match = property_match and (property not in mbox[root])
-
-        if not show_all and (not properties or not property_match) and (properties or mbox[root]['State'].upper() in [f.upper() for f in state['resolved']]): continue
-        if match_date and not date_match(util.parsedate(mbox[root]['date'])[0]): continue
+        return instances[cls]
 
-        if not list_properties:
-            summaries.append((_summary_line(mbox, root, issue[len(issues_path)+1:], formats),     # +1 for trailing /
-                              _find_mbox_date(mbox, root, order)))
-        else:
-            for lp in list_properties:
-                if lp in mbox[root]:    list_properties_dict.setdefault(lp, set()).add(mbox[root][lp])
-
-    if not list_properties:
-        summaries.sort(lambda (s1,d1),(s2,d2): cmp(d2,d1))
-        for s,d in summaries:
-            ui.write(s + '\n')
-    else:
-        for lp in list_properties_dict.keys():
-            ui.write("%s:\n" % lp)
-            for value in sorted(list_properties_dict[lp]):
-                ui.write("  %s\n" % value)
+    return getinstance
 
 
-def iadd(ui, repo, id = None, comment = 0, **opts):
-    """Adds a new issue, or comment to an existing issue ID or its comment COMMENT"""
-
-    comment = int(comment)
-
-    # First, make sure issues have a directory
-    issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
-    issues_path = os.path.join(repo.root, issues_dir)
-    if not os.path.exists(issues_path): os.mkdir(issues_path)
+# @singleton
+class Artemis:
+    """Artemis static and common functions
+    """
+    state = {
+        'new'     : ['new'],
+        'resolved': [
+            'fixed',
+            'resolved'
+        ]
+    }
+    default_state = 'new'
+    default_issues_dir = ".issues"
+    filter_prefix = ".filter"
+    date_format = '%a, %d %b %Y %H:%M:%S %1%2'
+    maildir_dirs = ['new', 'cur', 'tmp']
+    default_format = '%(id)s (%(len)3d) [%(state)s]: %(Subject)s'
 
-    if id:
-        issue_fn, issue_id = _find_issue(ui, repo, id)
-        if not issue_fn:
-            ui.warn('No such issue\n')
-            return
-        _create_missing_dirs(issues_path, issue_id)
-        mbox = mailbox.Maildir(issue_fn, factory=mailbox.MaildirMessage)
-        keys = _order_keys_date(mbox)
-        root = keys[0]
-
-    user = ui.username()
+    # def __init__(self):
+    #     pass
 
-    default_issue_text  =         "From: %s\nDate: %s\n" % (user, util.datestr(format = date_format))
-    if not id:
-        default_issue_text +=     "State: %s\n" % default_state
-        default_issue_text +=     "Subject: brief description\n\n"
-    else:
-        subject = mbox[(comment < len(mbox) and keys[comment]) or root]['Subject']
-        if not subject.startswith('Re: '): subject = 'Re: ' + subject
-        default_issue_text +=     "Subject: %s\n\n" % subject
-    default_issue_text +=         "Detailed description."
-
-    # Get properties, and figure out if we need an explicit comment
-    properties = _get_properties(opts['property'])
-    no_comment = id and properties and opts['no_property_comment']
-    message = opts['message']
+    @staticmethod
+    def create_all_missing_dirs(issues_path, issues):
+        for issue in issues:
+            Artemis.create_missing_dirs(issues_path, issue)
 
-    # Create the text
-    if message:
-        if not id:
-            state_str = 'State: %s\n' % default_state
-        else:
-            state_str = ''
-        issue = "From: %s\nDate: %s\nSubject: %s\n%s" % \
-                (user, util.datestr(format=date_format), message, state_str)
-    elif not no_comment:
-        issue = ui.edit(default_issue_text, user)
+    @staticmethod
+    def find_issue(ui, repo, id):
+        issues_dir = ui.config('artemis', 'issues',
+                               default=Artemis.default_issues_dir)
+        issues_path = os.path.join(repo.root, issues_dir)
+        if not os.path.exists(issues_path):
+            return False
+
+        issues = glob.glob(os.path.join(issues_path, id + '*'))
+
+        if len(issues) == 0:
+            return False, 0
 
-        if issue.strip() == '':
-            ui.warn('Empty issue, ignoring\n')
-            return
-        if issue.strip() == default_issue_text:
-            ui.warn('Unchanged issue text, ignoring\n')
-            return
-    else:
-        # Write down a comment about updated properties
-        properties_subject = ', '.join(['%s=%s' % (property, value) for (property, value) in properties])
+        elif len(issues) > 1:
+            ui.status("Multiple choices:\n")
+            for issue in issues:
+                ui.status('  ', Artemis.get_issue_id(ui, repo, issue), '\n')
 
-        issue =     "From: %s\nDate: %s\nSubject: changed properties (%s)\n" % \
-                     (user, util.datestr(format = date_format), properties_subject)
+            return False, 0
 
-    # Create the message
-    msg = mailbox.MaildirMessage(issue)
-    if opts['attach']:
-        outer = _attach_files(msg, opts['attach'])
-    else:
-        outer = msg
+        else:
+            return issues[0], Artemis.get_issue_id(ui, repo, issues[0])
 
-    # Pick random filename
-    if not id:
-        issue_fn = issues_path
-        while os.path.exists(issue_fn):
-            issue_id = _random_id()
-            issue_fn = os.path.join(issues_path, issue_id)
-        mbox = mailbox.Maildir(issue_fn, factory=mailbox.MaildirMessage)
-        keys = _order_keys_date(mbox)
-    # else: issue_fn already set
+    @staticmethod
+    def get_issues_dir(ui):
+        issues_dir = ui.config('artemis',
+                               'issues',
+                               default=Artemis.default_issues_dir)
+
+        return issues_dir
 
-    # Add message to the mailbox
-    mbox.lock()
-    if id and comment >= len(mbox):
-        ui.warn('No such comment number in mailbox, commenting on the issue itself\n')
+    @staticmethod
+    def get_issues_path(ui, repo):
+        """gets the full path of the issues directory. returns nothing
+        if the path does not exist.
 
-    if not id:
-        outer.add_header('Message-Id', "<%s-0-artemis@%s>" % (issue_id, socket.gethostname()))
-    else:
-        root = keys[0]
-        outer.add_header('Message-Id', "<%s-%s-artemis@%s>" % (issue_id, _random_id(), socket.gethostname()))
-        outer.add_header('References', mbox[(comment < len(mbox) and keys[comment]) or root]['Message-Id'])
-        outer.add_header('In-Reply-To', mbox[(comment < len(mbox) and keys[comment]) or root]['Message-Id'])
-    new_bug_path = issue_fn + '/new/' + mbox.add(outer)
-    commands.add(ui, repo, new_bug_path)
+        :Example:
+            issues_path = Artemis.get_issues_path(ui, repo)
+            if not issues_path:
+                # error
+            else
+                # path exists
 
-    # Fix properties in the root message
-    if properties:
-        root = _find_root_key(mbox)
-        msg = mbox[root]
-        for property, value in properties:
-            if property in msg:
-                msg.replace_header(property, value)
-            else:
-                msg.add_header(property, value)
-        mbox[root] = msg
+        """
+        issues_dir = Artemis.get_issues_dir(ui)
+        issues_path = os.path.join(repo.root, issues_dir)
+
+        if not os.path.exists(issues_path):
+            return
+
+        return issues_path
 
-    mbox.close()
-
-    if opts['commit']:
-        commands.commit(ui, repo, issue_fn)
+    @staticmethod
+    def get_all_issues(ui, repo):
+        # Find issues
+        issues_path = Artemis.get_issues_path(ui, repo)
+        if not issues_path:
+            return []
 
-    # If adding issue, add the new mailbox to the repository
-    if not id:
-        ui.status('Added new issue %s\n' % issue_id)
-    else:
-        _show_mbox(ui, mbox, 0)
-
-def ishow(ui, repo, id, comment = 0, **opts):
-    """Shows issue ID, or possibly its comment COMMENT"""
+        issues = glob.glob(os.path.join(issues_path, '*'))
 
-    comment = int(comment)
-    issue, id = _find_issue(ui, repo, id)
-    if not issue:
-        return ui.warn('No such issue\n')
+        Artemis.create_all_missing_dirs(issues_path, issues)
 
-    issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
-    _create_missing_dirs(os.path.join(repo.root, issues_dir), issue)
-
-    if opts.get('mutt'):
-        return util.system('mutt -R -f %s' % issue)
+        return issues
 
-    mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
-
-    if opts['all']:
-        ui.write('='*70 + '\n')
-        i = 0
-        keys = _order_keys_date(mbox)
-        for k in keys:
-            _write_message(ui, mbox[k], i, skip = opts['skip'])
-            ui.write('-'*70 + '\n')
-            i += 1
-        return
+    @staticmethod
+    def get_all_mboxes(ui, repo):
+        """gets a list of all available mboxes with an added extra
+        property "issue"
 
-    _show_mbox(ui, mbox, comment, skip = opts['skip'])
+        :param ui: mercurial ui object
+        :param repo: mercurial repo object
+        :return: list of all available mboxes
+        """
+        issues = Artemis.get_all_issues(ui, repo)
+        if not issues:
+            return []
 
-    if opts['extract']:
-        attachment_numbers = map(int, opts['extract'])
-        keys = _order_keys_date(mbox)
-        msg = mbox[keys[comment]]
-        counter = 1
-        for part in msg.walk():
-            ctype = part.get_content_type()
-            maintype, subtype = ctype.split('/', 1)
-            if maintype == 'multipart' or ctype == 'text/plain': continue
-            if counter in attachment_numbers:
-                filename = part.get_filename()
-                if not filename:
-                    ext = mimetypes.guess_extension(part.get_content_type()) or ''
-                    filename = 'attachment-%03d%s' % (counter, ext)
-                else:
-                    filename = os.path.basename(filename)
-                fp = open(filename, 'wb')
-                fp.write(part.get_payload(decode = True))
-                fp.close()
-            counter += 1
+        mboxes = []
+        for issue in issues:
+            mbox = mailbox.Maildir(issue,
+                                   factory=mailbox.MaildirMessage)
+
+            root = Artemis.find_root_key(mbox)
+            if not root:  # root is None
+                continue
+
+            # add extra property
+            mbox.issue = issue
+            mboxes.append(mbox)
+
+        return mboxes
 
 
-def _find_issue(ui, repo, id):
-    issues_dir = ui.config('artemis', 'issues', default = default_issues_dir)
-    issues_path = os.path.join(repo.root, issues_dir)
-    if not os.path.exists(issues_path): return False
-
-    issues = glob.glob(os.path.join(issues_path, id + '*'))
+    @staticmethod
+    def get_properties(property_list):
+        return [p.split('=') for p in property_list]
 
-    if len(issues) == 0:
-        return False, 0
-    elif len(issues) > 1:
-        ui.status("Multiple choices:\n")
-        for i in issues: ui.status('  ', i[len(issues_path)+1:], '\n')
-        return False, 0
+    @staticmethod
+    def write_message(ui, message, index=0, skip=None):
+        if index:
+            ui.write("Comment: %d\n" % index)
 
-    return issues[0], issues[0][len(issues_path)+1:]
-
-def _get_properties(property_list):
-    return [p.split('=') for p in property_list]
+        if ui.verbose:
+            Artemis.show_text(ui, message.as_string().strip(), skip)
+            return
 
-def _write_message(ui, message, index = 0, skip = None):
-    if index: ui.write("Comment: %d\n" % index)
-    if ui.verbose:
-        _show_text(ui, message.as_string().strip(), skip)
-    else:
-        if 'From' in message: ui.write('From: %s\n' % message['From'])
-        if 'Date' in message: ui.write('Date: %s\n' % message['Date'])
-        if 'Subject' in message: ui.write('Subject: %s\n' % message['Subject'])
-        if 'State' in message: ui.write('State: %s\n' % message['State'])
+        if 'From' in message:
+            ui.write('From: %s\n' % message['From'])
+        if 'Date' in message:
+            ui.write('Date: %s\n' % message['Date'])
+        if 'Subject' in message:
+            ui.write('Subject: %s\n' % message['Subject'])
+        if 'State' in message:
+            ui.write('State: %s\n' % message['State'])
+
         counter = 1
         for part in message.walk():
             ctype = part.get_content_type()
             maintype, subtype = ctype.split('/', 1)
-            if maintype == 'multipart': continue
+
+            if maintype == 'multipart':
+                continue
+
             if ctype == 'text/plain':
                 ui.write('\n')
-                _show_text(ui, part.get_payload().strip(), skip)
+                Artemis.show_text(ui, part.get_payload().strip(),
+                                  skip)
+
             else:
                 filename = part.get_filename()
-                ui.write('\n' + '%d: Attachment [%s, %s]: %s' % (counter, ctype, _humanreadable(len(part.get_payload())), filename) + '\n')
+                ui.write('\n' + '%d: Attachment [%s, %s]: %s' % (
+                    counter, ctype,
+                    Artemis.humanreadable(len(part.get_payload())),
+                    filename) + '\n')
                 counter += 1
 
-def _show_text(ui, text, skip = None):
-    for line in text.splitlines():
-        if not skip or not line.startswith(skip):
-            ui.write(line + '\n')
-    ui.write('\n')
+    @staticmethod
+    def show_text(ui, text, skip=None):
+        for line in text.splitlines():
+            if not skip or not line.startswith(skip):
+                ui.write(line + '\n')
+        ui.write('\n')
 
-def _show_mbox(ui, mbox, comment, **opts):
-    # Output the issue (or comment)
-    if comment >= len(mbox):
-        comment = 0
-        ui.warn('Comment out of range, showing the issue itself\n')
-    keys = _order_keys_date(mbox)
-    root = keys[0]
-    msg = mbox[keys[comment]]
-    ui.write('='*70 + '\n')
-    if comment:
-        ui.write('Subject: %s\n' % mbox[root]['Subject'])
-        ui.write('State: %s\n' % mbox[root]['State'])
-        ui.write('-'*70 + '\n')
-    _write_message(ui, msg, comment, skip = ('skip' in opts) and opts['skip'])
-    ui.write('-'*70 + '\n')
+    @staticmethod
+    def show_mbox(ui, mbox, comment, **opts):
+        # Output the issue (or comment)
+        if comment >= len(mbox):
+            comment = 0
+            ui.warn(
+                'Comment out of range, showing the issue itself\n')
 
-    # Read the mailbox into the messages and children dictionaries
-    messages = {}
-    children = {}
-    i = 0
-    for k in keys:
-        m = mbox[k]
-        messages[m['Message-Id']] = (i,m)
-        children.setdefault(m['In-Reply-To'], []).append(m['Message-Id'])
-        i += 1
-    children[None] = []                # Safeguard against infinte loop on empty Message-Id
+        keys = Artemis.order_keys_date(mbox)
+        root = keys[0]
+        msg = mbox[keys[comment]]
+        ui.write('=' * 70 + '\n')
+
+        if comment:
+            ui.write('Subject: %s\n' % mbox[root]['Subject'])
+            ui.write('State: %s\n' % mbox[root]['State'])
+            ui.write('-' * 70 + '\n')
 
-    # Iterate over children
-    id = msg['Message-Id']
-    id_stack = (id in children and map(lambda x: (x, 1), reversed(children[id]))) or []
-    if not id_stack: return
-    ui.write('Comments:\n')
-    while id_stack:
-        id,offset = id_stack.pop()
-        id_stack += (id in children and map(lambda x: (x, offset+1), reversed(children[id]))) or []
-        index, msg = messages[id]
-        ui.write('  '*offset + '%d: [%s] %s\n' % (index, util.shortuser(msg['From']), msg['Subject']))
-    ui.write('-'*70 + '\n')
-
-def _find_root_key(maildir):
-    for k,m in maildir.iteritems():
-        if 'in-reply-to' not in m:
-            return k
-
-def _order_keys_date(mbox):
-    keys = mbox.keys()
-    root = _find_root_key(mbox)
-    keys.sort(lambda k1,k2: -(k1 == root) or cmp(util.parsedate(mbox[k1]['date']), util.parsedate(mbox[k2]['date'])))
-    return keys
+        Artemis.write_message(ui, msg, comment,
+                              skip=('skip' in opts) and opts['skip'])
+        ui.write('-' * 70 + '\n')
 
-def _find_mbox_date(mbox, root, order):
-    if order == 'latest':
-        keys = _order_keys_date(mbox)
-        msg = mbox[keys[-1]]
-    else:   # new
-        msg = mbox[root]
-    return util.parsedate(msg['date'])
-
-def _random_id():
-    return "%x" % random.randint(2**63, 2**64-1)
+        # Read the mailbox into the messages and children dictionaries
+        messages = {}
+        children = {}
+        i = 0
+        for k in keys:
+            m = mbox[k]
+            messages[m['Message-Id']] = (i, m)
+            children.setdefault(m['In-Reply-To'], []).append(
+                m['Message-Id'])
+            i += 1
 
-def _create_missing_dirs(issues_path, issue):
-    for d in maildir_dirs:
-        path = os.path.join(issues_path,issue,d)
-        if not os.path.exists(path): os.mkdir(path)
+        # Safeguard against infinte loop on empty Message-Id
+        children[
+            None] = []
 
-def _create_all_missing_dirs(issues_path, issues):
-    for i in issues:
-        _create_missing_dirs(issues_path, i)
-
-def _humanreadable(size):
-    if size > 1024*1024:
-        return '%5.1fM' % (float(size) / (1024*1024))
-    elif size > 1024:
-        return '%5.1fK' % (float(size) / 1024)
-    else:
-        return '%dB' % size
+        # Iterate over children
+        id = msg['Message-Id']
+        id_stack = (id in children and
+                    map(lambda x: (x, 1),
+                        reversed(children[id]))) or []
+        if not id_stack:
+            return
 
-def _attach_files(msg, filenames):
-    outer = MIMEMultipart()
-    for k in msg.keys(): outer[k] = msg[k]
-    outer.attach(MIMEText(msg.get_payload()))
+        ui.write('Comments:\n')
+        while id_stack:
+            id, offset = id_stack.pop()
+            id_stack += (id in children and
+                         map(lambda x: (x, offset + 1),
+                             reversed(children[id]))
+                         ) or []
+
+            index, msg = messages[id]
+            ui.write('  ' * offset +
+                     '%d: [%s] %s\n' %
+                     (index,
+                      util.shortuser(msg['From']), msg['Subject']))
 
-    for filename in filenames:
-        ctype, encoding = mimetypes.guess_type(filename)
-        if ctype is None or encoding is not None:
-            # No guess could be made, or the file is encoded (compressed), so
-            # use a generic bag-of-bits type.
-            ctype = 'application/octet-stream'
-        maintype, subtype = ctype.split('/', 1)
-        if maintype == 'text':
-            fp = open(filename)
-            # Note: we should handle calculating the charset
-            attachment = MIMEText(fp.read(), _subtype=subtype)
-            fp.close()
-        elif maintype == 'image':
-            fp = open(filename, 'rb')
-            attachment = MIMEImage(fp.read(), _subtype=subtype)
-            fp.close()
-        elif maintype == 'audio':
-            fp = open(filename, 'rb')
-            attachment = MIMEAudio(fp.read(), _subtype=subtype)
-            fp.close()
-        else:
-            fp = open(filename, 'rb')
-            attachment = MIMEBase(maintype, subtype)
-            attachment.set_payload(fp.read())
-            fp.close()
-            # Encode the payload using Base64
-            encoders.encode_base64(attachment)
-        # Set the filename parameter
-        attachment.add_header('Content-Disposition', 'attachment', filename=os.path.basename(filename))
-        outer.attach(attachment)
-    return outer
+        ui.write('-' * 70 + '\n')
+
+    @staticmethod
+    def find_root_key(maildir):
+        for k, m in maildir.iteritems():
+            if 'in-reply-to' not in m:
+                return k
 
-def _read_formats(ui):
-    formats = []
-    global default_format
+    @staticmethod
+    def order_keys_date(mbox):
+        keys = mbox.keys()
+        root = Artemis.find_root_key(mbox)
+        keys.sort(lambda k1, k2: -(k1 == root) or
+                                 cmp(util.parsedate(mbox[k1]['date']),
+                                     util.parsedate(
+                                         mbox[k2]['date'])))
 
-    for k,v in ui.configitems('artemis'):
-        if not k.startswith('format'): continue
-        if k == 'format':
-            default_format = v
-            continue
-        formats.append((k.split(':')[1], v))
-
-    return formats
+        return keys
 
-def _format_match(props, formats):
-    for k,v in formats:
-        eq = k.split('&')
-        eq = [e.split('*') for e in eq]
-        for e in eq:
-            if props[e[0]] != e[1]:
-                break
-        else:
-            return v
+    @staticmethod
+    def create_missing_dirs(issues_path, issue):
+        for dir in Artemis.maildir_dirs:
+            path = os.path.join(issues_path, issue, dir)
+            if not os.path.exists(path):
+                os.mkdir(path)
 
-    return default_format
+    @staticmethod
+    def humanreadable(size):
+        if size > 1024 * 1024:
+            return '%5.1fM' % (float(size) / (1024 * 1024))
 
-def _summary_line(mbox, root, issue, formats):
-    props = PropertiesDictionary(mbox[root])
-    props['id']  = issue
-    props['len'] = len(mbox)-1              # number of replies (-1 for self)
+        elif size > 1024:
+            return '%5.1fK' % (float(size) / 1024)
 
-    return _format_match(props, formats) % props
+        else:
+            return '%dB' % size
 
-class PropertiesDictionary(dict):
-    def __init__(self, msg):
-        # Borrowed from termcolor
-        for k,v in zip(['bold', 'dark', '', 'underline', 'blink', '', 'reverse', 'concealed'], range(1, 9)) + \
-                   zip(['grey', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'], range(30, 38)):
-            self[k] = '\033[' + str(v) + 'm'
-        self['reset']  = '\033[0m'
-        del self['']
+    @staticmethod
+    def get_issue_id(ui, repo, issue):
+        """get the issue id provided the issue"""
+        # Find issues
+        issues_dir = ui.config('artemis', 'issues',
+                               default=Artemis.default_issues_dir)
+        issues_path = os.path.join(repo.root, issues_dir)
+        if not os.path.exists(issues_path):
+            return ""
 
-        for k,v in msg.items():
-            self[k] = v
-
-    def __contains__(self, k):
-        return super(PropertiesDictionary, self).__contains__(k.lower())
-
-    def __getitem__(self, k):
-        if k not in self: return ''
-        return super(PropertiesDictionary, self).__getitem__(k.lower())
-
-    def __setitem__(self, k, v):
-        super(PropertiesDictionary, self).__setitem__(k.lower(), v)
+        # +1 for trailing /
+        return issue[len(issues_path) + 1:]
 
 
-cmdtable = {
-    'ilist':    (ilist,
-                 [('a', 'all', False,
-                   'list all issues (by default only those with state new)'),
-                  ('p', 'property', [],
-                   'list issues with specific field values (e.g., -p state=fixed); lists all possible values of a property if no = sign'),
-                  ('o', 'order', 'new', 'order of the issues; choices: "new" (date submitted), "latest" (date of the last message)'),
-                  ('d', 'date', '', 'restrict to issues matching the date (e.g., -d ">12/28/2007)"'),
-                  ('f', 'filter', '', 'restrict to pre-defined filter (in %s/%s*)' % (default_issues_dir, filter_prefix))],
-                 _('hg ilist [OPTIONS]')),
-    'iadd':       (iadd,
-                 [('a', 'attach', [],
-                   'attach file(s) (e.g., -a filename1 -a filename2)'),
-                  ('p', 'property', [],
-                   'update properties (e.g., -p state=fixed)'),
-                  ('n', 'no-property-comment', None,
-                   'do not add a comment about changed properties'),
-                  ('m', 'message', '',
-                   'use <text> as an issue subject'),
-                  ('c', 'commit', False,
-                   'perform a commit after the addition')],
-                 _('hg iadd [OPTIONS] [ID] [COMMENT]')),
-    'ishow':      (ishow,
-                 [('a', 'all', None, 'list all comments'),
-                  ('s', 'skip', '>', 'skip lines starting with a substring'),
-                  ('x', 'extract', [], 'extract attachments (provide attachment number as argument)'),
-                  ('', 'mutt', False, 'use mutt to show issue')],
-                 _('hg ishow [OPTIONS] ID [COMMENT]')),
-}
-
 # vim: expandtab
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artemis/list.py	Fri Mar 04 09:51:09 2016 +0900
@@ -0,0 +1,219 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from mercurial import hg, util, commands
+import sys, os, time, random, mailbox, glob, socket, ConfigParser
+from properties import ArtemisProperties
+from artemis import Artemis
+
+__author__ = 'frostbane'
+__date__ = '2016/03/04'
+
+
+class ArtemisList:
+    commands = [
+        ('a', 'all', False, 'list all issues (by default only those '
+                            'with state new)'),
+        ('p', 'property', [], 'list issues with specific field '
+                              'values (e.g., -p state=fixed); '
+                              'lists all possible values of a '
+                              'property if no = sign'),
+        ("", "all-properties", None, "list all available properties"),
+        ('o', 'order', 'new', 'order of the issues; '
+                              'choices: "new" (date submitted), '
+                              '"latest" (date of the last message)'),
+        ('d', 'date', '', 'restrict to issues matching the date '
+                          '(e.g., -d ">12/28/2007)"'),
+        ('f', 'filter', '', 'restrict to pre-defined filter '
+                            '(in %s/%s*)'
+                            '' % (Artemis.default_issues_dir,
+                                  Artemis.filter_prefix))
+    ]
+
+    usage = 'hg ilist [OPTIONS]'
+
+    def __init__(self):
+        pass
+
+    def __get_properties(self, ui, repo):
+        """get the list of all available properties
+        :param ui: mercurial ui object
+        :param repo: mercurial repo object
+        :return: returns a list of all available properties
+        """
+
+        properties = []
+
+        mboxes = Artemis.get_all_mboxes(ui, repo)
+        for mbox in mboxes:
+            root = Artemis.find_root_key(mbox)
+            properties = list(set(properties + mbox[root].keys()))
+
+        return properties
+
+    def __read_formats(self, ui):
+        formats = []
+
+        for key, value in ui.configitems('artemis'):
+            if not key.startswith('format'):
+                continue
+            if key == 'format':
+                Artemis.default_format = value
+                continue
+            formats.append((key.split(':')[1], value))
+
+        return formats
+
+    def __format_match(self, props, formats):
+        for key, value in formats:
+            eq = key.split('&')
+            eq = [e.split('*') for e in eq]
+
+            for e in eq:
+                # todo check if else
+                if props[e[0]] != e[1]:
+                    break
+            else:
+                return value
+
+        return Artemis.default_format
+
+    def __summary_line(self, mbox, root, issue, formats):
+        props = ArtemisProperties(mbox[root])
+        props['id'] = issue
+        # number of replies (-1 for self)
+        props['len'] = len(mbox) - 1
+
+        return self.__format_match(props, formats) % props
+
+    def __find_mbox_date(self, mbox, root, order):
+        if order == 'latest':
+            keys = Artemis.order_keys_date(mbox)
+            msg = mbox[keys[-1]]
+
+        else:  # new
+            msg = mbox[root]
+
+        return util.parsedate(msg['date'])
+
+    def __find_issues(self, ui, repo):
+        issues_path = Artemis.get_issues_path(ui, repo)
+        if not issues_path:
+            return
+
+        issues = Artemis.get_all_issues(ui, repo)
+
+        return issues
+
+    def __proc_filters(self, properties, ui, repo, opts):
+        if opts['filter']:
+            filters = glob.glob(
+                os.path.join(Artemis.get_issues_path(ui, repo),
+                             Artemis.filter_prefix + '*'))
+            config = ConfigParser.SafeConfigParser()
+            config.read(filters)
+            if not config.has_section(opts['filter']):
+                ui.write('No filter %s defined\n' % opts['filter'])
+            else:
+                properties += config.items(opts['filter'])
+
+        return properties
+
+    def __list_summaries(self, issues, properties, ui, repo, opts):
+        # Process options
+        show_all = opts['all']
+        match_date, date_match = False, lambda x: True
+        if opts['date']:
+            match_date, date_match = True, util.matchdate(
+                opts['date'])
+        order = 'new'
+        if opts['order']:
+            order = opts['order']
+
+        # Formats
+        formats = self.__read_formats(ui)
+
+        cmd_properties = Artemis.get_properties(opts['property'])
+        list_properties = [p[0] for p in cmd_properties if
+                           len(p) == 1]
+        list_properties_dict = {}
+        properties += filter(lambda p: len(p) > 1, cmd_properties)
+
+        summaries = []
+        for issue in issues:
+            mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
+            root = Artemis.find_root_key(mbox)
+            if not root:
+                continue
+
+            property_match = True
+            for property, value in properties:
+                if value:
+                    property_match = property_match and (
+                        mbox[root][property] == value)
+                else:
+                    property_match = property_match and (
+                        property not in mbox[root])
+
+            has_property = properties or \
+                           mbox[root]['State'].upper() in \
+                           [f.upper() for f in Artemis.state['resolved']]
+
+            if not show_all and (not properties or
+                        not property_match) and has_property:
+                continue
+
+            if match_date and not date_match(
+                    util.parsedate(mbox[root]['date'])[0]):
+                continue
+
+            if not list_properties:
+                mbox_date = self.__find_mbox_date(mbox, root, order)
+                sum_line = self.__summary_line(
+                    mbox,
+                    root,
+                    Artemis.get_issue_id(ui, repo, issue),
+                    formats)
+
+                summaries.append((sum_line, mbox_date))
+
+            else:
+                for lp in list_properties:
+                    if lp in mbox[root]:
+                        list_properties_dict\
+                            .setdefault(lp, set())\
+                            .add(mbox[root][lp])
+
+        if not list_properties:
+            summaries.sort(lambda (s1, d1), (s2, d2): cmp(d2, d1))
+            for s, d in summaries:
+                ui.write(s + '\n')
+
+        else:
+            for lp in list_properties_dict.keys():
+                ui.write("%s:\n" % lp)
+                for value in sorted(list_properties_dict[lp]):
+                    ui.write("  %s\n" % value)
+
+    def list(self, ui, repo, **opts):
+        """List issues associated with the project"""
+
+        # Find issues
+        issues = self.__find_issues(ui, repo)
+        if not issues:
+            return
+
+        properties = []
+        if opts["all_properties"]:
+            properties = self.__get_properties(ui, repo)
+            for property in properties:
+                ui.write("%s\n" % property)
+
+            return
+
+        # Process filter
+        properties = self.__proc_filters(properties, ui, repo, opts)
+
+        self.__list_summaries(issues, properties, ui, repo, opts)
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artemis/main.py	Fri Mar 04 09:51:09 2016 +0900
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from mercurial import hg, util, commands
+from mercurial.i18n import _
+import sys, os, time, random, mailbox, glob, socket, ConfigParser
+import mimetypes
+from email import encoders
+from email.generator import Generator
+from email.mime.audio import MIMEAudio
+from email.mime.base import MIMEBase
+from email.mime.image import MIMEImage
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+from itertools import izip
+from artemis import Artemis
+from list import ArtemisList
+from add import ArtemisAdd
+from show import ArtemisShow
+
+__author__ = 'frostbane'
+__date__ = '2016/03/02'
+
+
+cmdtable = {
+    'ilist'       : (ArtemisList().list,
+                     ArtemisList.commands,
+                     _(ArtemisList.usage)),
+    'iadd'        : (ArtemisAdd().add,
+                     ArtemisAdd.commands,
+                     _(ArtemisAdd.usage)),
+    'ishow'       : (ArtemisShow().show,
+                     ArtemisShow.commands,
+                     _(ArtemisShow.usage)),
+}
+
+if __name__ == "__main__":
+    pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artemis/properties.py	Fri Mar 04 09:51:09 2016 +0900
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+__author__ = 'frostbane'
+__date__ = '2016/03/03'
+
+class ArtemisProperties(dict):
+    def __init__(self, msg):
+        # Borrowed from termcolor
+        for k, v in zip(['bold', 'dark', '', 'underline', 'blink', '',
+                         'reverse', 'concealed'], range(1, 9)) + \
+                zip(['grey', 'red', 'green', 'yellow', 'blue',
+                     'magenta', 'cyan', 'white'], range(30, 38)):
+            self[k] = '\033[' + str(v) + 'm'
+        self['reset'] = '\033[0m'
+        del self['']
+
+        for k, v in msg.items():
+            self[k] = v
+
+    def __contains__(self, k):
+        return super(ArtemisProperties, self).__contains__(
+            k.lower())
+
+    def __getitem__(self, k):
+        if k not in self:
+            return ''
+        return super(ArtemisProperties, self).__getitem__(
+            k.lower())
+
+    def __setitem__(self, k, v):
+        super(ArtemisProperties, self).__setitem__(k.lower(), v)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/artemis/show.py	Fri Mar 04 09:51:09 2016 +0900
@@ -0,0 +1,127 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from mercurial import hg, util, commands
+from mercurial.i18n import _
+import sys, os, time, random, mailbox, glob, socket, ConfigParser
+import mimetypes
+from email import encoders
+from email.generator import Generator
+from email.mime.audio import MIMEAudio
+from email.mime.base import MIMEBase
+from email.mime.image import MIMEImage
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+from itertools import izip
+from artemis import Artemis
+
+__author__ = 'frostbane'
+__date__ = '2016/03/04'
+
+
+class ArtemisShow:
+    commands = [
+        ('a', 'all', None, 'List all comments.'),
+        ('s', 'skip', '>', 'Skip lines starting with a substring.'),
+        ('x', 'extract', [], 'Extract attachment(s) (provide '
+                             'attachment number as argument). If the '
+                             '"message" option is not specified the '
+                             'attachment of the first message (the '
+                             'issue itself) will be extracted. File '
+                             'will be overwritten if it is already '
+                             'existing. '
+                             '(e.g. -x 1 -x 2 -m 1 -o tmp)'),
+        ('i', 'index', 0, 'Message number to be shown (0 based '
+                          'index, 0 is the issue and 1 onwards will '
+                          'be the rest of the replies). If "extract" '
+                          'option is set then the attachment of the '
+                          'message will be extracted. This option is '
+                          'ignored if the "all" option is specified.'),
+        ('o', 'output', '"./tmp"', 'Extract output directory '
+                                   '(e.g. -o "./files/attachments")'),
+        ('', 'mutt', False, 'Use mutt to show the issue.')
+    ]
+    usage = 'hg ishow [OPTIONS] ID'
+
+    def __init__(self):
+        pass
+
+    def show(self, ui, repo, id, **opts):
+        """Shows issue ID, or possibly its comment COMMENT"""
+
+        issue, id = Artemis.find_issue(ui, repo, id)
+        if not issue:
+            return ui.warn('No such issue\n')
+
+        issues_dir = ui.config('artemis', 'issues',
+                               default=Artemis.default_issues_dir)
+        Artemis.create_missing_dirs(
+                os.path.join(repo.root, issues_dir),
+                issue)
+
+        if opts.get('mutt'):
+            return util.system('mutt -R -f %s' % issue)
+
+        mbox = mailbox.Maildir(issue, factory=mailbox.MaildirMessage)
+
+        # show the first mail
+        ui.write('=' * 70 + '\n')
+        keys = Artemis.order_keys_date(mbox)
+        Artemis.write_message(ui, mbox[keys[0]], 0, skip=opts['skip'])
+
+        if opts['all']:
+            ui.write('=' * 70 + '\n')
+            i = 0
+            for k in keys:
+                if (i > 0):
+                    Artemis.write_message(ui, mbox[k], i,
+                                          skip=opts['skip'])
+                    ui.write('-' * 70 + '\n')
+                i += 1
+            return
+
+        elif int(opts["index"]) > 0 and len(keys) > int(
+                opts["index"]):
+            # todo comments replied to this comment should also be shown
+            reply_num = int(opts["index"])
+            ui.write('-' * 70 + '\n')
+            Artemis.write_message(
+                    ui, mbox[keys[reply_num]], reply_num,
+                    skip=opts['skip'])
+            ui.write('=' * 70 + '\n')
+
+        if opts['extract']:
+            attachment_numbers = map(int, opts['extract'])
+            msg = mbox[keys[opts["index"]]]
+
+            counter = 1
+            for part in msg.walk():
+                ctype = part.get_content_type()
+                maintype, subtype = ctype.split('/', 1)
+
+                if maintype == 'multipart' or ctype == 'text/plain':
+                    continue
+
+                if counter in attachment_numbers:
+
+                    filename = part.get_filename()
+                    if not filename:
+                        ext = mimetypes.guess_extension(
+                                part.get_content_type()) or ''
+                        filename = 'attachment-%03d%s' % (
+                            counter, ext)
+
+                    else:
+                        filename = os.path.basename(filename)
+
+                    dirname = opts["output"]
+                    if not os.path.exists(dirname):
+                        os.makedirs(dirname)
+
+                    pathFileName = os.path.join(dirname, filename)
+
+                    fp = open(pathFileName, 'wb')
+                    fp.write(part.get_payload(decode=True))
+                    fp.close()
+
+                counter += 1