Index: mailer.py =================================================================== --- mailer.py (revision 37578) +++ 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] @@ -177,7 +179,7 @@ pipe_ob.wait() -class MailedOutput(OutputBase): +class MessageOutput(OutputBase): def __init__(self, cfg, repos, prefix_param): OutputBase.__init__(self, cfg, repos, prefix_param) @@ -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,69 @@ and self.reply_to[2] == ']': self.reply_to = self.reply_to[3:] - def mail_headers(self, group, params): + def message_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, ', '.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) + self.to_groups=[group_prefix + x + group_suffix for x in self.to_groups] + 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 __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): - MailedOutput.start(self, group, params) + MessageOutput.start(self, group, params) self.buffer = StringIO() self.write = self.buffer.write - self.write(self.mail_headers(group, params)) + self.write(self.message_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() + 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: + retcode = subprocess.call([self.nntp_error_cmd, str(errormsg)]) + if 0==retcode: + nntpserver.post(self.buffer) + else: + nntpserver.quit() class StandardOutput(OutputBase): @@ -262,17 +302,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) + MessageOutput.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 @@ -284,7 +324,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)) def finish(self): # signal that we're done sending content @@ -302,8 +342,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 Index: mailer.conf.example =================================================================== --- mailer.conf.example (revision 37578) +++ 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/false + # -------------------------------------------------------------------------- # @@ -171,11 +182,86 @@ # 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 = es es + + +# group pre and suffix is a text string which is pre or appended +# to the group usually derived from the auto_to_group feature. +group_prefix = t +group_suffix = t +# 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 + +# ================================================== +# the prefix + to_group + suffix forms the word test +# ================================================== + + +# 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 3 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. +# +# 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 = single /trunk/name name +# auto_to_group = single /name/trunk name +# +# aspath means that the group order will be just as the path +# +# reverse means that the order is reverse from the path +# +# single 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. +# +# 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 = single +# ---------------------- +# path group +# /name/branches/branchname name +# /branches/branchname/name name +# +# auto_to_group works by detecting the 3 special words, trunk, branches and tags +# in position /field1/ or /.*/field2/ +# if the word branches or tags is detected, then the script knows that the next +# field is the branchname/tagname and ignores it. +# +# 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.