summaryrefslogtreecommitdiff
path: root/Mailman/Digester.py
diff options
context:
space:
mode:
Diffstat (limited to 'Mailman/Digester.py')
-rw-r--r--Mailman/Digester.py384
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')