diff options
| author | bwarsaw | 1999-11-10 17:52:24 +0000 |
|---|---|---|
| committer | bwarsaw | 1999-11-10 17:52:24 +0000 |
| commit | bee68db0f6dd45c5e01bf51729e8b7a446ad0872 (patch) | |
| tree | cbc24ddf542ffc3c5748ede500f1401acec6d40b | |
| parent | 4f53b8839234c367ccc7403e60b7bd710df1b4d2 (diff) | |
| download | mailman-bee68db0f6dd45c5e01bf51729e8b7a446ad0872.tar.gz mailman-bee68db0f6dd45c5e01bf51729e8b7a446ad0872.tar.zst mailman-bee68db0f6dd45c5e01bf51729e8b7a446ad0872.zip | |
New pipeline delivery module
"""Add the message to the list's current digest and possibly send it.
This handler will add the current message to the list's currently accumulating
digest. If the digest has reached its size threshold, it is delivered by
creating an OutgoingMessage of the digest, setting the `isdigest' attribute,
and injecting it into the pipeline.
"""
| -rw-r--r-- | Mailman/Handlers/ToDigest.py | 346 |
1 files changed, 346 insertions, 0 deletions
diff --git a/Mailman/Handlers/ToDigest.py b/Mailman/Handlers/ToDigest.py new file mode 100644 index 000000000..f4647bee9 --- /dev/null +++ b/Mailman/Handlers/ToDigest.py @@ -0,0 +1,346 @@ +# Copyright (C) 1998 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Add the message to the list's current digest and possibly send it. + +This handler will add the current message to the list's currently accumulating +digest. If the digest has reached its size threshold, it is delivered by +creating an OutgoingMessage of the digest, setting the `isdigest' attribute, +and injecting it into the pipeline. +""" + +import os +import string +import re +from Mailman import Utils +from Mailman import Message +from Mailman import mm_cfg + +from stat import ST_SIZE +from errno import ENOENT + +MIME_SEPARATOR = '__--__--' +MIME_NONSEPARATOR = ' %s ' % MIME_SEPARATOR +EXCLUDE_HEADERS = ('received', 'errors-to') + + + +def process(mlist, msg): + # short circuit non-digestable lists, or for messages that are already + # digests + if not mlist.digestable or getattr(msg, 'isdigest', 0): + return + digestfile = os.path.join(mlist.fullpath(), 'next-digest') + topicsfile = os.path.join(mlist.fullpath(), 'next-digest-topics') + omask = os.umask(002) + try: + digestfp = open(digestfile, 'a+') + topicsfp = open(topicsfile, 'a+') + finally: + os.umask(omask) + sender = quotemime(msg.GetSenderName()) + fromline = quotemime(msg.getheader('from')) + date = quotemime(msg.getheader('date')) + body = quotemime(msg.body) + subject = quotemime(msg.getheader('subject')) + # don't include the redundant subject prefix in the TOC entries + mo = re.match('(re:? *)?(%s)' % re.escape(mlist.subject_prefix), + subject, re.IGNORECASE) + if mo: + subject = subject[:mo.start(2)] + subject[mo.end(2):] + topicsfp.write(' %d. %s (%s)\n' % (mlist.next_post_number, + subject, sender)) + # We exclude specified headers and all X-* headers + kept_headers = [] + keeping = 0 + have_content_type = 0 + have_content_description = 0 + # speed up the inner loop + lower, split, excludes = string.lower, string.split, EXCLUDE_HEADERS + for h in msg.headers: + if lower(h[:2]) == 'x-' or lower(split(h, ':')[0]) in excludes: + keeping = 0 + elif h and h[0] in (' ', '\t'): + if keeping and kept_headers: + # continuation of something we're keeping + kept_headers[-1] = kept_headers[-1] + h + else: + keeping = 1 + if lower(h[:7]) == 'content-': + kept_headers.append(h) + if lower(h[:12]) == 'content-type': + have_content_type = 1 + elif lower(h[:19]) == 'content-description': + have_content_description = 1 + else: + kept_headers.append(quotemime(h)) + # after processing the headers + if have_content_type and not have_content_description: + kept_headers.append('Content-Description: %s\n' % subject) + # TBD: reply-to munging happens elsewhere in the pipeline + digestfp.write('--%s\n\nMessage: %d\n%s\n%s' % + (MIME_SEPARATOR, mlist.next_post_number, + string.join(kept_headers, ''), + body)) + mlist.next_post_number = mlist.next_post_number + 1 + topicsfp.close() + digestfp.close() + # if the current digest size exceeds the threshold, send the digest by + # injection into the list's message pipeline + try: + size = os.stat(digestfile)[ST_SIZE] + if size/1024.0 >= mlist.digest_size_threshhold: + injectdigest(mlist, digestfile, topicsfile) + except os.error, e: + code, msg = e + if code == ENOENT: + mlist.LogMsg('error', 'Lost digest file: %s' % digestfile) + mlist.LogMsg('error', str(e)) + + + +def inject_digest(mlist, digestfile, topicsfile): + fp = open(topicsfile, 'r+') + topicsdata = fp.read() + fp.close() + topicscount = string.count(topicsdata, '\n') + fp = open(digestfile) + # + # filters for recipient calculation + def delivery_enabled_p(x, s=self, v=mm_cfg.DisableDelivery): + return not s.GetUserOption(x, v) + def likes_mime_p(x, s=self, v=mm_cfg.DisableMime): + return not s.GetUserOption(x, v) + def hates_mime_p(x, s=self, v=mm_cfg.DisableMime): + return s.GetUserOption(x, v) + # + # these people have switched their options from digest delivery to + # non-digest delivery. they need to get one last digest... + try: + final_digesters = mlist.one_last_digest.keys() + mlist.one_last_digest = {} + except AttributeError: + final_digesters = [] + # + # calculate various recipient lists + digestmembers = mlist.GetDigestMembers() + final_digesters + recipients = filter(delivery_enabled_p, digestmembers) + mime_recips = filter(likes_mime_p, recipients) + text_recips = filter(hates_mime_p, recipients) + # + # log this digest injection + mlist.LogMsg('digest', + '%s v %d - %d msgs, %d recips (%d mime, %d text, %d disabled)' + % (mlist.real_name, mlist.next_digest_number, topicscount, + len(digestmembers), len(mime_recips), len(text_recips), + len(digestmembers) - len(recipients))) + # do any deliveries + if mime_recips or text_recips: + digest = Digest(mlist, topicsdata, fp.read()) + mimemsg = digest.asMIME() + mimemsg.recips = mime_recips + mlist.inject(mimemsg) + textmsg = digest.asText() + textmsg.recips = text_recips + mlist.inject(textmsg) + # zap accumulated digest information for the next round + os.unlink(digestfile) + os.unlink(topicsfile) + mlist.next_digest_number = mlist.next_digest_number + 1 + mlist.next_post_number = 1 + + + +def quotemime(text): + if not text: + return text + return string.join(string.split(text, MIME_SEPARATOR), MIME_NONSEPARATOR) + + +class Digest: + """A digest, representable as either a MIME or plain text message.""" + def __init__(self, mlist, toc, body): + self.__mlist = mlist + self.__toc = toc + self.__body = body + self.__volume = 'Vol %d #%d' % (mlist.volume, mlist.next_digest_number) + numtopics = string.count(self.__toc, '\n') + self.__numinfo = '%d msg%s' % (numtopics, numtopics <> 1 and 's' or '') + + def ComposeBaseHeaders(self, msg): + """Populate the message with the presentation-independent headers.""" + mlist = self.__mlist + msg['From'] = mlist.GetAdminEmail() + msg['Subject'] = ('%s digest, %s - %s' % + (mlist.real_name, self.__volume, self.__numinfo)) + msg['Reply-to'] = mlist.GetListEmail() + msg['X-Mailer'] = "Mailman v%s" % mm_cfg.VERSION + msg['MIME-version'] = '1.0' + + def TemplateRefs(self): + """Resolve references in a format string against list settings. + + The resolution is done against a copy of the lists attribute + dictionary, with the addition of some of settings for computed + items - got_listinfo_url, got_request_email, got_list_email, and + got_owner_email. + + """ + # Collect the substitutions: + if hasattr(self, 'substitutions'): + return Utils.SafeDict(self.substitutions) + mlist = self.__mlist + substs = Utils.SafeDict() + substs.update(mlist.__dict__) + substs.update( + {'got_listinfo_url' : mlist.GetAbsoluteScriptURL('listinfo'), + 'got_request_email': mlist.GetRequestEmail(), + 'got_list_email' : mlist.GetListEmail(), + 'got_owner_email' : mlist.GetAdminEmail(), + 'cgiext' : mm_cfg.CGIEXT, + }) + return substs + + def asMIME(self): + return self.Present(mime=1) + + def asText(self): + return self.Present(mime=0) + + def Present(self, mime): + """Produce a rendering of the digest, as an OutgoingMessage.""" + msg = Message.OutgoingMessage() + self.ComposeBaseHeaders(msg) + digestboundary = MIME_SEPARATOR + if mime: + import mimetools + envboundary = mimetools.choose_boundary() + msg['Content-type'] = 'multipart/mixed; boundary=' + envboundary + else: + envboundary = MIME_SEPARATOR + msg['Content-type'] = 'text/plain' + dashbound = "--" + envboundary + # holds lines of the message + lines = [] + # Masthead: + if mime: + lines.append(dashbound) + lines.append("Content-type: text/plain; charset=us-ascii") + lines.append("Content-description: Masthead (%s digest, %s)" + % (self.__mlist.real_name, self.__volume)) + lines.append(Utils.maketext('masthead.txt', self.TemplateRefs())) + # List-specific header: + if self.__mlist.digest_header: + lines.append('') + if mime: + lines.append(dashbound) + lines.append("Content-type: text/plain; charset=us-ascii") + lines.append("Content-description: Digest Header") + lines.append('') + lines.append(self.__mlist.digest_header % self.TemplateRefs()) + # Table of contents: + lines.append('') + if mime: + lines.append(dashbound) + lines.append("Content-type: text/plain; charset=us-ascii") + lines.append("Content-description: Today's Topics (%s)" % + self.__numinfo) + lines.append('') + lines.append("Today's Topics:") + lines.append('') + lines.append(self.__toc) + # Digest text: + if mime: + lines.append(dashbound) + lines.append('Content-type: multipart/digest; boundary="%s"' + % digestboundary) + lines.append('') + lines.append(self.__body) + # End multipart digest text part + lines.append('') + lines.append("--" + digestboundary + "--") + else: + lines.append( + filterDigestHeaders(self.__body, + mm_cfg.DEFAULT_PLAIN_DIGEST_KEEP_HEADERS, + digestboundary)) + # List-specific footer: + if self.__mlist.digest_footer: + lines.append('') + lines.append(dashbound) + if mime: + lines.append("Content-type: text/plain; charset=us-ascii") + lines.append("Content-description: Digest Footer") + lines.append('') + lines.append(self.__mlist.digest_footer % self.TemplateRefs()) + # Close: + if mime: + # Close encompassing mime envelope. + lines.append('') + lines.append(dashbound + "--") + lines.append('') + lines.append("End of %s Digest" % self.__mlist.real_name) + msg.SetBody(string.join(lines, "\n")) + return msg + + + +def filterDigestHeaders(body, keep_headers, mimesep): + """Return copy of body that omits non-crucial headers.""" + state = "sep" # "sep", "head", or "body" + lines = string.split(body, "\n") + at = 1 + text = [lines[0]] + kept_last = 0 + while at < len(lines): + l, at = lines[at], at + 1 + if state == "body": + # Snarf the body up to, and including, the next separator: + text.append(l) + if string.strip(l) == '--' + mimesep: + state = "sep" + continue + elif state == "sep": + state = "head" + # Keep the one (blank) line between separator and headers. + text.append(l) + kept_last = 0 + continue + elif state == "head": + l = string.strip(l) + if l == '': + state = "body" + text.append(l) + continue + elif l[0] in [' ', '\t']: + # Continuation line - keep if the prior line was kept. + if kept_last: + text.append(l) + continue + else: + where = string.find(l, ':') + if where == -1: + # Malformed header line - interesting, keep it. + text.append(l) + kept_last = 1 + else: + field = l[:where] + if string.lower(field) in keep_headers: + text.append(l) + kept_last = 1 + else: + kept_last = 0 + return string.join(text, '\n') |
