infocalypse
 
(Steve Dougherty)
2013-07-09: Add initial notification checking support.

Add initial notification checking support. Supports multiple matching messages. Comments. Displays recognized requests and gives feedback otherwise.

diff --git a/infocalypse/wot.py b/infocalypse/wot.py
--- a/infocalypse/wot.py
+++ b/infocalypse/wot.py
@@ -61,7 +61,7 @@ HG: The first line has "{0}" added befor
 HG: The second line should be blank.
 HG: Following lines are the body of the message.
 HG: Below is the machine-readable footer describing the request. Modifying it
-HG: or putting things below it has the potential to cause problems.
+HG: might make it not work.
 {1}""".format(VCS_PREFIX, footer), from_identifier)
     # TODO: Abort in the case of a blank message?
     # Markdown support would be on receiving end. Maybe CLI preview eventually.
@@ -108,6 +108,9 @@ def check_notifications(ui, from_identit
     # http://bugs.python.org/issue917120
     reply_type, message_numbers = imap.search(None, '(SUBJECT %s)' % VCS_PREFIX)
 
+    # imaplib returns numbers in a singleton string separated by whitespace.
+    message_numbers = message_numbers[0].split()
+
     # fetch() expects strings for both. Individual message numbers are
     # separated by commas. It seems desirable to peek because it's not yet
     # apparent that this is a [vcs] message with YAML.
@@ -115,12 +118,9 @@ def check_notifications(ui, from_identit
     status, subjects = imap.fetch(','.join(message_numbers),
                                   r'(body.peek[header.fields Subject])')
 
-    # Expecting:
-    # ('5 (body[HEADER.FIELDS Subject] {47}', 'Subject: [vcs] test\r\n\r\n')
-    # However see http://bpaste.net/show/112243/ - imaplib does not appear to
-    # properly parse the closing parenthesis of the name/value pair,
-    # giving it instead as a single string.
-    # https://tools.ietf.org/html/rfc2060.html#section-7
+    # Expecting 2 list items from imaplib for each message, for example:
+    # ('5 (body[HEADER.FIELDS Subject] {47}', 'Subject: [vcs]  ...\r\n\r\n'),
+    # ')',
 
     # Exclude closing parens, which are of length one.
     subjects = filter(lambda x: len(x) == 2, subjects)
@@ -128,11 +128,76 @@ def check_notifications(ui, from_identit
     subjects = [x[1] for x in subjects]
 
     # Remove field name and trim whitespace.
-    subjects = [subject.rstrip()[len('Subject: '):] for subject in subjects]
+    subjects = dict((message_number, subject[len('Subject: '):].rstrip()) for
+                    message_number, subject in zip(message_numbers, subjects))
 
-    for subject in subjects:
+    for message_number, subject in subjects.iteritems():
         if subject.startswith(VCS_PREFIX):
-            print "Found VCS email '%s'" % subject
+            # Read the message at this point.
+            status, fetched = imap.fetch(str(message_number),
+                                         r'(body[text] '
+                                         r'body[header.fields From)')
+
+            # Expecting 3 list items, as with the subject fetch above.
+            body = fetched[0][1]
+            from_address = fetched[1][1][len('From: '):].rstrip()
+
+            read_message_yaml(ui, from_address, subject, body)
+
+
+def read_message_yaml(ui, from_address, subject, body):
+    # TODO: Will these line endings always be present? splitlines() doesn't
+    # seem clean either due to the work required to figure out the index.
+    # Find start and end in an attempt to tolerate things after the footer.
+    yaml_start = body.rfind('---\r\n')
+    yaml_end = body.rfind('...\r\n')
+
+    if yaml_start == -1 or yaml_end == -1:
+        ui.status("Notification '%s' does not have a request.\n" % subject)
+        return
+
+    def require(field, request):
+        if field not in request:
+            ui.status("Notification '%s' has a properly formatted request "
+                      "that does not include necessary information. ('%s')\n"
+                      % (subject, field))
+            return False
+        return True
+
+    try:
+        request = yaml.safe_load(body[yaml_start:yaml_end])
+
+        if not require('vcs', request) or not require('request', request):
+            return
+    except yaml.YAMLError, e:
+        ui.status("Notification '%s' has a request but it is not properly"
+                  " formatted. Details:\n%s\n" % (subject, e))
+        return
+
+    if request['vcs'] != 'Infocalypse':
+        ui.status("Notification '%s' is for '%s', not Infocalypse.\n"
+                  % (subject, request['vcs']))
+        return
+
+    if request['request'] == 'pull':
+        ui.status("Found pull request from '%s':\n" % from_address)
+        separator = ('-' * len(subject)) + '\n'
+        
+        ui.status(separator)
+        ui.status(subject[len(VCS_PREFIX):] + '\n')
+
+        ui.status(separator)
+        ui.status(body[:yaml_start])
+        ui.status(separator)
+
+        ui.status("To accept this request, pull from: %s\n"
+                  "               To your repository: %s\n" %
+                  (request['source'], request['target']))
+        return
+
+    ui.status("Notification '%s' has an unrecognized request of type '%s'"
+              % (subject, request['request']))
+
 
 
 def update_repo_listing(ui, for_identity):