##
## 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 <filename> 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)