?? mailer.py
字號:
#!/usr/bin/env python## mailer.py: send email describing a commit## $HeadURL: https://svn.collab.net/repos/svn/branches/1.4.x/tools/hook-scripts/mailer/mailer.py $# $LastChangedDate: 2006-09-11 13:00:53 -0500 (Mon, 11 Sep 2006) $# $LastChangedBy: dlr $# $LastChangedRevision: 21431 $## USAGE: mailer.py commit REPOS REVISION [CONFIG-FILE]# mailer.py propchange REPOS REVISION AUTHOR REVPROPNAME [CONFIG-FILE]# mailer.py propchange2 REPOS REVISION AUTHOR REVPROPNAME ACTION \# [CONFIG-FILE]# mailer.py lock REPOS AUTHOR [CONFIG-FILE]# mailer.py unlock REPOS AUTHOR [CONFIG-FILE]## Using CONFIG-FILE, deliver an email describing the changes between# REV and REV-1 for the repository REPOS.## ACTION was added as a fifth argument to the post-revprop-change hook# in Subversion 1.2.0. Its value is one of 'A', 'M' or 'D' to indicate# if the property was added, modified or deleted, respectively.## This version of mailer.py requires the python bindings from# subversion 1.2.0 or later.#import osimport sysimport stringimport ConfigParserimport timeimport popen2import cStringIOimport smtplibimport reimport tempfileimport typesimport urllibimport svn.fsimport svn.deltaimport svn.reposimport svn.coreSEPARATOR = '=' * 78def main(pool, cmd, config_fname, repos_dir, cmd_args): ### TODO: Sanity check the incoming args if cmd == 'commit': revision = int(cmd_args[0]) repos = Repository(repos_dir, revision, pool) cfg = Config(config_fname, repos, { 'author' : repos.author }) messenger = Commit(pool, cfg, repos) elif cmd == 'propchange' or cmd == 'propchange2': revision = int(cmd_args[0]) author = cmd_args[1] propname = cmd_args[2] action = (cmd == 'propchange2' and cmd_args[3] or 'A') repos = Repository(repos_dir, revision, pool) # Override the repos revision author with the author of the propchange repos.author = author cfg = Config(config_fname, repos, { 'author' : author }) messenger = PropChange(pool, cfg, repos, author, propname, action) elif cmd == 'lock' or cmd == 'unlock': author = cmd_args[0] repos = Repository(repos_dir, 0, pool) ### any old revision will do # Override the repos revision author with the author of the lock/unlock repos.author = author cfg = Config(config_fname, repos, { 'author' : author }) messenger = Lock(pool, cfg, repos, author, cmd == 'lock') else: raise UnknownSubcommand(cmd) messenger.generate()# Minimal, incomplete, versions of popen2.Popen[34] for those platforms# for which popen2 does not provide them.try: Popen3 = popen2.Popen3 Popen4 = popen2.Popen4except AttributeError: class Popen3: def __init__(self, cmd, capturestderr = False): if type(cmd) != types.StringType: cmd = svn.core.argv_to_command_string(cmd) if capturestderr: self.fromchild, self.tochild, self.childerr \ = popen2.popen3(cmd, mode='b') else: self.fromchild, self.tochild = popen2.popen2(cmd, mode='b') self.childerr = None def wait(self): rv = self.fromchild.close() rv = self.tochild.close() or rv if self.childerr is not None: rv = self.childerr.close() or rv return rv class Popen4: def __init__(self, cmd): if type(cmd) != types.StringType: cmd = svn.core.argv_to_command_string(cmd) self.fromchild, self.tochild = popen2.popen4(cmd, mode='b') def wait(self): rv = self.fromchild.close() rv = self.tochild.close() or rv return rvclass OutputBase: "Abstract base class to formalize the inteface of output methods" def __init__(self, cfg, repos, prefix_param): self.cfg = cfg self.repos = repos self.prefix_param = prefix_param self._CHUNKSIZE = 128 * 1024 # This is a public member variable. This must be assigned a suitable # piece of descriptive text before make_subject() is called. self.subject = "" def make_subject(self, group, params): prefix = self.cfg.get(self.prefix_param, group, params) if prefix: subject = prefix + ' ' + self.subject else: subject = self.subject try: truncate_subject = int( self.cfg.get('truncate_subject', group, params)) except ValueError: truncate_subject = 0 if truncate_subject and len(subject) > truncate_subject: subject = subject[:(truncate_subject - 3)] + "..." return subject def start(self, group, params): """Override this method. Begin writing an output representation. GROUP is the name of the configuration file group which is causing this output to be produced. PARAMS is a dictionary of any named subexpressions of regular expressions defined in the configuration file, plus the key 'author' contains the author of the action being reported.""" raise NotImplementedError def finish(self): """Override this method. Flush any cached information and finish writing the output representation.""" raise NotImplementedError def write(self, output): """Override this method. Append the literal text string OUTPUT to the output representation.""" raise NotImplementedError def run(self, cmd): """Override this method, if the default implementation is not sufficient. Execute CMD, writing the stdout produced to the output representation.""" # By default we choose to incorporate child stderr into the output pipe_ob = Popen4(cmd) buf = pipe_ob.fromchild.read(self._CHUNKSIZE) while buf: self.write(buf) buf = pipe_ob.fromchild.read(self._CHUNKSIZE) # wait on the child so we don't end up with a billion zombies pipe_ob.wait()class MailedOutput(OutputBase): def __init__(self, cfg, repos, prefix_param): OutputBase.__init__(self, cfg, repos, prefix_param) def start(self, group, params): # whitespace-separated list of addresses; split into a clean list: self.to_addrs = \ filter(None, string.split(self.cfg.get('to_addr', group, params))) self.from_addr = self.cfg.get('from_addr', group, params) \ or self.repos.author or 'no_author' self.reply_to = self.cfg.get('reply_to', group, params) def mail_headers(self, group, params): subject = self.make_subject(group, params) try: subject.encode('ascii') except UnicodeError: from email.Header import Header subject = Header(subject, 'utf-8').encode() hdrs = 'From: %s\n' \ 'To: %s\n' \ 'Subject: %s\n' \ 'MIME-Version: 1.0\n' \ 'Content-Type: text/plain; charset=UTF-8\n' \ 'Content-Transfer-Encoding: 8bit\n' \ % (self.from_addr, string.join(self.to_addrs, ', '), subject) if self.reply_to: hdrs = '%sReply-To: %s\n' % (hdrs, self.reply_to) return hdrs + '\n'class SMTPOutput(MailedOutput): "Deliver a mail message to an MTA using SMTP." def start(self, group, params): MailedOutput.start(self, group, params) self.buffer = cStringIO.StringIO() self.write = self.buffer.write self.write(self.mail_headers(group, params)) def finish(self): server = smtplib.SMTP(self.cfg.general.smtp_hostname) if self.cfg.is_set('general.smtp_username'): server.login(self.cfg.general.smtp_username, self.cfg.general.smtp_password) server.sendmail(self.from_addr, self.to_addrs, self.buffer.getvalue()) server.quit()class StandardOutput(OutputBase): "Print the commit message to stdout." def __init__(self, cfg, repos, prefix_param): OutputBase.__init__(self, cfg, repos, prefix_param) self.write = sys.stdout.write def start(self, group, params): self.write("Group: " + (group or "defaults") + "\n") self.write("Subject: " + self.make_subject(group, params) + "\n\n") def finish(self): passclass PipeOutput(MailedOutput): "Deliver a mail message to an MTA via a pipe." def __init__(self, cfg, repos, prefix_param): MailedOutput.__init__(self, cfg, repos, prefix_param) # figure out the command for delivery self.cmd = string.split(cfg.general.mail_command) def start(self, group, params): MailedOutput.start(self, group, params) ### gotta fix this. this is pretty specific to sendmail and qmail's ### mailwrapper program. should be able to use option param substitution cmd = self.cmd + [ '-f', self.from_addr ] + self.to_addrs # construct the pipe for talking to the mailer self.pipe = Popen3(cmd) self.write = self.pipe.tochild.write # we don't need the read-from-mailer descriptor, so close it self.pipe.fromchild.close() # start writing out the mail message self.write(self.mail_headers(group, params)) def finish(self): # signal that we're done sending content self.pipe.tochild.close() # wait to avoid zombies self.pipe.wait()class Messenger: def __init__(self, pool, cfg, repos, prefix_param): self.pool = pool self.cfg = cfg self.repos = repos if cfg.is_set('general.mail_command'): cls = PipeOutput elif cfg.is_set('general.smtp_hostname'): cls = SMTPOutput else: cls = StandardOutput self.output = cls(cfg, repos, prefix_param)class Commit(Messenger): def __init__(self, pool, cfg, repos): Messenger.__init__(self, pool, cfg, repos, 'commit_subject_prefix') # get all the changes and sort by path editor = svn.repos.ChangeCollector(repos.fs_ptr, repos.root_this, self.pool) e_ptr, e_baton = svn.delta.make_editor(editor, self.pool) svn.repos.replay(repos.root_this, e_ptr, e_baton, self.pool) self.changelist = editor.get_changes().items() self.changelist.sort() # collect the set of groups and the unique sets of params for the options self.groups = { } for path, change in self.changelist: for (group, params) in self.cfg.which_groups(path): # turn the params into a hashable object and stash it away param_list = params.items() param_list.sort() # collect the set of paths belonging to this group if self.groups.has_key( (group, tuple(param_list)) ): old_param, paths = self.groups[group, tuple(param_list)] else: paths = { } paths[path] = None self.groups[group, tuple(param_list)] = (params, paths) # figure out the changed directories dirs = { } for path, change in self.changelist: if change.item_kind == svn.core.svn_node_dir: dirs[path] = None else: idx = string.rfind(path, '/') if idx == -1: dirs[''] = None else: dirs[path[:idx]] = None dirlist = dirs.keys() commondir, dirlist = get_commondir(dirlist) # compose the basic subject line. later, we can prefix it. dirlist.sort() dirlist = string.join(dirlist) if commondir: self.output.subject = 'r%d - in %s: %s' % (repos.rev, commondir, dirlist) else: self.output.subject = 'r%d - %s' % (repos.rev, dirlist) def generate(self): "Generate email for the various groups and option-params." ### the groups need to be further compressed. if the headers and ### body are the same across groups, then we can have multiple To: ### addresses. SMTPOutput holds the entire message body in memory, ### so if the body doesn't change, then it can be sent N times ### rather than rebuilding it each time. subpool = svn.core.svn_pool_create(self.pool) # build a renderer, tied to our output stream renderer = TextCommitRenderer(self.output) for (group, param_tuple), (params, paths) in self.groups.items(): self.output.start(group, params) # generate the content for this group and set of params generate_content(renderer, self.cfg, self.repos, self.changelist, group, params, paths, subpool) self.output.finish() svn.core.svn_pool_clear(subpool) svn.core.svn_pool_destroy(subpool)try: from tempfile import NamedTemporaryFileexcept ImportError: # NamedTemporaryFile was added in Python 2.3, so we need to emulate it # for older Pythons. class NamedTemporaryFile: def __init__(self): self.name = tempfile.mktemp() self.file = open(self.name, 'w+b') def __del__(self): os.remove(self.name) def write(self, data): self.file.write(data) def flush(self): self.file.flush()class PropChange(Messenger): def __init__(self, pool, cfg, repos, author, propname, action): Messenger.__init__(self, pool, cfg, repos, 'propchange_subject_prefix') self.author = author self.propname = propname self.action = action # collect the set of groups and the unique sets of params for the options self.groups = { } for (group, params) in self.cfg.which_groups(''): # turn the params into a hashable object and stash it away param_list = params.items() param_list.sort() self.groups[group, tuple(param_list)] = params self.output.subject = 'r%d - %s' % (repos.rev, propname) def generate(self): actions = { 'A': 'added', 'M': 'modified', 'D': 'deleted' } for (group, param_tuple), params in self.groups.items(): self.output.start(group, params) self.output.write('Author: %s\n' 'Revision: %s\n' 'Property Name: %s\n' 'Action: %s\n' '\n' % (self.author, self.repos.rev, self.propname, actions.get(self.action, 'Unknown (\'%s\')' \ % self.action))) if self.action == 'A' or not actions.has_key(self.action): self.output.write('Property value:\n') propvalue = self.repos.get_rev_prop(self.propname) self.output.write(propvalue) elif self.action == 'M': self.output.write('Property diff:\n') tempfile1 = NamedTemporaryFile() tempfile1.write(sys.stdin.read()) tempfile1.flush() tempfile2 = NamedTemporaryFile() tempfile2.write(self.repos.get_rev_prop(self.propname)) tempfile2.flush() self.output.run(self.cfg.get_diff_cmd(group, { 'label_from' : 'old property value', 'label_to' : 'new property value', 'from' : tempfile1.name, 'to' : tempfile2.name, })) self.output.finish()def get_commondir(dirlist):
?? 快捷鍵說明
復制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -