Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/buildbot/buildbot/changes/mail.py
diff options
context:
space:
mode:
Diffstat (limited to 'buildbot/buildbot/changes/mail.py')
-rw-r--r--buildbot/buildbot/changes/mail.py458
1 files changed, 458 insertions, 0 deletions
diff --git a/buildbot/buildbot/changes/mail.py b/buildbot/buildbot/changes/mail.py
new file mode 100644
index 0000000..7d86d47
--- /dev/null
+++ b/buildbot/buildbot/changes/mail.py
@@ -0,0 +1,458 @@
+# -*- test-case-name: buildbot.test.test_mailparse -*-
+
+"""
+Parse various kinds of 'CVS notify' email.
+"""
+import os, re
+from email import message_from_file
+from email.Utils import parseaddr
+from email.Iterators import body_line_iterator
+
+from zope.interface import implements
+from twisted.python import log
+from buildbot import util
+from buildbot.interfaces import IChangeSource
+from buildbot.changes import changes
+from buildbot.changes.maildir import MaildirService
+
+class MaildirSource(MaildirService, util.ComparableMixin):
+ """This source will watch a maildir that is subscribed to a FreshCVS
+ change-announcement mailing list.
+ """
+ implements(IChangeSource)
+
+ compare_attrs = ["basedir", "pollinterval"]
+ name = None
+
+ def __init__(self, maildir, prefix=None):
+ MaildirService.__init__(self, maildir)
+ self.prefix = prefix
+ if prefix and not prefix.endswith("/"):
+ log.msg("%s: you probably want your prefix=('%s') to end with "
+ "a slash")
+
+ def describe(self):
+ return "%s mailing list in maildir %s" % (self.name, self.basedir)
+
+ def messageReceived(self, filename):
+ path = os.path.join(self.basedir, "new", filename)
+ change = self.parse_file(open(path, "r"), self.prefix)
+ if change:
+ self.parent.addChange(change)
+ os.rename(os.path.join(self.basedir, "new", filename),
+ os.path.join(self.basedir, "cur", filename))
+
+ def parse_file(self, fd, prefix=None):
+ m = message_from_file(fd)
+ return self.parse(m, prefix)
+
+class FCMaildirSource(MaildirSource):
+ name = "FreshCVS"
+
+ def parse(self, m, prefix=None):
+ """Parse mail sent by FreshCVS"""
+
+ # FreshCVS sets From: to "user CVS <user>", but the <> part may be
+ # modified by the MTA (to include a local domain)
+ name, addr = parseaddr(m["from"])
+ if not name:
+ return None # no From means this message isn't from FreshCVS
+ cvs = name.find(" CVS")
+ if cvs == -1:
+ return None # this message isn't from FreshCVS
+ who = name[:cvs]
+
+ # we take the time of receipt as the time of checkin. Not correct,
+ # but it avoids the out-of-order-changes issue. See the comment in
+ # parseSyncmail about using the 'Date:' header
+ when = util.now()
+
+ files = []
+ comments = ""
+ isdir = 0
+ lines = list(body_line_iterator(m))
+ while lines:
+ line = lines.pop(0)
+ if line == "Modified files:\n":
+ break
+ while lines:
+ line = lines.pop(0)
+ if line == "\n":
+ break
+ line = line.rstrip("\n")
+ linebits = line.split(None, 1)
+ file = linebits[0]
+ if prefix:
+ # insist that the file start with the prefix: FreshCVS sends
+ # changes we don't care about too
+ if file.startswith(prefix):
+ file = file[len(prefix):]
+ else:
+ continue
+ if len(linebits) == 1:
+ isdir = 1
+ elif linebits[1] == "0 0":
+ isdir = 1
+ files.append(file)
+ while lines:
+ line = lines.pop(0)
+ if line == "Log message:\n":
+ break
+ # message is terminated by "ViewCVS links:" or "Index:..." (patch)
+ while lines:
+ line = lines.pop(0)
+ if line == "ViewCVS links:\n":
+ break
+ if line.find("Index: ") == 0:
+ break
+ comments += line
+ comments = comments.rstrip() + "\n"
+
+ if not files:
+ return None
+
+ change = changes.Change(who, files, comments, isdir, when=when)
+
+ return change
+
+class SyncmailMaildirSource(MaildirSource):
+ name = "Syncmail"
+
+ def parse(self, m, prefix=None):
+ """Parse messages sent by the 'syncmail' program, as suggested by the
+ sourceforge.net CVS Admin documentation. Syncmail is maintained at
+ syncmail.sf.net .
+ """
+ # pretty much the same as freshcvs mail, not surprising since CVS is
+ # the one creating most of the text
+
+ # The mail is sent from the person doing the checkin. Assume that the
+ # local username is enough to identify them (this assumes a one-server
+ # cvs-over-rsh environment rather than the server-dirs-shared-over-NFS
+ # model)
+ name, addr = parseaddr(m["from"])
+ if not addr:
+ return None # no From means this message isn't from FreshCVS
+ at = addr.find("@")
+ if at == -1:
+ who = addr # might still be useful
+ else:
+ who = addr[:at]
+
+ # we take the time of receipt as the time of checkin. Not correct (it
+ # depends upon the email latency), but it avoids the
+ # out-of-order-changes issue. Also syncmail doesn't give us anything
+ # better to work with, unless you count pulling the v1-vs-v2
+ # timestamp out of the diffs, which would be ugly. TODO: Pulling the
+ # 'Date:' header from the mail is a possibility, and
+ # email.Utils.parsedate_tz may be useful. It should be configurable,
+ # however, because there are a lot of broken clocks out there.
+ when = util.now()
+
+ subject = m["subject"]
+ # syncmail puts the repository-relative directory in the subject:
+ # mprefix + "%(dir)s %(file)s,%(oldversion)s,%(newversion)s", where
+ # 'mprefix' is something that could be added by a mailing list
+ # manager.
+ # this is the only reasonable way to determine the directory name
+ space = subject.find(" ")
+ if space != -1:
+ directory = subject[:space]
+ else:
+ directory = subject
+
+ files = []
+ comments = ""
+ isdir = 0
+ branch = None
+
+ lines = list(body_line_iterator(m))
+ while lines:
+ line = lines.pop(0)
+
+ if (line == "Modified Files:\n" or
+ line == "Added Files:\n" or
+ line == "Removed Files:\n"):
+ break
+
+ while lines:
+ line = lines.pop(0)
+ if line == "\n":
+ break
+ if line == "Log Message:\n":
+ lines.insert(0, line)
+ break
+ line = line.lstrip()
+ line = line.rstrip()
+ # note: syncmail will send one email per directory involved in a
+ # commit, with multiple files if they were in the same directory.
+ # Unlike freshCVS, it makes no attempt to collect all related
+ # commits into a single message.
+
+ # note: syncmail will report a Tag underneath the ... Files: line
+ # e.g.: Tag: BRANCH-DEVEL
+
+ if line.startswith('Tag:'):
+ branch = line.split(' ')[-1].rstrip()
+ continue
+
+ thesefiles = line.split(" ")
+ for f in thesefiles:
+ f = directory + "/" + f
+ if prefix:
+ # insist that the file start with the prefix: we may get
+ # changes we don't care about too
+ if f.startswith(prefix):
+ f = f[len(prefix):]
+ else:
+ continue
+ break
+ # TODO: figure out how new directories are described, set
+ # .isdir
+ files.append(f)
+
+ if not files:
+ return None
+
+ while lines:
+ line = lines.pop(0)
+ if line == "Log Message:\n":
+ break
+ # message is terminated by "Index:..." (patch) or "--- NEW FILE.."
+ # or "--- filename DELETED ---". Sigh.
+ while lines:
+ line = lines.pop(0)
+ if line.find("Index: ") == 0:
+ break
+ if re.search(r"^--- NEW FILE", line):
+ break
+ if re.search(r" DELETED ---$", line):
+ break
+ comments += line
+ comments = comments.rstrip() + "\n"
+
+ change = changes.Change(who, files, comments, isdir, when=when,
+ branch=branch)
+
+ return change
+
+# Bonsai mail parser by Stephen Davis.
+#
+# This handles changes for CVS repositories that are watched by Bonsai
+# (http://www.mozilla.org/bonsai.html)
+
+# A Bonsai-formatted email message looks like:
+#
+# C|1071099907|stephend|/cvs|Sources/Scripts/buildbot|bonsai.py|1.2|||18|7
+# A|1071099907|stephend|/cvs|Sources/Scripts/buildbot|master.cfg|1.1|||18|7
+# R|1071099907|stephend|/cvs|Sources/Scripts/buildbot|BuildMaster.py|||
+# LOGCOMMENT
+# Updated bonsai parser and switched master config to buildbot-0.4.1 style.
+#
+# :ENDLOGCOMMENT
+#
+# In the first example line, stephend is the user, /cvs the repository,
+# buildbot the directory, bonsai.py the file, 1.2 the revision, no sticky
+# and branch, 18 lines added and 7 removed. All of these fields might not be
+# present (during "removes" for example).
+#
+# There may be multiple "control" lines or even none (imports, directory
+# additions) but there is one email per directory. We only care about actual
+# changes since it is presumed directory additions don't actually affect the
+# build. At least one file should need to change (the makefile, say) to
+# actually make a new directory part of the build process. That's my story
+# and I'm sticking to it.
+
+class BonsaiMaildirSource(MaildirSource):
+ name = "Bonsai"
+
+ def parse(self, m, prefix=None):
+ """Parse mail sent by the Bonsai cvs loginfo script."""
+
+ # we don't care who the email came from b/c the cvs user is in the
+ # msg text
+
+ who = "unknown"
+ timestamp = None
+ files = []
+ lines = list(body_line_iterator(m))
+
+ # read the control lines (what/who/where/file/etc.)
+ while lines:
+ line = lines.pop(0)
+ if line == "LOGCOMMENT\n":
+ break;
+ line = line.rstrip("\n")
+
+ # we'd like to do the following but it won't work if the number of
+ # items doesn't match so...
+ # what, timestamp, user, repo, module, file = line.split( '|' )
+ items = line.split('|')
+ if len(items) < 6:
+ # not a valid line, assume this isn't a bonsai message
+ return None
+
+ try:
+ # just grab the bottom-most timestamp, they're probably all the
+ # same. TODO: I'm assuming this is relative to the epoch, but
+ # this needs testing.
+ timestamp = int(items[1])
+ except ValueError:
+ pass
+
+ user = items[2]
+ if user:
+ who = user
+
+ module = items[4]
+ file = items[5]
+ if module and file:
+ path = "%s/%s" % (module, file)
+ files.append(path)
+ sticky = items[7]
+ branch = items[8]
+
+ # if no files changed, return nothing
+ if not files:
+ return None
+
+ # read the comments
+ comments = ""
+ while lines:
+ line = lines.pop(0)
+ if line == ":ENDLOGCOMMENT\n":
+ break
+ comments += line
+ comments = comments.rstrip() + "\n"
+
+ # return buildbot Change object
+ return changes.Change(who, files, comments, when=timestamp,
+ branch=branch)
+
+# svn "commit-email.pl" handler. The format is very similar to freshcvs mail;
+# here's a sample:
+
+# From: username [at] apache.org [slightly obfuscated to avoid spam here]
+# To: commits [at] spamassassin.apache.org
+# Subject: svn commit: r105955 - in spamassassin/trunk: . lib/Mail
+# ...
+#
+# Author: username
+# Date: Sat Nov 20 00:17:49 2004 [note: TZ = local tz on server!]
+# New Revision: 105955
+#
+# Modified: [also Removed: and Added:]
+# [filename]
+# ...
+# Log:
+# [log message]
+# ...
+#
+#
+# Modified: spamassassin/trunk/lib/Mail/SpamAssassin.pm
+# [unified diff]
+#
+# [end of mail]
+
+class SVNCommitEmailMaildirSource(MaildirSource):
+ name = "SVN commit-email.pl"
+
+ def parse(self, m, prefix=None):
+ """Parse messages sent by the svn 'commit-email.pl' trigger.
+ """
+
+ # The mail is sent from the person doing the checkin. Assume that the
+ # local username is enough to identify them (this assumes a one-server
+ # cvs-over-rsh environment rather than the server-dirs-shared-over-NFS
+ # model)
+ name, addr = parseaddr(m["from"])
+ if not addr:
+ return None # no From means this message isn't from FreshCVS
+ at = addr.find("@")
+ if at == -1:
+ who = addr # might still be useful
+ else:
+ who = addr[:at]
+
+ # we take the time of receipt as the time of checkin. Not correct (it
+ # depends upon the email latency), but it avoids the
+ # out-of-order-changes issue. Also syncmail doesn't give us anything
+ # better to work with, unless you count pulling the v1-vs-v2
+ # timestamp out of the diffs, which would be ugly. TODO: Pulling the
+ # 'Date:' header from the mail is a possibility, and
+ # email.Utils.parsedate_tz may be useful. It should be configurable,
+ # however, because there are a lot of broken clocks out there.
+ when = util.now()
+
+ files = []
+ comments = ""
+ isdir = 0
+ lines = list(body_line_iterator(m))
+ rev = None
+ while lines:
+ line = lines.pop(0)
+
+ # "Author: jmason"
+ match = re.search(r"^Author: (\S+)", line)
+ if match:
+ who = match.group(1)
+
+ # "New Revision: 105955"
+ match = re.search(r"^New Revision: (\d+)", line)
+ if match:
+ rev = match.group(1)
+
+ # possible TODO: use "Date: ..." data here instead of time of
+ # commit message receipt, above. however, this timestamp is
+ # specified *without* a timezone, in the server's local TZ, so to
+ # be accurate buildbot would need a config setting to specify the
+ # source server's expected TZ setting! messy.
+
+ # this stanza ends with the "Log:"
+ if (line == "Log:\n"):
+ break
+
+ # commit message is terminated by the file-listing section
+ while lines:
+ line = lines.pop(0)
+ if (line == "Modified:\n" or
+ line == "Added:\n" or
+ line == "Removed:\n"):
+ break
+ comments += line
+ comments = comments.rstrip() + "\n"
+
+ while lines:
+ line = lines.pop(0)
+ if line == "\n":
+ break
+ if line.find("Modified:\n") == 0:
+ continue # ignore this line
+ if line.find("Added:\n") == 0:
+ continue # ignore this line
+ if line.find("Removed:\n") == 0:
+ continue # ignore this line
+ line = line.strip()
+
+ thesefiles = line.split(" ")
+ for f in thesefiles:
+ if prefix:
+ # insist that the file start with the prefix: we may get
+ # changes we don't care about too
+ if f.startswith(prefix):
+ f = f[len(prefix):]
+ else:
+ log.msg("ignored file from svn commit: prefix '%s' "
+ "does not match filename '%s'" % (prefix, f))
+ continue
+
+ # TODO: figure out how new directories are described, set
+ # .isdir
+ files.append(f)
+
+ if not files:
+ log.msg("no matching files found, ignoring commit")
+ return None
+
+ return changes.Change(who, files, comments, when=when, revision=rev)
+