Brought be-handle-mail up to date
authorW. Trevor King <wking@drexel.edu>
Thu, 31 Dec 2009 19:33:30 +0000 (14:33 -0500)
committerW. Trevor King <wking@drexel.edu>
Thu, 31 Dec 2009 19:33:30 +0000 (14:33 -0500)
interfaces/email/interactive/be-handle-mail

index 10f6884953da370d69719877aacb4525c4d29d71..f8792f1e787a8a68d4e15d51df28fe2dd70aee35 100755 (executable)
@@ -58,45 +58,51 @@ import shlex
 import sys
 import time
 import traceback
+import types
 import doctest
 import unittest
 
-from becommands import subscribe
-import libbe.cmdutil, libbe.encoding, libbe.utility, libbe.diff, \
-    libbe.bugdir, libbe.bug, libbe.comment
+import libbe.bugdir
+import libbe.bug
+import libbe.comment
+import libbe.diff
+import libbe.command
+import libbe.command.subscribe as subscribe
+import libbe.storage
+import libbe.ui.command_line
+import libbe.util.encoding
+import libbe.util.utility
 import send_pgp_mime
 
-THIS_SERVER = u"thor.physics.drexel.edu"
-THIS_ADDRESS = u"BE Bugs <wking@thor.physics.drexel.edu>"
-
+THIS_SERVER = u'thor.physics.drexel.edu'
+THIS_ADDRESS = u'BE Bugs <wking@thor.physics.drexel.edu>'
+UI = None
 _THIS_DIR = os.path.abspath(os.path.dirname(__file__))
-BE_DIR = _THIS_DIR
-LOGPATH = os.path.join(_THIS_DIR, u"be-handle-mail.log")
+LOGPATH = os.path.join(_THIS_DIR, u'be-handle-mail.log')
 LOGFILE = None
 
 # Tag strings generated by generate_global_tags()
-SUBJECT_TAG_BASE = u"be-bug"
+SUBJECT_TAG_BASE = u'be-bug'
 SUBJECT_TAG_RESPONSE = None
 SUBJECT_TAG_START = None
 SUBJECT_TAG_NEW = None
 SUBJECT_TAG_COMMENT = None
 SUBJECT_TAG_CONTROL = None
-SUBJECT_TAG_XML = None
-
-BREAK = u"--"
-NEW_REQUIRED_PSEUDOHEADERS = [u"Version"]
-NEW_OPTIONAL_PSEUDOHEADERS = [u"Reporter", u"Assign", u"Depend", u"Severity",
-                              u"Status", u"Tag", u"Target",
-                              u"Confirm", u"Subscribe"]
-CONTROL_COMMENT = u"#"
-ALLOWED_COMMANDS = [u"assign", u"comment", u"commit", u"depend", u"help",
-                    u"list", u"merge", u"new", u"open", u"severity", u"show",
-                    u"status", u"subscribe", u"tag", u"target", u"import-xml"]
+
+BREAK = u'--'
+NEW_REQUIRED_PSEUDOHEADERS = [u'Version']
+NEW_OPTIONAL_PSEUDOHEADERS = [u'Reporter', u'Assign', u'Depend', u'Severity',
+                              u'Status', u'Tag', u'Target',
+                              u'Confirm', u'Subscribe']
+CONTROL_COMMENT = u'#'
+ALLOWED_COMMANDS = [u'assign', u'comment', u'commit', u'depend', u'diff',
+                    u'due', u'help', u'list', u'merge', u'new', u'severity',
+                    u'show', u'status', u'subscribe', u'tag', u'target']
 
 AUTOCOMMIT = True
 
-libbe.encoding.ENCODING = u"utf-8" # force default encoding
-ENCODING = libbe.encoding.get_encoding()
+ENCODING = u'utf-8'
+libbe.util.encoding.ENCODING = ENCODING # force default encoding
 
 class InvalidEmail (ValueError):
     def __init__(self, msg, message):
@@ -104,10 +110,10 @@ class InvalidEmail (ValueError):
         self.msg = msg
     def response(self):
         header = self.msg.response_header
-        body = [u"Error processing email:\n",
-                self.response_body(), u""]
+        body = [u'Error processing email:\n',
+                self.response_body(), u'']
         response_generator = \
-            send_pgp_mime.PGPMimeMessageFactory(u"\n".join(body))
+            send_pgp_mime.PGPMimeMessageFactory(u'\n'.join(body))
         response = MIMEMultipart()
         response.attach(response_generator.plain())
         response.attach(self.msg.msg)
@@ -115,44 +121,44 @@ class InvalidEmail (ValueError):
         return ret
     def response_body(self):
         err_text = [unicode(self)]
-        return u"\n".join(err_text)
+        return u'\n'.join(err_text)
 
 class InvalidSubject (InvalidEmail):
     def __init__(self, msg, message=None):
         if message == None:
-            message = u"Invalid subject"
+            message = u'Invalid subject'
         InvalidEmail.__init__(self, msg, message)
     def response_body(self):
