## ## 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: //guest/jason_dillon/p4spam/main/pylib/p4spam/__init__.py#2 $ $Date: 2006/04/12 $ ## VERSION = "1.0-SNAPSHOT" import sys, getopt, smtplib, time from datetime import datetime from email.MIMEText import MIMEText import email.Utils from perforce import P4 ## ## TODO: Use a normalized logging system ## from perforce import logging from p4spam.htmlmessage import HtmlMessageBuilder from p4spam.parser import DescriptionParser ## ## HACK: Need config to be fixed!!! ## from p4spam.config import * ## ## ChangeInfo ## class ChangeInfo: def __init__(this, p4spam, change): assert p4spam != None assert change != None this.log = logging.Logger(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 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 = [] # First check the reviewers reviewers = this.p4.reviews('-c', this.change) for reviewer in reviewers: recipients.append(reviewer.email) # Next check for subscriptions ## ## TODO: Scan the subscription views for matching files in the change ## return recipients ## ## P4Spam ## class P4Spam: def __init__(this): this.log = None # Laxy config in configure() this.config = Configuration() 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 " --trace Enable trace output" print " --config,-c Specify the configuration file" print " Default: p4spam.conf" print "" def configure(this, args): # this.log.debug("Configuring...") sopts = "hc:V" lopts = [ "help", "config=", "debug", "trace", "version" ] try: opts, args = getopt.getopt(args, sopts, lopts) except getopt.GetoptError: this.usage() sys.exit(2) filename = "p4spam.conf" 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"): logging.DEBUG_ENABLED = True if o in ("--trace"): logging.TRACE_ENABLED = True if o in ("-c", "--config"): filename = a # Late config of logging this.log = logging.Logger(this) this.log.debug("Using Pytnon: %s" % sys.version) this.log.debug("Post option parsing...") # Read the configuration file this.config.read(filename) def getStartingChange(this): if FORCE_STARTING_CHANGE != None: this.log.warn("Forced starting change: %s" % FORCE_STARTING_CHANGE) return FORCE_STARTING_CHANGE lastchange = this.p4.counter(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 SAVE_LAST_CHANGE_ENABLED: this.log.warn("Last change save to counter has been disabled") return this.p4.counter(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)) if this.log.isTraceEnabled(): this.log.trace(">>>\n%s" % messageText) # Don't send mail when testing if not 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 = SMTP_HOST # HACK: Get this from configuration 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.warn("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 SPAM_USERS: recipients = [] this.log.debug("Recipients: %s" % (recipients)) addrs = recipients[:] # Add admin BCC if ADMIN_BCC_ADDR != None: addrs.append(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.warn("No receipients for change; skipping") return # Build the message body builder = HtmlMessageBuilder # TODO: Make configrable msg = builder(info).getMessage() ## ## HACK: This should probably be done by the builder ## # Set the To: header to the list of receipients COMMASPACE = ', ' if len(recipients) != 0: msg['To'] = COMMASPACE.join(recipients) # Figure out who this came from if FROM_ADDR != None: fromAddr = FROM_ADDR else: fromAddr = "\"%s (Perforce)\" <%s>" % (review.fullname, review.email) this.log.debug("From addr: %s" % fromAddr) msg['From'] = fromAddr # Email the message to all recipients this.sendmail(fromAddr, addrs, 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 = MAX_BATCH # FIXME: Pull this from configuration # 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 = REPEAT # TODO: Pull this from configuration sleeptime = SLEEPTIME # TODO: Pull this from configuration 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() 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)