diff options
37 files changed, 1125 insertions, 99 deletions
@@ -38,9 +38,9 @@ MODULES += ${DOC_DIR} RM = rm -#PREFIX = /usr/local +PREFIX = /usr/local PREFIX = ${HOME} -INSTALL_OPTIONS = "--prefix=${PREFIX}" +#INSTALL_OPTIONS = "--prefix=${PREFIX}" .PHONY: all @@ -57,8 +57,8 @@ build: libbe/_version.py .PHONY: install install: doc build python setup.py install ${INSTALL_OPTIONS} - cp -v interfaces/xml/* ${PREFIX}/bin - cp -v interfaces/email/catmutt ${PREFIX}/bin +#cp -v interfaces/xml/* ${PREFIX}/bin +#cp -v interfaces/email/catmutt ${PREFIX}/bin .PHONY: clean diff --git a/becommands/assign.py b/becommands/assign.py index 536bca6..ba79aac 100644 --- a/becommands/assign.py +++ b/becommands/assign.py @@ -20,7 +20,7 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os >>> bd = bugdir.simple_bug_dir() @@ -28,17 +28,17 @@ def execute(args, test=False): >>> bd.bug_from_shortname("a").assigned is None True - >>> execute(["a"], test=True) + >>> execute(["a"], manipulate_encodings=False) >>> bd._clear_bugs() >>> bd.bug_from_shortname("a").assigned == bd.user_id True - >>> execute(["a", "someone"], test=True) + >>> execute(["a", "someone"], manipulate_encodings=False) >>> bd._clear_bugs() >>> print bd.bug_from_shortname("a").assigned someone - >>> execute(["a","none"], test=True) + >>> execute(["a","none"], manipulate_encodings=False) >>> bd._clear_bugs() >>> bd.bug_from_shortname("a").assigned is None True @@ -53,7 +53,8 @@ def execute(args, test=False): if len(args) > 2: help() raise cmdutil.UsageError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) bug = bd.bug_from_shortname(args[0]) if len(args) == 1: bug.assigned = bd.user_id diff --git a/becommands/close.py b/becommands/close.py index 0ba8f50..05bdc10 100644 --- a/becommands/close.py +++ b/becommands/close.py @@ -20,7 +20,7 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> from libbe import bugdir >>> import os @@ -28,7 +28,7 @@ def execute(args, test=False): >>> os.chdir(bd.root) >>> print bd.bug_from_shortname("a").status open - >>> execute(["a"], test=True) + >>> execute(["a"], manipulate_encodings=False) >>> bd._clear_bugs() >>> print bd.bug_from_shortname("a").status closed @@ -41,7 +41,8 @@ def execute(args, test=False): raise cmdutil.UsageError("Please specify a bug id.") if len(args) > 1: raise cmdutil.UsageError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) bug = bd.bug_from_shortname(args[0]) bug.status = "closed" bd.save() diff --git a/becommands/comment.py b/becommands/comment.py index 66f8da1..918f922 100644 --- a/becommands/comment.py +++ b/becommands/comment.py @@ -25,12 +25,12 @@ except ImportError: # look for non-core module from elementtree import ElementTree __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import time >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) - >>> execute(["a", "This is a comment about a"], test=True) + >>> execute(["a", "This is a comment about a"], manipulate_encodings=False) >>> bd._clear_bugs() >>> bug = bd.bug_from_shortname("a") >>> bug.load_comments(load_full=False) @@ -47,12 +47,12 @@ def execute(args, test=False): >>> if 'EDITOR' in os.environ: ... del os.environ["EDITOR"] - >>> execute(["b"], test=True) + >>> execute(["b"], manipulate_encodings=False) Traceback (most recent call last): UserError: No comment supplied, and EDITOR not specified. >>> os.environ["EDITOR"] = "echo 'I like cheese' > " - >>> execute(["b"], test=True) + >>> execute(["b"], manipulate_encodings=False) >>> bd._clear_bugs() >>> bug = bd.bug_from_shortname("b") >>> bug.load_comments(load_full=False) @@ -80,7 +80,8 @@ def execute(args, test=False): bugname = shortname is_reply = False - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) bug = bd.bug_from_shortname(bugname) bug.load_comments(load_full=False) if is_reply: @@ -113,6 +114,10 @@ def execute(args, test=False): if options.XML == False: new = parent.new_reply(body=body) + if options.author != None: + new.From = options.author + if options.alt_id != None: + new.alt_id = options.alt_id if options.content_type != None: new.content_type = options.content_type else: # import XML comment [list] @@ -157,6 +162,10 @@ def execute(args, test=False): def get_parser(): parser = cmdutil.CmdOptionParser("be comment ID [COMMENT]") + parser.add_option("-a", "--author", metavar="AUTHOR", dest="author", + help="Set the comment author", default=None) + parser.add_option("--alt-id", metavar="ID", dest="alt_id", + help="Set an alternate comment ID", default=None) parser.add_option("-c", "--content-type", metavar="MIME", dest="content_type", help="Set comment content-type (e.g. text/plain)", default=None) parser.add_option("-x", "--xml", action="store_true", default=False, diff --git a/becommands/depend.py b/becommands/depend.py index 48e1527..977edae 100644 --- a/becommands/depend.py +++ b/becommands/depend.py @@ -18,22 +18,22 @@ from libbe import cmdutil, bugdir import os, copy __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> from libbe import utility >>> bd = bugdir.simple_bug_dir() >>> bd.save() >>> os.chdir(bd.root) - >>> execute(["a", "b"], test=True) + >>> execute(["a", "b"], manipulate_encodings=False) Blocks on a: b - >>> execute(["a"], test=True) + >>> execute(["a"], manipulate_encodings=False) Blocks on a: b - >>> execute(["--show-status", "a"], test=True) # doctest: +NORMALIZE_WHITESPACE + >>> execute(["--show-status", "a"], manipulate_encodings=False) # doctest: +NORMALIZE_WHITESPACE Blocks on a: b closed - >>> execute(["-r", "a", "b"], test=True) + >>> execute(["-r", "a", "b"], manipulate_encodings=False) """ parser = get_parser() options, args = parser.parse_args(args) @@ -47,7 +47,8 @@ def execute(args, test=False): help() raise cmdutil.UsageError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) bugA = bd.bug_from_shortname(args[0]) if len(args) == 2: bugB = bd.bug_from_shortname(args[1]) diff --git a/becommands/diff.py b/becommands/diff.py index f3474b3..20fcb4c 100644 --- a/becommands/diff.py +++ b/becommands/diff.py @@ -20,7 +20,7 @@ from libbe import cmdutil, bugdir, diff import os __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os >>> bd = bugdir.simple_bug_dir() @@ -31,7 +31,7 @@ def execute(args, test=False): >>> changed = bd.rcs.commit("Closed bug a") >>> os.chdir(bd.root) >>> if bd.rcs.versioned == True: - ... execute([original], test=True) + ... execute([original], manipulate_encodings=False) ... else: ... print "a:cm: Bug A\\nstatus: open -> closed\\n" Modified bug reports: @@ -48,7 +48,8 @@ def execute(args, test=False): revision = args[0] if len(args) > 1: raise cmdutil.UsageError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) if bd.rcs.versioned == False: print "This directory is not revision-controlled." else: diff --git a/becommands/help.py b/becommands/help.py index a8ae338..a8f346a 100644 --- a/becommands/help.py +++ b/becommands/help.py @@ -19,9 +19,11 @@ from libbe import cmdutil, utility __desc__ = __doc__ -def execute(args): +def execute(args, manipulate_encodings=False): """ - Print help of specified command. + Print help of specified command (the manipulate_encodings argument + is ignored). + >>> execute(["help"]) Usage: be help [COMMAND] <BLANKLINE> diff --git a/becommands/init.py b/becommands/init.py index 5b2a416..4156a26 100644 --- a/becommands/init.py +++ b/becommands/init.py @@ -19,7 +19,7 @@ import os.path from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> from libbe import utility, rcs >>> import os @@ -29,7 +29,7 @@ def execute(args, test=False): ... except bugdir.NoBugDir, e: ... True True - >>> execute(['--root', dir.path], test=True) + >>> execute(['--root', dir.path], manipulate_encodings=False) No revision control detected. Directory initialized. >>> del(dir) @@ -40,17 +40,17 @@ def execute(args, test=False): >>> rcs.init('.') >>> print rcs.name Arch - >>> execute([], test=True) + >>> execute([], manipulate_encodings=False) Using Arch for revision control. Directory initialized. >>> rcs.cleanup() >>> try: - ... execute(['--root', '.'], test=True) + ... execute(['--root', '.'], manipulate_encodings=False) ... except cmdutil.UserError, e: ... str(e).startswith("Directory already initialized: ") True - >>> execute(['--root', '/highly-unlikely-to-exist'], test=True) + >>> execute(['--root', '/highly-unlikely-to-exist'], manipulate_encodings=False) Traceback (most recent call last): UserError: No such directory: /highly-unlikely-to-exist >>> os.chdir('/') @@ -64,7 +64,7 @@ def execute(args, test=False): bd = bugdir.BugDir(options.root_dir, from_disk=False, sink_to_existing_root=False, assert_new_BugDir=True, - manipulate_encodings=not test) + manipulate_encodings=manipulate_encodings) except bugdir.NoRootEntry: raise cmdutil.UserError("No such directory: %s" % options.root_dir) except bugdir.AlreadyInitialized: diff --git a/becommands/list.py b/becommands/list.py index 5ba1821..50038e6 100644 --- a/becommands/list.py +++ b/becommands/list.py @@ -26,14 +26,14 @@ __desc__ = __doc__ AVAILABLE_CMPS = [fn[4:] for fn in dir(bug) if fn[:4] == 'cmp_'] AVAILABLE_CMPS.remove("attr") # a cmp_* template. -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) - >>> execute([], test=True) + >>> execute([], manipulate_encodings=False) a:om: Bug A - >>> execute(["--status", "all"], test=True) + >>> execute(["--status", "all"], manipulate_encodings=False) a:om: Bug A b:cm: Bug B """ @@ -46,11 +46,13 @@ def execute(args, test=False): if options.sort_by != None: for cmp in options.sort_by.split(','): if cmp not in AVAILABLE_CMPS: - raise cmdutil.UserError("Invalid sort on '%s'.\nValid sorts:\n %s" - % (cmp, '\n '.join(AVAILABLE_CMPS))) + raise cmdutil.UserError( + "Invalid sort on '%s'.\nValid sorts:\n %s" + % (cmp, '\n '.join(AVAILABLE_CMPS))) cmp_list.append(eval('bug.cmp_%s' % cmp)) - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) bd.load_all_bugs() # select status if options.status != None: diff --git a/becommands/merge.py b/becommands/merge.py index c030dd0..c7cae2b 100644 --- a/becommands/merge.py +++ b/becommands/merge.py @@ -18,7 +18,7 @@ from libbe import cmdutil, bugdir import os, copy __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> from libbe import utility >>> bd = bugdir.simple_bug_dir() @@ -37,7 +37,7 @@ def execute(args, test=False): >>> dummy.time = 2 >>> bd.save() >>> os.chdir(bd.root) - >>> execute(["a", "b"], test=True) + >>> execute(["a", "b"], manipulate_encodings=False) Merging bugs a and b >>> bd._clear_bugs() >>> a = bd.bug_from_shortname("a") @@ -133,7 +133,8 @@ def execute(args, test=False): help() raise cmdutil.UsageError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) bugA = bd.bug_from_shortname(args[0]) bugA.load_comments() bugB = bd.bug_from_shortname(args[1]) diff --git a/becommands/new.py b/becommands/new.py index 5325ccc..8512e22 100644 --- a/becommands/new.py +++ b/becommands/new.py @@ -19,14 +19,14 @@ from libbe import cmdutil, bugdir import sys __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os, time >>> from libbe import bug >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) >>> bug.uuid_gen = lambda: "X" - >>> execute (["this is a test",], test=True) + >>> execute (["this is a test",], manipulate_encodings=False) Created bug with ID X >>> bd.load() >>> bug = bd.bug_from_uuid("X") @@ -44,7 +44,8 @@ def execute(args, test=False): cmdutil.default_complete(options, args, parser) if len(args) != 1: raise cmdutil.UsageError("Please supply a summary message") - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) if args[0] == '-': # read summary from stdin summary = sys.stdin.readline() else: diff --git a/becommands/open.py b/becommands/open.py index b4b1025..ee81422 100644 --- a/becommands/open.py +++ b/becommands/open.py @@ -20,14 +20,14 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) >>> print bd.bug_from_shortname("b").status closed - >>> execute(["b"], test=True) + >>> execute(["b"], manipulate_encodings=False) >>> bd._clear_bugs() >>> print bd.bug_from_shortname("b").status open @@ -40,7 +40,8 @@ def execute(args, test=False): raise cmdutil.UsageError, "Please specify a bug id." if len(args) > 1: raise cmdutil.UsageError, "Too many arguments." - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) bug = bd.bug_from_shortname(args[0]) bug.status = "open" bd.save() diff --git a/becommands/remove.py b/becommands/remove.py index d441bfe..d6ba999 100644 --- a/becommands/remove.py +++ b/becommands/remove.py @@ -17,7 +17,7 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> from libbe import mapfile >>> import os @@ -25,7 +25,7 @@ def execute(args, test=False): >>> os.chdir(bd.root) >>> print bd.bug_from_shortname("b").status closed - >>> execute (["b"], test=True) + >>> execute (["b"], manipulate_encodings=False) Removed bug b >>> bd._clear_bugs() >>> try: @@ -40,7 +40,8 @@ def execute(args, test=False): bugid_args={0: lambda bug : bug.active==True}) if len(args) != 1: raise cmdutil.UsageError, "Please specify a bug id." - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) bug = bd.bug_from_shortname(args[0]) bd.remove_bug(bug) bd.save() diff --git a/becommands/set.py b/becommands/set.py index 510eca7..7bef644 100644 --- a/becommands/set.py +++ b/becommands/set.py @@ -32,18 +32,18 @@ def _value_string(bd, setting): val = None return str(val) -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) - >>> execute(["target"], test=True) + >>> execute(["target"], manipulate_encodings=False) None - >>> execute(["target", "tomorrow"], test=True) - >>> execute(["target"], test=True) + >>> execute(["target", "tomorrow"], manipulate_encodings=False) + >>> execute(["target"], manipulate_encodings=False) tomorrow - >>> execute(["target", "none"], test=True) - >>> execute(["target"], test=True) + >>> execute(["target", "none"], manipulate_encodings=False) + >>> execute(["target"], manipulate_encodings=False) None """ parser = get_parser() @@ -51,7 +51,8 @@ def execute(args, test=False): complete(options, args, parser) if len(args) > 2: raise cmdutil.UsageError, "Too many arguments" - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) if len(args) == 0: keys = bd.settings_properties keys.sort() diff --git a/becommands/severity.py b/becommands/severity.py index fde9fba..4e95638 100644 --- a/becommands/severity.py +++ b/becommands/severity.py @@ -20,17 +20,17 @@ from libbe import cmdutil, bugdir, bug __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) - >>> execute(["a"], test=True) + >>> execute(["a"], manipulate_encodings=False) minor - >>> execute(["a", "wishlist"], test=True) - >>> execute(["a"], test=True) + >>> execute(["a", "wishlist"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) wishlist - >>> execute(["a", "none"], test=True) + >>> execute(["a", "none"], manipulate_encodings=False) Traceback (most recent call last): UserError: Invalid severity level: none """ @@ -39,7 +39,8 @@ def execute(args, test=False): complete(options, args, parser) if len(args) not in (1,2): raise cmdutil.UsageError - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) bug = bd.bug_from_shortname(args[0]) if len(args) == 1: print bug.severity diff --git a/becommands/show.py b/becommands/show.py index d053cc3..ae1c7f3 100644 --- a/becommands/show.py +++ b/becommands/show.py @@ -22,12 +22,12 @@ import sys from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) - >>> execute (["a",], test=True) # doctest: +ELLIPSIS + >>> execute (["a",], manipulate_encodings=False) # doctest: +ELLIPSIS ID : a Short name : a Severity : minor @@ -39,7 +39,7 @@ def execute(args, test=False): Created : ... Bug A <BLANKLINE> - >>> execute (["--xml", "a"], test=True) # doctest: +ELLIPSIS + >>> execute (["--xml", "a"], manipulate_encodings=False) # doctest: +ELLIPSIS <?xml version="1.0" encoding="..." ?> <bug> <uuid>a</uuid> @@ -57,7 +57,8 @@ def execute(args, test=False): bugid_args={-1: lambda bug : bug.active==True}) if len(args) == 0: raise cmdutil.UsageError - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) for shortname in args: if shortname.count(':') > 1: raise cmdutil.UserError("Invalid id '%s'." % shortname) diff --git a/becommands/status.py b/becommands/status.py index 89ae49a..a122aec 100644 --- a/becommands/status.py +++ b/becommands/status.py @@ -17,17 +17,17 @@ from libbe import cmdutil, bugdir, bug __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) - >>> execute(["a"], test=True) + >>> execute(["a"], manipulate_encodings=False) open - >>> execute(["a", "closed"], test=True) - >>> execute(["a"], test=True) + >>> execute(["a", "closed"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) closed - >>> execute(["a", "none"], test=True) + >>> execute(["a", "none"], manipulate_encodings=False) Traceback (most recent call last): UserError: Invalid status: none """ @@ -36,7 +36,8 @@ def execute(args, test=False): complete(options, args, parser) if len(args) not in (1,2): raise cmdutil.UsageError - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) bug = bd.bug_from_shortname(args[0]) if len(args) == 1: print bug.status @@ -56,7 +57,8 @@ def get_parser(): def help(): try: # See if there are any per-tree status configurations - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=False) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) except bugdir.NoBugDir, e: pass # No tree, just show the defaults longest_status_len = max([len(s) for s in bug.status_values]) diff --git a/becommands/tag.py b/becommands/tag.py index a139528..2932589 100644 --- a/becommands/tag.py +++ b/becommands/tag.py @@ -18,7 +18,7 @@ from libbe import cmdutil, bugdir import os, copy __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> from libbe import utility >>> bd = bugdir.simple_bug_dir() @@ -26,25 +26,25 @@ def execute(args, test=False): >>> a = bd.bug_from_shortname("a") >>> print a.extra_strings [] - >>> execute(["a", "GUI"], test=True) + >>> execute(["a", "GUI"], manipulate_encodings=False) Tags for a: GUI >>> bd._clear_bugs() # resync our copy of bug >>> a = bd.bug_from_shortname("a") >>> print a.extra_strings ['TAG:GUI'] - >>> execute(["a", "later"], test=True) + >>> execute(["a", "later"], manipulate_encodings=False) Tags for a: GUI later - >>> execute(["a"], test=True) + >>> execute(["a"], manipulate_encodings=False) Tags for a: GUI later - >>> execute(["--list"], test=True) + >>> execute(["--list"], manipulate_encodings=False) GUI later - >>> execute(["a", "Alphabetically first"], test=True) + >>> execute(["a", "Alphabetically first"], manipulate_encodings=False) Tags for a: Alphabetically first GUI @@ -57,15 +57,15 @@ def execute(args, test=False): >>> print a.extra_strings [] >>> a.save() - >>> execute(["a"], test=True) + >>> execute(["a"], manipulate_encodings=False) >>> bd._clear_bugs() # resync our copy of bug >>> a = bd.bug_from_shortname("a") >>> print a.extra_strings [] - >>> execute(["a", "Alphabetically first"], test=True) + >>> execute(["a", "Alphabetically first"], manipulate_encodings=False) Tags for a: Alphabetically first - >>> execute(["--remove", "a", "Alphabetically first"], test=True) + >>> execute(["--remove", "a", "Alphabetically first"], manipulate_encodings=False) """ parser = get_parser() options, args = parser.parse_args(args) @@ -78,7 +78,8 @@ def execute(args, test=False): help() raise cmdutil.UsageError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) if options.list: bd.load_all_bugs() tags = [] diff --git a/becommands/target.py b/becommands/target.py index 905c639..66bacb8 100644 --- a/becommands/target.py +++ b/becommands/target.py @@ -22,20 +22,20 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args, test=False): +def execute(args, manipulate_encodings=True): """ >>> import os >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) - >>> execute(["a"], test=True) + >>> execute(["a"], manipulate_encodings=False) No target assigned. - >>> execute(["a", "tomorrow"], test=True) - >>> execute(["a"], test=True) + >>> execute(["a", "tomorrow"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) tomorrow - >>> execute(["--list"], test=True) + >>> execute(["--list"], manipulate_encodings=False) tomorrow - >>> execute(["a", "none"], test=True) - >>> execute(["a"], test=True) + >>> execute(["a", "none"], manipulate_encodings=False) + >>> execute(["a"], manipulate_encodings=False) No target assigned. """ parser = get_parser() @@ -46,7 +46,8 @@ def execute(args, test=False): if len(args) not in (1, 2): if not (options.list == True and len(args) == 0): raise cmdutil.UsageError - bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=manipulate_encodings) if options.list: ts = set([bd.bug_from_uuid(bug).target for bug in bd.list_uuids()]) for target in sorted(ts): diff --git a/interfaces/email/interactive/_procmailrc b/interfaces/email/interactive/_procmailrc new file mode 100644 index 0000000..56f11e5 --- /dev/null +++ b/interfaces/email/interactive/_procmailrc @@ -0,0 +1,22 @@ +# .procmailrc +# +# see man procmail, procmailrc, and procmailex +# +# If you already have a ~/.procmailrc file, you probably only need to +# insert the bug-email grabbing stanza in your ~/.procmailrc. +# +# This file is released to the Public Domain. + +MAILDIR=$HOME/be-mail +LOGFILE=$MAILDIR/procmail.log + +# Grab all incoming bug emails (but not replies). This rule eats +# matching emails (i.e. no further procmail processing). +:0 +* ^Subject: [be-mail] +* !^Subject:.*[be-bug].*Re: +| be-handle-mail + +# Drop everything else +:0 +/dev/null diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail new file mode 100755 index 0000000..c3769be --- /dev/null +++ b/interfaces/email/interactive/be-handle-mail @@ -0,0 +1,283 @@ +#!/usr/bin/env python +# +# Copyright (C) 2009 W. Trevor King <wking@drexel.edu> +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Provide and email interface to the distributed bugtracker Bugs +Everywhere. Recieves incoming email via procmail and allows users to +select actions with their subject lines. Subject lines follow the +format + [be-bug] command (options) (args) +With the body of the email being used as the final argument for the +commands "new" and "comment", and ignored otherwise. The options and +arguments are split on whitespace, so don't use whitespace inside a +single argument. + +Eventually we'll commit after every message. +""" + +import codecs +import cStringIO as StringIO +import email +import email.utils +import libbe.cmdutil, libbe.encoding, libbe.utility +import os +import os.path +import send_pgp_mime +import sys +import time +import traceback + +SUBJECT_COMMENT = "[be-bug]" +HANDLER_ADDRESS = "BE Bugs <wking@thor.physics.drexel.edu>" +_THIS_DIR = os.path.abspath(os.path.dirname(__file__)) +BE_DIR = _THIS_DIR +LOGPATH = os.path.join(_THIS_DIR, "be-handle-mail.log") +LOGFILE = None + +libbe.encoding.ENCODING = "utf-8" # force default encoding +ENCODING = libbe.encoding.get_encoding() + +ALLOWED_COMMANDS = ["new", "comment", "list", "show", "help"] + +class InvalidEmail (ValueError): + def __init__(self, msg, info, message): + ValueError.__init__(self, message) + self.msg = msg + self.info = info + def response(self): + ret = 1 + out_text = None + return (ret, out_text, self.stderr_msg(), self.info) + def stderr_msg(self): + err_text = [u"Invalid email (particular type unknown):\n", + unicode(self), u"", + send_pgp_mime.flatten(self.msg, to_unicode=True)] + return u"\n".join(err_text) + +class InvalidSubject (InvalidEmail): + def stderr_msg(self): + err_text = u"\n".join([u"InvalidSubject:\n", + unicode(self), u"", + u"full subject was:", + self.msg["subject"]]) + return err_text + +class InvalidCommand (InvalidEmail): + def __init__(self, msg, info, command): + message = "Invalid command '%s'" % command + InvalidEmail.__init__(self, msg, info, message) + self.command = command + def stderr_msg(self): + err_text = u"\n".join([u"InvalidCommand:\n", + unicode(self), u"", + u"full subject was:", + self.msg["subject"]]) + return err_text + +def get_body_type(msg): + for part in msg.walk(): + if part.is_multipart(): + continue + return (part.get_payload(decode=1), part.get_content_type()) + +def run_message(msg_text): + """ + Attempt to execute the email given in the email string msg_text. + Raises assorted subclasses of InvalidEmail in the case of invalid + messages, otherwise return the exit code, stdout, and stderr + produced by the command, as well as a dictionary of information + gleaned from the email. + """ + p=email.Parser.Parser() + msg=p.parsestr(msg_text) + + info = {} + author = send_pgp_mime.source_email(msg, return_realname=True) + info["author_name"] = author[0] + info["author_email"] = author[1] + info["author_addr"] = email.utils.formataddr( + (info["author_name"], info["author_email"])) + info["message-id"] = msg["message-id"] + if LOGFILE != None: + LOGFILE.write("handling %s\n" % (info["author_addr"])) + LOGFILE.write("\n%s\n\n" % msg_text) + if "subject" not in msg: + raise InvalidSubject(msg, info, "Email must contain a subject") + args = msg["subject"].split() + if len(args) < 1 or args[0] != SUBJECT_COMMENT: + raise InvalidSubject( + msg, info, "Subject must start with '%s '" % SUBJECT_COMMENT) + elif len(args) < 2: + raise InvalidCommand(msg, info, "") # don't accept blank commands + command = args[1] + info["command"] = command + if command not in ALLOWED_COMMANDS: + raise InvalidCommand(msg, info, command) + if len(args) > 2: + command_args = args[2:] + else: + command_args = [] + stdin = None + if command in ["new", "comment"]: + body,mime_type = get_body_type(msg) + if command == "new": + if "--reporter" not in args and "-r" not in args: + command_args = ["--reporter", info["author_addr"]]+command_args + body = body.strip().split("\n", 1)[0] # only take first line + elif command == "comment": + if "--author" not in args and "-a" not in args: + command_args = ["--author", info["author_addr"]] + command_args + if "--content-type" not in args and "-c" not in args: + command_args = ["--content-type", mime_type] + command_args + if "--alt-id" not in args: + command_args = ["--alt-id", msg["message-id"]] + command_args + command_args.append("-") + stdin = body + info["command-args"] = command_args + # set stdin and catch stdout and stderr + new_stdin = StringIO.StringIO(stdin) + new_stdout = codecs.getwriter(ENCODING)(StringIO.StringIO()) + new_stderr = codecs.getwriter(ENCODING)(StringIO.StringIO()) + orig_stdin = sys.stdin + orig_stdout = sys.stdout + orig_stderr = sys.stderr + sys.stdin = new_stdin + sys.stdout = new_stdout + sys.stderr = new_stderr + # run the command + err = None + os.chdir(BE_DIR) + try: + ret = libbe.cmdutil.execute(command, command_args, + manipulate_encodings=False) + except libbe.cmdutil.GetHelp: + print libbe.cmdutil.help(command) + except libbe.cmdutil.GetCompletions: + err = InvalidCommand(msg, info, "invalid option '--complete'") + except libbe.cmdutil.UsageError, e: + err = InvalidCommand(msg, info, e) + except libbe.cmdutil.UserError, e: + err = InvalidCommand(msg, info, e) + # restore stdin, stdout, and stderr + sys.stdout.flush() + sys.stderr.flush() + sys.stdin = orig_stdin + sys.stdout = orig_stdout + sys.stderr = orig_stderr + out_text = codecs.decode(new_stdout.getvalue(), ENCODING) + err_text = codecs.decode(new_stderr.getvalue(), ENCODING) + if err != None: + raise err + if LOGFILE != None: + LOGFILE.write(u"stdout? " + str(type(out_text))) + LOGFILE.write(u"\n%s\n\n" % out_text) + return (ret, out_text, err_text, info) + +def compose_response(ret, out_text, err_text, info): + if "author_addr" not in info: + return None + if "command" not in info: + info["command"] = u"-BLANK-" + if "command_args" not in info: + info["command_args"] = [] + response_header = [u"From: %s" % HANDLER_ADDRESS, + u"To: %s" % info["author_addr"], + u"Date: %s" % libbe.utility.time_to_str(time.time()), + u"Subject: %s Re: %s"%(SUBJECT_COMMENT,info["command"]), + ] + if "message-id" in info: + response_header.append(u"In-reply-to: %s" % info["message-id"]) + response_body = [u"Results of running: (exit code %d)" % ret, + u" %s %s" % (info["command"], + u" ".join(info["command_args"]))] + if out_text != None and len(out_text) > 0: + response_body.extend([u"", u"stdout:", u"", out_text]) + if err_text != None and len(err_text) > 0: + response_body.extend([u"", u"stderr:", u"", err_text]) + response_body.append(u"") # trailing endline + response_email = send_pgp_mime.Mail(u"\n".join(response_header), + u"\n".join(response_body)) + if LOGFILE != None: + LOGFILE.write("responding to %s: %s\n" + % (info["author_addr"], info["command"])) + LOGFILE.write("\n%s\n\n" + % send_pgp_mime.flatten(response_email.plain(), + to_unicode=True)) + return response_email + +def open_logfile(logpath=None): + """ + If logpath=None, default to global LOGPATH. + Special logpath strings: + "-" set LOGFILE to sys.stderr + "none" disable logging + Relative logpaths are expanded relative to _THIS_DIR + """ + global LOGPATH, LOGFILE + if logpath != None: + if logpath == "-": + LOGPATH = "stderr" + LOGFILE = sys.stderr + elif logpath == "none": + LOGPATH = "none" + LOGFILE = None + elif os.path.isabs(logpath): + LOGPATH = logpath + else: + LOGPATH = os.path.join(_THIS_DIR, logpath) + if LOGFILE == None and LOGPATH != "none": + LOGFILE = codecs.open(LOGPATH, "a+", ENCODING) + LOGFILE.write("Default encoding: %s\n" % ENCODING) + +def close_logfile(): + if LOGFILE != None and LOGPATH not in ["stderr", "none"]: + LOGFILE.close() + + +def main(): + from optparse import OptionParser + + usage="be-handle-mail [options]\n\n%s" % (__doc__) + parser = OptionParser(usage=usage) + 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.") + parser.add_option('-l', '--logfile', dest='logfile', metavar='LOGFILE', + help='Set the logfile to LOGFILE. Relative paths are relative to the location of this be-handle-mail file (%s). The special value of "-" directs the log output to stderr, and "none" disables logging.' % _THIS_DIR) + + options,args = parser.parse_args() + + msg_text = sys.stdin.read() + libbe.encoding.set_IO_stream_encodings(ENCODING) # _after_ reading message + open_logfile(options.logfile) + try: + ret,out_text,err_text,info = run_message(msg_text) + except InvalidEmail, e: + ret,out_text,err_text,info = e.response() + except Exception, e: + if LOGFILE != None: + LOGFILE.write("Uncaught exception:\n%s\n" % (e,)) + traceback.print_tb(sys.exc_traceback, file=LOGFILE) + close_logfile() + sys.exit(1) + response_email = compose_response(ret, out_text, err_text, info).plain() + if options.output == True: + print send_pgp_mime.flatten(response_email, to_unicode=True) + else: + send_pgp_mime.mail(response_email, send_pgp_mime.sendmail) + close_logfile() + +if __name__ == "__main__": + main() diff --git a/interfaces/email/interactive/becommands b/interfaces/email/interactive/becommands new file mode 120000 index 0000000..8af773c --- /dev/null +++ b/interfaces/email/interactive/becommands @@ -0,0 +1 @@ +../../../becommands
\ No newline at end of file diff --git a/interfaces/email/interactive/examples/blank b/interfaces/email/interactive/examples/blank new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/interfaces/email/interactive/examples/blank diff --git a/interfaces/email/interactive/examples/comment b/interfaces/email/interactive/examples/comment new file mode 100644 index 0000000..1d60748 --- /dev/null +++ b/interfaces/email/interactive/examples/comment @@ -0,0 +1,9 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: <xyz@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] comment a1d + +We sure do. diff --git a/interfaces/email/interactive/examples/help b/interfaces/email/interactive/examples/help new file mode 100644 index 0000000..14e887c --- /dev/null +++ b/interfaces/email/interactive/examples/help @@ -0,0 +1,9 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: <abcd@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] help + +Dummy content diff --git a/interfaces/email/interactive/examples/invalid_command b/interfaces/email/interactive/examples/invalid_command new file mode 100644 index 0000000..4d18f09 --- /dev/null +++ b/interfaces/email/interactive/examples/invalid_command @@ -0,0 +1,9 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: <abcd@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] close + +Dummy content diff --git a/interfaces/email/interactive/examples/invalid_subject b/interfaces/email/interactive/examples/invalid_subject new file mode 100644 index 0000000..e148d0b --- /dev/null +++ b/interfaces/email/interactive/examples/invalid_subject @@ -0,0 +1,9 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: <abcd@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: Spam! + +Dummy content diff --git a/interfaces/email/interactive/examples/list b/interfaces/email/interactive/examples/list new file mode 100644 index 0000000..333315f --- /dev/null +++ b/interfaces/email/interactive/examples/list @@ -0,0 +1,9 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: <abcd@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] list --status all + +Dummy content diff --git a/interfaces/email/interactive/examples/missing_command b/interfaces/email/interactive/examples/missing_command new file mode 100644 index 0000000..fefe41b --- /dev/null +++ b/interfaces/email/interactive/examples/missing_command @@ -0,0 +1,9 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: <abcd@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] abcde + +Dummy content diff --git a/interfaces/email/interactive/examples/new b/interfaces/email/interactive/examples/new new file mode 100644 index 0000000..7ac6dce --- /dev/null +++ b/interfaces/email/interactive/examples/new @@ -0,0 +1,9 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: <abcd@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] new + +Need tests for the email interface. diff --git a/interfaces/email/interactive/examples/show b/interfaces/email/interactive/examples/show new file mode 100644 index 0000000..3ff56f4 --- /dev/null +++ b/interfaces/email/interactive/examples/show @@ -0,0 +1,9 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: <abcd@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] show --xml 361 + +Dummy content diff --git a/interfaces/email/interactive/examples/unicode b/interfaces/email/interactive/examples/unicode new file mode 100644 index 0000000..e5b0775 --- /dev/null +++ b/interfaces/email/interactive/examples/unicode @@ -0,0 +1,9 @@ +From jdoe@example.com Fri Apr 18 11:18:58 2008 +Message-ID: <abcd@example.com> +Date: Fri, 18 Apr 2008 12:00:00 +0000 +From: John Doe <jdoe@example.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Subject: [be-bug] show --xml f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a + +Dummy content diff --git a/interfaces/email/interactive/libbe b/interfaces/email/interactive/libbe new file mode 120000 index 0000000..7d18612 --- /dev/null +++ b/interfaces/email/interactive/libbe @@ -0,0 +1 @@ +../../../libbe
\ No newline at end of file diff --git a/interfaces/email/interactive/send_pgp_mime.py b/interfaces/email/interactive/send_pgp_mime.py new file mode 100644 index 0000000..e0451c9 --- /dev/null +++ b/interfaces/email/interactive/send_pgp_mime.py @@ -0,0 +1,608 @@ +#!/usr/bin/python +# +# Copyright (C) 2009 W. Trevor King <wking@drexel.edu> +# +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" +Python module and command line tool for sending pgp/mime email. + +Mostly uses subprocess to call gpg and a sendmail-compatible mailer. +If you lack gpg, either don't use the encryption functions or adjust +the pgp_* commands. You may need to adjust the sendmail command to +point to whichever sendmail-compatible mailer you have on your system. +""" + +from cStringIO import StringIO +import os +import re +#import GnuPGInterface # Maybe should use this instead of subprocess +import smtplib +import subprocess +import sys +import tempfile +import types + +try: + from email.mime.text import MIMEText + from email.mime.multipart import MIMEMultipart + from email.mime.application import MIMEApplication + from email.encoders import encode_7or8bit + from email.generator import Generator + from email.parser import Parser + from email.utils import getaddress +except ImportError: + # adjust to old python 2.4 + from email.MIMEText import MIMEText + from email.MIMEMultipart import MIMEMultipart + from email.MIMENonMultipart import MIMENonMultipart + from email.Encoders import encode_7or8bit + from email.Generator import Generator + from email.parser import Parser + from email.Utils import getaddresses + + getaddress = getaddresses + class MIMEApplication (MIMENonMultipart): + def __init__(self, _data, _subtype, _encoder, **params): + MIMENonMultipart.__init__(self, 'application', _subtype, **params) + self.set_payload(_data) + _encoder(self) + +usage="""usage: %prog [options] + +Scriptable PGP MIME email using gpg. + +You can use gpg-agent for passphrase caching if your key requires a +passphrase (it better!). Example usage would be to install gpg-agent, +and then run + export GPG_TTY=`tty` + eval $(gpg-agent --daemon) +in your shell before invoking this script. See gpg-agent(1) for more +details. Alternatively, you can send your passphrase in on stdin + echo 'passphrase' | %prog [options] +or use the --passphrase-file option + %prog [options] --passphrase-file FILE [more options] +Both of these alternatives are much less secure than gpg-agent. You +have been warned. +""" + +verboseInvoke = False +PGP_SIGN_AS = None +PASSPHRASE = None + +# The following commands are adapted from my .mutt/pgp configuration +# +# Printf-like sequences: +# %a The value of PGP_SIGN_AS. +# %f Expands to the name of a file with text to be signed/encrypted. +# %p Expands to the passphrase argument. +# %R A string with some number (0 on up) of pgp_reciepient_arg +# strings. +# %r One key ID (e.g. recipient email address) to build a +# pgp_reciepient_arg string. +# +# The above sequences can be used to optionally print a string if +# their length is nonzero. For example, you may only want to pass the +# -u/--local-user argument to gpg if PGP_SIGN_AS is defined. To +# optionally print a string based upon one of the above sequences, the +# following construct is used +# %?<sequence_char>?<optional_string>? +# where sequence_char is a character from the table above, and +# optional_string is the string you would like printed if status_char +# is nonzero. optional_string may contain other sequence as well as +# normal text, but it may not contain any question marks. +# +# see http://codesorcery.net/old/mutt/mutt-gnupg-howto +# http://www.mutt.org/doc/manual/manual-6.html#pgp_autosign +# http://tldp.org/HOWTO/Mutt-GnuPG-PGP-HOWTO-8.html +# for more details + +pgp_recipient_arg='-r "%r"' +pgp_stdin_passphrase_arg='--passphrase-fd 0' +pgp_sign_command='/usr/bin/gpg --no-verbose --quiet --batch %p --output - --detach-sign --armor --textmode %?a?-u "%a"? %f' +pgp_encrypt_only_command='/usr/bin/gpg --no-verbose --quiet --batch --output - --encrypt --armor --textmode --always-trust --encrypt-to "%a" %R -- %f' +pgp_encrypt_sign_command='/usr/bin/gpg --no-verbose --quiet --batch %p --output - --encrypt --sign %?a?-u "%a"? --armor --textmode --always-trust --encrypt-to "%a" %R -- %f' +sendmail='/usr/sbin/sendmail -t' + +def execute(args, stdin=None, expect=(0,)): + """ + Execute a command (allows us to drive gpg). + """ + if verboseInvoke == True: + print >> sys.stderr, '$ '+args + try: + p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, close_fds=True) + except OSError, e: + strerror = '%s\nwhile executing %s' % (e.args[1], args) + raise Exception, strerror + output, error = p.communicate(input=stdin) + status = p.wait() + if verboseInvoke == True: + print >> sys.stderr, '(status: %d)\n%s%s' % (status, output, error) + if status not in expect: + strerror = '%s\nwhile executing %s\n%s\n%d' % (args[1], args, error, status) + raise Exception, strerror + return status, output, error + +def replace(template, format_char, replacement_text): + """ + >>> replace('--textmode %?a?-u %a? %f', 'f', 'file.in') + '--textmode %?a?-u %a? file.in' + >>> replace('--textmode %?a?-u %a? %f', 'a', '0xHEXKEY') + '--textmode -u 0xHEXKEY %f' + >>> replace('--textmode %?a?-u %a? %f', 'a', '') + '--textmode %f' + """ + if replacement_text == None: + replacement_text = "" + regexp = re.compile('%[?]'+format_char+'[?]([^?]*)[?]') + if len(replacement_text) > 0: + str = regexp.sub('\g<1>', template) + else: + str = regexp.sub('', template) + regexp = re.compile('%'+format_char) + str = regexp.sub(replacement_text, str) + return str + +def flatten(msg, to_unicode=False): + """ + Produce flat text output from an email Message instance. + """ + assert msg != None + fp = StringIO() + g = Generator(fp, mangle_from_=False) + g.flatten(msg) + text = fp.getvalue() + if to_unicode == True: + encoding = msg.get_content_charset() + text = unicode(text, encoding=encoding) + return text + +def source_email(msg, return_realname=False): + """ + Search the header of an email Message instance to find the + sender's email address. + """ + froms = msg.get_all('from', []) + from_tuples = getaddresses(froms) # [(realname, email_address), ...] + assert len(from_tuples) == 1 + if return_realname == True: + return from_tuples[0] # (realname, email_address) + return from_tuples[0][1] # email_address + +def target_emails(msg): + """ + Search the header of an email Message instance to find a + list of recipient's email addresses. + """ + tos = msg.get_all('to', []) + ccs = msg.get_all('cc', []) + bccs = msg.get_all('bcc', []) + resent_tos = msg.get_all('resent-to', []) + resent_ccs = msg.get_all('resent-cc', []) + resent_bccs = msg.get_all('resent-bcc', []) + all_recipients = getaddresses(tos + ccs + bccs + resent_tos + resent_ccs + resent_bccs) + return [addr[1] for addr in all_recipients] + +def mail(msg, sendmail=None): + """ + Send an email Message instance on its merry way. + + We can shell out to the user specified sendmail in case + the local host doesn't have an SMTP server set up + for easy smtplib usage. + """ + if sendmail != None: + execute(sendmail, stdin=flatten(msg)) + return None + s = smtplib.SMTP() + s.connect() + s.sendmail(from_addr=source_email(msg), + to_addrs=target_emails(msg), + msg=flatten(msg)) + s.close() + +class Mail (object): + """ + See http://www.ietf.org/rfc/rfc3156.txt for specification details. + >>> m = Mail('\\n'.join(['From: me@big.edu','To: you@big.edu','Subject: testing']), 'check 1 2\\ncheck 1 2\\n') + >>> print m.sourceEmail() + me@big.edu + >>> print m.targetEmails() + ['you@big.edu'] + >>> print flatten(m.clearBodyPart()) + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Disposition: inline + <BLANKLINE> + check 1 2 + check 1 2 + <BLANKLINE> + >>> print flatten(m.plain()) + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + From: me@big.edu + To: you@big.edu + Subject: testing + <BLANKLINE> + check 1 2 + check 1 2 + <BLANKLINE> + >>> m.sign() + >>> signed.set_boundary('boundsep') + >>> print m.stripSig(flatten(signed)).replace('\\t', ' '*4) + Content-Type: multipart/signed; + protocol="application/pgp-signature"; + micalg="pgp-sha1"; boundary="boundsep" + MIME-Version: 1.0 + From: me@big.edu + To: you@big.edu + Subject: testing + Content-Disposition: inline + <BLANKLINE> + --boundsep + Content-Type: text/plain; charset="us-ascii" + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Type: text/plain + Content-Disposition: inline + <BLANKLINE> + check 1 2 + check 1 2 + <BLANKLINE> + --boundsep + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Description: signature + Content-Type: application/pgp-signature; name="signature.asc"; + charset="us-ascii" + <BLANKLINE> + -----BEGIN PGP SIGNATURE----- + SIGNATURE STRIPPED (depends on current time) + -----END PGP SIGNATURE----- + <BLANKLINE> + --boundsep-- + >>> encrypted = m.encrypt() + >>> encrypted.set_boundary('boundsep') + >>> print m.stripPGP(flatten(encrypted)).replace('\\t', ' '*4) + Content-Type: multipart/encrypted; + protocol="application/pgp-encrypted"; + micalg="pgp-sha1"; boundary="boundsep" + MIME-Version: 1.0 + From: me@big.edu + To: you@big.edu + Subject: testing + Content-Disposition: inline + <BLANKLINE> + --boundsep + Content-Type: application/pgp-encrypted + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + <BLANKLINE> + Version: 1 + <BLANKLINE> + --boundsep + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Type: application/octet-stream; charset="us-ascii" + <BLANKLINE> + -----BEGIN PGP MESSAGE----- + MESSAGE STRIPPED (depends on current time) + -----END PGP MESSAGE----- + <BLANKLINE> + --boundsep-- + >>> signedAndEncrypted = m.signAndEncrypt() + >>> signedAndEncrypted.set_boundary('boundsep') + >>> print m.stripPGP(flatten(signedAndEncrypted)).replace('\\t', ' '*4) + Content-Type: multipart/encrypted; + protocol="application/pgp-encrypted"; + micalg="pgp-sha1"; boundary="boundsep" + MIME-Version: 1.0 + From: me@big.edu + To: you@big.edu + Subject: testing + Content-Disposition: inline + <BLANKLINE> + --boundsep + Content-Type: application/pgp-encrypted + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + <BLANKLINE> + Version: 1 + <BLANKLINE> + --boundsep + MIME-Version: 1.0 + Content-Transfer-Encoding: 7bit + Content-Type: application/octet-stream; charset="us-ascii" + <BLANKLINE> + -----BEGIN PGP MESSAGE----- + MESSAGE STRIPPED (depends on current time) + -----END PGP MESSAGE----- + <BLANKLINE> + --boundsep-- + """ + def __init__(self, header, body): + self.header = header.strip() + self.body = body + if type(self.header) == types.UnicodeType: + self.header = self.header.encode("ascii") + p = Parser() + self.headermsg = p.parsestr(self.header, headersonly=True) + def sourceEmail(self): + return source_email(self.headermsg) + def targetEmails(self): + return target_emails(self.headermsg) + def encodedMIMEText(self, body, encoding=None): + if encoding == None: + if type(body) == types.StringType: + encoding = "US-ASCII" + elif type(body) == types.UnicodeType: + for encoding in ["US-ASCII", "ISO-8859-1", "UTF-8"]: + try: + body.encode(encoding) + except UnicodeError: + pass + else: + break + assert encoding != None + # Create the message ('plain' stands for Content-Type: text/plain) + if encoding == "US-ASCII": + return MIMEText(body) + else: + return MIMEText(body.encode(encoding), 'plain', encoding) + def clearBodyPart(self): + body = self.encodedMIMEText(self.body) + body.add_header('Content-Disposition', 'inline') + return body + def passphrase_arg(self, passphrase=None): + if passphrase == None and PASSPHRASE != None: + passphrase = PASSPHRASE + if passphrase == None: + return (None,'') + return (passphrase, pgp_stdin_passphrase_arg) + def plain(self): + """ + text/plain + """ + msg = self.encodedMIMEText(self.body) + for k,v in self.headermsg.items(): + msg[k] = v + return msg + def sign(self, passphrase=None): + """ + multipart/signed + +-> text/plain (body) + +-> application/pgp-signature (signature) + """ + passphrase,pass_arg = self.passphrase_arg(passphrase) + body = self.clearBodyPart() + bfile = tempfile.NamedTemporaryFile() + bfile.write(flatten(body)) + bfile.flush() + + args = replace(pgp_sign_command, 'f', bfile.name) + if PGP_SIGN_AS == None: + pgp_sign_as = '<%s>' % self.sourceEmail() + else: + pgp_sign_as = PGP_SIGN_AS + args = replace(args, 'a', pgp_sign_as) + args = replace(args, 'p', pass_arg) + status,output,error = execute(args, stdin=passphrase) + signature = output + + sig = MIMEApplication(_data=signature, _subtype='pgp-signature; name="signature.asc"', _encoder=encode_7or8bit) + sig['Content-Description'] = 'signature' + sig.set_charset('us-ascii') + + msg = MIMEMultipart('signed', micalg='pgp-sha1', protocol='application/pgp-signature') + msg.attach(body) + msg.attach(sig) + + for k,v in self.headermsg.items(): + msg[k] = v + msg['Content-Disposition'] = 'inline' + return msg + def encrypt(self, passphrase=None): + """ + multipart/encrypted + +-> application/pgp-encrypted (control information) + +-> application/octet-stream (body) + """ + body = self.clearBodyPart() + bfile = tempfile.NamedTemporaryFile() + bfile.write(flatten(body)) + bfile.flush() + + recipient_string = ' '.join([replace(pgp_recipient_arg, 'r', recipient) for recipient in self.targetEmails()]) + args = replace(pgp_encrypt_only_command, 'R', recipient_string) + args = replace(args, 'f', bfile.name) + if PGP_SIGN_AS == None: + pgp_sign_as = '<%s>' % self.sourceEmail() + else: + pgp_sign_as = PGP_SIGN_AS + args = replace(args, 'a', pgp_sign_as) + status,output,error = execute(args) + encrypted = output + + enc = MIMEApplication(_data=encrypted, _subtype='octet-stream', _encoder=encode_7or8bit) + enc.set_charset('us-ascii') + + control = MIMEApplication(_data='Version: 1\n', _subtype='pgp-encrypted', _encoder=encode_7or8bit) + + msg = MIMEMultipart('encrypted', micalg='pgp-sha1', protocol='application/pgp-encrypted') + msg.attach(control) + msg.attach(enc) + + for k,v in self.headermsg.items(): + msg[k] = v + msg['Content-Disposition'] = 'inline' + return msg + def signAndEncrypt(self, passphrase=None): + """ + multipart/encrypted + +-> application/pgp-encrypted (control information) + +-> application/octet-stream (body) + """ + passphrase,pass_arg = self.passphrase_arg(passphrase) + body = self.sign() + body.__delitem__('Bcc') + bfile = tempfile.NamedTemporaryFile() + bfile.write(flatten(body)) + bfile.flush() + + recipient_string = ' '.join([replace(pgp_recipient_arg, 'r', recipient) for recipient in self.targetEmails()]) + args = replace(pgp_encrypt_only_command, 'R', recipient_string) + args = replace(args, 'f', bfile.name) + if PGP_SIGN_AS == None: + pgp_sign_as = '<%s>' % self.sourceEmail() + else: + pgp_sign_as = PGP_SIGN_AS + args = replace(args, 'a', pgp_sign_as) + args = replace(args, 'p', pass_arg) + status,output,error = execute(args, stdin=passphrase) + encrypted = output + + enc = MIMEApplication(_data=encrypted, _subtype='octet-stream', _encoder=encode_7or8bit) + enc.set_charset('us-ascii') + + control = MIMEApplication(_data='Version: 1\n', _subtype='pgp-encrypted', _encoder=encode_7or8bit) + + msg = MIMEMultipart('encrypted', micalg='pgp-sha1', protocol='application/pgp-encrypted') + msg.attach(control) + msg.attach(enc) + + for k,v in self.headermsg.items(): + msg[k] = v + msg['Content-Disposition'] = 'inline' + return msg + def stripChanging(self, text, start, stop, replacement): + stripping = False + lines = [] + for line in text.splitlines(): + line.strip() + if stripping == False: + lines.append(line) + if line == start: + stripping = True + lines.append(replacement) + else: + if line == stop: + stripping = False + lines.append(line) + return '\n'.join(lines) + def stripSig(self, text): + return self.stripChanging(text, + '-----BEGIN PGP SIGNATURE-----', + '-----END PGP SIGNATURE-----', + 'SIGNATURE STRIPPED (depends on current time)') + def stripPGP(self, text): + return self.stripChanging(text, + '-----BEGIN PGP MESSAGE-----', + '-----END PGP MESSAGE-----', + 'MESSAGE STRIPPED (depends on current time)') + +def test(): + import doctest + doctest.testmod() + + +if __name__ == '__main__': + from optparse import OptionParser + + parser = OptionParser(usage=usage) + parser.add_option('-t', '--test', dest='test', action='store_true', + help='Run doctests and exit') + + parser.add_option('-H', '--header-file', dest='header_filename', + help='file containing email header', metavar='FILE') + parser.add_option('-B', '--body-file', dest='body_filename', + help='file containing email body', metavar='FILE') + + parser.add_option('-P', '--passphrase-file', dest='passphrase_file', + help='file containing gpg passphrase', metavar='FILE') + parser.add_option('-p', '--passphrase-fd', dest='passphrase_fd', + help='file descriptor from which to read gpg passphrase (0 for stdin)', + type="int", metavar='DESCRIPTOR') + + parser.add_option('--mode', dest='mode', default='sign', + help="One of 'sign', 'encrypt', 'sign-encrypt', or 'plain'. Defaults to %default.", + metavar='MODE') + + parser.add_option('-a', '--sign-as', dest='sign_as', + help="The gpg key to sign with (gpg's -u/--local-user)", + metavar='KEY') + + parser.add_option('--output', dest='output', action='store_true', + help="Don't mail the generated message, print it to stdout instead.") + + (options, args) = parser.parse_args() + + stdin_used = False + + if options.passphrase_file != None: + PASSPHRASE = file(options.passphrase_file, 'r').read() + elif options.passphrase_fd != None: + if options.passphrase_fd == 0: + stdin_used = True + PASSPHRASE = sys.stdin.read() + else: + PASSPHRASE = os.read(options.passphrase_fd) + + if options.sign_as: + PGP_SIGN_AS = options.sign_as + + if options.test == True: + test() + sys.exit(0) + + header = None + if options.header_filename != None: + if options.header_filename == '-': + assert stdin_used == False + stdin_used = True + header = sys.stdin.read() + else: + header = file(options.header_filename, 'r').read() + if header == None: + raise Exception, "missing header" + body = None + if options.body_filename != None: + if options.body_filename == '-': + assert stdin_used == False + stdin_used = True + body = sys.stdin.read() + else: + body = file(options.body_filename, 'r').read() + if body == None: + raise Exception, "missing body" + + m = Mail(header, body) + if options.mode == "sign": + message = m.sign() + elif options.mode == "encrypt": + message = m.encrypt() + elif options.mode == "sign-encrypt": + message = m.signAndEncrypt() + elif options.mode == "plain": + message = m.plain() + else: + print "Unrecognized mode '%s'" % options.mode + + if options.output == True: + message = flatten(message) + print message + else: + mail(message, sendmail) diff --git a/interfaces/xml/be-mbox-to-xml b/interfaces/xml/be-mbox-to-xml index 840a2a6..a5bf13e 100755 --- a/interfaces/xml/be-mbox-to-xml +++ b/interfaces/xml/be-mbox-to-xml @@ -28,9 +28,6 @@ from libbe.encoding import get_encoding, set_IO_stream_encodings from mailbox import mbox, Message # the mailbox people really want an on-disk copy from time import asctime, gmtime import types -from xml.sax import make_parser -from xml.sax.handler import ContentHandler -from xml.sax.saxutils import escape DEFAULT_ENCODING = get_encoding() set_IO_stream_encodings(DEFAULT_ENCODING) diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py index 36d5d96..e9c16ed 100644 --- a/libbe/cmdutil.py +++ b/libbe/cmdutil.py @@ -70,10 +70,11 @@ def get_command(command_name): return cmd -def execute(cmd, args): +def execute(cmd, args, manipulate_encodings=True): enc = encoding.get_encoding() cmd = get_command(cmd) - cmd.execute([a.decode(enc) for a in args]) + cmd.execute([a.decode(enc) for a in args], + manipulate_encodings=manipulate_encodings) return 0 def help(cmd=None, parser=None): diff --git a/libbe/encoding.py b/libbe/encoding.py index d603602..4af864e 100644 --- a/libbe/encoding.py +++ b/libbe/encoding.py @@ -19,11 +19,15 @@ import locale import sys import doctest +ENCODING = None # override get_encoding() output by setting this + def get_encoding(): """ Guess a useful input/output/filesystem encoding... Maybe we need seperate encodings for input/output and filesystem? Hmm... """ + if ENCODING != None: + return ENCODING encoding = locale.getpreferredencoding() or sys.getdefaultencoding() if sys.platform != 'win32' or sys.version_info[:2] > (2, 3): encoding = locale.getlocale(locale.LC_TIME)[1] or encoding |