## ## Copyright (c) 2006 Jason Dillon ## ## Licensed under the Apache License, Version 2.0 (the "License"); ## you may not use this file except in compliance with the License. ## You may obtain a copy of the License at ## ## http://www.apache.org/licenses/LICENSE-2.0 ## ## Unless required by applicable law or agreed to in writing, software ## distributed under the License is distributed on an "AS IS" BASIS, ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ## See the License for the specific language governing permissions and ## limitations under the License. ## ## ## $Id: __init__.py 77 2009-08-25 12:06:19Z mindwanderer $ ## import sys, os, getopt, smtplib, time from datetime import datetime from email.MIMEText import MIMEText import email.Utils import re from p4spam import config, log, subscription from p4spam.perforce import P4 from p4spam.htmlmessage import HtmlMessageBuilder from p4spam.parser import DescriptionParser from p4spam.version import VERSION ## ## P4Spam ## DEFAULT_CONFIG_FILENAME = os.path.join(os.path.dirname(sys.argv[0]), "p4spam.conf") class P4Spam: def __init__(this): this.log = log.getLogger(this) this.p4 = P4() this.smtp = None # Lazy config in sendmail() def usage(this): print "usage: p4spam [options]" print "" print "[options]" print " --help,-h Show this help message" print " --version,-V Show the version of P4Spam" print " --debug Enable debug output" print " --quiet Try to be quiet" print " --config,-c <filename> Specify the configuration file" print " Default: %s" % DEFAULT_CONFIG_FILENAME print "" def configure(this, args): # this.log.debug("Configuring...") sopts = "hc:V" lopts = [ "help", "config=", "debug", "quiet", "version" ] try: opts, args = getopt.getopt(args, sopts, lopts) except getopt.GetoptError: this.usage() sys.exit(2) filename = DEFAULT_CONFIG_FILENAME for o, a in opts: if o in ("-h", "--help"): this.usage() sys.exit() if o in ("-V", "--version"): print "P4Spam %s" % VERSION sys.exit() if o in ("--debug"): log.setThresholdDebug() if o in ("--quiet"): log.setThresholdError() if o in ("-c", "--config"): filename = a this.log.debug("Post option parsing...") # Read the configuration file config.read(filename) def getStartingChange(this): if config.FORCE_STARTING_CHANGE != None: this.log.warning("Forced starting change: %s" % config.FORCE_STARTING_CHANGE) return config.FORCE_STARTING_CHANGE lastchange = this.p4.counter(config.LAST_CHANGELIST_COUNTER) this.log.debug("Last change: %s" % (lastchange.value)) # starting change is the next one return lastchange.value + 1 def saveLastChange(this, change): if not config.SAVE_LAST_CHANGE_ENABLED: this.log.warning("Last change save to counter has been disabled") return this.p4.counter(config.LAST_CHANGELIST_COUNTER, change) this.log.debug("Set last change: %s" % change) def sendmail(this, fromAddr, recipients, messageText): this.log.info("Sending mail to: %s" % (recipients)) this.log.debug(">>>\n%s" % messageText) # Don't send mail when testing if not config.SPAM_ENABLED: this.log.info("Skipping send mail; spamming is disabled") return i = 0 maxtries = 3 # TODO: Expose this as configuration while True: # Lazy init sendmail if this.smtp == None: mailhost = config.SMTP_HOST this.log.info("Connecting to mail server: %s" % mailhost) smtp = smtplib.SMTP() smtp.connect(mailhost) this.smtp = smtp this.log.info("Connected") try: this.smtp.sendmail(fromAddr, recipients, messageText) this.log.debug("Sent") break except smtplib.SMTPServerDisconnected, e: this.log.warning("Disconnected; trying again: %s" % e) this.smtp = None i = i + 1 if i >= maxtries: raise "Failed to connect to SMTP server after '%s' tries" % i def processReview(this, review): change = review.changenumber this.log.info("Processing review for change: %s" % change) info = ChangeInfo(this, change) # The list of email addresses recipients = info.getRecipients() if len(recipients) == 0: this.log.info("No one is interested in this change; skipping") return # Don't spam to users for testing if not config.SPAM_USERS: recipients = [] this.log.debug("Recipients: %s" % (recipients)) addrs = recipients[:] # Add admin BCC if config.ADMIN_BCC_ADDR != None: # TODO: Support list types addrs.append(config.ADMIN_BCC_ADDR) this.log.debug("Addrs: %s" % (addrs)) # If we don't have anyone to spam yet, then skip it if len(addrs) == 0: this.log.warning("No receipients for change; skipping") return # Figure out who this came from if config.FROM_ADDR != None: fromAddr = config.FROM_ADDR else: fromAddr = config.FROM_ADDR_FORMAT % {'fullname': review.fullname, 'email': review.email, 'user': review.user} this.log.debug("From addr: %s" % fromAddr) # Email the message to all recipients, one at a time so we can send different people different messages for recipient in addrs: # the first 'info'/change object is garbage collected by the time this loop runs the second # time, so let's instantiate a second, local change object and use that. Otherwise only the # first E-mail message will have the diffs in it, and the rest will be cut off at that point # in the E-mail body info2 = ChangeInfo(this, change) # Build the message body builder = HtmlMessageBuilder # TODO: Make configrable msg = builder(info2).getMessage(recipient) msg['From'] = fromAddr # use the recipient's username to get their E-mail address # unless this is a subscription, in which case the recipient # is already an E-mail address. detect this by searching for # an @ in the recipient's username have_at = re.match(".*\@", recipient) if have_at: revieweremail = recipient else: revieweremail = extractEmail(this, recipient) this.sendmail(fromAddr, revieweremail, msg.as_string()) def processOneBatch(this): this.log.info("Processing one batch...") # Where do we start working? startchange = this.getStartingChange() this.log.info("Starting with change: %s" % startchange) # Track the last change processed lastchange = None # Query all pending reviews since the last change we processed pending = this.p4.review('-c', startchange) # Iter count & max batch i = 0 maxbatch = config.MAX_BATCH # Process reviews for changes... for review in pending: this.processReview(review) # If we get this far we have full processed/emailed the change details lastchange = review.changenumber i = i + 1 # Limit changes per batch here if i >= maxbatch: this.log.info("Reached maximum batch threashold: %s; aborting further processing" % maxbatch) break # Save the change for the next round if lastchange != None: this.saveLastChange(lastchange) this.log.info("Finished one batch") def cleanup(this): this.log.info("Cleaning up...") if this.smtp != None: this.log.debug("Closing smtp connection...") this.smtp.close() this.smtp = None this.log.info("Done") def main(this, args): this.configure(args) ## ## TODO: Enable error email ## this.log.info("Starting main loop...") repeat = config.REPEAT sleeptime = config.SLEEPTIME if repeat: this.log.info("Processing batches; delay: %s seconds" % sleeptime) while True: this.processOneBatch() this.log.info("Sleeping for %s seconds" % sleeptime) time.sleep(sleeptime) else: this.processOneBatch() this.cleanup() ## ## ChangeInfo ## class ChangeInfo: def __init__(this, p4spam, change): assert p4spam != None assert change != None this.log = log.getLogger(this) this.p4spam = p4spam this.p4 = p4spam.p4 this.change = change # Parse the description stream = this.p4.rawstream('describe', '-du', this.change) parser = DescriptionParser(stream) this.desc = parser.parse() def getChange(this): return this.desc.change def getAuthor(this): return this.desc.author def getComment(this): return "".join(this.desc.comments).strip() def getComments(this): return this.desc.comments def getJobsFixed(this): return this.desc.jobs def getClient(this): return this.desc.client def getDateTime(this): return "%s %s" % (this.desc.date, this.desc.time) def getAffectedFiles(this): return this.desc.files def getDifferences(this): return this.desc.diffs def getRecipients(this): recipients = [] if config.USER_REVIEWS_ENABLED: # First check the reviewers reviewers = this.p4.reviews('-c', this.change) for reviewer in reviewers: recipients.append(reviewer.user) else: this.log.warning("User reviews disabled") if config.SUBSCRIPTIONS_ENABLED: # Next check for subscriptions subscription.applySubscriptions(recipients, this.desc.files) else: this.log.warning("Subscriptions disabled") return recipients def extractEmail(this, user): revieweremail = [] # extract the reviewer's E-mail from their username revieweruserdef = this.p4.raw('user', '-o', user) # suck out the E-mail address for line in revieweruserdef: matchObj = re.match( r'(Email:\s*)(.*)', line, re.M) if matchObj: revieweremail = str(matchObj.group(2)) return revieweremail def requirePythonVersion(_major, _minor, _micro=0): try: (major, minor, micro, releaselevel, serial) = sys.version_info if major >= _major and minor >= _minor and micro >= _micro: return except: pass raise "This program requires Python %s.%s.%s; detected: %s" % (major, minor, micro, sys.version) def main(args): # Need at least Python 2.3 requirePythonVersion(2,3) spammer = P4Spam() spammer.main(args)
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#3 | 8147 | Matthew Janulewicz |
Fixing subscription support. Since subscriptions are passed in as E-mail addresses already, there is no need to convert them (from p4 username to E-mail address.) |
||
#2 | 7833 | Matthew Janulewicz | P4Spam now obeys 'p4 protects' and will not expose code diffs to subscribers that do not have the proper p4 permissions to view that diff. | ||
#1 | 7731 | Matthew Janulewicz |
Adding P4Spam 1.1 code from http://p4spam.sourceforge.net/wiki/Main_Page "P4Spam is a Perforce change review daemon which spits out sexy HTML-styled notification emails." |