From 3b03b9aa1bd0d2550fab48940242453cff238508 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 21 Nov 2009 13:08:05 -0500 Subject: Added interfaces/email/interactive/examples/email_bugs For testing the new [be-bug:xml] interface we're about to write. --- interfaces/email/interactive/examples/email_bugs | 37 ++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 interfaces/email/interactive/examples/email_bugs (limited to 'interfaces/email/interactive') diff --git a/interfaces/email/interactive/examples/email_bugs b/interfaces/email/interactive/examples/email_bugs new file mode 100644 index 0000000..949e1c1 --- /dev/null +++ b/interfaces/email/interactive/examples/email_bugs @@ -0,0 +1,37 @@ +From jdoe@example.com Fri Apr 18 12:00:00 2008 +Content-Type: text/xml; charset="utf-8" +MIME-Version: 1.0 +Content-Transfer-Encoding: quoted-printable +From: jdoe@example.com +To: a@b.com +Date: Fri, 18 Apr 2008 12:00:00 +0000 +Subject: [be-bug:xml] Updates to a, b + + + + + 1.0.0 + be + 446 + wking@drexel.edu-20091119214553-iqyw2cpqluww3zna + + + a + a + minor + open + John Doe <jdoe@example.com> + Thu, 01 Jan 1970 00:00:00 +0000 + Bug A + + + b + b + minor + closed + Jane Doe <jdoe@example.com> + Thu, 01 Jan 1970 00:00:00 +0000 + Bug B + + + -- cgit From 65bf5f8d9ddf51625d6b3b282838a9a4c71868d3 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 21 Nov 2009 13:18:54 -0500 Subject: Fixed bug in be-handle-mail.Message.parse_comment() for emails w/o Message-id. You used to get: Uncaught exception: 'NoneType' object has no attribute 'decode' File "./be-handle-mail", line 857, in main m.run() File "./be-handle-mail", line 591, in run command.run() File "./be-handle-mail", line 244, in run manipulate_encodings=False) File "/tmp/be.email-bugs/interfaces/email/interactive/libbe/cmdutil.py", line 82, in execute ret = cmd.execute([a.decode(enc) for a in args], A `print args' in Message.parse_comment() revealed [..., u'--alt-id', None,...] --- interfaces/email/interactive/be-handle-mail | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'interfaces/email/interactive') diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index fa80698..8831e3c 100755 --- a/interfaces/email/interactive/be-handle-mail +++ b/interfaces/email/interactive/be-handle-mail @@ -564,8 +564,10 @@ class Message (object): if mime_type == "text/plain": body = self._strip_footer(body) content_type = mime_type - args = [u"--author", author, u"--alt-id", alt_id, - u"--content-type", content_type, bug_id, u"-"] + 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"-"]) commands = [Command(self, command, args, stdin=body)] return commands def parse_control(self): -- cgit From f1cb3e5d0f6341c4cdf457f4f029270037ecae16 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 21 Nov 2009 13:53:09 -0500 Subject: Added [be-tag:xml] processing to be-handle-mail. Now it will automatically apply and commit emails from be email-bugs ... --- interfaces/email/interactive/be-handle-mail | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) (limited to 'interfaces/email/interactive') diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index 8831e3c..bd37f55 100755 --- a/interfaces/email/interactive/be-handle-mail +++ b/interfaces/email/interactive/be-handle-mail @@ -81,6 +81,7 @@ 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"] @@ -90,7 +91,7 @@ NEW_OPTIONAL_PSEUDOHEADERS = [u"Reporter", u"Assign", u"Depend", u"Severity", 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"status", u"subscribe", u"tag", u"target", u"import-xml"] AUTOCOMMIT = True @@ -402,8 +403,8 @@ class Message (object): def _subject_tag_type(self): """ Parse subject tag, return (type, value), where type is one of - None, "new", "comment", or "control"; and value is None except - in the case of "comment", in which case it's the bug + None, "new", "comment", "control", or "xml"; and value is None + except in the case of "comment", in which case it's the bug ID/shortname. """ tag,subject = self._split_subject() @@ -413,6 +414,8 @@ class Message (object): type = u"new" elif tag == SUBJECT_TAG_CONTROL: type = u"control" + elif tag == SUBJECT_TAG_XML: + type = u"xml" else: match = SUBJECT_TAG_COMMENT.match(tag) if len(match.groups()) == 1: @@ -506,6 +509,8 @@ class Message (object): commands = self.parse_comment(value) 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 return commands @@ -585,6 +590,16 @@ class Message (object): if len(commands) == 0: 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"-"] + commands = [Command(self, command, args, stdin=body)] + return commands def run(self): self._begin_response() commands = self.parse() @@ -738,13 +753,15 @@ 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_NEW, SUBJECT_TAG_COMMENT, SUBJECT_TAG_CONTROL, \ + SUBJECT_TAG_XML 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_CONTROL = SUBJECT_TAG_RESPONSE + SUBJECT_TAG_XML = u"[%s:xml]" % tag_base def open_logfile(logpath=None): """ @@ -947,6 +964,10 @@ class GenerateGlobalTagsTestCase (unittest.TestCase): 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]") if __name__ == "__main__": main(sys.argv) -- cgit From 85fd7e2681005e8b47ffc1e73fcc0cca93025921 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 21 Nov 2009 14:03:00 -0500 Subject: Updated interfaces/email/interactive/README for [be-bug:xml] interface --- interfaces/email/interactive/README | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) (limited to 'interfaces/email/interactive') diff --git a/interfaces/email/interactive/README b/interfaces/email/interactive/README index 79ef9a9..2070973 100644 --- a/interfaces/email/interactive/README +++ b/interfaces/email/interactive/README @@ -23,13 +23,15 @@ execution. Once be-handle-mail receives the email, the parsing method is selected according to the subject tag that procmail used grab the email in the -first place. There are three parsing styles: +first place. There are four parsing styles: Style Subject creating bugs [be-bug:submit] new bug summary commenting on bugs [be-bug:] commit message control [be-bug] commit message + xml [be-bug:xml] commit message These are analogous to submit@bugs.debian.org, nnn@bugs.debian.org, -and control@bugs.debian.org respectively. +and control@bugs.debian.org respectively. The xml style has no Debian +analog. Creating bugs ============= @@ -106,6 +108,15 @@ shlex.split(). -- Goofy tagline ignored. +XML +=== + +This interface allows users without access to the versioned source of +the program to conveniently submit bugs and comments using be. You +should not attempt to compose emails for this interface by hand. See +the documentation for the `email-bugs' and `import-xml' be commands +for details. + Example emails ============== -- cgit From 614d4e40e148520ac511cbe0606bcbdcf24c8a08 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 21 Nov 2009 15:18:02 -0500 Subject: Added restrict_file_access to becommands' execute() args. + associated adjustments in other files. See cmdutil.restrict_file_access.__doc__ for an explanation of the security hole this closes. --- interfaces/email/interactive/be-handle-mail | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'interfaces/email/interactive') diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index bd37f55..e0e3490 100755 --- a/interfaces/email/interactive/be-handle-mail +++ b/interfaces/email/interactive/be-handle-mail @@ -242,7 +242,8 @@ class Command (object): os.chdir(BE_DIR) try: self.ret = libbe.cmdutil.execute(self.command, self.args, - manipulate_encodings=False) + manipulate_encodings=False, + restrict_file_access=True) except libbe.cmdutil.GetHelp: print libbe.cmdutil.help(command) except libbe.cmdutil.GetCompletions: -- cgit From 2156616c3dab8207a933295bfbc9d125dac4bc34 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 30 Nov 2009 06:28:01 -0500 Subject: be-handle-mail uses more conservative --add-only for be-bugs:xml --- interfaces/email/interactive/be-handle-mail | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'interfaces/email/interactive') diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index e0e3490..3b321cf 100755 --- a/interfaces/email/interactive/be-handle-mail +++ b/interfaces/email/interactive/be-handle-mail @@ -598,7 +598,7 @@ class Message (object): raise InvalidEmail(self, u"Emails to %s must have MIME type 'text/xml', not '%s'." % (SUBJECT_TAG_XML, mime_type)) - args = [u"-"] + args = [u"--add-only", u"-"] commands = [Command(self, command, args, stdin=body)] return commands def run(self): -- cgit From 129c100046231ed15d2f16eaa90b5c01e41a442c Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 01:58:41 -0500 Subject: Moved subscription types from becommands/subscribe.py to libbe/diff.py. --- interfaces/email/interactive/be-handle-mail | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'interfaces/email/interactive') diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index 3b321cf..952c16d 100755 --- a/interfaces/email/interactive/be-handle-mail +++ b/interfaces/email/interactive/be-handle-mail @@ -685,21 +685,21 @@ class Message (object): ordered_subscriptions.extend(subscriptions.items()) for id,types in ordered_subscriptions: if id == "DIR": - if subscribe.BUGDIR_TYPE_ALL in types: + if libbe.diff.BUGDIR_TYPE_ALL in types: parts.append(diff_tree.report_or_none()) break # we've attached everything, so stop checking. - if subscribe.BUGDIR_TYPE_NEW in types: + if libbe.diff.BUGDIR_TYPE_NEW in types: new = diff_tree.child_by_path("/bugs/new") parts.append(new.report_or_none()) continue # move on to next id # if we get this far, id refers to a bug. - assert types == [subscribe.BUG_TYPE_ALL], types + assert types == [libbe.diff.BUG_TYPE_ALL], types if id not in bug_index: continue # no changes here, move on to next id type,bug_root = bug_index[id] if type == "added" \ and "DIR" in subscriptions \ - and subscriptions["DIR"] == subscribe.BUGDIR_TYPE_NEW: + and subscriptions["DIR"] == libbe.diff.BUGDIR_TYPE_NEW: # this info already attached at the DIR level continue # move on to next id parts.append(bug_root.report_or_none()) -- cgit From cc58188259e36193c3174fbb55e37c790382a7ea Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 5 Dec 2009 07:41:50 -0500 Subject: Use new libbe.diff.Diff.report_tree(subscriptions) in be-handle-mail. This makes Message.subscriber_emails() much cleaner. Also fix libbe.diff.Diff._sub_report() to handle missing 'bugdir/settings'. Added libbe.diff.SubscriptionType.__cmp__ so that SubscriptionType('all') == SubscriptionType('all') This is important when comparing the types returned by becommands.subscribe.get_bugdir_subscribers() with the libbe.diff.*_TYPE_* types. --- interfaces/email/interactive/be-handle-mail | 67 ++++++++--------------------- 1 file changed, 17 insertions(+), 50 deletions(-) (limited to 'interfaces/email/interactive') diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index 952c16d..10f6884 100755 --- a/interfaces/email/interactive/be-handle-mail +++ b/interfaces/email/interactive/be-handle-mail @@ -306,6 +306,8 @@ class DiffTree (libbe.diff.DiffTree): """ def report_or_none(self): report = self.report() + if report == None: + return None payload = report.get_payload() if payload == None or len(payload) == 0: return None @@ -315,7 +317,7 @@ class DiffTree (libbe.diff.DiffTree): if report == None: return "No changes" else: - return send_pgp_mime.flatten(self.report(), to_unicode=True) + return send_pgp_mime.flatten(report, to_unicode=True) def make_root(self): return MIMEMultipart() def join(self, root, parent, data_part): @@ -658,64 +660,29 @@ class Message (object): bd.load_all_bugs() subscribers = subscribe.get_bugdir_subscribers(bd, THIS_SERVER) - if len(subscribers) == 0: - return [] + return [] + for subscriber,subscriptions in subscribers.items(): + subscribers[subscriber] = [] + for id,types in subscriptions.items(): + for type in types: + subscribers[subscriber].append( + libbe.diff.Subscription(id,type)) before_bd, after_bd = self._get_before_and_after_bugdirs(bd, previous_revision) diff = Diff(before_bd, after_bd) - diff_tree = diff.report_tree(diff_tree=DiffTree) - bug_index = {} - for child in diff_tree.child_by_path("/bugs/new"): - bug_index[child.name] = ("added", child) - for child in diff_tree.child_by_path("/bugs/mod"): - bug_index[child.name] = ("modified", child) - for child in diff_tree.child_by_path("/bugs/rem"): - bug_index[child.name] = ("removed", child) + diff.full_report(diff_tree=DiffTree) header = self._subscriber_header(bd, previous_revision) emails = [] for subscriber,subscriptions in subscribers.items(): header.replace_header("to", subscriber) - parts = [] - if "DIR" in subscriptions: # make sure we check the DIR level first - ordered_subscriptions = [("DIR", subscriptions.pop("DIR"))] - else: - ordered_subscriptions = [] - ordered_subscriptions.extend(subscriptions.items()) - for id,types in ordered_subscriptions: - if id == "DIR": - if libbe.diff.BUGDIR_TYPE_ALL in types: - parts.append(diff_tree.report_or_none()) - break # we've attached everything, so stop checking. - if libbe.diff.BUGDIR_TYPE_NEW in types: - new = diff_tree.child_by_path("/bugs/new") - parts.append(new.report_or_none()) - continue # move on to next id - # if we get this far, id refers to a bug. - assert types == [libbe.diff.BUG_TYPE_ALL], types - if id not in bug_index: - continue # no changes here, move on to next id - type,bug_root = bug_index[id] - if type == "added" \ - and "DIR" in subscriptions \ - and subscriptions["DIR"] == libbe.diff.BUGDIR_TYPE_NEW: - # this info already attached at the DIR level - continue # move on to next id - parts.append(bug_root.report_or_none()) - parts = [p for p in parts if p != None] - if len(parts) == 0: - continue # no email to this subscriber - elif len(parts) == 1: - root = parts[0] - else: # join subscription parts into a single body - root = MIMEMultipart() - root[u"Content-Description"] = u"Multiple subscription trees." - for part in parts: - root.attach(part) - emails.append(send_pgp_mime.attach_root(header, root)) - if LOGFILE != None: - LOGFILE.write(u"Preparing to notify %s of changes\n" % 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) return emails def _get_before_and_after_bugdirs(self, bd, previous_revision=None): if previous_revision == None: -- cgit From 072a46eefb66733ae570a9fb9abbc9570461a490 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 21:53:58 -0500 Subject: Emptied interfaces directory Mostly throwing out a bunch of outdated GUIs. The email interface hasn't been moved over to the new 'Command' format yet... --- interfaces/email/interactive/becommands | 1 - 1 file changed, 1 deletion(-) delete mode 120000 interfaces/email/interactive/becommands (limited to 'interfaces/email/interactive') diff --git a/interfaces/email/interactive/becommands b/interfaces/email/interactive/becommands deleted file mode 120000 index 8af773c..0000000 --- a/interfaces/email/interactive/becommands +++ /dev/null @@ -1 +0,0 @@ -../../../becommands \ No newline at end of file -- cgit From 977ec6a64c8238bbed54bb41839aec2098a509ec Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 31 Dec 2009 14:33:30 -0500 Subject: Brought be-handle-mail up to date --- interfaces/email/interactive/be-handle-mail | 491 +++++++++++++--------------- 1 file changed, 229 insertions(+), 262 deletions(-) (limited to 'interfaces/email/interactive') diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index 10f6884..f8792f1 100755 --- a/interfaces/email/interactive/be-handle-mail +++ b/interfaces/email/interactive/be-handle-mail @@ -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 " - +THIS_SERVER = u'thor.physics.drexel.edu' +THIS_ADDRESS = u'BE Bugs ' +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 "" % repr(self.command) - return "" % self.extract_id() + return '' % repr(self.command) + return '' % 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 "" % (self.command, " ".join([str(s) for s in self.args])) + return '' % (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) -- cgit From 4d4283ecd654f1efb058cd7f7dba6be88b70ee92 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 1 Jan 2010 08:11:08 -0500 Subject: Updated copyright information --- interfaces/email/interactive/be-handle-mail | 2 +- interfaces/email/interactive/send_pgp_mime.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'interfaces/email/interactive') diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index f8792f1..14ae6ac 100755 --- a/interfaces/email/interactive/be-handle-mail +++ b/interfaces/email/interactive/be-handle-mail @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright (C) 2009 W. Trevor King +# Copyright (C) 2009-2010 W. Trevor King # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/interfaces/email/interactive/send_pgp_mime.py b/interfaces/email/interactive/send_pgp_mime.py index c19483e..517b1f0 100644 --- a/interfaces/email/interactive/send_pgp_mime.py +++ b/interfaces/email/interactive/send_pgp_mime.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -# Copyright (C) 2009 W. Trevor King +# Copyright (C) 2009-2010 W. Trevor King # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -- cgit From ebcf057be68161a2a2ceee34622f24d89a7022a0 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 19 Jan 2010 10:09:12 -0500 Subject: Fixed libbe.utuil->libbe.util typo in be-handle-mail. Also removed some references to the old XML interface. --- interfaces/email/interactive/README | 13 +------------ interfaces/email/interactive/be-handle-mail | 6 +++--- 2 files changed, 4 insertions(+), 15 deletions(-) (limited to 'interfaces/email/interactive') diff --git a/interfaces/email/interactive/README b/interfaces/email/interactive/README index 2070973..b25054c 100644 --- a/interfaces/email/interactive/README +++ b/interfaces/email/interactive/README @@ -28,10 +28,8 @@ first place. There are four parsing styles: creating bugs [be-bug:submit] new bug summary commenting on bugs [be-bug:] commit message control [be-bug] commit message - xml [be-bug:xml] commit message These are analogous to submit@bugs.debian.org, nnn@bugs.debian.org, -and control@bugs.debian.org respectively. The xml style has no Debian -analog. +and control@bugs.debian.org respectively. Creating bugs ============= @@ -108,15 +106,6 @@ shlex.split(). -- Goofy tagline ignored. -XML -=== - -This interface allows users without access to the versioned source of -the program to conveniently submit bugs and comments using be. You -should not attempt to compose emails for this interface by hand. See -the documentation for the `email-bugs' and `import-xml' be commands -for details. - Example emails ============== diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index 14ae6ac..161e118 100755 --- a/interfaces/email/interactive/be-handle-mail +++ b/interfaces/email/interactive/be-handle-mail @@ -374,8 +374,8 @@ class Message (object): def _subject_tag_type(self): """ Parse subject tag, return (type, value), where type is one of - None, "new", "comment", "control", or "xml"; and value is None - except in the case of "comment", in which case it's the bug + None, "new", "comment", or "control"; and value is None except + in the case of "comment", in which case it's the bug ID/shortname. """ tag,subject = self._split_subject() @@ -718,7 +718,7 @@ def open_logfile(logpath=None): LOGPATH = os.path.join(_THIS_DIR, logpath) if LOGFILE == None and LOGPATH != u'none': LOGFILE = codecs.open(LOGPATH, u'a+', - libbe.utuil.encoding.get_filesystem_encoding()) + libbe.util.encoding.get_filesystem_encoding()) def close_logfile(): if LOGFILE != None and LOGPATH not in [u'stderr', u'none']: -- cgit From b3d2784971e7a4fc088da46122c8351e1aea405f Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 19 Jan 2010 10:37:31 -0500 Subject: Allow forward slashes (/) in commit email tags in be-handle-mail. Also move unitsuite definition to the end of the file so it picks up GenerateGlobalTagsTestCase. --- interfaces/email/interactive/be-handle-mail | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'interfaces/email/interactive') diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index 161e118..036caa0 100755 --- a/interfaces/email/interactive/be-handle-mail +++ b/interfaces/email/interactive/be-handle-mail @@ -693,7 +693,7 @@ def generate_global_tags(tag_base=u'be-bug'): 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_COMMENT = re.compile(u'\[%s:([\-0-9a-z/]*)]' % tag_base) SUBJECT_TAG_CONTROL = SUBJECT_TAG_RESPONSE def open_logfile(logpath=None): @@ -724,9 +724,6 @@ def close_logfile(): 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(): result = unittest.TextTestRunner(verbosity=2).run(suite) num_errors = len(result.errors) @@ -900,9 +897,12 @@ class GenerateGlobalTagsTestCase (unittest.TestCase): 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]') + m = SUBJECT_TAG_COMMENT.match('[projectX-bug:abc/xyz-123]') self.failUnlessEqual(len(m.groups()), 1) - self.failUnlessEqual(m.group(1), u'xyz-123') + self.failUnlessEqual(m.group(1), u'abc/xyz-123') + +unitsuite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) if __name__ == "__main__": main(sys.argv) -- cgit From 508c0c0ec73bdcb802d18b30a6e5f40a04dfed52 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 21 Jan 2010 13:07:05 -0500 Subject: Fix be-handle-mail's notification creation for new libbe structure --- interfaces/email/interactive/be-handle-mail | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'interfaces/email/interactive') diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index 036caa0..c8343fc 100755 --- a/interfaces/email/interactive/be-handle-mail +++ b/interfaces/email/interactive/be-handle-mail @@ -619,7 +619,7 @@ class Message (object): 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 + if bd.storage.versioned == False: # no way to tell what's changed bd.storage.writeable = writeable raise NotificationFailed('Not versioned') @@ -656,7 +656,7 @@ class Message (object): commit_msg = self.commit_command.stdout assert commit_msg.startswith('Committed '), commit_msg after_revision = commit_msg[len('Committed '):] - before_revision = bd.vcs.revision_id(-2) + before_revision = bd.storage.revision_id(-2) else: before_revision = previous_revision if before_revision == None: @@ -664,12 +664,12 @@ class Message (object): before_bd = libbe.bugdir.BugDir(from_disk=False, manipulate_encodings=False) else: - before_bd = bd.duplicate_bugdir(before_revision) + before_bd = libbe.bugdir.RevisionedBugDir(bd, before_revision) #after_bd = bd.duplicate_bugdir(after_revision) after_bd = bd # assume no changes since commit a few cycles ago return (before_bd, after_bd) def _subscriber_header(self, bd, previous_revision=None): - root_dir = os.path.basename(bd.root) + root_dir = os.path.basename(bd.storage.repo) if previous_revision == None: subject = 'Changes to %s on %s by %s' \ % (root_dir, THIS_SERVER, self.author_addr()) @@ -795,7 +795,6 @@ def main(args): 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) @@ -816,6 +815,7 @@ def main(args): LOGFILE.write(u'Uncaught exception:\n%s\n' % (e,)) traceback.print_tb(sys.exc_traceback, file=LOGFILE) close_logfile() + m.commit_command.cleanup() UI.cleanup() sys.exit(1) else: @@ -847,6 +847,7 @@ def main(args): send_pgp_mime.mail(msg, send_pgp_mime.sendmail) close_logfile() + m.commit_command.cleanup() UI.cleanup() class GenerateGlobalTagsTestCase (unittest.TestCase): -- cgit From 53074356bf715c820d3b9b852cd45e5073ba765d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 24 Jan 2010 11:20:47 -0500 Subject: Rewrote documentation --- interfaces/email/interactive/README | 101 ++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 45 deletions(-) (limited to 'interfaces/email/interactive') diff --git a/interfaces/email/interactive/README b/interfaces/email/interactive/README index b25054c..f7d57ad 100644 --- a/interfaces/email/interactive/README +++ b/interfaces/email/interactive/README @@ -2,15 +2,14 @@ Overview ======== The interactive email interface to Bugs Everywhere (BE) attempts to -provide a Debian-bug-tracking-system-style interface to a BE +provide a `Debian-bug-tracking-system-style`_ interface to a BE repository. Users can mail in bug reports, comments, or control requests, which will be committed to the served repository. Developers can then pull the changes they approve of from the served repository into their other repositories and push updates back onto the served repository. -For details about the Debian bug tracking system that inspired this -interface, see http://www.debian.org/Bugs . +.. _Debian-bug-tracking-system-style: http://www.debian.org/Bugs Architecture ============ @@ -18,27 +17,34 @@ Architecture In order to reduce setup costs, the entire interface can piggyback on an existing email address, although from a security standpoint it's probably best to create a dedicated user. Incoming email is filtered -by procmail, with matching emails being piped into be-handle-mail for -execution. - -Once be-handle-mail receives the email, the parsing method is selected -according to the subject tag that procmail used grab the email in the -first place. There are four parsing styles: - Style Subject - creating bugs [be-bug:submit] new bug summary - commenting on bugs [be-bug:] commit message - control [be-bug] commit message -These are analogous to submit@bugs.debian.org, nnn@bugs.debian.org, -and control@bugs.debian.org respectively. +by procmail, with matching emails being piped into ``be-handle-mail`` +for execution. + +Once ``be-handle-mail`` receives the email, the parsing method is +selected according to the subject tag that procmail used grab the +email in the first place. There are four parsing styles: + + +--------------------+----------------------------------+ + | Style | Subject | + +====================+==================================+ + | creating bugs | [be-bug:submit] new bug summary | + +--------------------+----------------------------------+ + | commenting on bugs | [be-bug:] commit message | + +--------------------+----------------------------------+ + | control | [be-bug] commit message | + +--------------------+----------------------------------+ + +These are analogous to ``submit@bugs.debian.org``, +``nnn@bugs.debian.org``, and ``control@bugs.debian.org`` respectively. Creating bugs ============= This interface creates a bug whose summary is given by the email's post-tag subject. The body of the email must begin with a -pseudo-header containing at least the "Version" field. Anything after -the pseudo-header and before a line starting with '--' is, if present, -attached as the bug's first comment. +pseudo-header containing at least the ``Version`` field. Anything after +the pseudo-header and before a line starting with ``--`` is, if present, +attached as the bug's first comment.:: From jdoe@example.com Fri Apr 18 12:00:00 2008 From: John Doe @@ -51,22 +57,22 @@ attached as the bug's first comment. Severity: minor Someone should write up a series of test emails to send into - be-handle mail so we can test changes quickly without having to + be-handle-mail so we can test changes quickly without having to use procmail. -- Goofy tagline not included. -Available pseudo-headers are Version, Reporter, Assign, Depend, -Severity, Status, Tag, and Target. +Available pseudo-headers are ``Version``, ``Reporter``, ``Assign``, +``Depend``, ``Severity``, ``Status``, ``Tag``, and ``Target``. Commenting on bugs ================== This interface appends a comment to the bug specified in the subject tag. The the first non-multipart body is attached with the -appropriate content-type. In the case of "text/plain" contents, -anything following a line starting with '--' is stripped. +appropriate content-type. In the case of ``text/plain`` contents, +anything following a line starting with ``--`` is stripped.:: From jdoe@example.com Fri Apr 18 12:00:00 2008 From: John Doe @@ -85,11 +91,11 @@ Controlling bugs ================ This interface consists of a list of allowed be commands, with one -command per line. Blank lines and lines beginning with '#' are -ignored, as well anything following a line starting with '--'. All +command per line. Blank lines and lines beginning with ``#`` are +ignored, as well anything following a line starting with ``--``. All the listed commands are executed in order and their output returned. The commands are split into arguments with the POSIX-compliant -shlex.split(). +shlex.split().:: From jdoe@example.com Fri Apr 18 12:00:00 2008 From: John Doe @@ -109,37 +115,42 @@ shlex.split(). Example emails ============== -Take a look at my interfaces/email/interactive/examples for some +Take a look at ``interfaces/email/interactive/examples`` for some more examples. Procmail rules ============== -The file _procmailrc as it stands is fairly appropriate for as a -dedicated user's ~/.procmailrc. It forwards matching mail to -be-handle-mail, which should be installed somewhere in the user's -path. All non-matching mail is dumped into /dev/null. Everything -procmail does will be logged to ~/be-mail/procmail.log. +The file ``_procmailrc`` as it stands is fairly appropriate for as a +dedicated user's ``~/.procmailrc``. It forwards matching mail to +``be-handle-mail``, which should be installed somewhere in the user's +path. All non-matching mail is dumped into ``/dev/null``. Everything +procmail does will be logged to ``~/be-mail/procmail.log``. If you're piggybacking the interface on top of an existing account, -you probably only need to add the be-handle-mail stanza to your -existing ~/.procmailrc, since you will still want to receive non-bug -emails. +you probably only need to add the ``be-handle-mail`` stanza to your +existing ``~/.procmailrc``, since you will still want to receive +non-bug emails. -Note that you will probably have to add a - --be-dir /path/to/served/repository -option to the be-handle-mail invocation so it knows what repository to +Note that you will probably have to add a:: + + --repo /path/to/served/repository + +option to the ``be-handle-mail`` invocation so it knows what repository to serve. Multiple repositories may be served by the same email address by adding -multiple be-handle-mail stanzas, each matching a different tag, for -example the "[be-bug" portion of the stanza could be "[projectX-bug", -"[projectY-bug", etc. If you change the base tag, be sure to add a - --tag-base "projectX-bug" -or equivalent to your be-handle-mail invocation. +multiple ``be-handle-mail`` stanzas, each matching a different tag, for +example the ``[be-bug`` portion of the stanza could be ``[projectX-bug``, +``[projectY-bug``, etc. If you change the base tag, be sure to add a:: + + --tag-base "projectX-bug" + +or equivalent to your ``be-handle-mail`` invocation. Testing ======= -Send test emails in to be-handle-mail with something like - cat examples/blank | ./be-handle-mail -o -l - -a +Send test emails in to ``be-handle-mail`` with something like:: + + cat examples/blank | ./be-handle-mail -o -l - -a -- cgit From da8309e67c669b1cca5d39c8e7da34c9b431bef6 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 6 Feb 2010 09:47:20 -0500 Subject: Added page titles to the documentation & adjusted section levels. --- interfaces/email/interactive/README | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'interfaces/email/interactive') diff --git a/interfaces/email/interactive/README b/interfaces/email/interactive/README index f7d57ad..48bccdd 100644 --- a/interfaces/email/interactive/README +++ b/interfaces/email/interactive/README @@ -1,3 +1,7 @@ +*************** +Email Interface +*************** + Overview ======== -- cgit