-        err_text = u"\n".join([unicode(self), u"",
-                               u"full subject was:",
+        err_text = u'\n'.join([unicode(self), u'',
+                               u'full subject was:',
                                self.msg.subject()])
         return err_text
 
 class InvalidPseudoHeader (InvalidEmail):
     def response_body(self):
-        err_text = [u"Invalid pseudo-header:\n",
+        err_text = [u'Invalid pseudo-header:\n',
                     unicode(self)]
-        return u"\n".join(err_text)
+        return u'\n'.join(err_text)
 
 class InvalidCommand (InvalidEmail):
     def __init__(self, msg, command, message=None):
-        bigmessage = u"Invalid execution command '%s'" % command
+        bigmessage = u'Invalid execution command "%s"' % command
         if message != None:
-            bigmessage += u"\n%s" % message
+            bigmessage += u'\n%s' % message
         InvalidEmail.__init__(self, msg, bigmessage)
         self.command = command
 
 class InvalidOption (InvalidCommand):
     def __init__(self, msg, option, message=None):
-        bigmessage = u"Invalid option '%s'" % (option)
+        bigmessage = u'Invalid option "%s"' % (option)
         if message != None:
-            bigmessage += u"\n%s" % message
+            bigmessage += u'\n%s' % message
         InvalidCommand.__init__(self, msg, info, command, bigmessage)
         self.option = option
 
 class NotificationFailed (Exception):
     def __init__(self, msg):
-        bigmessage = "Notification failed: %s" % msg
+        bigmessage = 'Notification failed: %s' % msg
         Exception.__init__(self, bigmessage)
         self.short_msg = msg
 
@@ -166,11 +172,11 @@ class ID (object):
     def __init__(self, command):
         self.command = command
     def extract_id(self):
-        if hasattr(self, "cached_id"):
+        if hasattr(self, 'cached_id'):
             return self._cached_id
         assert self.command.ret == 0, self.command.ret
-        if self.command.command == u"new":
-            regexp = re.compile(u"Created bug with ID (.*)")
+        if self.command.command.name == u'new':
+            regexp = re.compile(u'Created bug with ID (.*)')
         else:
             raise NotImplementedError, self.command.command
         match = regexp.match(self.command.stdout)
@@ -179,13 +185,12 @@ class ID (object):
         return self._cached_id
     def __str__(self):
         if self.command.ret != 0:
-            return "<id for %s>" % repr(self.command)
-        return "<id %s>" % self.extract_id()
+            return '<id for %s>' % repr(self.command)
+        return '<id %s>' % self.extract_id()
 
 class Command (object):
     """
-    A becommands command wrapper.
-    Doesn't validate input, so do that before initializing.
+    A libbe.command.Command handler.
 
     Initialize with
       Command(msg, command, args=None, stdin=None)
@@ -197,18 +202,17 @@ class Command (object):
     """
     def __init__(self, msg, command, args=None, stdin=None):
         self.msg = msg
-        self.command = command
         if args == None:
             self.args = []
         else:
             self.args = args
-        self.stdin = stdin
+        self.command = libbe.command.get_command_class(command_name=command)()
+        self.command._setup_io = lambda i_enc,o_enc : None
         self.ret = None
+        self.stdin = stdin
         self.stdout = None
-        self.stderr = None
-        self.err = None
     def __str__(self):
-        return "<command: %s %s>" % (self.command, " ".join([str(s) for s in self.args]))
+        return '<command: %s %s>' % (self.command, ' '.join([str(s) for s in self.args]))
     def normalize_args(self):
         """
         Expand any ID placeholders in self.args.
@@ -222,61 +226,25 @@ class Command (object):
         info.  Returns the exit code, stdout, and stderr produced by the
         command.
         """
-        if self.command in [None, u""]: # don't accept blank commands
-            raise InvalidCommand(self.msg, self, "Blank")
-        elif self.command not in ALLOWED_COMMANDS:
-            raise InvalidCommand(self.msg, self, "Not allowed")
-        assert self.ret == None, u"running %s twice!" % unicode(self)
+        if self.command.name in [None, u'']: # don't accept blank commands
+            raise InvalidCommand(self.msg, self, 'Blank')
+        elif self.command.name not in ALLOWED_COMMANDS:
+            raise InvalidCommand(self.msg, self, 'Not allowed')
+        assert self.ret == None, u'running %s twice!' % unicode(self)
         self.normalize_args()
-        # set stdin and catch stdout and stderr
-        if self.stdin != None:
-            orig_stdin = sys.stdin
-            sys.stdin = StringIO.StringIO(self.stdin)
-        new_stdout = codecs.getwriter(ENCODING)(StringIO.StringIO())
-        new_stderr = codecs.getwriter(ENCODING)(StringIO.StringIO())
-        orig_stdout = sys.stdout
-        orig_stderr = sys.stderr
-        sys.stdout = new_stdout
-        sys.stderr = new_stderr
-        # run the command
-        os.chdir(BE_DIR)
-        try:
-            self.ret = libbe.cmdutil.execute(self.command, self.args,
-                                             manipulate_encodings=False,
-                                             restrict_file_access=True)
-        except libbe.cmdutil.GetHelp:
-            print libbe.cmdutil.help(command)
-        except libbe.cmdutil.GetCompletions:
-            self.err = InvalidOption(self.msg, self.command, u"--complete")
-        except libbe.cmdutil.UsageError, e:
-            self.err = InvalidCommand(self.msg, self,
-                                      "%s\n%s" % (type(e), unicode(e)))
-        except libbe.cmdutil.UserError, e:
-            self.err = InvalidCommand(self.msg, self,
-                                      "%s\n%s" % (type(e), unicode(e)))
-        # restore stdin, stdout, and stderr
-        if self.stdin != None:
-            sys.stdin = orig_stdin
-        sys.stdout.flush()
-        sys.stderr.flush()
-        sys.stdout = orig_stdout
-        sys.stderr = orig_stderr
-        self.stdout = codecs.decode(new_stdout.getvalue(), ENCODING)
-        self.stderr = codecs.decode(new_stderr.getvalue(), ENCODING)
-        if self.err != None:
-            raise self.err
-        return (self.ret, self.stdout, self.stderr)
+        UI.io.set_stdin(self.stdin)
+        self.ret = libbe.ui.command_line.dispatch(UI, self.command, self.args)
+        self.stdout = UI.io.get_stdout()
+        return (self.ret, self.stdout)
     def response_msg(self):
         if self.ret == None: self.ret = -1
-        response_body = [u"Results of running: (exit code %d)" % self.ret,
-                         u"  %s %s" % (self.command, u" ".join(self.args))]
+        response_body = [u'Results of running: (exit code %d)' % self.ret,
+                         u'  %s %s' % (self.command.name,u' '.join(self.args))]
         if self.stdout != None and len(self.stdout) > 0:
-            response_body.extend([u"", u"stdout:", u"", self.stdout])
-        if self.stderr != None and len(self.stderr) > 0:
-            response_body.extend([u"", u"stderr:", u"", self.stderr])
-        response_body.append(u"") # trailing endline
+            response_body.extend([u'', u'output:', u'', self.stdout])
+        response_body.append(u'') # trailing endline
         response_generator = \
-            send_pgp_mime.PGPMimeMessageFactory(u"\n".join(response_body))
+            send_pgp_mime.PGPMimeMessageFactory(u'\n'.join(response_body))
         return response_generator.plain()
 
 class DiffTree (libbe.diff.DiffTree):
@@ -315,27 +283,27 @@ class DiffTree (libbe.diff.DiffTree):
     def report_string(self):
         report = self.report_or_none()
         if report == None:
-            return "No changes"
+            return 'No changes'
         else:
             return send_pgp_mime.flatten(report, to_unicode=True)
     def make_root(self):
         return MIMEMultipart()
     def join(self, root, parent, data_part):
-        if hasattr(parent, "attach_child_text"):
+        if hasattr(parent, 'attach_child_text'):
             self.attach_child_text = True
             if data_part != None:
-                send_pgp_mime.append_text(parent.data_mime_part, u"\n\n%s" % (data_part))
+                send_pgp_mime.append_text(parent.data_mime_part, u'\n\n%s' % (data_part))
             self.data_mime_part = parent.data_mime_part
         else:
             self.data_mime_part = None
             if data_part != None:
                 self.data_mime_part = send_pgp_mime.encodedMIMEText(data_part)
-            if parent != None and parent.name in [u"new", u"rem", u"mod"]:
+            if parent != None and parent.name in [u'new', u'rem', u'mod']:
                 self.attach_child_text = True
                 if data_part == None: # make blank data_mime_part for children's appends
-                    self.data_mime_part = send_pgp_mime.encodedMIMEText(u"")
+                    self.data_mime_part = send_pgp_mime.encodedMIMEText(u'')
             if self.data_mime_part != None:
-                self.data_mime_part[u"Content-Description"] = self.name
+                self.data_mime_part[u'Content-Description'] = self.name
                 root.attach(self.data_mime_part)
     def data_part(self, depth, indent=False):
         return libbe.diff.DiffTree.data_part(self, depth, indent=indent)
@@ -357,19 +325,19 @@ class Message (object):
             p=email.Parser.Parser()
             self.msg=p.parsestr(self.text)
             if LOGFILE != None:
-                LOGFILE.write(u"handling %s\n" % self.author_addr())
-                LOGFILE.write(u"\n%s\n\n" % self.text)
+                LOGFILE.write(u'handling %s\n' % self.author_addr())
+                LOGFILE.write(u'\n%s\n\n' % self.text)
         self.confirm = True # enable/disable confirmation email
     def _yes_no(self, boolean):
         if boolean == True:
-            return "yes"
-        return "no"
+            return 'yes'
+        return 'no'
     def author_tuple(self):
         """
         Extract and normalize the sender's email address.  Returns a
         (name, email) tuple.
         """
-        if not hasattr(self, "author_tuple_cache"):
+        if not hasattr(self, 'author_tuple_cache'):
             self._author_tuple_cache = \
                 send_pgp_mime.source_email(self.msg, return_realname=True)
         return self._author_tuple_cache
@@ -384,24 +352,24 @@ class Message (object):
             return self.msg[attr_name]
         return default
     def message_id(self, default=None):
-        return self.default_msg_attribute_access("message-id", default=default)
+        return self.default_msg_attribute_access('message-id', default=default)
     def subject(self):
-        if "subject" not in self.msg:
-            raise InvalidSubject(self, u"Email must contain a subject")
-        return self.msg["subject"]
+        if 'subject' not in self.msg:
+            raise InvalidSubject(self, u'Email must contain a subject')
+        return self.msg['subject']
     def _split_subject(self):
         """
         Returns (tag, subject), with missing values replaced by None.
         """
-        if hasattr(self, "_split_subject_cache"):
+        if hasattr(self, '_split_subject_cache'):
             return self._split_subject_cache
-        args = self.subject().split(u"]",1)
+        args = self.subject().split(u']',1)
         if len(args) < 1:
             self._split_subject_cache = (None, None)
         elif len(args) < 2:
-            self._split_subject_cache = (args[0]+u"]", None)
+            self._split_subject_cache = (args[0]+u']', None)
         else:
-            self._split_subject_cache = (args[0]+u"]", args[1].strip())
+            self._split_subject_cache = (args[0]+u']', args[1].strip())
         return self._split_subject_cache
     def _subject_tag_type(self):
         """
@@ -414,15 +382,13 @@ class Message (object):
         type = None
         value = None
         if tag == SUBJECT_TAG_NEW:
-            type = u"new"
+            type = u'new'
         elif tag == SUBJECT_TAG_CONTROL:
-            type = u"control"
-        elif tag == SUBJECT_TAG_XML:
-            type = u"xml"
+            type = u'control'
         else:
             match = SUBJECT_TAG_COMMENT.match(tag)
             if len(match.groups()) == 1:
-                type = u"comment"
+                type = u'comment'
                 value = match.group(1)
         return (type, value)
     def validate_subject(self):
@@ -432,14 +398,14 @@ class Message (object):
         tag,subject = self._split_subject()
         if not tag.startswith(SUBJECT_TAG_START):
             raise InvalidSubject(
-                self, u"Subject must start with '%s'" % SUBJECT_TAG_START)
+                self, u'Subject must start with "%s"' % SUBJECT_TAG_START)
         tag_type,value = self._subject_tag_type()
         if tag_type == None:
-            raise InvalidSubject(self, u"Invalid tag '%s'" % tag)
-        elif tag_type == u"new" and len(subject) == 0:
-            raise InvalidSubject(self, u"Cannot create a bug with blank title")
-        elif tag_type == u"comment" and len(value) == 0:
-            raise InvalidSubject(self, u"Must specify a bug ID to comment")
+            raise InvalidSubject(self, u'Invalid tag "%s"' % tag)
+        elif tag_type == u'new' and len(subject) == 0:
+            raise InvalidSubject(self, u'Cannot create a bug with blank title')
+        elif tag_type == u'comment' and len(value) == 0:
+            raise InvalidSubject(self, u'Must specify a bug ID to comment')
     def _get_bodies_and_mime_types(self):
         """
         Traverse the email message returning (body, mime_type) for
@@ -451,7 +417,7 @@ class Message (object):
                 continue
             body,mime_type=(part.get_payload(decode=True),part.get_content_type())
             charset = part.get_content_charset(msg_charset).lower()
-            if mime_type.startswith("text/"):
+            if mime_type.startswith('text/'):
                 body = unicode(body, charset) # convert text types to unicode
             yield (body, mime_type)
     def _parse_body_pseudoheaders(self, body, required, optional,
@@ -471,15 +437,15 @@ class Message (object):
             line = line.strip()
             if len(line) == 0:
                 break
-            if ":" not in line:
+            if ':' not in line:
                 raise InvalidPseudoheader(self, line)
-            key,value = line.split(":", 1)
+            key,value = line.split(':', 1)
             value = value.strip()
             if key not in all:
                 raise InvalidPseudoHeader(self, key)
             if len(value) == 0:
                 raise InvalidEmail(
-                    self, u"Blank value for: %s" % key)
+                    self, u'Blank value for: %s' % key)
             dictionary[key] = value
         missing = []
         for key in required:
@@ -487,9 +453,9 @@ class Message (object):
                 missing.append(key)
         if len(missing) > 0:
             raise InvalidPseudoHeader(self,
-                                      u"Missing required pseudo-headers:\n%s"
-                                      % u", ".join(missing))
-        remaining_body = u"\n".join(body_lines[i:]).strip()
+                                      u'Missing required pseudo-headers:\n%s'
+                                      % u', '.join(missing))
+        remaining_body = u'\n'.join(body_lines[i:]).strip()
         return (remaining_body, dictionary)
     def _strip_footer(self, body):
         body_lines = body.splitlines()
@@ -497,7 +463,7 @@ class Message (object):
             if line.startswith(BREAK):
                 break
             i += 1 # increment past the current valid line.
-        return u"\n".join(body_lines[:i]).strip()
+        return u'\n'.join(body_lines[:i]).strip()
     def parse(self):
         """
         Parse the commands given in the email.  Raises assorted
@@ -506,24 +472,22 @@ class Message (object):
         """
         self.validate_subject()
         tag_type,value = self._subject_tag_type()
-        if tag_type == u"new":
+        if tag_type == u'new':
             commands = self.parse_new()
-        elif tag_type == u"comment":
+        elif tag_type == u'comment':
             commands = self.parse_comment(value)
-        elif tag_type == u"control":
+        elif tag_type == u'control':
             commands = self.parse_control()
-        elif tag_type == u"xml":
-            commands = self.parse_xml()
         else:
-            raise Exception, u"Unrecognized tag type '%s'" % tag_type
+            raise Exception, u'Unrecognized tag type "%s"' % tag_type
         return commands
     def parse_new(self):
-        command = u"new"
+        command = u'new'
         tag,subject = self._split_subject()
         summary = subject
-        options = {u"Reporter": self.author_addr(),
-                   u"Confirm": self._yes_no(self.confirm),
-                   u"Subscribe": "no",
+        options = {u'Reporter': self.author_addr(),
+                   u'Confirm': self._yes_no(self.confirm),
+                   u'Subscribe': 'no',
                    }
         body,mime_type = list(self._get_bodies_and_mime_types())[0]
         comment_body,options = \
@@ -531,51 +495,54 @@ class Message (object):
                                            NEW_REQUIRED_PSEUDOHEADERS,
                                            NEW_OPTIONAL_PSEUDOHEADERS,
                                            options)
-        if options[u"Confirm"].lower() == "no":
+        if options[u'Confirm'].lower() == 'no':
             self.confirm = False
-        if options[u"Subscribe"].lower() == "yes" and self.confirm == True:
+        if options[u'Subscribe'].lower() == 'yes' and self.confirm == True:
             # respond with the subscription format rather than the
             # normal command-output format, because the subscription
             # format is more user-friendly.
             self.confirm = False
-        args = [u"--reporter", options[u"Reporter"]]
+        args = [u'--reporter', options[u'Reporter']]
         args.append(summary)
         commands = [Command(self, command, args)]
         id = ID(commands[0])
         comment_body = self._strip_footer(comment_body)
         if len(comment_body) > 0:
-            command = u"comment"
-            comment = u"Version: %s\n\n"%options[u"Version"] + comment_body
-            args = [u"--author", self.author_addr(),
-                    u"--alt-id", self.message_id(),
-                    u"--content-type", mime_type]
+            command = u'comment'
+            comment = u'Version: %s\n\n'%options[u'Version'] + comment_body
+            args = [u'--author', self.author_addr(),
+                    u'--alt-id', self.message_id(),
+                    u'--content-type', mime_type]
             args.append(id)
-            args.append(u"-")
-            commands.append(Command(self, u"comment", args, stdin=comment))
+            args.append(u'-')
+            commands.append(Command(self, u'comment', args, stdin=comment))
         for key,value in options.items():
-            if key in [u"Version", u"Reporter", u"Confirm"]:
+            if key in [u'Version', u'Reporter', u'Confirm']:
                 continue # we've already handled these options
             command = key.lower()
-            args = [id, value]
-            if key == u"Subscribe":
-                if value.lower() != "yes":
+            if key in [u'Depend', u'Tag', u'Target', u'Subscribe']:
+                args = [id, value]
+            else:
+                args = [value, id]
+            if key == u'Subscribe':
+                if value.lower() != 'yes':
                     continue
-                args = ["--subscriber", self.author_addr(), id]
+                args = ['--subscriber', self.author_addr(), id]
             commands.append(Command(self, command, args))
         return commands
     def parse_comment(self, bug_uuid):
-        command = u"comment"
+        command = u'comment'
         bug_id = bug_uuid
         author = self.author_addr()
         alt_id = self.message_id()
         body,mime_type = list(self._get_bodies_and_mime_types())[0]
-        if mime_type == "text/plain":
+        if mime_type == 'text/plain':
             body = self._strip_footer(body)
         content_type = mime_type
-        args = [u"--author", author]
+        args = [u'--author', author]
         if alt_id != None:
-            args.extend([u"--alt-id", alt_id])
-        args.extend([u"--content-type", content_type, bug_id, u"-"])
+            args.extend([u'--alt-id', alt_id])
+        args.extend([u'--content-type', content_type, bug_id, u'-'])
         commands = [Command(self, command, args, stdin=body)]
         return commands
     def parse_control(self):
@@ -587,49 +554,46 @@ class Message (object):
                 continue
             if line.startswith(BREAK):
                 break
+            if type(line) == types.UnicodeType:
+                # work around http://bugs.python.org/issue1170
+                line = line.encode('unicode escape')
             fields = shlex.split(line)
+            if type(line) == types.UnicodeType:
+                # work around http://bugs.python.org/issue1170
+                for field in fields:
+                    field = unicode(field, 'unicode escape')
             command,args = (fields[0], fields[1:])
             commands.append(Command(self, command, args))
         if len(commands) == 0:
-            raise InvalidEmail(self, u"No commands in control email.")
+            raise InvalidEmail(self, u'No commands in control email.')
         return commands
-    def parse_xml(self):
-        command = u"import-xml"
-        body,mime_type = list(self._get_bodies_and_mime_types())[0]
-        if mime_type != "text/xml":
-            raise InvalidEmail(self,
-                u"Emails to %s must have MIME type 'text/xml', not '%s'."
-                % (SUBJECT_TAG_XML, mime_type))
-        args = [u"--add-only", u"-"]
-        commands = [Command(self, command, args, stdin=body)]
-        return commands
-    def run(self):
+    def run(self, repo='.'):
         self._begin_response()
         commands = self.parse()
         try:
-            for command in commands:
+            for i,command in enumerate(commands):
                 command.run()
                 self._add_response(command.response_msg())
         finally:
             if AUTOCOMMIT == True:
                 tag,subject = self._split_subject()
-                self.commit_command = Command(self, "commit", [subject])
+                self.commit_command = Command(self, 'commit', [subject])
                 self.commit_command.run()
                 if LOGFILE != None:
-                    LOGFILE.write(u"Autocommit:\n%s\n\n" %
+                    LOGFILE.write(u'Autocommit:\n%s\n\n' %
                       send_pgp_mime.flatten(self.commit_command.response_msg(),
                                             to_unicode=True))
     def _begin_response(self):
         tag,subject = self._split_subject()
-        response_header = [u"From: %s" % THIS_ADDRESS,
-                           u"To: %s" % self.author_addr(),
-                           u"Date: %s" % libbe.utility.time_to_str(time.time()),
-                           u"Subject: %s Re: %s"%(SUBJECT_TAG_RESPONSE,subject)
+        response_header = [u'From: %s' % THIS_ADDRESS,
+                           u'To: %s' % self.author_addr(),
+                           u'Date: %s' % libbe.util.utility.time_to_str(time.time()),
+                           u'Subject: %s Re: %s'%(SUBJECT_TAG_RESPONSE,subject)
                            ]
         if self.message_id() != None:
-            response_header.append(u"In-reply-to: %s" % self.message_id())
+            response_header.append(u'In-reply-to: %s' % self.message_id())
         self.response_header = \
-            send_pgp_mime.header_from_text(text=u"\n".join(response_header))
+            send_pgp_mime.header_from_text(text=u'\n'.join(response_header))
         self._response_messages = []
     def _add_response(self, response_message):
         self._response_messages.append(response_message)
@@ -645,22 +609,24 @@ class Message (object):
     def subscriber_emails(self, previous_revision=None):
         if previous_revision == None:
             if AUTOCOMMIT != True: # no way to tell what's changed
-                raise NotificationFailed("Autocommit dissabled")
+                raise NotificationFailed('Autocommit dissabled')
             if len(self._response_messages) == 0:
-                raise NotificationFailed("Initial email failed.")
+                raise NotificationFailed('Initial email failed.')
             if self.commit_command.ret != 0:
                 # commit failed.  Error already logged.
-                raise NotificationFailed("Commit failed")
+                raise NotificationFailed('Commit failed')
 
-        # read only bugdir.
-        bd = libbe.bugdir.BugDir(from_disk=True,
-                                 manipulate_encodings=False)
+        bd = UI.storage_callbacks.get_bugdir()
+        writeable = bd.storage.writeable
+        bd.storage.writeable = False
         if bd.vcs.versioned == False: # no way to tell what's changed
-            raise NotificationFailed("Not versioned")
+            bd.storage.writeable = writeable
+            raise NotificationFailed('Not versioned')
 
         bd.load_all_bugs()
         subscribers = subscribe.get_bugdir_subscribers(bd, THIS_SERVER)
         if len(subscribers) == 0:
+            bd.storage.writeable = writeable
             return []
         for subscriber,subscriptions in subscribers.items():
             subscribers[subscriber] = []
@@ -676,19 +642,20 @@ class Message (object):
 
         emails = []
         for subscriber,subscriptions in subscribers.items():
-            header.replace_header("to", subscriber)
+            header.replace_header('to', subscriber)
             report = diff.report_tree(subscriptions, diff_tree=DiffTree)
             root = report.report_or_none()
             if root != None:
                 emails.append(send_pgp_mime.attach_root(header, root))
                 if LOGFILE != None:
-                    LOGFILE.write(u"Preparing to notify %s of changes\n" % subscriber)
+                    LOGFILE.write(u'Preparing to notify %s of changes\n' % subscriber)
+        bd.storage.writeable = writeable
         return emails
     def _get_before_and_after_bugdirs(self, bd, previous_revision=None):
         if previous_revision == None:
             commit_msg = self.commit_command.stdout
-            assert commit_msg.startswith("Committed "), commit_msg
-            after_revision = commit_msg[len("Committed "):]
+            assert commit_msg.startswith('Committed '), commit_msg
+            after_revision = commit_msg[len('Committed '):]
             before_revision = bd.vcs.revision_id(-2)
         else:
             before_revision = previous_revision
@@ -704,32 +671,30 @@ class Message (object):
     def _subscriber_header(self, bd, previous_revision=None):
         root_dir = os.path.basename(bd.root)
         if previous_revision == None:
-            subject = "Changes to %s on %s by %s" \
+            subject = 'Changes to %s on %s by %s' \
                 % (root_dir, THIS_SERVER, self.author_addr())
         else:
-            subject = "Changes to %s on %s since revision %s" \
+            subject = 'Changes to %s on %s since revision %s' \
                 % (root_dir, THIS_SERVER, previous_revision)
-        header = [u"From: %s" % THIS_ADDRESS,
-                  u"To: %s" % u"DUMMY-AUTHOR",
-                  u"Date: %s" % libbe.utility.time_to_str(time.time()),
-                  u"Subject: %s Re: %s" % (SUBJECT_TAG_RESPONSE, subject)
+        header = [u'From: %s' % THIS_ADDRESS,
+                  u'To: %s' % u'DUMMY-AUTHOR',
+                  u'Date: %s' % libbe.util.utility.time_to_str(time.time()),
+                  u'Subject: %s Re: %s' % (SUBJECT_TAG_RESPONSE, subject)
                   ]
-        return send_pgp_mime.header_from_text(text=u"\n".join(header))
+        return send_pgp_mime.header_from_text(text=u'\n'.join(header))
 
-def generate_global_tags(tag_base=u"be-bug"):
+def generate_global_tags(tag_base=u'be-bug'):
     """
     Generate a series of tags from a base tag string.
     """
     global SUBJECT_TAG_BASE, SUBJECT_TAG_START, SUBJECT_TAG_RESPONSE, \
-        SUBJECT_TAG_NEW, SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL, \
-        SUBJECT_TAG_XML
+        SUBJECT_TAG_NEW, SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL
     SUBJECT_TAG_BASE = tag_base
-    SUBJECT_TAG_START = u"[%s" % tag_base
-    SUBJECT_TAG_RESPONSE = u"[%s]" % tag_base
-    SUBJECT_TAG_NEW = u"[%s:submit]" % tag_base
-    SUBJECT_TAG_COMMENT = re.compile(u"\[%s:([\-0-9a-z]*)]" % tag_base)
+    SUBJECT_TAG_START = u'[%s' % tag_base
+    SUBJECT_TAG_RESPONSE = u'[%s]' % tag_base
+    SUBJECT_TAG_NEW = u'[%s:submit]' % tag_base
+    SUBJECT_TAG_COMMENT = re.compile(u'\[%s:([\-0-9a-z]*)]' % tag_base)
     SUBJECT_TAG_CONTROL = SUBJECT_TAG_RESPONSE
-    SUBJECT_TAG_XML = u"[%s:xml]" % tag_base
 
 def open_logfile(logpath=None):
     """
@@ -741,27 +706,28 @@ def open_logfile(logpath=None):
     """
     global LOGPATH, LOGFILE
     if logpath != None:
-        if logpath == u"-":
-            LOGPATH = u"stderr"
+        if logpath == u'-':
+            LOGPATH = u'stderr'
             LOGFILE = sys.stderr
-        elif logpath == u"none":
-            LOGPATH = u"none"
+        elif logpath == u'none':
+            LOGPATH = u'none'
             LOGFILE = None
         elif os.path.isabs(logpath):
             LOGPATH = logpath
         else:
             LOGPATH = os.path.join(_THIS_DIR, logpath)
-    if LOGFILE == None and LOGPATH != u"none":
-        LOGFILE = codecs.open(LOGPATH, u"a+", ENCODING)
-        LOGFILE.write(u"Default encoding: %s\n" % ENCODING)
+    if LOGFILE == None and LOGPATH != u'none':
+        LOGFILE = codecs.open(LOGPATH, u'a+',
+            libbe.utuil.encoding.get_filesystem_encoding())
 
 def close_logfile():
-    if LOGFILE != None and LOGPATH not in [u"stderr", u"none"]:
+    if LOGFILE != None and LOGPATH not in [u'stderr', u'none']:
         LOGFILE.close()
 
+unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
+suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])
+
 def test():
-    unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
-    suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])
     result = unittest.TextTestRunner(verbosity=2).run(suite)
     num_errors = len(result.errors)
     num_failures = len(result.failures)
@@ -770,15 +736,15 @@ def test():
 
 def main(args):
     from optparse import OptionParser
-    global AUTOCOMMIT, BE_DIR
+    global AUTOCOMMIT, UI
 
-    usage="be-handle-mail [options]\n\n%s" % (__doc__)
+    usage='be-handle-mail [options]\n\n%s' % (__doc__)
     parser = OptionParser(usage=usage)
-    parser.add_option('-b', '--be-dir', dest='be_dir', default=BE_DIR,
-                      metavar="DIR",
-                      help='Select the BE directory to serve (%default).')
+    parser.add_option('-r', '--repo', dest='repo', default=_THIS_DIR,
+                      metavar='REPO',
+                      help='Select the BE repository to serve (%default).')
     parser.add_option('-t', '--tag-base', dest='tag_base',
-                      default=SUBJECT_TAG_BASE, metavar="TAG",
+                      default=SUBJECT_TAG_BASE, metavar='TAG',
                       help='Set the subject tag base (%default).')
     parser.add_option('-o', '--output', dest='output', action='store_true',
                       help="Don't mail the generated message, print it to stdout instead.  Useful for testing be-handle-mail functionality without the whole mail transfer agent and procmail setup.")
@@ -804,40 +770,44 @@ def main(args):
             num_bad = 1
         sys.exit(num_bad)
     
-    BE_DIR = options.be_dir
     AUTOCOMMIT = options.autocommit
 
     if options.notify_since == None:
         msg_text = sys.stdin.read()
 
-    libbe.encoding.set_IO_stream_encodings(ENCODING) # _after_ reading message
     open_logfile(options.logfile)
     generate_global_tags(options.tag_base)
 
+    io = libbe.command.StringInputOutput()
+    UI = libbe.command.UserInterface(io, location=options.repo)
+
     if options.notify_since != None:
         if options.subscribers == True:
             if LOGFILE != None:
-                LOGFILE.write(u"Checking for subscribers to notify since revision %s\n"
+                LOGFILE.write(u'Checking for subscribers to notify since revision %s\n'
                               % options.notify_since)
             try:
                 m = Message(disable_parsing=True)
                 emails = m.subscriber_emails(options.notify_since)
             except NotificationFailed, e:
                 if LOGFILE != None:
-                    LOGFILE.write(unicode(e) + u"\n")
+                    LOGFILE.write(unicode(e) + u'\n')
             else:
                 for msg in emails:
                     if options.output == True:
                         print send_pgp_mime.flatten(msg, to_unicode=True)
                     else:
                         send_pgp_mime.mail(msg, send_pgp_mime.sendmail)
+            self.commit_command.cleanup()
             close_logfile()
+        UI.cleanup()
         sys.exit(0)
 
     if len(msg_text.strip()) == 0: # blank email!?
         if LOGFILE != None:
-            LOGFILE.write(u"Blank email!\n")
+            LOGFILE.write(u'Blank email!\n')
             close_logfile()
+        UI.cleanup()
         sys.exit(1)
     try:
         m = Message(msg_text)
@@ -846,9 +816,10 @@ def main(args):
         response = e.response()
     except Exception, e:
         if LOGFILE != None:
-            LOGFILE.write(u"Uncaught exception:\n%s\n" % (e,))
+            LOGFILE.write(u'Uncaught exception:\n%s\n' % (e,))
             traceback.print_tb(sys.exc_traceback, file=LOGFILE)
             close_logfile()
+        UI.cleanup()
         sys.exit(1)
     else:
         response = m.response_email()
@@ -856,21 +827,21 @@ def main(args):
         print send_pgp_mime.flatten(response, to_unicode=True)
     elif m.confirm == True:
         if LOGFILE != None:
-            LOGFILE.write(u"Sending response to %s\n" % m.author_addr())
-            LOGFILE.write(u"\n%s\n\n" % send_pgp_mime.flatten(response,
+            LOGFILE.write(u'Sending response to %s\n' % m.author_addr())
+            LOGFILE.write(u'\n%s\n\n' % send_pgp_mime.flatten(response,
                                                               to_unicode=True))
         send_pgp_mime.mail(response, send_pgp_mime.sendmail)
     else:
         if LOGFILE != None:
-            LOGFILE.write(u"Response declined by %s\n" % m.author_addr())
+            LOGFILE.write(u'Response declined by %s\n' % m.author_addr())
     if options.subscribers == True:
         if LOGFILE != None:
-            LOGFILE.write(u"Checking for subscribers\n")
+            LOGFILE.write(u'Checking for subscribers\n')
         try:
             emails = m.subscriber_emails()
         except NotificationFailed, e:
             if LOGFILE != None:
-                LOGFILE.write(unicode(e) + u"\n")
+                LOGFILE.write(unicode(e) + u'\n')
         else:
             for msg in emails:
                 if options.output == True:
@@ -879,7 +850,7 @@ def main(args):
                     send_pgp_mime.mail(msg, send_pgp_mime.sendmail)
 
     close_logfile()
-
+    UI.cleanup()
 
 class GenerateGlobalTagsTestCase (unittest.TestCase):
     def setUp(self):
@@ -901,41 +872,37 @@ class GenerateGlobalTagsTestCase (unittest.TestCase):
     def test_restore_global_tags(self):
         "Test global tag restoration by teardown function."
         global SUBJECT_TAG_BASE
-        self.failUnlessEqual(SUBJECT_TAG_BASE, u"be-bug")
-        SUBJECT_TAG_BASE = "projectX-bug"
-        self.failUnlessEqual(SUBJECT_TAG_BASE, u"projectX-bug")
+        self.failUnlessEqual(SUBJECT_TAG_BASE, u'be-bug')
+        SUBJECT_TAG_BASE = 'projectX-bug'
+        self.failUnlessEqual(SUBJECT_TAG_BASE, u'projectX-bug')
         self.restore_global_tags()
-        self.failUnlessEqual(SUBJECT_TAG_BASE, u"be-bug")
+        self.failUnlessEqual(SUBJECT_TAG_BASE, u'be-bug')
     def test_subject_tag_base(self):
         "Should set SUBJECT_TAG_BASE global correctly"
-        generate_global_tags(u"projectX-bug")
-        self.failUnlessEqual(SUBJECT_TAG_BASE, u"projectX-bug")
+        generate_global_tags(u'projectX-bug')
+        self.failUnlessEqual(SUBJECT_TAG_BASE, u'projectX-bug')
     def test_subject_tag_start(self):
         "Should set SUBJECT_TAG_START global correctly"
-        generate_global_tags(u"projectX-bug")
-        self.failUnlessEqual(SUBJECT_TAG_START, u"[projectX-bug")
+        generate_global_tags(u'projectX-bug')
+        self.failUnlessEqual(SUBJECT_TAG_START, u'[projectX-bug')
     def test_subject_tag_response(self):
         "Should set SUBJECT_TAG_RESPONSE global correctly"
-        generate_global_tags(u"projectX-bug")
-        self.failUnlessEqual(SUBJECT_TAG_RESPONSE, u"[projectX-bug]")
+        generate_global_tags(u'projectX-bug')
+        self.failUnlessEqual(SUBJECT_TAG_RESPONSE, u'[projectX-bug]')
     def test_subject_tag_new(self):
         "Should set SUBJECT_TAG_NEW global correctly"
-        generate_global_tags(u"projectX-bug")
-        self.failUnlessEqual(SUBJECT_TAG_NEW, u"[projectX-bug:submit]")
+        generate_global_tags(u'projectX-bug')
+        self.failUnlessEqual(SUBJECT_TAG_NEW, u'[projectX-bug:submit]')
     def test_subject_tag_control(self):
         "Should set SUBJECT_TAG_CONTROL global correctly"
-        generate_global_tags(u"projectX-bug")
-        self.failUnlessEqual(SUBJECT_TAG_CONTROL, u"[projectX-bug]")
+        generate_global_tags(u'projectX-bug')
+        self.failUnlessEqual(SUBJECT_TAG_CONTROL, u'[projectX-bug]')
     def test_subject_tag_comment(self):
         "Should set SUBJECT_TAG_COMMENT global correctly"
-        generate_global_tags(u"projectX-bug")
-        m = SUBJECT_TAG_COMMENT.match("[projectX-bug:xyz-123]")
+        generate_global_tags(u'projectX-bug')
+        m = SUBJECT_TAG_COMMENT.match('[projectX-bug:xyz-123]')
         self.failUnlessEqual(len(m.groups()), 1)
-        self.failUnlessEqual(m.group(1), u"xyz-123")
-    def test_subject_tag_xml(self):
-        "Should set SUBJECT_TAG_XML global correctly"
-        generate_global_tags(u"projectX-bug")
-        self.failUnlessEqual(SUBJECT_TAG_XML, u"[projectX-bug:xml]")
+        self.failUnlessEqual(m.group(1), u'xyz-123')
 
 if __name__ == "__main__":
     main(sys.argv)