diff options
author | Chris Ball <cjb@laptop.org> | 2010-06-20 19:19:06 -0400 |
---|---|---|
committer | Chris Ball <cjb@laptop.org> | 2010-06-20 19:19:06 -0400 |
commit | 0df4bd7ae194bb07f36a2a69a0549037de01cb52 (patch) | |
tree | ea9128bbbedd8df9b1d6c737f704260874680a6b /interfaces/email/interactive | |
parent | 429e33fb4c7be8daa791fb744a14024ef27a72c2 (diff) | |
parent | a2a51929a848ffa6db92ec7218994461ecccb50a (diff) | |
download | bugseverywhere-0df4bd7ae194bb07f36a2a69a0549037de01cb52.tar.gz |
Merge with Trevor.
Diffstat (limited to 'interfaces/email/interactive')
-rw-r--r-- | interfaces/email/interactive/README | 105 | ||||
-rwxr-xr-x | interfaces/email/interactive/be-handle-mail | 541 | ||||
l--------- | interfaces/email/interactive/becommands | 1 | ||||
-rw-r--r-- | interfaces/email/interactive/examples/email_bugs | 37 | ||||
-rw-r--r-- | interfaces/email/interactive/send_pgp_mime.py | 2 |
5 files changed, 348 insertions, 338 deletions
diff --git a/interfaces/email/interactive/README b/interfaces/email/interactive/README index 79ef9a9..48bccdd 100644 --- a/interfaces/email/interactive/README +++ b/interfaces/email/interactive/README @@ -1,16 +1,19 @@ +*************** +Email Interface +*************** + 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 +21,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 three parsing styles: - Style Subject - creating bugs [be-bug:submit] new bug summary - commenting on bugs [be-bug:<bug-id>] 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:<bug-id>] 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 <jdoe@example.com> @@ -51,22 +61,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 <jdoe@example.com> @@ -85,11 +95,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 <jdoe@example.com> @@ -109,37 +119,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:: + + --repo /path/to/served/repository -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 +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 diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index fa80698..c8343fc 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 <wking@drexel.edu> +# Copyright (C) 2009-2010 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 @@ -58,44 +58,51 @@ import shlex import sys import time import traceback +import types import doctest import unittest -from becommands import subscribe -import libbe.cmdutil, libbe.encoding, libbe.utility, libbe.diff, \ - libbe.bugdir, libbe.bug, libbe.comment +import libbe.bugdir +import libbe.bug +import libbe.comment +import libbe.diff +import libbe.command +import libbe.command.subscribe as subscribe +import libbe.storage +import libbe.ui.command_line +import libbe.util.encoding +import libbe.util.utility import send_pgp_mime -THIS_SERVER = u"thor.physics.drexel.edu" -THIS_ADDRESS = u"BE Bugs <wking@thor.physics.drexel.edu>" - +THIS_SERVER = u'thor.physics.drexel.edu' +THIS_ADDRESS = u'BE Bugs <wking@thor.physics.drexel.edu>' +UI = None _THIS_DIR = os.path.abspath(os.path.dirname(__file__)) -BE_DIR = _THIS_DIR -LOGPATH = os.path.join(_THIS_DIR, u"be-handle-mail.log") +LOGPATH = os.path.join(_THIS_DIR, u'be-handle-mail.log') LOGFILE = None # Tag strings generated by generate_global_tags() -SUBJECT_TAG_BASE = u"be-bug" +SUBJECT_TAG_BASE = u'be-bug' SUBJECT_TAG_RESPONSE = None SUBJECT_TAG_START = None SUBJECT_TAG_NEW = None SUBJECT_TAG_COMMENT = None SUBJECT_TAG_CONTROL = None -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"] +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): @@ -103,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) @@ -114,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 @@ -165,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) @@ -178,13 +185,12 @@ class ID (object): return self._cached_id def __str__(self): if self.command.ret != 0: - return "<id for %s>" % repr(self.command) - return "<id %s>" % self.extract_id() + return '<id for %s>' % repr(self.command) + return '<id %s>' % self.extract_id() class Command (object): """ - A becommands command wrapper. - Doesn't validate input, so do that before initializing. + A libbe.command.Command handler. Initialize with Command(msg, command, args=None, stdin=None) @@ -196,18 +202,17 @@ class Command (object): """ def __init__(self, msg, command, args=None, stdin=None): self.msg = msg - self.command = command if args == None: self.args = [] else: self.args = args - self.stdin = stdin + self.command = libbe.command.get_command_class(command_name=command)() + self.command._setup_io = lambda i_enc,o_enc : None self.ret = None + self.stdin = stdin self.stdout = None - self.stderr = None - self.err = None def __str__(self): - return "<command: %s %s>" % (self.command, " ".join([str(s) for s in self.args])) + return '<command: %s %s>' % (self.command, ' '.join([str(s) for s in self.args])) def normalize_args(self): """ Expand any ID placeholders in self.args. @@ -221,60 +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) - 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): @@ -304,6 +274,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 @@ -311,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(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): - 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) @@ -353,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 @@ -380,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): """ @@ -410,13 +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" + 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): @@ -426,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 @@ -445,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, @@ -465,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: @@ -481,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() @@ -491,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 @@ -500,22 +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() 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 = \ @@ -523,49 +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, 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): @@ -577,39 +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 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) @@ -625,86 +609,54 @@ 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) - if bd.vcs.versioned == False: # no way to tell what's changed - raise NotificationFailed("Not versioned") + bd = UI.storage_callbacks.get_bugdir() + writeable = bd.storage.writeable + bd.storage.writeable = False + if bd.storage.versioned == False: # no way to tell what's changed + bd.storage.writeable = writeable + raise NotificationFailed('Not versioned') bd.load_all_bugs() subscribers = subscribe.get_bugdir_subscribers(bd, THIS_SERVER) - if len(subscribers) == 0: - return [] + bd.storage.writeable = writeable + 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 subscribe.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: - 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 - 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: - # 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) + 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) + 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 "):] - before_revision = bd.vcs.revision_id(-2) + assert commit_msg.startswith('Committed '), commit_msg + after_revision = commit_msg[len('Committed '):] + before_revision = bd.storage.revision_id(-2) else: before_revision = previous_revision if before_revision == None: @@ -712,36 +664,36 @@ 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" \ + 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_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 def open_logfile(logpath=None): @@ -754,27 +706,25 @@ 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.util.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() 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) @@ -783,15 +733,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.") @@ -817,27 +767,28 @@ 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: @@ -845,12 +796,14 @@ def main(args): else: send_pgp_mime.mail(msg, send_pgp_mime.sendmail) 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) @@ -859,9 +812,11 @@ 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() + m.commit_command.cleanup() + UI.cleanup() sys.exit(1) else: response = m.response_email() @@ -869,21 +824,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: @@ -892,7 +847,8 @@ def main(args): send_pgp_mime.mail(msg, send_pgp_mime.sendmail) close_logfile() - + m.commit_command.cleanup() + UI.cleanup() class GenerateGlobalTagsTestCase (unittest.TestCase): def setUp(self): @@ -914,37 +870,40 @@ 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: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) 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 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 + +<?xml version="1.0" encoding="utf-8" ?> +<be-xml> + <version> + <tag>1.0.0</tag> + <branch-nick>be</branch-nick> + <revno>446</revno> + <revision-id>wking@drexel.edu-20091119214553-iqyw2cpqluww3zna</revision-id> + </version> + <bug> + <uuid>a</uuid> + <short-name>a</short-name> + <severity>minor</severity> + <status>open</status> + <creator>John Doe <jdoe@example.com></creator> + <created>Thu, 01 Jan 1970 00:00:00 +0000</created> + <summary>Bug A</summary> + </bug> + <bug> + <uuid>b</uuid> + <short-name>b</short-name> + <severity>minor</severity> + <status>closed</status> + <creator>Jane Doe <jdoe@example.com></creator> + <created>Thu, 01 Jan 1970 00:00:00 +0000</created> + <summary>Bug B</summary> + </bug> +</be-xml> + 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 <wking@drexel.edu> +# Copyright (C) 2009-2010 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 |