Index: mailer.py =================================================================== --- mailer.py (revision 37683) +++ mailer.py (working copy) @@ -44,8 +44,10 @@ # Python <3.0 from cStringIO import StringIO import smtplib +import nntplib import re import tempfile +import subprocess # Minimal version of Subversion's bindings required _MIN_SVN_VERSION = [1, 5, 0] @@ -140,7 +142,7 @@ subject = subject[:(truncate_subject - 3)] + "..." return subject - def start(self, group, params): + def start(self, group, params, paths): """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. @@ -177,11 +179,11 @@ pipe_ob.wait() -class MailedOutput(OutputBase): +class MessageOutput(OutputBase): def __init__(self, cfg, repos, prefix_param): OutputBase.__init__(self, cfg, repos, prefix_param) - def start(self, group, params): + def start(self, group, params, paths): # whitespace (or another character) separated list of addresses # which must be split into a clean list to_addr_in = self.cfg.get('to_addr', group, params) @@ -194,6 +196,18 @@ [_f for _f in to_addr_in[3:].split(to_addr_in[1]) if _f] else: self.to_addrs = [_f for _f in to_addr_in.split() if _f] + # whitespace (or another character) separated list of groups + # which must be split into a clean list + to_group_in = self.cfg.get('to_group', group, params) + # if list of addresses starts with '[.]' + # use the character between the square brackets as split char + # else use whitespaces + if len(to_group_in) >= 3 and to_group_in[0] == '[' \ + and to_group_in[2] == ']': + self.to_groups = \ + [_f for _f in to_group_in[3:].split(to_group_in[1]) if _f] + else: + self.to_groups = [_f for _f in to_group_in.split() if _f] self.from_addr = self.cfg.get('from_addr', group, params) \ or self.repos.author or 'no_author' # if the from_addr (also) starts with '[.]' (may happen if one @@ -208,43 +222,180 @@ and self.reply_to[2] == ']': self.reply_to = self.reply_to[3:] - def mail_headers(self, group, params): + def message_headers(self, group, params, paths): 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, ', '.join(self.to_addrs), subject) + # add prefix and suffix + group_prefix = self.cfg.get('group_prefix', group, params) + group_suffix = self.cfg.get('group_suffix', group, params) + auto_to_group = self.cfg.get('auto_to_group', group, params) + # because there is only 4 valid auto_to_group values, compared to + # most likely more than 4 paths because there is 1 path for each + # changed file. Thus the order is as it is below + glist=[] + if 'project' == auto_to_group: + for p in paths: + plist = p.split('/') + try: + # looking at field1 + if 'trunk' == plist[0]: + glist = glist + [plist[1]] + continue + if 'branches' == plist[0]: + glist = glist + [plist[2]] + continue + if 'tags' == plist[0]: + continue + # looking at field2 + if 'trunk' == plist[1]: + glist = glist + [plist[0]] + continue + if 'branches' == plist[1]: + glist = glist + [plist[0]] + continue + if 'tags' == plist[1]: + continue + # no special words in field1 or field2, we return field1 as the (project)name + glist = glist + [plist[0]] + except IndexError: + continue + if 'branches' == auto_to_group: + for p in paths: + plist = p.split('/') + try: + # looking at field1 + if 'trunk' == plist[0]: + glist = glist + ['trunk'] + continue + if 'branches' == plist[0]: + glist = glist + [plist[1]] + continue + if 'tags' == plist[0]: + continue + # looking at field2 + if 'trunk' == plist[1]: + glist = glist + ['trunk'] + continue + if 'branches' == plist[1]: + glist = glist + [plist[2]] + continue + if 'tags' == plist[1]: + continue + # no special words in field1 or field2, we do nothing + except IndexError: + continue + if 'aspath' == auto_to_group: + for p in paths: + plist = p.split('/') + try: + # looking at field1 + if 'trunk' == plist[0]: + glist = glist + [plist[0] + '.' + plist[1]] + continue + if 'branches' == plist[0]: + glist = glist + [plist[1] + '.' + plist[2]] + continue + if 'tags' == plist[0]: + continue + # looking at field2 + if 'trunk' == plist[1]: + glist = glist + [plist[0] + '.' + plist[1]] + continue + if 'branches' == plist[1]: + glist = glist + [plist[0] + '.' + plist[2]] + continue + if 'tags' == plist[1]: + continue + # no special words in field1 or field2, we just use field1 + glist = glist + [plist[0]] + except IndexError: + continue + if 'reverse' == auto_to_group: + for p in paths: + plist = p.split('/') + try: + # looking at field1 + if 'trunk' == plist[0]: + glist = glist + [plist[1] + '.' + plist[0]] + continue + if 'branches' == plist[0]: + glist = glist + [plist[2] + '.' + plist[1]] + continue + if 'tags' == plist[0]: + continue + # looking at field2 + if 'trunk' == plist[1]: + glist = glist + [plist[1] + '.' + plist[0]] + continue + if 'branches' == plist[1]: + glist = glist + [plist[2] + '.' + plist[0]] + continue + if 'tags' == plist[1]: + continue + # no special words in field1 or field2, we just use field1 + glist = glist + [plist[0]] + except IndexError: + continue + gset = set(glist) + gshort = [ group_prefix + g + group_suffix for g in gset ] + self.to_groups = self.to_groups + gshort + hdrs = 'From: %s\n' \ + 'Subject: %s\n' \ + % (self.from_addr, subject) + if self.to_groups: + hdrs += 'Newsgroups: %s\n' % ', '.join(self.to_groups) + if self.to_addrs: + hdrs += 'To: %s\n' % ', '.join(self.to_addrs) if self.reply_to: - hdrs = '%sReply-To: %s\n' % (hdrs, self.reply_to) + hdrs += 'Reply-To: %s\n' % self.reply_to + hdrs += 'MIME-Version: 1.0\n' \ + 'Content-Type: text/plain; charset=UTF-8\n' \ + 'Content-Transfer-Encoding: 8bit\n' return hdrs + '\n' -class SMTPOutput(MailedOutput): +class SMTPAndNNTPOutput(MessageOutput): "Deliver a mail message to an MTA using SMTP." - def start(self, group, params): - MailedOutput.start(self, group, params) + def __init__(self, cfg, repos, prefix_param): + OutputBase.__init__(self, cfg, repos, prefix_param) + self.do_smtp = cfg.is_set('general.smtp_hostname') + self.do_nntp = cfg.is_set('general.nntp_hostname') + self.smtp_error_cmd = cfg.is_set('general.smtp_error_cmd') + self.nntp_error_cmd = cfg.is_set('general.nntp_error_cmd') + def start(self, group, params, paths): + MessageOutput.start(self, group, params, paths) + self.buffer = StringIO() self.write = self.buffer.write - self.write(self.mail_headers(group, params)) + self.write(self.message_headers(group, params, paths)) 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() + if self.do_smtp and self.to_addrs: + smtpserver = smtplib.SMTP(self.cfg.general.smtp_hostname) + if self.cfg.is_set('general.smtp_username'): + smtpserver.login(self.cfg.general.smtp_username, + self.cfg.general.smtp_password) + smtpserver.sendmail(self.from_addr, self.to_addrs, self.buffer.getvalue()) + smtpserver.quit() + if self.do_nntp and self.to_groups: + nntpserver = nntplib.NNTP(self.cfg.general.nntp_hostname) + self.buffer.seek(0) + try: + nntpserver.post(self.buffer) + except nntplib.NNTPError, errormsg: + # not raised if just one of the groups that is posted to exists. + retcode = subprocess.call([self.nntp_error_cmd, str(errormsg)]) + if 0==retcode: + nntpserver.post(self.buffer) + else: + nntpserver.quit() class StandardOutput(OutputBase): @@ -254,7 +405,7 @@ OutputBase.__init__(self, cfg, repos, prefix_param) self.write = sys.stdout.write - def start(self, group, params): + def start(self, group, params, paths): self.write("Group: " + (group or "defaults") + "\n") self.write("Subject: " + self.make_subject(group, params) + "\n\n") @@ -262,17 +413,17 @@ pass -class PipeOutput(MailedOutput): +class PipeOutput(MessageOutput): "Deliver a mail message to an MTA via a pipe." def __init__(self, cfg, repos, prefix_param): - MailedOutput.__init__(self, cfg, repos, prefix_param) + MessageOutput.__init__(self, cfg, repos, prefix_param) # figure out the command for delivery self.cmd = cfg.general.mail_command.split() - def start(self, group, params): - MailedOutput.start(self, group, params) + def start(self, group, params, paths): + MessageOutput.start(self, group, params, paths) ### gotta fix this. this is pretty specific to sendmail and qmail's ### mailwrapper program. should be able to use option param substitution @@ -284,7 +435,7 @@ self.write = self.pipe.stdin.write # start writing out the mail message - self.write(self.mail_headers(group, params)) + self.write(self.message_headers(group, params, paths)) def finish(self): # signal that we're done sending content @@ -302,8 +453,9 @@ if cfg.is_set('general.mail_command'): cls = PipeOutput - elif cfg.is_set('general.smtp_hostname'): - cls = SMTPOutput + elif cfg.is_set('general.smtp_hostname') or \ + cfg.is_set('general.nntp_hostname'): + cls = SMTPAndNNTPOutput else: cls = StandardOutput @@ -375,7 +527,7 @@ renderer = TextCommitRenderer(self.output) for (group, param_tuple), (params, paths) in self.groups.items(): - self.output.start(group, params) + self.output.start(group, params, paths) # generate the content for this group and set of params generate_content(renderer, self.cfg, self.repos, self.changelist, Index: mailer.conf.example =================================================================== --- mailer.conf.example (revision 37683) +++ mailer.conf.example (working copy) @@ -27,6 +27,17 @@ #smtp_username = example #smtp_password = example +# This option specifies the hostname for delivery via NNTP. +nntp_hostname = nntp.example.com + +# This option specifies what command to run if posting to nntp server +# fails. This can be used to log error messages or if you post to a +# non existing group, like PROJECTNAME.branches.BRANCHNAME, then the +# group can be created by the nntp_error_cmd script. +# If nntp_error_cmd returns 0, then the post/email will be tried again, +# else silently dropped. (unless you log it in the error_cmd script) +nntp_error_cmd = /bin/true + # -------------------------------------------------------------------------- # @@ -171,11 +182,105 @@ # NOTE: If you want to use a different character for separating the # addresses put it in front of the addresses included in square # brackets '[ ]'. -to_addr = invalid@example.com +#to_addr = invalid@example.com # If this is set, then a Reply-To: will be inserted into the message. reply_to = +# The default Newsgroup: group for message. One or more groups, +# separated by whitespace (no commas). +# NOTE: If you want to use a different character for separating the +# groups put it in front of the groups included in square +# brackets. '[ ]'. +to_group = test + + +# group pre and suffix is a text string which is pre or appended +# to the group derived from the auto_to_group feature. to_group +# is NOT changed. +group_prefix = svn. +#group_suffix = .svn +# the pre and/or suffix may include a . but spaces are removed +# there is no check that tests if the pre/postfix is a valid NNTP groupname +# so a wrong pre/postfix may lead to crashes + + +# Automatically detect the group to post to based on name+branch/trunk +# the branchname field is also removed. +# Tags are totally ignored and the result if the path contains /tags/ +# is at the time of writting this undefined and unknown. +# +# the auto_to_group configoption has only 4 valid definitions, see +# below, any other input will be treated as if the value is undefined. +# Only one of them can be defined at any given time, specifying more +# is undefined and the result is unknown. There is no default value. +# +# If both to_group and auto_to_group is defined, then you will get posts +# to 2 or more groups. +# +# configoption path group +# auto_to_group = aspath /name/trunk name.trunk +# auto_to_group = aspath /trunk/name trunk.name +# auto_to_group = reverse /name/trunk trunk.name +# auto_to_group = reverse /trunk/name name.trunk +# auto_to_group = project /name/trunk name +# auto_to_group = project /trunk/name name +# auto_to_group = branches /name/trunk trunk +# auto_to_group = branches /trunk/name trunk +# +# aspath means that the group order will be just as the path +# +# reverse means that the order is reverse from the path +# +# project means that special words like trunk, branches and tags + +# branchname/number and tagname/number are removed from the path +# and only the first non special pathfield is used as the name. +# The thought is that the namefield is the projectname. + +# branches is ment to post the branchname. trunk is considered a +# branchname. Tags are ignored. +# +# lets see some more examples with branches: +# +# auto_to_group = aspath +# ---------------------- +# path group +# /name/branches/branchname name.branchname +# /branches/branchname/name branchname.name +# +# auto_to_group = reverse +# ----------------------- +# path group +# /name/branches/branchname branchname.name +# /branches/branchname/name name.branchname +# +# auto_to_group = project +# ---------------------- +# path group +# /name/branches/branchname name +# /branches/branchname/name name +# +# auto_to_group = branches +# ---------------------- +# path group +# /name/branches/branchname branchname +# /branches/branchname/name branchname +# +# auto_to_group works by detecting the 3 special words, trunk, branches and tags +# in position 1 or 2, field1/field2/field3/... +# if the word branches or tags is detected, then the script knows that the next +# field is the branchname/tagname. +# +# if neither of the 3 special words, trunk, branches or tags is detected in +# position 1 or 2, field1/field2/..., then auto_to_group will behave as 'project' +# regardless of if the auto_to_group value is 'aspath' or 'reverse' only the +# value of field1 will be used as the group name +# if auto_to_group is 'branches' then nothing will be done is trunk or branches +# is not detected +# +# auto_to_group = aspath + + # Specify which types of repository changes mailer.py will create # diffs for. Valid options are any combination of # 'add copy modify delete', or 'none' to never create diffs.