diff options
| -rw-r--r-- | Mailman/Digester.py | 384 |
1 files changed, 3 insertions, 381 deletions
diff --git a/Mailman/Digester.py b/Mailman/Digester.py index 5abb35427..ce9b907c5 100644 --- a/Mailman/Digester.py +++ b/Mailman/Digester.py @@ -17,37 +17,12 @@ """Mixin class with list-digest handling methods and settings.""" -import os -import string -import time -import re -import Errors -import Message -import mm_cfg - - -# See templates/masthead.txt -# XXX: this needs conversion -DIGEST_MASTHEAD = """ -Send %(real_name)s mailing list submissions to - %(got_list_email)s - -To subscribe or unsubscribe via the web, visit - %(got_listinfo_url)s -or, via email, send a message with subject or body 'help' to - %(got_request_email)s -You can reach the person managing the list at - %(got_owner_email)s - -When replying, please edit your Subject line so it is more specific than -"Re: Contents of %(real_name)s digest..." -""" +from Mailman import Errors +from Mailman import mm_cfg + class Digester: - def InitTempVars(self): - self._mime_separator = '__--__--' - def InitVars(self): # Configurable self.digestable = mm_cfg.DEFAULT_DIGESTABLE @@ -58,7 +33,6 @@ class Digester: self.next_post_number = 1 self.digest_header = mm_cfg.DEFAULT_DIGEST_HEADER self.digest_footer = mm_cfg.DEFAULT_DIGEST_FOOTER - # Non-configurable. self.digest_members = {} self.next_digest_number = 1 @@ -126,355 +100,3 @@ class Digester: del self.digest_members[addr] self.members[addr] = cpuser self.Save() - - # Internal function, don't call this. - def SaveForDigest(self, post): - """Add message to index, and to the digest. If the digest is large - enough when we're done writing, send it out.""" - ou = os.umask(002) - try: - digest_file = open(os.path.join(self._full_path, "next-digest"), - "a+") - topics_file = open(os.path.join(self._full_path, - "next-digest-topics"), - "a+") - finally: - os.umask(ou) - sender = self.QuoteMime(post.GetSenderName()) - fromline = self.QuoteMime(post.getheader("from")) - date = self.QuoteMime(post.getheader("date")) - body = self.QuoteMime(post.body) - subject = self.QuoteMime(post.getheader("subject")) - # Don't include the redundant subject prefix in the toc entries: - matched = re.match("(re:? *)?(%s)" % re.escape(self.subject_prefix), - subject, re.I) - if matched: - subject = subject[:matched.start(2)] + subject[matched.end(2):] - topics_file.write(" %d. %s (%s)\n" % (self.next_post_number, - subject, sender)) - # We exclude specified headers *and* all "X-*" headers. - exclude_headers = ['received', 'errors-to'] - kept_headers = [] - keeping = 0 - have_content_type = 0 - have_content_description = 0 - lower, split = string.lower, string.split - for h in post.headers: - if (lower(h[:2]) == "x-" - or lower(split(h, ':')[0]) in exclude_headers): - 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 - if lower(h[:19]) == "content-description": - have_content_description = 1 - else: - kept_headers.append(self.QuoteMime(h)) - if (have_content_type and not have_content_description): - kept_headers.append("Content-Description: %s\n" % subject) - if self.reply_goes_to_list: - # Munge the reply-to - sigh. - kept_headers.append('Reply-To: %s\n' - % self.QuoteMime(self.GetListEmail())) - - # Do the save. - digest_file.write("--%s\n\nMessage: %d\n%s\n%s" - % (self._mime_separator, self.next_post_number, - string.join(kept_headers, ""), - body)) - self.next_post_number = self.next_post_number + 1 - topics_file.close() - digest_file.close() - self.SendDigestOnSize(self.digest_size_threshhold) - - def SendDigestIfAny(self): - """Send the digest if there are any messages pending.""" - self.SendDigestOnSize(0) - - def SendDigestOnSize(self, threshhold): - """Call SendDigest if accumulated digest exceeds threshhold. - - (There must be some content, even if threshhold is 0.)""" - try: - ndf = os.path.join(self._full_path, "next-digest") - size = os.stat(ndf)[6] - if size == 0: - return - elif (size/1024.) >= threshhold: - self.SendDigest() - except os.error, err: - if err[0] == 2: - # No such file or directory - self.LogMsg("error", "mm_digest lost digest file %s, %s", - ndf, err) - -# If the mime separator appears in the text anywhere, throw a space on -# both sides of it, so it doesn't get interpreted as a real mime separator. - def QuoteMime(self, text): - if not text: - return text - return string.join(string.split(text, self._mime_separator), ' %s ' % - self._mime_separator) - - def FakeDigest(self): - def DeliveryEnabled(x, s=self, v=mm_cfg.DisableDelivery): - return not s.GetUserOption(x, v) - - def LikesMime(x, s=self, v=mm_cfg.DisableMime): - return not s.GetUserOption(x, v) - - def HatesMime(x, s=self, v=mm_cfg.DisableMime): - return s.GetUserOption(x, v) - - digestmembers = self.GetDigestDeliveryMembers() - recipients = filter(DeliveryEnabled, digestmembers) - mime_recipients = filter(LikesMime, recipients) - text_recipients = filter(HatesMime, recipients) - self.LogMsg("digest", - 'Fake %s digest %d log--', - self.real_name, self.next_digest_number) - self.LogMsg("digest", - ('Fake %d digesters, %d disabled. ' - 'Active: %d MIMEers, %d non.'), - len(digestmembers), - len(digestmembers) - len(recipients), - len(mime_recipients), len(text_recipients)) - - def SendDigest(self): - topics_file = open(os.path.join(self._full_path, 'next-digest-topics'), - 'r+') - topics_text = topics_file.read() - topics_number = string.count(topics_text, '\n') - topics_plural = ((topics_number != 1) and "s") or "" - digest_file = open(os.path.join(self._full_path, 'next-digest'), 'r+') - - def DeliveryEnabled(x, s=self, v=mm_cfg.DisableDelivery): - return not s.GetUserOption(x, v) - def LikesMime(x, s=self, v=mm_cfg.DisableMime): - return not s.GetUserOption(x, v) - def HatesMime(x, s=self, v=mm_cfg.DisableMime): - return s.GetUserOption(x, v) - try: - final_digesters = self.one_last_digest.keys() - except AttributeError: - final_digesters = [] - digestmembers = self.GetDigestMembers() + final_digesters - recipients = filter(DeliveryEnabled, digestmembers) - mime_recipients = filter(LikesMime, recipients) - text_recipients = filter(HatesMime, recipients) - - self.LogMsg("digest", - ('%s v %d - ' - '%d msgs %d dgstrs: %d m %d non %d dis'), - self.real_name, - self.next_digest_number, - topics_number, - len(digestmembers), - len(mime_recipients), - len(text_recipients), - len(digestmembers) - len(recipients)) - if mime_recipients or text_recipients: - d = Digest(self, topics_text, digest_file.read()) - else: - d = None - # Zero the digest files only just before the messages go out. - topics_file.truncate(0) - topics_file.close() - digest_file.truncate(0) - digest_file.close() - self.next_digest_number = self.next_digest_number + 1 - self.next_post_number = 1 - # these folks already got their last digest - self.one_last_digest = {} - self.Save() - - if text_recipients: - self.DeliverToList(d.Present(mime=0), - text_recipients, remove_to=1) - if mime_recipients: - self.DeliverToList(d.Present(mime=1), - mime_recipients, - remove_to=1, tmpfile_prefix = "mime.") - -class Digest: - "Represent a list digest, presentable in either plain or mime format." - def __init__(self, list, toc, body): - self.list = list - self.toc = toc - self.body = body - self.baseheaders = [] - self.volinfo = "Vol %d #%d" % (list.volume, list.next_digest_number) - numtopics = string.count(self.toc, '\n') - plural = ((numtopics != 1) and "s") or "" - self.numinfo = "%d msg%s" % (numtopics, plural) - - def ComposeBaseHeaders(self, msg): - """Populate the message with the presentation-independent headers.""" - lst = self.list - msg.SetSender(lst.GetAdminEmail()) - msg.SetHeader('Subject', - ('%s digest, %s - %s' % - (lst.real_name, self.volinfo, self.numinfo))) - msg.SetHeader('Reply-to', lst.GetListEmail()) - msg.SetHeader('X-Mailer', "Mailman v%s" % mm_cfg.VERSION) - msg.SetHeader('MIME-version', '1.0') - - def SatisfyRefs(self, text): - """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'): - substs = self.substitutions - else: - lst = self.list - substs = {} - substs.update(lst.__dict__) - substs.update({'got_listinfo_url': - lst.GetAbsoluteScriptURL('listinfo'), - 'got_request_email': lst.GetRequestEmail(), - 'got_list_email': lst.GetListEmail(), - 'got_owner_email': lst.GetAdminEmail(), - 'cgiext': mm_cfg.CGIEXT, - }) - return text % substs - - def Present(self, mime=0): - """Produce a rendering of the digest, as an OutgoingMessage.""" - msg = Message.OutgoingMessage() - self.ComposeBaseHeaders(msg) - digestboundary = self.list._mime_separator - if mime: - import mimetools - envboundary = mimetools.choose_boundary() - msg.SetHeader('Content-type', - 'multipart/mixed; boundary="%s"' % envboundary) - else: - envboundary = self.list._mime_separator - msg.SetHeader('Content-type', 'text/plain') - dashbound = "--" + envboundary - - 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.list.real_name, self.volinfo)) - lines.append(self.SatisfyRefs(DIGEST_MASTHEAD)) - - # List-specific header: - if self.list.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.SatisfyRefs(self.list.digest_header)) - - # 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.list.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.SatisfyRefs(self.list.digest_footer)) - - # Close: - if mime: - # Close encompassing mime envelope. - lines.append("") - lines.append(dashbound + "--") - lines.append("") - lines.append("End of %s Digest" % self.list.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') |
