summaryrefslogtreecommitdiff
path: root/Mailman/Handlers
diff options
context:
space:
mode:
Diffstat (limited to 'Mailman/Handlers')
-rw-r--r--Mailman/Handlers/Acknowledge.py65
-rw-r--r--Mailman/Handlers/AfterDelivery.py29
-rw-r--r--Mailman/Handlers/AvoidDuplicates.py93
-rw-r--r--Mailman/Handlers/CalcRecips.py128
-rw-r--r--Mailman/Handlers/Cleanse.py58
-rw-r--r--Mailman/Handlers/CleanseDKIM.py36
-rw-r--r--Mailman/Handlers/CookHeaders.py338
-rw-r--r--Mailman/Handlers/Decorate.py207
-rw-r--r--Mailman/Handlers/FileRecips.py46
-rw-r--r--Mailman/Handlers/MimeDel.py261
-rw-r--r--Mailman/Handlers/Moderate.py168
-rw-r--r--Mailman/Handlers/OwnerRecips.py27
-rw-r--r--Mailman/Handlers/Replybot.py111
-rw-r--r--Mailman/Handlers/SMTPDirect.py389
-rw-r--r--Mailman/Handlers/Scrubber.py500
-rw-r--r--Mailman/Handlers/Tagger.py157
-rw-r--r--Mailman/Handlers/ToArchive.py37
-rw-r--r--Mailman/Handlers/ToDigest.py420
-rw-r--r--Mailman/Handlers/ToOutgoing.py57
-rw-r--r--Mailman/Handlers/ToUsenet.py49
-rw-r--r--Mailman/Handlers/__init__.py0
21 files changed, 0 insertions, 3176 deletions
diff --git a/Mailman/Handlers/Acknowledge.py b/Mailman/Handlers/Acknowledge.py
deleted file mode 100644
index 078c3ac92..000000000
--- a/Mailman/Handlers/Acknowledge.py
+++ /dev/null
@@ -1,65 +0,0 @@
-# Copyright (C) 1998-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Send an acknowledgement of the successful post to the sender.
-
-This only happens if the sender has set their AcknowledgePosts attribute.
-This module must appear after the deliverer in the message pipeline in order
-to send acks only after successful delivery.
-
-"""
-
-from Mailman import Errors
-from Mailman import Message
-from Mailman import Utils
-from Mailman.configuration import config
-from Mailman.i18n import _
-
-__i18n_templates__ = True
-
-
-
-def process(mlist, msg, msgdata):
- # Extract the sender's address and find them in the user database
- sender = msgdata.get('original_sender', msg.get_sender())
- member = mlist.members.get_member(sender)
- if member is None:
- return
- ack = member.acknowledge_posts
- if not ack:
- return
- # Okay, they want acknowledgement of their post. Give them their original
- # subject. BAW: do we want to use the decoded header?
- origsubj = msgdata.get('origsubj', msg.get('subject', _('(no subject)')))
- # Get the user's preferred language
- lang = msgdata.get('lang', member.preferred_language)
- # Now get the acknowledgement template
- realname = mlist.real_name
- text = Utils.maketext(
- 'postack.txt',
- {'subject' : Utils.oneline(origsubj, Utils.GetCharSet(lang)),
- 'listname' : realname,
- 'listinfo_url': mlist.script_url('listinfo'),
- 'optionsurl' : member.options_url,
- }, lang=lang, mlist=mlist, raw=True)
- # Craft the outgoing message, with all headers and attributes
- # necessary for general delivery. Then enqueue it to the outgoing
- # queue.
- subject = _('$realname post acknowledgment')
- usermsg = Message.UserNotification(sender, mlist.bounces_address,
- subject, text, lang)
- usermsg.send(mlist)
diff --git a/Mailman/Handlers/AfterDelivery.py b/Mailman/Handlers/AfterDelivery.py
deleted file mode 100644
index 16caf5773..000000000
--- a/Mailman/Handlers/AfterDelivery.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# Copyright (C) 1998-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Perform some bookkeeping after a successful post.
-
-This module must appear after the delivery module in the message pipeline.
-"""
-
-import datetime
-
-
-
-def process(mlist, msg, msgdata):
- mlist.last_post_time = datetime.datetime.now()
- mlist.post_id += 1
diff --git a/Mailman/Handlers/AvoidDuplicates.py b/Mailman/Handlers/AvoidDuplicates.py
deleted file mode 100644
index a652906a9..000000000
--- a/Mailman/Handlers/AvoidDuplicates.py
+++ /dev/null
@@ -1,93 +0,0 @@
-# Copyright (C) 2002-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""If the user wishes it, do not send duplicates of the same message.
-
-This module keeps an in-memory dictionary of Message-ID: and recipient pairs.
-If a message with an identical Message-ID: is about to be sent to someone who
-has already received a copy, we either drop the message, add a duplicate
-warning header, or pass it through, depending on the user's preferences.
-"""
-
-from email.Utils import getaddresses, formataddr
-from Mailman.configuration import config
-
-COMMASPACE = ', '
-
-
-
-def process(mlist, msg, msgdata):
- recips = msgdata.get('recips')
- # Short circuit
- if not recips:
- return
- # Seed this set with addresses we don't care about dup avoiding.
- listaddrs = set((mlist.posting_address,
- mlist.bounces_address,
- mlist.owner_address,
- mlist.request_address))
- explicit_recips = listaddrs.copy()
- # Figure out the set of explicit recipients
- cc_addresses = {}
- for header in ('to', 'cc', 'resent-to', 'resent-cc'):
- addrs = getaddresses(msg.get_all(header, []))
- header_addresses = dict((addr, formataddr((name, addr)))
- for name, addr in addrs
- if addr)
- if header == 'cc':
- # Yes, it's possible that an address is mentioned in multiple CC
- # headers using different names. In that case, the last real name
- # will win, but that doesn't seem like such a big deal. Besides,
- # how else would you chose?
- cc_addresses.update(header_addresses)
- # Ignore the list addresses for purposes of dup avoidance.
- explicit_recips |= set(header_addresses)
- # Now strip out the list addresses
- explicit_recips -= listaddrs
- if not explicit_recips:
- # No one was explicitly addressed, so we can't do any dup collapsing
- return
- newrecips = set()
- for r in recips:
- # If this recipient is explicitly addressed...
- if r in explicit_recips:
- send_duplicate = True
- # If the member wants to receive duplicates, or if the recipient
- # is not a member at all, they will get a copy.
- # header.
- member = mlist.members.get_member(r)
- if member and not member.receive_list_copy:
- send_duplicate = False
- # We'll send a duplicate unless the user doesn't wish it. If
- # personalization is enabled, the add-dupe-header flag will add a
- # X-Mailman-Duplicate: yes header for this user's message.
- if send_duplicate:
- msgdata.setdefault('add-dup-header', set()).add(r)
- newrecips.add(r)
- elif r in cc_addresses:
- del cc_addresses[r]
- else:
- # Otherwise, this is the first time they've been in the recips
- # list. Add them to the newrecips list and flag them as having
- # received this message.
- newrecips.add(r)
- # Set the new list of recipients. XXX recips should always be a set.
- msgdata['recips'] = list(newrecips)
- # RFC 2822 specifies zero or one CC header
- if cc_addresses:
- del msg['cc']
- msg['CC'] = COMMASPACE.join(cc_addresses.values())
diff --git a/Mailman/Handlers/CalcRecips.py b/Mailman/Handlers/CalcRecips.py
deleted file mode 100644
index ebaf941c9..000000000
--- a/Mailman/Handlers/CalcRecips.py
+++ /dev/null
@@ -1,128 +0,0 @@
-# Copyright (C) 1998-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Calculate the regular (i.e. non-digest) recipients of the message.
-
-This module calculates the non-digest recipients for the message based on the
-list's membership and configuration options. It places the list of recipients
-on the `recips' attribute of the message. This attribute is used by the
-SendmailDeliver and BulkDeliver modules.
-"""
-
-from Mailman import Errors
-from Mailman import Message
-from Mailman import Utils
-from Mailman.configuration import config
-from Mailman.i18n import _
-from Mailman.interfaces import DeliveryStatus
-
-
-
-def process(mlist, msg, msgdata):
- # Short circuit if we've already calculated the recipients list,
- # regardless of whether the list is empty or not.
- if 'recips' in msgdata:
- return
- # Should the original sender should be included in the recipients list?
- include_sender = True
- sender = msg.get_sender()
- member = mlist.members.get_member(sender)
- if member and not member.receive_own_postings:
- include_sender = False
- # Support for urgent messages, which bypasses digests and disabled
- # delivery and forces an immediate delivery to all members Right Now. We
- # are specifically /not/ allowing the site admins password to work here
- # because we want to discourage the practice of sending the site admin
- # password through email in the clear. (see also Approve.py)
- missing = []
- password = msg.get('urgent', missing)
- if password is not missing:
- if mlist.Authenticate((config.AuthListModerator,
- config.AuthListAdmin),
- password):
- recips = mlist.getMemberCPAddresses(mlist.getRegularMemberKeys() +
- mlist.getDigestMemberKeys())
- msgdata['recips'] = recips
- return
- else:
- # Bad Urgent: password, so reject it instead of passing it on. I
- # think it's better that the sender know they screwed up than to
- # deliver it normally.
- realname = mlist.real_name
- text = _("""\
-Your urgent message to the %(realname)s mailing list was not authorized for
-delivery. The original message as received by Mailman is attached.
-""")
- raise Errors.RejectMessage, Utils.wrap(text)
- # Calculate the regular recipients of the message
- recips = set(member.address.address
- for member in mlist.regular_members.members
- if member.delivery_status == DeliveryStatus.enabled)
- # Remove the sender if they don't want to receive their own posts
- if not include_sender and member.address.address in recips:
- recips.remove(member.address.address)
- # Handle topic classifications
- do_topic_filters(mlist, msg, msgdata, recips)
- # Bookkeeping
- msgdata['recips'] = recips
-
-
-
-def do_topic_filters(mlist, msg, msgdata, recips):
- if not mlist.topics_enabled:
- # MAS: if topics are currently disabled for the list, send to all
- # regardless of ReceiveNonmatchingTopics
- return
- hits = msgdata.get('topichits')
- zaprecips = []
- if hits:
- # The message hit some topics, so only deliver this message to those
- # who are interested in one of the hit topics.
- for user in recips:
- utopics = mlist.getMemberTopics(user)
- if not utopics:
- # This user is not interested in any topics, so they get all
- # postings.
- continue
- # BAW: Slow, first-match, set intersection!
- for topic in utopics:
- if topic in hits:
- # The user wants this message
- break
- else:
- # The user was interested in topics, but not any of the ones
- # this message matched, so zap him.
- zaprecips.append(user)
- else:
- # The semantics for a message that did not hit any of the pre-canned
- # topics is to troll through the membership list, looking for users
- # who selected at least one topic of interest, but turned on
- # ReceiveNonmatchingTopics.
- for user in recips:
- if not mlist.getMemberTopics(user):
- # The user did not select any topics of interest, so he gets
- # this message by default.
- continue
- if not mlist.getMemberOption(user,
- config.ReceiveNonmatchingTopics):
- # The user has interest in some topics, but elects not to
- # receive message that match no topics, so zap him.
- zaprecips.append(user)
- # Otherwise, the user wants non-matching messages.
- # Prune out the non-receiving users
- for user in zaprecips:
- recips.remove(user)
diff --git a/Mailman/Handlers/Cleanse.py b/Mailman/Handlers/Cleanse.py
deleted file mode 100644
index bf25a4591..000000000
--- a/Mailman/Handlers/Cleanse.py
+++ /dev/null
@@ -1,58 +0,0 @@
-# Copyright (C) 1998-2008 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Cleanse certain headers from all messages."""
-
-import logging
-
-from email.Utils import formataddr
-
-from Mailman.Handlers.CookHeaders import uheader
-
-log = logging.getLogger('mailman.post')
-
-
-
-def process(mlist, msg, msgdata):
- # Always remove this header from any outgoing messages. Be sure to do
- # this after the information on the header is actually used, but before a
- # permanent record of the header is saved.
- del msg['approved']
- # Remove this one too.
- del msg['approve']
- # Also remove this header since it can contain a password
- del msg['urgent']
- # We remove other headers from anonymous lists
- if mlist.anonymous_list:
- log.info('post to %s from %s anonymized',
- mlist.fqdn_listname, msg.get('from'))
- del msg['from']
- del msg['reply-to']
- del msg['sender']
- # Hotmail sets this one
- del msg['x-originating-email']
- i18ndesc = str(uheader(mlist, mlist.description, 'From'))
- msg['From'] = formataddr((i18ndesc, mlist.posting_address))
- msg['Reply-To'] = mlist.posting_address
- # Some headers can be used to fish for membership
- del msg['return-receipt-to']
- del msg['disposition-notification-to']
- del msg['x-confirm-reading-to']
- # Pegasus mail uses this one... sigh
- del msg['x-pmrqc']
- # Don't let this header be spoofed. See RFC 5064.
- del msg['archived-at']
diff --git a/Mailman/Handlers/CleanseDKIM.py b/Mailman/Handlers/CleanseDKIM.py
deleted file mode 100644
index 9dee4fcb0..000000000
--- a/Mailman/Handlers/CleanseDKIM.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# Copyright (C) 2006-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Remove any 'DomainKeys' (or similar) header lines.
-
-The values contained in these header lines are intended to be used by the
-recipient to detect forgery or tampering in transit, and the modifications
-made by Mailman to the headers and body of the message will cause these keys
-to appear invalid. Removing them will at least avoid this misleading result,
-and it will also give the MTA the opportunity to regenerate valid keys
-originating at the Mailman server for the outgoing message.
-"""
-
-from Mailman.configuration import config
-
-
-
-def process(mlist, msg, msgdata):
- if config.REMOVE_DKIM_HEADERS:
- del msg['domainkey-signature']
- del msg['dkim-signature']
- del msg['authentication-results']
diff --git a/Mailman/Handlers/CookHeaders.py b/Mailman/Handlers/CookHeaders.py
deleted file mode 100644
index 4797de62b..000000000
--- a/Mailman/Handlers/CookHeaders.py
+++ /dev/null
@@ -1,338 +0,0 @@
-# Copyright (C) 1998-2008 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Cook a message's headers."""
-
-import re
-
-from email.Charset import Charset
-from email.Errors import HeaderParseError
-from email.Header import Header, decode_header, make_header
-from email.Utils import parseaddr, formataddr, getaddresses
-
-from Mailman import Utils
-from Mailman import Version
-from Mailman.app.archiving import get_archiver
-from Mailman.configuration import config
-from Mailman.i18n import _
-from Mailman.interfaces import Personalization, ReplyToMunging
-
-CONTINUATION = ',\n\t'
-COMMASPACE = ', '
-MAXLINELEN = 78
-
-nonascii = re.compile('[^\s!-~]')
-
-
-
-def uheader(mlist, s, header_name=None, continuation_ws='\t', maxlinelen=None):
- # Get the charset to encode the string in. Then search if there is any
- # non-ascii character is in the string. If there is and the charset is
- # us-ascii then we use iso-8859-1 instead. If the string is ascii only
- # we use 'us-ascii' if another charset is specified.
- charset = Utils.GetCharSet(mlist.preferred_language)
- if nonascii.search(s):
- # use list charset but ...
- if charset == 'us-ascii':
- charset = 'iso-8859-1'
- else:
- # there is no nonascii so ...
- charset = 'us-ascii'
- return Header(s, charset, maxlinelen, header_name, continuation_ws)
-
-
-
-def process(mlist, msg, msgdata):
- # Set the "X-Ack: no" header if noack flag is set.
- if msgdata.get('noack'):
- del msg['x-ack']
- msg['X-Ack'] = 'no'
- # Because we're going to modify various important headers in the email
- # message, we want to save some of the information in the msgdata
- # dictionary for later. Specifically, the sender header will get waxed,
- # but we need it for the Acknowledge module later.
- msgdata['original_sender'] = msg.get_sender()
- # VirginRunner sets _fasttrack for internally crafted messages.
- fasttrack = msgdata.get('_fasttrack')
- if not msgdata.get('isdigest') and not fasttrack:
- try:
- prefix_subject(mlist, msg, msgdata)
- except (UnicodeError, ValueError):
- # TK: Sometimes subject header is not MIME encoded for 8bit
- # simply abort prefixing.
- pass
- # Mark message so we know we've been here, but leave any existing
- # X-BeenThere's intact.
- msg['X-BeenThere'] = mlist.posting_address
- # Add Precedence: and other useful headers. None of these are standard
- # and finding information on some of them are fairly difficult. Some are
- # just common practice, and we'll add more here as they become necessary.
- # Good places to look are:
- #
- # http://www.dsv.su.se/~jpalme/ietf/jp-ietf-home.html
- # http://www.faqs.org/rfcs/rfc2076.html
- #
- # None of these headers are added if they already exist. BAW: some
- # consider the advertising of this a security breach. I.e. if there are
- # known exploits in a particular version of Mailman and we know a site is
- # using such an old version, they may be vulnerable. It's too easy to
- # edit the code to add a configuration variable to handle this.
- if 'x-mailman-version' not in msg:
- msg['X-Mailman-Version'] = Version.VERSION
- # We set "Precedence: list" because this is the recommendation from the
- # sendmail docs, the most authoritative source of this header's semantics.
- if 'precedence' not in msg:
- msg['Precedence'] = 'list'
- # Reply-To: munging. Do not do this if the message is "fast tracked",
- # meaning it is internally crafted and delivered to a specific user. BAW:
- # Yuck, I really hate this feature but I've caved under the sheer pressure
- # of the (very vocal) folks want it. OTOH, RFC 2822 allows Reply-To: to
- # be a list of addresses, so instead of replacing the original, simply
- # augment it. RFC 2822 allows max one Reply-To: header so collapse them
- # if we're adding a value, otherwise don't touch it. (Should we collapse
- # in all cases?)
- if not fasttrack:
- # A convenience function, requires nested scopes. pair is (name, addr)
- new = []
- d = {}
- def add(pair):
- lcaddr = pair[1].lower()
- if lcaddr in d:
- return
- d[lcaddr] = pair
- new.append(pair)
- # List admin wants an explicit Reply-To: added
- if mlist.reply_goes_to_list == ReplyToMunging.explicit_header:
- add(parseaddr(mlist.reply_to_address))
- # If we're not first stripping existing Reply-To: then we need to add
- # the original Reply-To:'s to the list we're building up. In both
- # cases we'll zap the existing field because RFC 2822 says max one is
- # allowed.
- if not mlist.first_strip_reply_to:
- orig = msg.get_all('reply-to', [])
- for pair in getaddresses(orig):
- add(pair)
- # Set Reply-To: header to point back to this list. Add this last
- # because some folks think that some MUAs make it easier to delete
- # addresses from the right than from the left.
- if mlist.reply_goes_to_list == ReplyToMunging.point_to_list:
- i18ndesc = uheader(mlist, mlist.description, 'Reply-To')
- add((str(i18ndesc), mlist.posting_address))
- del msg['reply-to']
- # Don't put Reply-To: back if there's nothing to add!
- if new:
- # Preserve order
- msg['Reply-To'] = COMMASPACE.join(
- [formataddr(pair) for pair in new])
- # The To field normally contains the list posting address. However
- # when messages are fully personalized, that header will get
- # overwritten with the address of the recipient. We need to get the
- # posting address in one of the recipient headers or they won't be
- # able to reply back to the list. It's possible the posting address
- # was munged into the Reply-To header, but if not, we'll add it to a
- # Cc header. BAW: should we force it into a Reply-To header in the
- # above code?
- # Also skip Cc if this is an anonymous list as list posting address
- # is already in From and Reply-To in this case.
- if (mlist.personalize == Personalization.full and
- mlist.reply_goes_to_list <> ReplyToMunging.point_to_list and
- not mlist.anonymous_list):
- # Watch out for existing Cc headers, merge, and remove dups. Note
- # that RFC 2822 says only zero or one Cc header is allowed.
- new = []
- d = {}
- for pair in getaddresses(msg.get_all('cc', [])):
- add(pair)
- i18ndesc = uheader(mlist, mlist.description, 'Cc')
- add((str(i18ndesc), mlist.posting_address))
- del msg['Cc']
- msg['Cc'] = COMMASPACE.join([formataddr(pair) for pair in new])
- # Add list-specific headers as defined in RFC 2369 and RFC 2919, but only
- # if the message is being crafted for a specific list (e.g. not for the
- # password reminders).
- #
- # BAW: Some people really hate the List-* headers. It seems that the free
- # version of Eudora (possibly on for some platforms) does not hide these
- # headers by default, pissing off their users. Too bad. Fix the MUAs.
- if msgdata.get('_nolist') or not mlist.include_rfc2369_headers:
- return
- # This will act like an email address for purposes of formataddr()
- listid = '%s.%s' % (mlist.list_name, mlist.host_name)
- cset = Utils.GetCharSet(mlist.preferred_language)
- if mlist.description:
- # Don't wrap the header since here we just want to get it properly RFC
- # 2047 encoded.
- i18ndesc = uheader(mlist, mlist.description, 'List-Id', maxlinelen=998)
- listid_h = formataddr((str(i18ndesc), listid))
- else:
- # without desc we need to ensure the MUST brackets
- listid_h = '<%s>' % listid
- # We always add a List-ID: header.
- del msg['list-id']
- msg['List-Id'] = listid_h
- # For internally crafted messages, we also add a (nonstandard),
- # "X-List-Administrivia: yes" header. For all others (i.e. those coming
- # from list posts), we add a bunch of other RFC 2369 headers.
- requestaddr = mlist.request_address
- subfieldfmt = '<%s>, <mailto:%s>'
- listinfo = mlist.script_url('listinfo')
- headers = {}
- # XXX reduced_list_headers used to suppress List-Help, List-Subject, and
- # List-Unsubscribe from UserNotification. That doesn't seem to make sense
- # any more, so always add those three headers (others will still be
- # suppressed).
- headers.update({
- 'List-Help' : '<mailto:%s?subject=help>' % requestaddr,
- 'List-Unsubscribe': subfieldfmt % (listinfo, mlist.leave_address),
- 'List-Subscribe' : subfieldfmt % (listinfo, mlist.join_address),
- })
- archiver = get_archiver()
- if msgdata.get('reduced_list_headers'):
- headers['X-List-Administrivia'] = 'yes'
- else:
- # List-Post: is controlled by a separate attribute
- if mlist.include_list_post_header:
- headers['List-Post'] = '<mailto:%s>' % mlist.posting_address
- # Add this header if we're archiving
- if mlist.archive:
- archiveurl = archiver.get_list_url(mlist)
- headers['List-Archive'] = '<%s>' % archiveurl
- # XXX RFC 2369 also defines a List-Owner header which we are not currently
- # supporting, but should.
- #
- # Draft RFC 5064 defines an Archived-At header which contains the pointer
- # directly to the message in the archive. If the currently defined
- # archiver can tell us the URL, go ahead and include this header.
- archived_at = archiver.get_message_url(mlist, msg)
- if archived_at is not None:
- headers['Archived-At'] = archived_at
- # First we delete any pre-existing headers because the RFC permits only
- # one copy of each, and we want to be sure it's ours.
- for h, v in headers.items():
- del msg[h]
- # Wrap these lines if they are too long. 78 character width probably
- # shouldn't be hardcoded, but is at least text-MUA friendly. The
- # adding of 2 is for the colon-space separator.
- if len(h) + 2 + len(v) > 78:
- v = CONTINUATION.join(v.split(', '))
- msg[h] = v
-
-
-
-def prefix_subject(mlist, msg, msgdata):
- # Add the subject prefix unless the message is a digest or is being fast
- # tracked (e.g. internally crafted, delivered to a single user such as the
- # list admin).
- if not mlist.subject_prefix.strip():
- return
- prefix = mlist.subject_prefix
- subject = msg.get('subject', '')
- # Try to figure out what the continuation_ws is for the header
- if isinstance(subject, Header):
- lines = str(subject).splitlines()
- else:
- lines = subject.splitlines()
- ws = '\t'
- if len(lines) > 1 and lines[1] and lines[1][0] in ' \t':
- ws = lines[1][0]
- msgdata['origsubj'] = subject
- # The subject may be multilingual but we take the first charset as major
- # one and try to decode. If it is decodable, returned subject is in one
- # line and cset is properly set. If fail, subject is mime-encoded and
- # cset is set as us-ascii. See detail for ch_oneline() (CookHeaders one
- # line function).
- subject, cset = ch_oneline(subject)
- # TK: Python interpreter has evolved to be strict on ascii charset code
- # range. It is safe to use unicode string when manupilating header
- # contents with re module. It would be best to return unicode in
- # ch_oneline() but here is temporary solution.
- subject = unicode(subject, cset)
- # If the subject_prefix contains '%d', it is replaced with the
- # mailing list sequential number. Sequential number format allows
- # '%d' or '%05d' like pattern.
- prefix_pattern = re.escape(prefix)
- # unescape '%' :-<
- prefix_pattern = '%'.join(prefix_pattern.split(r'\%'))
- p = re.compile('%\d*d')
- if p.search(prefix, 1):
- # prefix have number, so we should search prefix w/number in subject.
- # Also, force new style.
- prefix_pattern = p.sub(r'\s*\d+\s*', prefix_pattern)
- subject = re.sub(prefix_pattern, '', subject)
- rematch = re.match('((RE|AW|SV|VS)(\[\d+\])?:\s*)+', subject, re.I)
- if rematch:
- subject = subject[rematch.end():]
- recolon = 'Re:'
- else:
- recolon = ''
- # At this point, subject may become null if someone post mail with
- # subject: [subject prefix]
- if subject.strip() == '':
- subject = _('(no subject)')
- cset = Utils.GetCharSet(mlist.preferred_language)
- # and substitute %d in prefix with post_id
- try:
- prefix = prefix % mlist.post_id
- except TypeError:
- pass
- # Get the header as a Header instance, with proper unicode conversion
- if not recolon:
- h = uheader(mlist, prefix, 'Subject', continuation_ws=ws)
- else:
- h = uheader(mlist, prefix, 'Subject', continuation_ws=ws)
- h.append(recolon)
- # TK: Subject is concatenated and unicode string.
- subject = subject.encode(cset, 'replace')
- h.append(subject, cset)
- del msg['subject']
- msg['Subject'] = h
- ss = uheader(mlist, recolon, 'Subject', continuation_ws=ws)
- ss.append(subject, cset)
- msgdata['stripped_subject'] = ss
-
-
-
-def ch_oneline(headerstr):
- # Decode header string in one line and convert into single charset
- # copied and modified from ToDigest.py and Utils.py
- # return (string, cset) tuple as check for failure
- try:
- d = decode_header(headerstr)
- # At this point, we should rstrip() every string because some
- # MUA deliberately add trailing spaces when composing return
- # message.
- d = [(s.rstrip(),c) for (s,c) in d]
- # Find all charsets in the original header. We use 'utf-8' rather
- # than using the first charset (in mailman 2.1.x) if multiple
- # charsets are used.
- csets = []
- for (s,c) in d:
- if c and c not in csets:
- csets.append(c)
- if len(csets) == 0:
- cset = 'us-ascii'
- elif len(csets) == 1:
- cset = csets[0]
- else:
- cset = 'utf-8'
- h = make_header(d)
- ustr = unicode(h)
- oneline = u''.join(ustr.splitlines())
- return oneline.encode(cset, 'replace'), cset
- except (LookupError, UnicodeError, ValueError, HeaderParseError):
- # possibly charset problem. return with undecoded string in one line.
- return ''.join(headerstr.splitlines()), 'us-ascii'
diff --git a/Mailman/Handlers/Decorate.py b/Mailman/Handlers/Decorate.py
deleted file mode 100644
index 6891fed8d..000000000
--- a/Mailman/Handlers/Decorate.py
+++ /dev/null
@@ -1,207 +0,0 @@
-# Copyright (C) 1998-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Decorate a message by sticking the header and footer around it."""
-
-import re
-import logging
-
-from email.MIMEText import MIMEText
-from string import Template
-
-from Mailman import Errors
-from Mailman import Utils
-from Mailman.Message import Message
-from Mailman.configuration import config
-from Mailman.i18n import _
-
-log = logging.getLogger('mailman.error')
-
-
-
-def process(mlist, msg, msgdata):
- # Digests and Mailman-craft messages should not get additional headers
- if msgdata.get('isdigest') or msgdata.get('nodecorate'):
- return
- d = {}
- if msgdata.get('personalize'):
- # Calculate the extra personalization dictionary. Note that the
- # length of the recips list better be exactly 1.
- recips = msgdata.get('recips')
- assert isinstance(recips, list) and len(recips) == 1, (
- 'The number of intended recipients must be exactly 1')
- member = recips[0].lower()
- d['user_address'] = member
- try:
- d['user_delivered_to'] = mlist.getMemberCPAddress(member)
- # BAW: Hmm, should we allow this?
- d['user_password'] = mlist.getMemberPassword(member)
- d['user_language'] = mlist.getMemberLanguage(member)
- username = mlist.getMemberName(member) or None
- d['user_name'] = username or d['user_delivered_to']
- d['user_optionsurl'] = mlist.GetOptionsURL(member)
- except Errors.NotAMemberError:
- pass
- # These strings are descriptive for the log file and shouldn't be i18n'd
- d.update(msgdata.get('decoration-data', {}))
- header = decorate(mlist, mlist.msg_header, d)
- footer = decorate(mlist, mlist.msg_footer, d)
- # Escape hatch if both the footer and header are empty
- if not header and not footer:
- return
- # Be MIME smart here. We only attach the header and footer by
- # concatenation when the message is a non-multipart of type text/plain.
- # Otherwise, if it is not a multipart, we make it a multipart, and then we
- # add the header and footer as text/plain parts.
- #
- # BJG: In addition, only add the footer if the message's character set
- # matches the charset of the list's preferred language. This is a
- # suboptimal solution, and should be solved by allowing a list to have
- # multiple headers/footers, for each language the list supports.
- #
- # Also, if the list's preferred charset is us-ascii, we can always
- # safely add the header/footer to a plain text message since all
- # charsets Mailman supports are strict supersets of us-ascii --
- # no, UTF-16 emails are not supported yet.
- #
- # TK: Message with 'charset=' cause trouble. So, instead of
- # mgs.get_content_charset('us-ascii') ...
- mcset = msg.get_content_charset() or 'us-ascii'
- lcset = Utils.GetCharSet(mlist.preferred_language)
- msgtype = msg.get_content_type()
- # BAW: If the charsets don't match, should we add the header and footer by
- # MIME multipart chroming the message?
- wrap = True
- if not msg.is_multipart() and msgtype == 'text/plain':
- # Save the RFC-3676 format parameters.
- format = msg.get_param('format')
- delsp = msg.get_param('delsp')
- # Save 'Content-Transfer-Encoding' header in case decoration fails.
- cte = msg.get('content-transfer-encoding')
- # header/footer is now in unicode (2.2)
- try:
- oldpayload = unicode(msg.get_payload(decode=True), mcset)
- del msg['content-transfer-encoding']
- frontsep = endsep = u''
- if header and not header.endswith('\n'):
- frontsep = u'\n'
- if footer and not oldpayload.endswith('\n'):
- endsep = u'\n'
- payload = header + frontsep + oldpayload + endsep + footer
- # When setting the payload for the message, try various charset
- # encodings until one does not produce a UnicodeError. We'll try
- # charsets in this order: the list's charset, the message's
- # charset, then utf-8. It's okay if some of these are duplicates.
- for cset in (lcset, mcset, 'utf-8'):
- try:
- msg.set_payload(payload.encode(cset), cset)
- except UnicodeError:
- pass
- else:
- if format:
- msg.set_param('format', format)
- if delsp:
- msg.set_param('delsp', delsp)
- wrap = False
- break
- except (LookupError, UnicodeError):
- if cte:
- # Restore the original c-t-e.
- del msg['content-transfer-encoding']
- msg['Content-Transfer-Encoding'] = cte
- elif msg.get_content_type() == 'multipart/mixed':
- # The next easiest thing to do is just prepend the header and append
- # the footer as additional subparts
- payload = msg.get_payload()
- if not isinstance(payload, list):
- payload = [payload]
- if footer:
- mimeftr = MIMEText(footer.encode(lcset), 'plain', lcset)
- mimeftr['Content-Disposition'] = 'inline'
- payload.append(mimeftr)
- if header:
- mimehdr = MIMEText(header.encode(lcset), 'plain', lcset)
- mimehdr['Content-Disposition'] = 'inline'
- payload.insert(0, mimehdr)
- msg.set_payload(payload)
- wrap = False
- # If we couldn't add the header or footer in a less intrusive way, we can
- # at least do it by MIME encapsulation. We want to keep as much of the
- # outer chrome as possible.
- if not wrap:
- return
- # Because of the way Message objects are passed around to process(), we
- # need to play tricks with the outer message -- i.e. the outer one must
- # remain the same instance. So we're going to create a clone of the outer
- # message, with all the header chrome intact, then copy the payload to it.
- # This will give us a clone of the original message, and it will form the
- # basis of the interior, wrapped Message.
- inner = Message()
- # Which headers to copy? Let's just do the Content-* headers
- for h, v in msg.items():
- if h.lower().startswith('content-'):
- inner[h] = v
- inner.set_payload(msg.get_payload())
- # For completeness
- inner.set_unixfrom(msg.get_unixfrom())
- inner.preamble = msg.preamble
- inner.epilogue = msg.epilogue
- # Don't copy get_charset, as this might be None, even if
- # get_content_charset isn't. However, do make sure there is a default
- # content-type, even if the original message was not MIME.
- inner.set_default_type(msg.get_default_type())
- # BAW: HACK ALERT.
- if hasattr(msg, '__version__'):
- inner.__version__ = msg.__version__
- # Now, play games with the outer message to make it contain three
- # subparts: the header (if any), the wrapped message, and the footer (if
- # any).
- payload = [inner]
- if header:
- mimehdr = MIMEText(header.encode(lcset), 'plain', lcset)
- mimehdr['Content-Disposition'] = 'inline'
- payload.insert(0, mimehdr)
- if footer:
- mimeftr = MIMEText(footer.encode(lcset), 'plain', lcset)
- mimeftr['Content-Disposition'] = 'inline'
- payload.append(mimeftr)
- msg.set_payload(payload)
- del msg['content-type']
- del msg['content-transfer-encoding']
- del msg['content-disposition']
- msg['Content-Type'] = 'multipart/mixed'
-
-
-
-def decorate(mlist, template, extradict=None):
- # Create a dictionary which includes the default set of interpolation
- # variables allowed in headers and footers. These will be augmented by
- # any key/value pairs in the extradict.
- d = dict(real_name = mlist.real_name,
- list_name = mlist.list_name,
- fqdn_listname = mlist.fqdn_listname,
- host_name = mlist.host_name,
- web_page_url = mlist.web_page_url,
- description = mlist.description,
- info = mlist.info,
- cgiext = config.CGIEXT,
- )
- if extradict is not None:
- d.update(extradict)
- text = Template(template).safe_substitute(d)
- # Turn any \r\n line endings into just \n
- return re.sub(r' *\r?\n', r'\n', text)
diff --git a/Mailman/Handlers/FileRecips.py b/Mailman/Handlers/FileRecips.py
deleted file mode 100644
index 8ce07b432..000000000
--- a/Mailman/Handlers/FileRecips.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# Copyright (C) 2001-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Get the normal delivery recipients from a Sendmail style :include: file."""
-
-from __future__ import with_statement
-
-import os
-import errno
-
-from Mailman import Errors
-
-
-
-def process(mlist, msg, msgdata):
- if 'recips' in msgdata:
- return
- filename = os.path.join(mlist.full_path, 'members.txt')
- try:
- with open(filename) as fp:
- addrs = set(line.strip() for line in fp)
- except IOError, e:
- if e.errno <> errno.ENOENT:
- raise
- msgdata['recips'] = set()
- return
- # If the sender is a member of the list, remove them from the file recips.
- sender = msg.get_sender()
- member = mlist.members.get_member(sender)
- if member is not None:
- addrs.discard(member.address.address)
- msgdata['recips'] = addrs
diff --git a/Mailman/Handlers/MimeDel.py b/Mailman/Handlers/MimeDel.py
deleted file mode 100644
index 93349cb23..000000000
--- a/Mailman/Handlers/MimeDel.py
+++ /dev/null
@@ -1,261 +0,0 @@
-# Copyright (C) 2002-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""MIME-stripping filter for Mailman.
-
-This module scans a message for MIME content, removing those sections whose
-MIME types match one of a list of matches. multipart/alternative sections are
-replaced by the first non-empty component, and multipart/mixed sections
-wrapping only single sections after other processing are replaced by their
-contents.
-"""
-
-import os
-import errno
-import logging
-import tempfile
-
-from email.Iterators import typed_subpart_iterator
-from os.path import splitext
-
-from Mailman import Errors
-from Mailman.Message import UserNotification
-from Mailman.Utils import oneline
-from Mailman.Version import VERSION
-from Mailman.configuration import config
-from Mailman.i18n import _
-from Mailman.queue import Switchboard
-
-log = logging.getLogger('mailman.error')
-
-
-
-def process(mlist, msg, msgdata):
- # Short-circuits
- if not mlist.filter_content:
- return
- if msgdata.get('isdigest'):
- return
- # We also don't care about our own digests or plaintext
- ctype = msg.get_content_type()
- mtype = msg.get_content_maintype()
- # Check to see if the outer type matches one of the filter types
- filtertypes = mlist.filter_mime_types
- passtypes = mlist.pass_mime_types
- if ctype in filtertypes or mtype in filtertypes:
- dispose(mlist, msg, msgdata,
- _("The message's content type was explicitly disallowed"))
- # Check to see if there is a pass types and the outer type doesn't match
- # one of these types
- if passtypes and not (ctype in passtypes or mtype in passtypes):
- dispose(mlist, msg, msgdata,
- _("The message's content type was not explicitly allowed"))
- # Filter by file extensions
- filterexts = mlist.filter_filename_extensions
- passexts = mlist.pass_filename_extensions
- fext = get_file_ext(msg)
- if fext:
- if fext in filterexts:
- dispose(mlist, msg, msgdata,
- _("The message's file extension was explicitly disallowed"))
- if passexts and not (fext in passexts):
- dispose(mlist, msg, msgdata,
- _("The message's file extension was not explicitly allowed"))
- numparts = len([subpart for subpart in msg.walk()])
- # If the message is a multipart, filter out matching subparts
- if msg.is_multipart():
- # Recursively filter out any subparts that match the filter list
- prelen = len(msg.get_payload())
- filter_parts(msg, filtertypes, passtypes, filterexts, passexts)
- # If the outer message is now an empty multipart (and it wasn't
- # before!) then, again it gets discarded.
- postlen = len(msg.get_payload())
- if postlen == 0 and prelen > 0:
- dispose(mlist, msg, msgdata,
- _("After content filtering, the message was empty"))
- # Now replace all multipart/alternatives with just the first non-empty
- # alternative. BAW: We have to special case when the outer part is a
- # multipart/alternative because we need to retain most of the outer part's
- # headers. For now we'll move the subpart's payload into the outer part,
- # and then copy over its Content-Type: and Content-Transfer-Encoding:
- # headers (any others?).
- if mlist.collapse_alternatives:
- collapse_multipart_alternatives(msg)
- if ctype == 'multipart/alternative':
- firstalt = msg.get_payload(0)
- reset_payload(msg, firstalt)
- # If we removed some parts, make note of this
- changedp = 0
- if numparts <> len([subpart for subpart in msg.walk()]):
- changedp = 1
- # Now perhaps convert all text/html to text/plain
- if mlist.convert_html_to_plaintext and config.HTML_TO_PLAIN_TEXT_COMMAND:
- changedp += to_plaintext(msg)
- # If we're left with only two parts, an empty body and one attachment,
- # recast the message to one of just that part
- if msg.is_multipart() and len(msg.get_payload()) == 2:
- if msg.get_payload(0).get_payload() == '':
- useful = msg.get_payload(1)
- reset_payload(msg, useful)
- changedp = 1
- if changedp:
- msg['X-Content-Filtered-By'] = 'Mailman/MimeDel %s' % VERSION
-
-
-
-def reset_payload(msg, subpart):
- # Reset payload of msg to contents of subpart, and fix up content headers
- payload = subpart.get_payload()
- msg.set_payload(payload)
- del msg['content-type']
- del msg['content-transfer-encoding']
- del msg['content-disposition']
- del msg['content-description']
- msg['Content-Type'] = subpart.get('content-type', 'text/plain')
- cte = subpart.get('content-transfer-encoding')
- if cte:
- msg['Content-Transfer-Encoding'] = cte
- cdisp = subpart.get('content-disposition')
- if cdisp:
- msg['Content-Disposition'] = cdisp
- cdesc = subpart.get('content-description')
- if cdesc:
- msg['Content-Description'] = cdesc
-
-
-
-def filter_parts(msg, filtertypes, passtypes, filterexts, passexts):
- # Look at all the message's subparts, and recursively filter
- if not msg.is_multipart():
- return 1
- payload = msg.get_payload()
- prelen = len(payload)
- newpayload = []
- for subpart in payload:
- keep = filter_parts(subpart, filtertypes, passtypes,
- filterexts, passexts)
- if not keep:
- continue
- ctype = subpart.get_content_type()
- mtype = subpart.get_content_maintype()
- if ctype in filtertypes or mtype in filtertypes:
- # Throw this subpart away
- continue
- if passtypes and not (ctype in passtypes or mtype in passtypes):
- # Throw this subpart away
- continue
- # check file extension
- fext = get_file_ext(subpart)
- if fext:
- if fext in filterexts:
- continue
- if passexts and not (fext in passexts):
- continue
- newpayload.append(subpart)
- # Check to see if we discarded all the subparts
- postlen = len(newpayload)
- msg.set_payload(newpayload)
- if postlen == 0 and prelen > 0:
- # We threw away everything
- return 0
- return 1
-
-
-
-def collapse_multipart_alternatives(msg):
- if not msg.is_multipart():
- return
- newpayload = []
- for subpart in msg.get_payload():
- if subpart.get_content_type() == 'multipart/alternative':
- try:
- firstalt = subpart.get_payload(0)
- newpayload.append(firstalt)
- except IndexError:
- pass
- else:
- newpayload.append(subpart)
- msg.set_payload(newpayload)
-
-
-
-def to_plaintext(msg):
- changedp = 0
- for subpart in typed_subpart_iterator(msg, 'text', 'html'):
- filename = tempfile.mktemp('.html')
- fp = open(filename, 'w')
- try:
- fp.write(subpart.get_payload(decode=1))
- fp.close()
- cmd = os.popen(config.HTML_TO_PLAIN_TEXT_COMMAND %
- {'filename': filename})
- plaintext = cmd.read()
- rtn = cmd.close()
- if rtn:
- log.error('HTML->text/plain error: %s', rtn)
- finally:
- try:
- os.unlink(filename)
- except OSError, e:
- if e.errno <> errno.ENOENT: raise
- # Now replace the payload of the subpart and twiddle the Content-Type:
- del subpart['content-transfer-encoding']
- subpart.set_payload(plaintext)
- subpart.set_type('text/plain')
- changedp = 1
- return changedp
-
-
-
-def dispose(mlist, msg, msgdata, why):
- # filter_action == 0 just discards, see below
- if mlist.filter_action == 1:
- # Bounce the message to the original author
- raise Errors.RejectMessage, why
- if mlist.filter_action == 2:
- # Forward it on to the list owner
- listname = mlist.internal_name()
- mlist.ForwardMessage(
- msg,
- text=_("""\
-The attached message matched the %(listname)s mailing list's content filtering
-rules and was prevented from being forwarded on to the list membership. You
-are receiving the only remaining copy of the discarded message.
-
-"""),
- subject=_('Content filtered message notification'))
- if mlist.filter_action == 3 and \
- config.OWNERS_CAN_PRESERVE_FILTERED_MESSAGES:
- badq = Switchboard(config.BADQUEUE_DIR)
- badq.enqueue(msg, msgdata)
- # Most cases also discard the message
- raise Errors.DiscardMessage
-
-def get_file_ext(m):
- """
- Get filename extension. Caution: some virus don't put filename
- in 'Content-Disposition' header.
-"""
- fext = ''
- filename = m.get_filename('') or m.get_param('name', '')
- if filename:
- fext = splitext(oneline(filename,'utf-8'))[1]
- if len(fext) > 1:
- fext = fext[1:]
- else:
- fext = ''
- return fext
diff --git a/Mailman/Handlers/Moderate.py b/Mailman/Handlers/Moderate.py
deleted file mode 100644
index 464215d25..000000000
--- a/Mailman/Handlers/Moderate.py
+++ /dev/null
@@ -1,168 +0,0 @@
-# Copyright (C) 2001-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Posting moderation filter."""
-
-import re
-
-from email.MIMEMessage import MIMEMessage
-from email.MIMEText import MIMEText
-
-from Mailman import Errors
-from Mailman import Message
-from Mailman import Utils
-from Mailman.Handlers import Hold
-from Mailman.configuration import config
-from Mailman.i18n import _
-
-
-
-class ModeratedMemberPost(Hold.ModeratedPost):
- # BAW: I wanted to use the reason below to differentiate between this
- # situation and normal ModeratedPost reasons. Greg Ward and Stonewall
- # Ballard thought the language was too harsh and mentioned offense taken
- # by some list members. I'd still like this class's reason to be
- # different than the base class's reason, but we'll use this until someone
- # can come up with something more clever but inoffensive.
- #
- # reason = _('Posts by member are currently quarantined for moderation')
- pass
-
-
-
-def process(mlist, msg, msgdata):
- if msgdata.get('approved') or msgdata.get('fromusenet'):
- return
- # First of all, is the poster a member or not?
- for sender in msg.get_senders():
- if mlist.isMember(sender):
- break
- else:
- sender = None
- if sender:
- # If the member's moderation flag is on, then perform the moderation
- # action.
- if mlist.getMemberOption(sender, config.Moderate):
- # Note that for member_moderation_action, 0==Hold, 1=Reject,
- # 2==Discard
- if mlist.member_moderation_action == 0:
- # Hold. BAW: WIBNI we could add the member_moderation_notice
- # to the notice sent back to the sender?
- msgdata['sender'] = sender
- Hold.hold_for_approval(mlist, msg, msgdata,
- ModeratedMemberPost)
- elif mlist.member_moderation_action == 1:
- # Reject
- text = mlist.member_moderation_notice
- if text:
- text = Utils.wrap(text)
- else:
- # Use the default RejectMessage notice string
- text = None
- raise Errors.RejectMessage, text
- elif mlist.member_moderation_action == 2:
- # Discard. BAW: Again, it would be nice if we could send a
- # discard notice to the sender
- raise Errors.DiscardMessage
- else:
- assert 0, 'bad member_moderation_action'
- # Should we do anything explict to mark this message as getting past
- # this point? No, because further pipeline handlers will need to do
- # their own thing.
- return
- else:
- sender = msg.get_sender()
- # From here on out, we're dealing with non-members.
- if matches_p(sender, mlist.accept_these_nonmembers):
- return
- if matches_p(sender, mlist.hold_these_nonmembers):
- Hold.hold_for_approval(mlist, msg, msgdata, Hold.NonMemberPost)
- # No return
- if matches_p(sender, mlist.reject_these_nonmembers):
- do_reject(mlist)
- # No return
- if matches_p(sender, mlist.discard_these_nonmembers):
- do_discard(mlist, msg)
- # No return
- # Okay, so the sender wasn't specified explicitly by any of the non-member
- # moderation configuration variables. Handle by way of generic non-member
- # action.
- assert 0 <= mlist.generic_nonmember_action <= 4
- if mlist.generic_nonmember_action == 0:
- # Accept
- return
- elif mlist.generic_nonmember_action == 1:
- Hold.hold_for_approval(mlist, msg, msgdata, Hold.NonMemberPost)
- elif mlist.generic_nonmember_action == 2:
- do_reject(mlist)
- elif mlist.generic_nonmember_action == 3:
- do_discard(mlist, msg)
-
-
-
-def matches_p(sender, nonmembers):
- # First strip out all the regular expressions
- plainaddrs = [addr for addr in nonmembers if not addr.startswith('^')]
- addrdict = Utils.List2Dict(plainaddrs, foldcase=1)
- if addrdict.has_key(sender):
- return 1
- # Now do the regular expression matches
- for are in nonmembers:
- if are.startswith('^'):
- try:
- cre = re.compile(are, re.IGNORECASE)
- except re.error:
- continue
- if cre.search(sender):
- return 1
- return 0
-
-
-
-def do_reject(mlist):
- listowner = mlist.GetOwnerEmail()
- if mlist.nonmember_rejection_notice:
- raise Errors.RejectMessage, \
- Utils.wrap(_(mlist.nonmember_rejection_notice))
- else:
- raise Errors.RejectMessage, Utils.wrap(_("""\
-You are not allowed to post to this mailing list, and your message has been
-automatically rejected. If you think that your messages are being rejected in
-error, contact the mailing list owner at %(listowner)s."""))
-
-
-
-def do_discard(mlist, msg):
- sender = msg.get_sender()
- # Do we forward auto-discards to the list owners?
- if mlist.forward_auto_discards:
- lang = mlist.preferred_language
- varhelp = '%s/?VARHELP=privacy/sender/discard_these_nonmembers' % \
- mlist.GetScriptURL('admin', absolute=1)
- nmsg = Message.UserNotification(mlist.GetOwnerEmail(),
- mlist.GetBouncesEmail(),
- _('Auto-discard notification'),
- lang=lang)
- nmsg.set_type('multipart/mixed')
- text = MIMEText(Utils.wrap(_(
- 'The attached message has been automatically discarded.')),
- _charset=Utils.GetCharSet(lang))
- nmsg.attach(text)
- nmsg.attach(MIMEMessage(msg))
- nmsg.send(mlist)
- # Discard this sucker
- raise Errors.DiscardMessage
diff --git a/Mailman/Handlers/OwnerRecips.py b/Mailman/Handlers/OwnerRecips.py
deleted file mode 100644
index c70e17777..000000000
--- a/Mailman/Handlers/OwnerRecips.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Copyright (C) 2001-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Calculate the list owner recipients (includes moderators)."""
-
-
-
-def process(mlist, msg, msgdata):
- # The recipients are the owner and the moderator
- msgdata['recips'] = mlist.owner + mlist.moderator
- # Don't decorate these messages with the header/footers
- msgdata['nodecorate'] = 1
- msgdata['personalize'] = 0
diff --git a/Mailman/Handlers/Replybot.py b/Mailman/Handlers/Replybot.py
deleted file mode 100644
index 7017d9dd5..000000000
--- a/Mailman/Handlers/Replybot.py
+++ /dev/null
@@ -1,111 +0,0 @@
-# Copyright (C) 1998-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Handler for auto-responses."""
-
-import time
-import logging
-import datetime
-
-from string import Template
-
-from Mailman import Message
-from Mailman import Utils
-from Mailman.i18n import _
-
-log = logging.getLogger('mailman.error')
-
-__i18n_templates__ = True
-NODELTA = datetime.timedelta()
-
-
-
-def process(mlist, msg, msgdata):
- # Normally, the replybot should get a shot at this message, but there are
- # some important short-circuits, mostly to suppress 'bot storms, at least
- # for well behaved email bots (there are other governors for misbehaving
- # 'bots). First, if the original message has an "X-Ack: No" header, we
- # skip the replybot. Then, if the message has a Precedence header with
- # values bulk, junk, or list, and there's no explicit "X-Ack: yes" header,
- # we short-circuit. Finally, if the message metadata has a true 'noack'
- # key, then we skip the replybot too.
- ack = msg.get('x-ack', '').lower()
- if ack == 'no' or msgdata.get('noack'):
- return
- precedence = msg.get('precedence', '').lower()
- if ack <> 'yes' and precedence in ('bulk', 'junk', 'list'):
- return
- # Check to see if the list is even configured to autorespond to this email
- # message. Note: the mailowner script sets the `toadmin' or `toowner' key
- # (which for replybot purposes are equivalent), and the mailcmd script
- # sets the `torequest' key.
- toadmin = msgdata.get('toowner')
- torequest = msgdata.get('torequest')
- if ((toadmin and not mlist.autorespond_admin) or
- (torequest and not mlist.autorespond_requests) or \
- (not toadmin and not torequest and not mlist.autorespond_postings)):
- return
- # Now see if we're in the grace period for this sender. graceperiod <= 0
- # means always autorespond, as does an "X-Ack: yes" header (useful for
- # debugging).
- sender = msg.get_sender()
- now = time.time()
- graceperiod = mlist.autoresponse_graceperiod
- if graceperiod > NODELTA and ack <> 'yes':
- if toadmin:
- quiet_until = mlist.admin_responses.get(sender, 0)
- elif torequest:
- quiet_until = mlist.request_responses.get(sender, 0)
- else:
- quiet_until = mlist.postings_responses.get(sender, 0)
- if quiet_until > now:
- return
- # Okay, we know we're going to auto-respond to this sender, craft the
- # message, send it, and update the database.
- realname = mlist.real_name
- subject = _(
- 'Auto-response for your message to the "$realname" mailing list')
- # Do string interpolation into the autoresponse text
- d = dict(listname = realname,
- listurl = mlist.script_url('listinfo'),
- requestemail = mlist.request_address,
- owneremail = mlist.owner_address,
- )
- if toadmin:
- rtext = mlist.autoresponse_admin_text
- elif torequest:
- rtext = mlist.autoresponse_request_text
- else:
- rtext = mlist.autoresponse_postings_text
- # Interpolation and Wrap the response text.
- text = Utils.wrap(Template(rtext).safe_substitute(d))
- outmsg = Message.UserNotification(sender, mlist.bounces_address,
- subject, text, mlist.preferred_language)
- outmsg['X-Mailer'] = _('The Mailman Replybot')
- # prevent recursions and mail loops!
- outmsg['X-Ack'] = 'No'
- outmsg.send(mlist)
- # update the grace period database
- if graceperiod > NODELTA:
- # graceperiod is in days, we need # of seconds
- quiet_until = now + graceperiod * 24 * 60 * 60
- if toadmin:
- mlist.admin_responses[sender] = quiet_until
- elif torequest:
- mlist.request_responses[sender] = quiet_until
- else:
- mlist.postings_responses[sender] = quiet_until
diff --git a/Mailman/Handlers/SMTPDirect.py b/Mailman/Handlers/SMTPDirect.py
deleted file mode 100644
index 0037d676a..000000000
--- a/Mailman/Handlers/SMTPDirect.py
+++ /dev/null
@@ -1,389 +0,0 @@
-# Copyright (C) 1998-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Local SMTP direct drop-off.
-
-This module delivers messages via SMTP to a locally specified daemon. This
-should be compatible with any modern SMTP server. It is expected that the MTA
-handles all final delivery. We have to play tricks so that the list object
-isn't locked while delivery occurs synchronously.
-
-Note: This file only handles single threaded delivery. See SMTPThreaded.py
-for a threaded implementation.
-"""
-
-import copy
-import time
-import email
-import socket
-import logging
-import smtplib
-
-from email.Charset import Charset
-from email.Header import Header
-from email.Utils import formataddr
-
-from Mailman import Errors
-from Mailman import Utils
-from Mailman.Handlers import Decorate
-from Mailman.SafeDict import MsgSafeDict
-from Mailman.configuration import config
-from Mailman.interfaces import Personalization
-
-DOT = '.'
-
-log = logging.getLogger('mailman.smtp')
-flog = logging.getLogger('mailman.smtp-failure')
-every_log = logging.getLogger('mailman.' + config.SMTP_LOG_EVERY_MESSAGE[0])
-success_log = logging.getLogger('mailman.' + config.SMTP_LOG_SUCCESS[0])
-refused_log = logging.getLogger('mailman.' + config.SMTP_LOG_REFUSED[0])
-failure_log = logging.getLogger('mailman.' + config.SMTP_LOG_EACH_FAILURE[0])
-
-
-
-# Manage a connection to the SMTP server
-class Connection:
- def __init__(self):
- self.__conn = None
-
- def __connect(self):
- self.__conn = smtplib.SMTP()
- self.__conn.connect(config.SMTPHOST, config.SMTPPORT)
- self.__numsessions = config.SMTP_MAX_SESSIONS_PER_CONNECTION
-
- def sendmail(self, envsender, recips, msgtext):
- if self.__conn is None:
- self.__connect()
- try:
- results = self.__conn.sendmail(envsender, recips, msgtext)
- except smtplib.SMTPException:
- # For safety, close this connection. The next send attempt will
- # automatically re-open it. Pass the exception on up.
- self.quit()
- raise
- # This session has been successfully completed.
- self.__numsessions -= 1
- # By testing exactly for equality to 0, we automatically handle the
- # case for SMTP_MAX_SESSIONS_PER_CONNECTION <= 0 meaning never close
- # the connection. We won't worry about wraparound <wink>.
- if self.__numsessions == 0:
- self.quit()
- return results
-
- def quit(self):
- if self.__conn is None:
- return
- try:
- self.__conn.quit()
- except smtplib.SMTPException:
- pass
- self.__conn = None
-
-
-
-def process(mlist, msg, msgdata):
- recips = msgdata.get('recips')
- if not recips:
- # Nobody to deliver to!
- return
- # Calculate the non-VERP envelope sender.
- envsender = msgdata.get('envsender')
- if envsender is None:
- if mlist:
- envsender = mlist.GetBouncesEmail()
- else:
- envsender = Utils.get_site_noreply()
- # Time to split up the recipient list. If we're personalizing or VERPing
- # then each chunk will have exactly one recipient. We'll then hand craft
- # an envelope sender and stitch a message together in memory for each one
- # separately. If we're not VERPing, then we'll chunkify based on
- # SMTP_MAX_RCPTS. Note that most MTAs have a limit on the number of
- # recipients they'll swallow in a single transaction.
- deliveryfunc = None
- if (not msgdata.has_key('personalize') or msgdata['personalize']) and (
- msgdata.get('verp') or mlist.personalize <> Personalization.none):
- chunks = [[recip] for recip in recips]
- msgdata['personalize'] = 1
- deliveryfunc = verpdeliver
- elif config.SMTP_MAX_RCPTS <= 0:
- chunks = [recips]
- else:
- chunks = chunkify(recips, config.SMTP_MAX_RCPTS)
- # See if this is an unshunted message for which some were undelivered
- if msgdata.has_key('undelivered'):
- chunks = msgdata['undelivered']
- # If we're doing bulk delivery, then we can stitch up the message now.
- if deliveryfunc is None:
- # Be sure never to decorate the message more than once!
- if not msgdata.get('decorated'):
- Decorate.process(mlist, msg, msgdata)
- msgdata['decorated'] = True
- deliveryfunc = bulkdeliver
- refused = {}
- t0 = time.time()
- # Open the initial connection
- origrecips = msgdata['recips']
- # MAS: get the message sender now for logging. If we're using 'sender'
- # and not 'from', bulkdeliver changes it for bounce processing. If we're
- # VERPing, it doesn't matter because bulkdeliver is working on a copy, but
- # otherwise msg gets changed. If the list is anonymous, the original
- # sender is long gone, but Cleanse.py has logged it.
- origsender = msgdata.get('original_sender', msg.get_sender())
- # `undelivered' is a copy of chunks that we pop from to do deliveries.
- # This seems like a good tradeoff between robustness and resource
- # utilization. If delivery really fails (i.e. qfiles/shunt type
- # failures), then we'll pick up where we left off with `undelivered'.
- # This means at worst, the last chunk for which delivery was attempted
- # could get duplicates but not every one, and no recips should miss the
- # message.
- conn = Connection()
- try:
- msgdata['undelivered'] = chunks
- while chunks:
- chunk = chunks.pop()
- msgdata['recips'] = chunk
- try:
- deliveryfunc(mlist, msg, msgdata, envsender, refused, conn)
- except Exception:
- # If /anything/ goes wrong, push the last chunk back on the
- # undelivered list and re-raise the exception. We don't know
- # how many of the last chunk might receive the message, so at
- # worst, everyone in this chunk will get a duplicate. Sigh.
- chunks.append(chunk)
- raise
- del msgdata['undelivered']
- finally:
- conn.quit()
- msgdata['recips'] = origrecips
- # Log the successful post
- t1 = time.time()
- d = MsgSafeDict(msg, {'time' : t1-t0,
- # BAW: Urg. This seems inefficient.
- 'size' : len(msg.as_string()),
- '#recips' : len(recips),
- '#refused': len(refused),
- 'listname': mlist.internal_name(),
- 'sender' : origsender,
- })
- # We have to use the copy() method because extended call syntax requires a
- # concrete dictionary object; it does not allow a generic mapping (XXX is
- # this still true in Python 2.3?).
- if config.SMTP_LOG_EVERY_MESSAGE:
- every_log.info('%s', config.SMTP_LOG_EVERY_MESSAGE[1] % d)
-
- if refused:
- if config.SMTP_LOG_REFUSED:
- refused_log.info('%s', config.SMTP_LOG_REFUSED[1] % d)
-
- elif msgdata.get('tolist'):
- # Log the successful post, but only if it really was a post to the
- # mailing list. Don't log sends to the -owner, or -admin addrs.
- # -request addrs should never get here. BAW: it may be useful to log
- # the other messages, but in that case, we should probably have a
- # separate configuration variable to control that.
- if config.SMTP_LOG_SUCCESS:
- success_log.info('%s', config.SMTP_LOG_SUCCESS[1] % d)
-
- # Process any failed deliveries.
- tempfailures = []
- permfailures = []
- for recip, (code, smtpmsg) in refused.items():
- # DRUMS is an internet draft, but it says:
- #
- # [RFC-821] incorrectly listed the error where an SMTP server
- # exhausts its implementation limit on the number of RCPT commands
- # ("too many recipients") as having reply code 552. The correct
- # reply code for this condition is 452. Clients SHOULD treat a 552
- # code in this case as a temporary, rather than permanent failure
- # so the logic below works.
- #
- if code >= 500 and code <> 552:
- # A permanent failure
- permfailures.append(recip)
- else:
- # Deal with persistent transient failures by queuing them up for
- # future delivery. TBD: this could generate lots of log entries!
- tempfailures.append(recip)
- if config.SMTP_LOG_EACH_FAILURE:
- d.update({'recipient': recip,
- 'failcode' : code,
- 'failmsg' : smtpmsg})
- failure_log.info('%s', config.SMTP_LOG_EACH_FAILURE[1] % d)
- # Return the results
- if tempfailures or permfailures:
- raise Errors.SomeRecipientsFailed(tempfailures, permfailures)
-
-
-
-def chunkify(recips, chunksize):
- # First do a simple sort on top level domain. It probably doesn't buy us
- # much to try to sort on MX record -- that's the MTA's job. We're just
- # trying to avoid getting a max recips error. Split the chunks along
- # these lines (as suggested originally by Chuq Von Rospach and slightly
- # elaborated by BAW).
- chunkmap = {'com': 1,
- 'net': 2,
- 'org': 2,
- 'edu': 3,
- 'us' : 3,
- 'ca' : 3,
- }
- buckets = {}
- for r in recips:
- tld = None
- i = r.rfind('.')
- if i >= 0:
- tld = r[i+1:]
- bin = chunkmap.get(tld, 0)
- bucket = buckets.get(bin, [])
- bucket.append(r)
- buckets[bin] = bucket
- # Now start filling the chunks
- chunks = []
- currentchunk = []
- chunklen = 0
- for bin in buckets.values():
- for r in bin:
- currentchunk.append(r)
- chunklen = chunklen + 1
- if chunklen >= chunksize:
- chunks.append(currentchunk)
- currentchunk = []
- chunklen = 0
- if currentchunk:
- chunks.append(currentchunk)
- currentchunk = []
- chunklen = 0
- return chunks
-
-
-
-def verpdeliver(mlist, msg, msgdata, envsender, failures, conn):
- for recip in msgdata['recips']:
- # We now need to stitch together the message with its header and
- # footer. If we're VERPIng, we have to calculate the envelope sender
- # for each recipient. Note that the list of recipients must be of
- # length 1.
- #
- # BAW: ezmlm includes the message number in the envelope, used when
- # sending a notification to the user telling her how many messages
- # they missed due to bouncing. Neat idea.
- msgdata['recips'] = [recip]
- # Make a copy of the message and decorate + delivery that
- msgcopy = copy.deepcopy(msg)
- Decorate.process(mlist, msgcopy, msgdata)
- # Calculate the envelope sender, which we may be VERPing
- if msgdata.get('verp'):
- bmailbox, bdomain = Utils.ParseEmail(envsender)
- rmailbox, rdomain = Utils.ParseEmail(recip)
- if rdomain is None:
- # The recipient address is not fully-qualified. We can't
- # deliver it to this person, nor can we craft a valid verp
- # header. I don't think there's much we can do except ignore
- # this recipient.
- log.info('Skipping VERP delivery to unqual recip: %s', recip)
- continue
- d = {'bounces': bmailbox,
- 'mailbox': rmailbox,
- 'host' : DOT.join(rdomain),
- }
- envsender = '%s@%s' % ((config.VERP_FORMAT % d), DOT.join(bdomain))
- if mlist.personalize == Personalization.full:
- # When fully personalizing, we want the To address to point to the
- # recipient, not to the mailing list
- del msgcopy['to']
- name = None
- if mlist.isMember(recip):
- name = mlist.getMemberName(recip)
- if name:
- # Convert the name to an email-safe representation. If the
- # name is a byte string, convert it first to Unicode, given
- # the character set of the member's language, replacing bad
- # characters for which we can do nothing about. Once we have
- # the name as Unicode, we can create a Header instance for it
- # so that it's properly encoded for email transport.
- charset = Utils.GetCharSet(mlist.getMemberLanguage(recip))
- if charset == 'us-ascii':
- # Since Header already tries both us-ascii and utf-8,
- # let's add something a bit more useful.
- charset = 'iso-8859-1'
- charset = Charset(charset)
- codec = charset.input_codec or 'ascii'
- if not isinstance(name, unicode):
- name = unicode(name, codec, 'replace')
- name = Header(name, charset).encode()
- msgcopy['To'] = formataddr((name, recip))
- else:
- msgcopy['To'] = recip
- # We can flag the mail as a duplicate for each member, if they've
- # already received this message, as calculated by Message-ID. See
- # AvoidDuplicates.py for details.
- del msgcopy['x-mailman-copy']
- if msgdata.get('add-dup-header', {}).has_key(recip):
- msgcopy['X-Mailman-Copy'] = 'yes'
- # For the final delivery stage, we can just bulk deliver to a party of
- # one. ;)
- bulkdeliver(mlist, msgcopy, msgdata, envsender, failures, conn)
-
-
-
-def bulkdeliver(mlist, msg, msgdata, envsender, failures, conn):
- # Do some final cleanup of the message header. Start by blowing away
- # any the Sender: and Errors-To: headers so remote MTAs won't be
- # tempted to delivery bounces there instead of our envelope sender
- #
- # BAW An interpretation of RFCs 2822 and 2076 could argue for not touching
- # the Sender header at all. Brad Knowles points out that MTAs tend to
- # wipe existing Return-Path headers, and old MTAs may still honor
- # Errors-To while new ones will at worst ignore the header.
- del msg['sender']
- del msg['errors-to']
- msg['Sender'] = envsender
- msg['Errors-To'] = envsender
- # Get the plain, flattened text of the message, sans unixfrom
- msgtext = msg.as_string()
- refused = {}
- recips = msgdata['recips']
- msgid = msg['message-id']
- try:
- # Send the message
- refused = conn.sendmail(envsender, recips, msgtext)
- except smtplib.SMTPRecipientsRefused, e:
- flog.error('All recipients refused: %s, msgid: %s', e, msgid)
- refused = e.recipients
- except smtplib.SMTPResponseException, e:
- flog.error('SMTP session failure: %s, %s, msgid: %s',
- e.smtp_code, e.smtp_error, msgid)
- # If this was a permanent failure, don't add the recipients to the
- # refused, because we don't want them to be added to failures.
- # Otherwise, if the MTA rejects the message because of the message
- # content (e.g. it's spam, virii, or has syntactic problems), then
- # this will end up registering a bounce score for every recipient.
- # Definitely /not/ what we want.
- if e.smtp_code < 500 or e.smtp_code == 552:
- # It's a temporary failure
- for r in recips:
- refused[r] = (e.smtp_code, e.smtp_error)
- except (socket.error, IOError, smtplib.SMTPException), e:
- # MTA not responding, or other socket problems, or any other kind of
- # SMTPException. In that case, nothing got delivered, so treat this
- # as a temporary failure.
- flog.error('Low level smtp error: %s, msgid: %s', e, msgid)
- error = str(e)
- for r in recips:
- refused[r] = (-1, error)
- failures.update(refused)
diff --git a/Mailman/Handlers/Scrubber.py b/Mailman/Handlers/Scrubber.py
deleted file mode 100644
index fb1b6e602..000000000
--- a/Mailman/Handlers/Scrubber.py
+++ /dev/null
@@ -1,500 +0,0 @@
-# Copyright (C) 2001-2008 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Cleanse a message for archiving."""
-
-from __future__ import with_statement
-
-import os
-import re
-import sha
-import time
-import errno
-import logging
-import binascii
-
-from email.charset import Charset
-from email.generator import Generator
-from email.utils import make_msgid, parsedate
-from locknix.lockfile import Lock
-from mimetypes import guess_all_extensions
-
-from Mailman import Utils
-from Mailman.Errors import DiscardMessage
-from Mailman.app.archiving import get_archiver
-from Mailman.configuration import config
-from Mailman.i18n import _
-
-# Path characters for common platforms
-pre = re.compile(r'[/\\:]')
-# All other characters to strip out of Content-Disposition: filenames
-# (essentially anything that isn't an alphanum, dot, dash, or underscore).
-sre = re.compile(r'[^-\w.]')
-# Regexp to strip out leading dots
-dre = re.compile(r'^\.*')
-
-BR = '<br>\n'
-SPACE = ' '
-
-log = logging.getLogger('mailman.error')
-
-
-
-def guess_extension(ctype, ext):
- # mimetypes maps multiple extensions to the same type, e.g. .doc, .dot,
- # and .wiz are all mapped to application/msword. This sucks for finding
- # the best reverse mapping. If the extension is one of the giving
- # mappings, we'll trust that, otherwise we'll just guess. :/
- all = guess_all_extensions(ctype, strict=False)
- if ext in all:
- return ext
- return all and all[0]
-
-
-
-# We're using a subclass of the standard Generator because we want to suppress
-# headers in the subparts of multiparts. We use a hack -- the ctor argument
-# skipheaders to accomplish this. It's set to true for the outer Message
-# object, but false for all internal objects. We recognize that
-# sub-Generators will get created passing only mangle_from_ and maxheaderlen
-# to the ctors.
-#
-# This isn't perfect because we still get stuff like the multipart boundaries,
-# but see below for how we corrupt that to our nefarious goals.
-class ScrubberGenerator(Generator):
- def __init__(self, outfp, mangle_from_=True,
- maxheaderlen=78, skipheaders=True):
- Generator.__init__(self, outfp, mangle_from_=False)
- self.__skipheaders = skipheaders
-
- def _write_headers(self, msg):
- if not self.__skipheaders:
- Generator._write_headers(self, msg)
-
-
-def safe_strftime(fmt, t):
- try:
- return time.strftime(fmt, t)
- except (TypeError, ValueError, OverflowError):
- return None
-
-
-def calculate_attachments_dir(mlist, msg, msgdata):
- # Calculate the directory that attachments for this message will go
- # under. To avoid inode limitations, the scheme will be:
- # archives/private/<listname>/attachments/YYYYMMDD/<msgid-hash>/<files>
- # Start by calculating the date-based and msgid-hash components.
- fmt = '%Y%m%d'
- datestr = msg.get('Date')
- if datestr:
- now = parsedate(datestr)
- else:
- now = time.gmtime(msgdata.get('received_time', time.time()))
- datedir = safe_strftime(fmt, now)
- if not datedir:
- datestr = msgdata.get('X-List-Received-Date')
- if datestr:
- datedir = safe_strftime(fmt, datestr)
- if not datedir:
- # What next? Unixfrom, I guess.
- parts = msg.get_unixfrom().split()
- try:
- month = {'Jan':1, 'Feb':2, 'Mar':3, 'Apr':4, 'May':5, 'Jun':6,
- 'Jul':7, 'Aug':8, 'Sep':9, 'Oct':10, 'Nov':11, 'Dec':12,
- }.get(parts[3], 0)
- day = int(parts[4])
- year = int(parts[6])
- except (IndexError, ValueError):
- # Best we can do I think
- month = day = year = 0
- datedir = '%04d%02d%02d' % (year, month, day)
- assert datedir
- # As for the msgid hash, we'll base this part on the Message-ID: so that
- # all attachments for the same message end up in the same directory (we'll
- # uniquify the filenames in that directory as needed). We use the first 2
- # and last 2 bytes of the SHA1 hash of the message id as the basis of the
- # directory name. Clashes here don't really matter too much, and that
- # still gives us a 32-bit space to work with.
- msgid = msg['message-id']
- if msgid is None:
- msgid = msg['Message-ID'] = make_msgid()
- # We assume that the message id actually /is/ unique!
- digest = sha.new(msgid).hexdigest()
- return os.path.join('attachments', datedir, digest[:4] + digest[-4:])
-
-
-def replace_payload_by_text(msg, text, charset):
- # TK: This is a common function in replacing the attachment and the main
- # message by a text (scrubbing).
- del msg['content-type']
- del msg['content-transfer-encoding']
- if isinstance(text, unicode):
- text = text.encode(charset)
- if not isinstance(charset, str):
- charset = str(charset)
- msg.set_payload(text, charset)
-
-
-
-def process(mlist, msg, msgdata=None):
- sanitize = config.ARCHIVE_HTML_SANITIZER
- outer = True
- if msgdata is None:
- msgdata = {}
- if msgdata:
- # msgdata is available if it is in GLOBAL_PIPELINE
- # ie. not in digest or archiver
- # check if the list owner want to scrub regular delivery
- if not mlist.scrub_nondigest:
- return
- dir = calculate_attachments_dir(mlist, msg, msgdata)
- charset = format = delsp = None
- lcset = Utils.GetCharSet(mlist.preferred_language)
- lcset_out = Charset(lcset).output_charset or lcset
- # Now walk over all subparts of this message and scrub out various types
- for part in msg.walk():
- ctype = part.get_content_type()
- # If the part is text/plain, we leave it alone
- if ctype == 'text/plain':
- # We need to choose a charset for the scrubbed message, so we'll
- # arbitrarily pick the charset of the first text/plain part in the
- # message.
- #
- # Also get the RFC 3676 stuff from this part. This seems to
- # work okay for scrub_nondigest. It will also work as far as
- # scrubbing messages for the archive is concerned, but Pipermail
- # doesn't pay any attention to the RFC 3676 parameters. The plain
- # format digest is going to be a disaster in any case as some of
- # messages will be format="flowed" and some not. ToDigest creates
- # its own Content-Type: header for the plain digest which won't
- # have RFC 3676 parameters. If the message Content-Type: headers
- # are retained for display in the digest, the parameters will be
- # there for information, but not for the MUA. This is the best we
- # can do without having get_payload() process the parameters.
- if charset is None:
- charset = part.get_content_charset(lcset)
- format = part.get_param('format')
- delsp = part.get_param('delsp')
- # TK: if part is attached then check charset and scrub if none
- if part.get('content-disposition') and \
- not part.get_content_charset():
- url = save_attachment(mlist, part, dir)
- filename = part.get_filename(_('not available'))
- filename = Utils.oneline(filename, lcset)
- replace_payload_by_text(part, _("""\
-An embedded and charset-unspecified text was scrubbed...
-Name: %(filename)s
-URL: %(url)s
-"""), lcset)
- elif ctype == 'text/html' and isinstance(sanitize, int):
- if sanitize == 0:
- if outer:
- raise DiscardMessage
- replace_payload_by_text(part,
- _('HTML attachment scrubbed and removed'),
- # Adding charset arg and removing content-type
- # sets content-type to text/plain
- lcset)
- elif sanitize == 2:
- # By leaving it alone, Pipermail will automatically escape it
- pass
- elif sanitize == 3:
- # Pull it out as an attachment but leave it unescaped. This
- # is dangerous, but perhaps useful for heavily moderated
- # lists.
- url = save_attachment(mlist, part, dir, filter_html=False)
- replace_payload_by_text(part, _("""\
-An HTML attachment was scrubbed...
-URL: %(url)s
-"""), lcset)
- else:
- # HTML-escape it and store it as an attachment, but make it
- # look a /little/ bit prettier. :(
- payload = Utils.websafe(part.get_payload(decode=True))
- # For whitespace in the margin, change spaces into
- # non-breaking spaces, and tabs into 8 of those. Then use a
- # mono-space font. Still looks hideous to me, but then I'd
- # just as soon discard them.
- def doreplace(s):
- return s.replace(' ', '&nbsp;').replace('\t', '&nbsp'*8)
- lines = [doreplace(s) for s in payload.split('\n')]
- payload = '<tt>\n' + BR.join(lines) + '\n</tt>\n'
- part.set_payload(payload)
- # We're replacing the payload with the decoded payload so this
- # will just get in the way.
- del part['content-transfer-encoding']
- url = save_attachment(mlist, part, dir, filter_html=False)
- replace_payload_by_text(part, _("""\
-An HTML attachment was scrubbed...
-URL: %(url)s
-"""), lcset)
- elif ctype == 'message/rfc822':
- # This part contains a submessage, so it too needs scrubbing
- submsg = part.get_payload(0)
- url = save_attachment(mlist, part, dir)
- subject = submsg.get('subject', _('no subject'))
- date = submsg.get('date', _('no date'))
- who = submsg.get('from', _('unknown sender'))
- size = len(str(submsg))
- replace_payload_by_text(part, _("""\
-An embedded message was scrubbed...
-From: %(who)s
-Subject: %(subject)s
-Date: %(date)s
-Size: %(size)s
-URL: %(url)s
-"""), lcset)
- # If the message isn't a multipart, then we'll strip it out as an
- # attachment that would have to be separately downloaded. Pipermail
- # will transform the url into a hyperlink.
- elif part._payload and not part.is_multipart():
- payload = part.get_payload(decode=True)
- ctype = part.get_content_type()
- # XXX Under email 2.5, it is possible that payload will be None.
- # This can happen when you have a Content-Type: multipart/* with
- # only one part and that part has two blank lines between the
- # first boundary and the end boundary. In email 3.0 you end up
- # with a string in the payload. I think in this case it's safe to
- # ignore the part.
- if payload is None:
- continue
- size = len(payload)
- url = save_attachment(mlist, part, dir)
- desc = part.get('content-description', _('not available'))
- desc = Utils.oneline(desc, lcset)
- filename = part.get_filename(_('not available'))
- filename = Utils.oneline(filename, lcset)
- replace_payload_by_text(part, _("""\
-A non-text attachment was scrubbed...
-Name: %(filename)s
-Type: %(ctype)s
-Size: %(size)d bytes
-Desc: %(desc)s
-URL: %(url)s
-"""), lcset)
- outer = False
- # We still have to sanitize multipart messages to flat text because
- # Pipermail can't handle messages with list payloads. This is a kludge;
- # def (n) clever hack ;).
- if msg.is_multipart() and sanitize <> 2:
- # By default we take the charset of the first text/plain part in the
- # message, but if there was none, we'll use the list's preferred
- # language's charset.
- if not charset or charset == 'us-ascii':
- charset = lcset_out
- else:
- # normalize to the output charset if input/output are different
- charset = Charset(charset).output_charset or charset
- # We now want to concatenate all the parts which have been scrubbed to
- # text/plain, into a single text/plain payload. We need to make sure
- # all the characters in the concatenated string are in the same
- # encoding, so we'll use the 'replace' key in the coercion call.
- # BAW: Martin's original patch suggested we might want to try
- # generalizing to utf-8, and that's probably a good idea (eventually).
- text = []
- charsets = []
- for part in msg.walk():
- # TK: bug-id 1099138 and multipart
- # MAS test payload - if part may fail if there are no headers.
- if not part._payload or part.is_multipart():
- continue
- # All parts should be scrubbed to text/plain by now.
- partctype = part.get_content_type()
- if partctype <> 'text/plain':
- text.append(_('Skipped content of type %(partctype)s\n'))
- continue
- try:
- t = part.get_payload(decode=True) or ''
- # MAS: TypeError exception can occur if payload is None. This
- # was observed with a message that contained an attached
- # message/delivery-status part. Because of the special parsing
- # of this type, this resulted in a text/plain sub-part with a
- # null body. See bug 1430236.
- except (binascii.Error, TypeError):
- t = part.get_payload() or ''
- # Email problem was solved by Mark Sapiro. (TK)
- partcharset = part.get_content_charset('us-ascii')
- try:
- t = unicode(t, partcharset, 'replace')
- except (UnicodeError, LookupError, ValueError, TypeError,
- AssertionError):
- # We can get here if partcharset is bogus in come way.
- # Replace funny characters. We use errors='replace'.
- t = unicode(t, 'ascii', 'replace')
- # Separation is useful
- if isinstance(t, basestring):
- if not t.endswith('\n'):
- t += '\n'
- text.append(t)
- if partcharset not in charsets:
- charsets.append(partcharset)
- # Now join the text and set the payload
- sep = _('-------------- next part --------------\n')
- assert isinstance(sep, unicode), (
- 'Expected a unicode separator, got %s' % type(sep))
- rept = sep.join(text)
- # Replace entire message with text and scrubbed notice.
- # Try with message charsets and utf-8
- if 'utf-8' not in charsets:
- charsets.append('utf-8')
- for charset in charsets:
- try:
- replace_payload_by_text(msg, rept, charset)
- break
- # Bogus charset can throw several exceptions
- except (UnicodeError, LookupError, ValueError, TypeError,
- AssertionError):
- pass
- if format:
- msg.set_param('format', format)
- if delsp:
- msg.set_param('delsp', delsp)
- return msg
-
-
-
-def makedirs(dir):
- # Create all the directories to store this attachment in and try to make
- # sure that the permissions of the directories are set correctly.
- try:
- os.makedirs(dir, 02775)
- except OSError, e:
- if e.errno == errno.EEXIST:
- return
- # Some systems such as FreeBSD ignore mkdir's mode, so walk the just
- # created directories and try to set the mode, ignoring any OSErrors that
- # occur here.
- for dirpath, dirnames, filenames in os.walk(dir):
- try:
- os.chmod(dirpath, 02775)
- except OSError:
- pass
-
-
-
-def save_attachment(mlist, msg, dir, filter_html=True):
- fsdir = os.path.join(config.PRIVATE_ARCHIVE_FILE_DIR,
- mlist.fqdn_listname, dir)
- makedirs(fsdir)
- # Figure out the attachment type and get the decoded data
- decodedpayload = msg.get_payload(decode=True)
- # BAW: mimetypes ought to handle non-standard, but commonly found types,
- # e.g. image/jpg (should be image/jpeg). For now we just store such
- # things as application/octet-streams since that seems the safest.
- ctype = msg.get_content_type()
- # i18n file name is encoded
- lcset = Utils.GetCharSet(mlist.preferred_language)
- filename = Utils.oneline(msg.get_filename(''), lcset)
- filename, fnext = os.path.splitext(filename)
- # For safety, we should confirm this is valid ext for content-type
- # but we can use fnext if we introduce fnext filtering
- if config.SCRUBBER_USE_ATTACHMENT_FILENAME_EXTENSION:
- # HTML message doesn't have filename :-(
- ext = fnext or guess_extension(ctype, fnext)
- else:
- ext = guess_extension(ctype, fnext)
- if not ext:
- # We don't know what it is, so assume it's just a shapeless
- # application/octet-stream, unless the Content-Type: is
- # message/rfc822, in which case we know we'll coerce the type to
- # text/plain below.
- if ctype == 'message/rfc822':
- ext = '.txt'
- else:
- ext = '.bin'
- # Allow only alphanumerics, dash, underscore, and dot
- ext = sre.sub('', ext)
- path = None
- # We need a lock to calculate the next attachment number
- with Lock(os.path.join(fsdir, 'attachments.lock')):
- # Now base the filename on what's in the attachment, uniquifying it if
- # necessary.
- if not filename or config.SCRUBBER_DONT_USE_ATTACHMENT_FILENAME:
- filebase = 'attachment'
- else:
- # Sanitize the filename given in the message headers
- parts = pre.split(filename)
- filename = parts[-1]
- # Strip off leading dots
- filename = dre.sub('', filename)
- # Allow only alphanumerics, dash, underscore, and dot
- filename = sre.sub('', filename)
- # If the filename's extension doesn't match the type we guessed,
- # which one should we go with? For now, let's go with the one we
- # guessed so attachments can't lie about their type. Also, if the
- # filename /has/ no extension, then tack on the one we guessed.
- # The extension was removed from the name above.
- filebase = filename
- # Now we're looking for a unique name for this file on the file
- # system. If msgdir/filebase.ext isn't unique, we'll add a counter
- # after filebase, e.g. msgdir/filebase-cnt.ext
- counter = 0
- extra = ''
- while True:
- path = os.path.join(fsdir, filebase + extra + ext)
- # Generally it is not a good idea to test for file existance
- # before just trying to create it, but the alternatives aren't
- # wonderful (i.e. os.open(..., O_CREAT | O_EXCL) isn't
- # NFS-safe). Besides, we have an exclusive lock now, so we're
- # guaranteed that no other process will be racing with us.
- if os.path.exists(path):
- counter += 1
- extra = '-%04d' % counter
- else:
- break
- # `path' now contains the unique filename for the attachment. There's
- # just one more step we need to do. If the part is text/html and
- # ARCHIVE_HTML_SANITIZER is a string (which it must be or we wouldn't be
- # here), then send the attachment through the filter program for
- # sanitization
- if filter_html and ctype == 'text/html':
- base, ext = os.path.splitext(path)
- tmppath = base + '-tmp' + ext
- fp = open(tmppath, 'w')
- try:
- fp.write(decodedpayload)
- fp.close()
- cmd = config.ARCHIVE_HTML_SANITIZER % {'filename' : tmppath}
- progfp = os.popen(cmd, 'r')
- decodedpayload = progfp.read()
- status = progfp.close()
- if status:
- log.error('HTML sanitizer exited with non-zero status: %s',
- status)
- finally:
- os.unlink(tmppath)
- # BAW: Since we've now sanitized the document, it should be plain
- # text. Blarg, we really want the sanitizer to tell us what the type
- # if the return data is. :(
- ext = '.txt'
- path = base + '.txt'
- # Is it a message/rfc822 attachment?
- elif ctype == 'message/rfc822':
- submsg = msg.get_payload()
- # BAW: I'm sure we can eventually do better than this. :(
- decodedpayload = Utils.websafe(str(submsg))
- fp = open(path, 'w')
- fp.write(decodedpayload)
- fp.close()
- # Now calculate the url to the list's archive.
- baseurl = get_archiver().get_list_url(mlist)
- if not baseurl.endswith('/'):
- baseurl += '/'
- # Trailing space will definitely be a problem with format=flowed.
- # Bracket the URL instead.
- url = '<' + baseurl + '%s/%s%s%s>' % (dir, filebase, extra, ext)
- return url
diff --git a/Mailman/Handlers/Tagger.py b/Mailman/Handlers/Tagger.py
deleted file mode 100644
index 023148fd7..000000000
--- a/Mailman/Handlers/Tagger.py
+++ /dev/null
@@ -1,157 +0,0 @@
-# Copyright (C) 2001-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Extract topics from the original mail message."""
-
-import re
-import email
-import email.Errors
-import email.Iterators
-import email.Parser
-
-OR = '|'
-CRNL = '\r\n'
-EMPTYSTRING = ''
-NLTAB = '\n\t'
-
-
-
-def process(mlist, msg, msgdata):
- if not mlist.topics_enabled:
- return
- # Extract the Subject:, Keywords:, and possibly body text
- matchlines = []
- matchlines.append(msg.get('subject', None))
- matchlines.append(msg.get('keywords', None))
- if mlist.topics_bodylines_limit == 0:
- # Don't scan any body lines
- pass
- elif mlist.topics_bodylines_limit < 0:
- # Scan all body lines
- matchlines.extend(scanbody(msg))
- else:
- # Scan just some of the body lines
- matchlines.extend(scanbody(msg, mlist.topics_bodylines_limit))
- matchlines = filter(None, matchlines)
- # For each regular expression in the topics list, see if any of the lines
- # of interest from the message match the regexp. If so, the message gets
- # added to the specific topics bucket.
- hits = {}
- for name, pattern, desc, emptyflag in mlist.topics:
- pattern = OR.join(pattern.splitlines())
- cre = re.compile(pattern, re.IGNORECASE)
- for line in matchlines:
- if cre.search(line):
- hits[name] = 1
- break
- if hits:
- msgdata['topichits'] = hits.keys()
- msg['X-Topics'] = NLTAB.join(hits.keys())
-
-
-
-def scanbody(msg, numlines=None):
- # We only scan the body of the message if it is of MIME type text/plain,
- # or if the outer type is multipart/alternative and there is a text/plain
- # part. Anything else, and the body is ignored for header-scan purposes.
- found = None
- if msg.get_content_type() == 'text/plain':
- found = msg
- elif msg.is_multipart()\
- and msg.get_content_type() == 'multipart/alternative':
- for found in msg.get_payload():
- if found.get_content_type() == 'text/plain':
- break
- else:
- found = None
- if not found:
- return []
- # Now that we have a Message object that meets our criteria, let's extract
- # the first numlines of body text.
- lines = []
- lineno = 0
- reader = list(email.Iterators.body_line_iterator(msg))
- while numlines is None or lineno < numlines:
- try:
- line = reader.pop(0)
- except IndexError:
- break
- # Blank lines don't count
- if not line.strip():
- continue
- lineno += 1
- lines.append(line)
- # Concatenate those body text lines with newlines, and then create a new
- # message object from those lines.
- p = _ForgivingParser()
- msg = p.parsestr(EMPTYSTRING.join(lines))
- return msg.get_all('subject', []) + msg.get_all('keywords', [])
-
-
-
-class _ForgivingParser(email.Parser.HeaderParser):
- # Be a little more forgiving about non-header/continuation lines, since
- # we'll just read as much as we can from "header-like" lines in the body.
- #
- # BAW: WIBNI we didn't have to cut-n-paste this whole thing just to
- # specialize the way it returns?
- def _parseheaders(self, container, fp):
- # Parse the headers, returning a list of header/value pairs. None as
- # the header means the Unix-From header.
- lastheader = ''
- lastvalue = []
- lineno = 0
- while 1:
- # Don't strip the line before we test for the end condition,
- # because whitespace-only header lines are RFC compliant
- # continuation lines.
- line = fp.readline()
- if not line:
- break
- line = line.splitlines()[0]
- if not line:
- break
- # Ignore the trailing newline
- lineno += 1
- # Check for initial Unix From_ line
- if line.startswith('From '):
- if lineno == 1:
- container.set_unixfrom(line)
- continue
- else:
- break
- # Header continuation line
- if line[0] in ' \t':
- if not lastheader:
- break
- lastvalue.append(line)
- continue
- # Normal, non-continuation header. BAW: this should check to make
- # sure it's a legal header, e.g. doesn't contain spaces. Also, we
- # should expose the header matching algorithm in the API, and
- # allow for a non-strict parsing mode (that ignores the line
- # instead of raising the exception).
- i = line.find(':')
- if i < 0:
- break
- if lastheader:
- container[lastheader] = NLTAB.join(lastvalue)
- lastheader = line[:i]
- lastvalue = [line[i+1:].lstrip()]
- # Make sure we retain the last header
- if lastheader:
- container[lastheader] = NLTAB.join(lastvalue)
diff --git a/Mailman/Handlers/ToArchive.py b/Mailman/Handlers/ToArchive.py
deleted file mode 100644
index c65e86f60..000000000
--- a/Mailman/Handlers/ToArchive.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# Copyright (C) 1998-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Add the message to the archives."""
-
-from Mailman.configuration import config
-from Mailman.queue import Switchboard
-
-
-
-def process(mlist, msg, msgdata):
- # short circuits
- if msgdata.get('isdigest') or not mlist.archive:
- return
- # Common practice seems to favor "X-No-Archive: yes". No other value for
- # this header seems to make sense, so we'll just test for it's presence.
- # I'm keeping "X-Archive: no" for backwards compatibility.
- if 'x-no-archive' in msg or msg.get('x-archive', '').lower() == 'no':
- return
- # Send the message to the archiver queue
- archq = Switchboard(config.ARCHQUEUE_DIR)
- # Send the message to the queue
- archq.enqueue(msg, msgdata)
diff --git a/Mailman/Handlers/ToDigest.py b/Mailman/Handlers/ToDigest.py
deleted file mode 100644
index 6f06df43b..000000000
--- a/Mailman/Handlers/ToDigest.py
+++ /dev/null
@@ -1,420 +0,0 @@
-# Copyright (C) 1998-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Add the message to the list's current digest and possibly send it."""
-
-# Messages are accumulated to a Unix mailbox compatible file containing all
-# the messages destined for the digest. This file must be parsable by the
-# mailbox.UnixMailbox class (i.e. it must be ^From_ quoted).
-#
-# When the file reaches the size threshold, it is moved to the qfiles/digest
-# directory and the DigestRunner will craft the MIME, rfc1153, and
-# (eventually) URL-subject linked digests from the mbox.
-
-from __future__ import with_statement
-
-import os
-import re
-import copy
-import time
-import logging
-
-from StringIO import StringIO # cStringIO can't handle unicode.
-from email.charset import Charset
-from email.generator import Generator
-from email.header import decode_header, make_header, Header
-from email.mime.base import MIMEBase
-from email.mime.message import MIMEMessage
-from email.mime.text import MIMEText
-from email.parser import Parser
-from email.utils import formatdate, getaddresses, make_msgid
-
-from Mailman import Errors
-from Mailman import Message
-from Mailman import Utils
-from Mailman import i18n
-from Mailman.Handlers.Decorate import decorate
-from Mailman.Handlers.Scrubber import process as scrubber
-from Mailman.Mailbox import Mailbox
-from Mailman.Mailbox import Mailbox
-from Mailman.configuration import config
-from Mailman.interfaces import DeliveryMode, DeliveryStatus
-from Mailman.queue import Switchboard
-
-_ = i18n._
-__i18n_templates__ = True
-
-UEMPTYSTRING = u''
-EMPTYSTRING = ''
-
-log = logging.getLogger('mailman.error')
-
-
-
-def process(mlist, msg, msgdata):
- # Short circuit non-digestable lists.
- if not mlist.digestable or msgdata.get('isdigest'):
- return
- mboxfile = os.path.join(mlist.full_path, 'digest.mbox')
- mboxfp = open(mboxfile, 'a+')
- mbox = Mailbox(mboxfp)
- mbox.AppendMessage(msg)
- # Calculate the current size of the accumulation file. This will not tell
- # us exactly how big the MIME, rfc1153, or any other generated digest
- # message will be, but it's the most easily available metric to decide
- # whether the size threshold has been reached.
- mboxfp.flush()
- size = os.path.getsize(mboxfile)
- if size / 1024.0 >= mlist.digest_size_threshold:
- # This is a bit of a kludge to get the mbox file moved to the digest
- # queue directory.
- try:
- # Enclose in try/except here because a error in send_digest() can
- # silently stop regular delivery. Unsuccessful digest delivery
- # should be tried again by cron and the site administrator will be
- # notified of any error explicitly by the cron error message.
- mboxfp.seek(0)
- send_digests(mlist, mboxfp)
- os.unlink(mboxfile)
- except Exception, errmsg:
- # Bare except is generally prohibited in Mailman, but we can't
- # forecast what exceptions can occur here.
- log.exception('send_digests() failed: %s', errmsg)
- mboxfp.close()
-
-
-
-def send_digests(mlist, mboxfp):
- # Set the digest volume and time
- if mlist.digest_last_sent_at:
- bump = False
- # See if we should bump the digest volume number
- timetup = time.localtime(mlist.digest_last_sent_at)
- now = time.localtime(time.time())
- freq = mlist.digest_volume_frequency
- if freq == 0 and timetup[0] < now[0]:
- # Yearly
- bump = True
- elif freq == 1 and timetup[1] <> now[1]:
- # Monthly, but we take a cheap way to calculate this. We assume
- # that the clock isn't going to be reset backwards.
- bump = True
- elif freq == 2 and (timetup[1] % 4 <> now[1] % 4):
- # Quarterly, same caveat
- bump = True
- elif freq == 3:
- # Once again, take a cheap way of calculating this
- weeknum_last = int(time.strftime('%W', timetup))
- weeknum_now = int(time.strftime('%W', now))
- if weeknum_now > weeknum_last or timetup[0] > now[0]:
- bump = True
- elif freq == 4 and timetup[7] <> now[7]:
- # Daily
- bump = True
- if bump:
- mlist.bump_digest_volume()
- mlist.digest_last_sent_at = time.time()
- # Wrapper around actually digest crafter to set up the language context
- # properly. All digests are translated to the list's preferred language.
- with i18n.using_language(mlist.preferred_language):
- send_i18n_digests(mlist, mboxfp)
-
-
-
-def send_i18n_digests(mlist, mboxfp):
- mbox = Mailbox(mboxfp)
- # Prepare common information (first lang/charset)
- lang = mlist.preferred_language
- lcset = Utils.GetCharSet(lang)
- lcset_out = Charset(lcset).output_charset or lcset
- # Common Information (contd)
- realname = mlist.real_name
- volume = mlist.volume
- issue = mlist.next_digest_number
- digestid = _('$realname Digest, Vol $volume, Issue $issue')
- digestsubj = Header(digestid, lcset, header_name='Subject')
- # Set things up for the MIME digest. Only headers not added by
- # CookHeaders need be added here.
- # Date/Message-ID should be added here also.
- mimemsg = Message.Message()
- mimemsg['Content-Type'] = 'multipart/mixed'
- mimemsg['MIME-Version'] = '1.0'
- mimemsg['From'] = mlist.request_address
- mimemsg['Subject'] = digestsubj
- mimemsg['To'] = mlist.posting_address
- mimemsg['Reply-To'] = mlist.posting_address
- mimemsg['Date'] = formatdate(localtime=1)
- mimemsg['Message-ID'] = make_msgid()
- # Set things up for the rfc1153 digest
- plainmsg = StringIO()
- rfc1153msg = Message.Message()
- rfc1153msg['From'] = mlist.request_address
- rfc1153msg['Subject'] = digestsubj
- rfc1153msg['To'] = mlist.posting_address
- rfc1153msg['Reply-To'] = mlist.posting_address
- rfc1153msg['Date'] = formatdate(localtime=1)
- rfc1153msg['Message-ID'] = make_msgid()
- separator70 = '-' * 70
- separator30 = '-' * 30
- # In the rfc1153 digest, the masthead contains the digest boilerplate plus
- # any digest header. In the MIME digests, the masthead and digest header
- # are separate MIME subobjects. In either case, it's the first thing in
- # the digest, and we can calculate it now, so go ahead and add it now.
- mastheadtxt = Utils.maketext(
- 'masthead.txt',
- {'real_name' : mlist.real_name,
- 'got_list_email': mlist.posting_address,
- 'got_listinfo_url': mlist.script_url('listinfo'),
- 'got_request_email': mlist.request_address,
- 'got_owner_email': mlist.owner_address,
- }, mlist=mlist)
- # MIME
- masthead = MIMEText(mastheadtxt.encode(lcset), _charset=lcset)
- masthead['Content-Description'] = digestid
- mimemsg.attach(masthead)
- # RFC 1153
- print >> plainmsg, mastheadtxt
- print >> plainmsg
- # Now add the optional digest header
- if mlist.digest_header:
- headertxt = decorate(mlist, mlist.digest_header, _('digest header'))
- # MIME
- header = MIMEText(headertxt.encode(lcset), _charset=lcset)
- header['Content-Description'] = _('Digest Header')
- mimemsg.attach(header)
- # RFC 1153
- print >> plainmsg, headertxt
- print >> plainmsg
- # Now we have to cruise through all the messages accumulated in the
- # mailbox file. We can't add these messages to the plainmsg and mimemsg
- # yet, because we first have to calculate the table of contents
- # (i.e. grok out all the Subjects). Store the messages in a list until
- # we're ready for them.
- #
- # Meanwhile prepare things for the table of contents
- toc = StringIO()
- print >> toc, _("Today's Topics:\n")
- # Now cruise through all the messages in the mailbox of digest messages,
- # building the MIME payload and core of the RFC 1153 digest. We'll also
- # accumulate Subject: headers and authors for the table-of-contents.
- messages = []
- msgcount = 0
- msg = mbox.next()
- while msg is not None:
- if msg == '':
- # It was an unparseable message
- msg = mbox.next()
- continue
- msgcount += 1
- messages.append(msg)
- # Get the Subject header
- msgsubj = msg.get('subject', _('(no subject)'))
- subject = Utils.oneline(msgsubj, in_unicode=True)
- # Don't include the redundant subject prefix in the toc
- mo = re.match('(re:? *)?(%s)' % re.escape(mlist.subject_prefix),
- subject, re.IGNORECASE)
- if mo:
- subject = subject[:mo.start(2)] + subject[mo.end(2):]
- username = ''
- addresses = getaddresses([Utils.oneline(msg.get('from', ''),
- in_unicode=True)])
- # Take only the first author we find
- if isinstance(addresses, list) and addresses:
- username = addresses[0][0]
- if not username:
- username = addresses[0][1]
- if username:
- username = ' (%s)' % username
- # Put count and Wrap the toc subject line
- wrapped = Utils.wrap('%2d. %s' % (msgcount, subject), 65)
- slines = wrapped.split('\n')
- # See if the user's name can fit on the last line
- if len(slines[-1]) + len(username) > 70:
- slines.append(username)
- else:
- slines[-1] += username
- # Add this subject to the accumulating topics
- first = True
- for line in slines:
- if first:
- print >> toc, ' ', line
- first = False
- else:
- print >> toc, ' ', line.lstrip()
- # We do not want all the headers of the original message to leak
- # through in the digest messages. For this phase, we'll leave the
- # same set of headers in both digests, i.e. those required in RFC 1153
- # plus a couple of other useful ones. We also need to reorder the
- # headers according to RFC 1153. Later, we'll strip out headers for
- # for the specific MIME or plain digests.
- keeper = {}
- all_keepers = {}
- for header in (config.MIME_DIGEST_KEEP_HEADERS +
- config.PLAIN_DIGEST_KEEP_HEADERS):
- all_keepers[header] = True
- all_keepers = all_keepers.keys()
- for keep in all_keepers:
- keeper[keep] = msg.get_all(keep, [])
- # Now remove all unkempt headers :)
- for header in msg.keys():
- del msg[header]
- # And add back the kept header in the RFC 1153 designated order
- for keep in all_keepers:
- for field in keeper[keep]:
- msg[keep] = field
- # And a bit of extra stuff
- msg['Message'] = `msgcount`
- # Get the next message in the digest mailbox
- msg = mbox.next()
- # Now we're finished with all the messages in the digest. First do some
- # sanity checking and then on to adding the toc.
- if msgcount == 0:
- # Why did we even get here?
- return
- toctext = toc.getvalue()
- # MIME
- try:
- tocpart = MIMEText(toctext.encode(lcset), _charset=lcset)
- except UnicodeError:
- tocpart = MIMEText(toctext.encode('utf-8'), _charset='utf-8')
- tocpart['Content-Description']= _("Today's Topics ($msgcount messages)")
- mimemsg.attach(tocpart)
- # RFC 1153
- print >> plainmsg, toctext
- print >> plainmsg
- # For RFC 1153 digests, we now need the standard separator
- print >> plainmsg, separator70
- print >> plainmsg
- # Now go through and add each message
- mimedigest = MIMEBase('multipart', 'digest')
- mimemsg.attach(mimedigest)
- first = True
- for msg in messages:
- # MIME. Make a copy of the message object since the rfc1153
- # processing scrubs out attachments.
- mimedigest.attach(MIMEMessage(copy.deepcopy(msg)))
- # rfc1153
- if first:
- first = False
- else:
- print >> plainmsg, separator30
- print >> plainmsg
- # Use Mailman.Handlers.Scrubber.process() to get plain text
- try:
- msg = scrubber(mlist, msg)
- except Errors.DiscardMessage:
- print >> plainmsg, _('[Message discarded by content filter]')
- continue
- # Honor the default setting
- for h in config.PLAIN_DIGEST_KEEP_HEADERS:
- if msg[h]:
- uh = Utils.wrap('%s: %s' % (h, Utils.oneline(msg[h],
- in_unicode=True)))
- uh = '\n\t'.join(uh.split('\n'))
- print >> plainmsg, uh
- print >> plainmsg
- # If decoded payload is empty, this may be multipart message.
- # -- just stringfy it.
- payload = msg.get_payload(decode=True) \
- or msg.as_string().split('\n\n',1)[1]
- mcset = msg.get_content_charset('us-ascii')
- try:
- payload = unicode(payload, mcset, 'replace')
- except (LookupError, TypeError):
- # unknown or empty charset
- payload = unicode(payload, 'us-ascii', 'replace')
- print >> plainmsg, payload
- if not payload.endswith('\n'):
- print >> plainmsg
- # Now add the footer
- if mlist.digest_footer:
- footertxt = decorate(mlist, mlist.digest_footer, _('digest footer'))
- # MIME
- footer = MIMEText(footertxt.encode(lcset), _charset=lcset)
- footer['Content-Description'] = _('Digest Footer')
- mimemsg.attach(footer)
- # RFC 1153
- # BAW: This is not strictly conformant RFC 1153. The trailer is only
- # supposed to contain two lines, i.e. the "End of ... Digest" line and
- # the row of asterisks. If this screws up MUAs, the solution is to
- # add the footer as the last message in the RFC 1153 digest. I just
- # hate the way that VM does that and I think it's confusing to users,
- # so don't do it unless there's a clamor.
- print >> plainmsg, separator30
- print >> plainmsg
- print >> plainmsg, footertxt
- print >> plainmsg
- # Do the last bit of stuff for each digest type
- signoff = _('End of ') + digestid
- # MIME
- # BAW: This stuff is outside the normal MIME goo, and it's what the old
- # MIME digester did. No one seemed to complain, probably because you
- # won't see it in an MUA that can't display the raw message. We've never
- # got complaints before, but if we do, just wax this. It's primarily
- # included for (marginally useful) backwards compatibility.
- mimemsg.postamble = signoff
- # rfc1153
- print >> plainmsg, signoff
- print >> plainmsg, '*' * len(signoff)
- # Do our final bit of housekeeping, and then send each message to the
- # outgoing queue for delivery.
- mlist.next_digest_number += 1
- virginq = Switchboard(config.VIRGINQUEUE_DIR)
- # Calculate the recipients lists
- plainrecips = set()
- mimerecips = set()
- # When someone turns off digest delivery, they will get one last digest to
- # ensure that there will be no gaps in the messages they receive.
- # Currently, this dictionary contains the email addresses of those folks
- # who should get one last digest. We need to find the corresponding
- # IMember records.
- digest_members = set(mlist.digest_members.members)
- for address in mlist.one_last_digest:
- member = mlist.digest_members.get_member(address)
- if member:
- digest_members.add(member)
- for member in digest_members:
- if member.delivery_status <> DeliveryStatus.enabled:
- continue
- # Send the digest to the case-preserved address of the digest members.
- email_address = member.address.original_address
- if member.delivery_mode == DeliveryMode.plaintext_digests:
- plainrecips.add(email_address)
- elif member.delivery_mode == DeliveryMode.mime_digests:
- mimerecips.add(email_address)
- else:
- raise AssertionError(
- 'Digest member "%s" unexpected delivery mode: %s' %
- (email_address, member.delivery_mode))
- # Zap this since we're now delivering the last digest to these folks.
- mlist.one_last_digest.clear()
- # MIME
- virginq.enqueue(mimemsg,
- recips=mimerecips,
- listname=mlist.fqdn_listname,
- isdigest=True)
- # RFC 1153
- # If the entire digest message can't be encoded by list charset, fall
- # back to 'utf-8'.
- try:
- rfc1153msg.set_payload(plainmsg.getvalue().encode(lcset), lcset)
- except UnicodeError:
- rfc1153msg.set_payload(plainmsg.getvalue().encode('utf-8'), 'utf-8')
- virginq.enqueue(rfc1153msg,
- recips=plainrecips,
- listname=mlist.fqdn_listname,
- isdigest=True)
diff --git a/Mailman/Handlers/ToOutgoing.py b/Mailman/Handlers/ToOutgoing.py
deleted file mode 100644
index 9c8650a98..000000000
--- a/Mailman/Handlers/ToOutgoing.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# Copyright (C) 1998-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Re-queue the message to the outgoing queue.
-
-This module is only for use by the IncomingRunner for delivering messages
-posted to the list membership. Anything else that needs to go out to some
-recipient should just be placed in the out queue directly.
-"""
-
-from Mailman.configuration import config
-from Mailman.interfaces import Personalization
-from Mailman.queue import Switchboard
-
-
-
-def process(mlist, msg, msgdata):
- interval = config.VERP_DELIVERY_INTERVAL
- # Should we VERP this message? If personalization is enabled for this
- # list and VERP_PERSONALIZED_DELIVERIES is true, then yes we VERP it.
- # Also, if personalization is /not/ enabled, but VERP_DELIVERY_INTERVAL is
- # set (and we've hit this interval), then again, this message should be
- # VERPed. Otherwise, no.
- #
- # Note that the verp flag may already be set, e.g. by mailpasswds using
- # VERP_PASSWORD_REMINDERS. Preserve any existing verp flag.
- if 'verp' in msgdata:
- pass
- elif mlist.personalize <> Personalization.none:
- if config.VERP_PERSONALIZED_DELIVERIES:
- msgdata['verp'] = True
- elif interval == 0:
- # Never VERP
- pass
- elif interval == 1:
- # VERP every time
- msgdata['verp'] = True
- else:
- # VERP every `interval' number of times
- msgdata['verp'] = not (int(mlist.post_id) % interval)
- # And now drop the message in qfiles/out
- outq = Switchboard(config.OUTQUEUE_DIR)
- outq.enqueue(msg, msgdata, listname=mlist.fqdn_listname)
diff --git a/Mailman/Handlers/ToUsenet.py b/Mailman/Handlers/ToUsenet.py
deleted file mode 100644
index 09bb28bdd..000000000
--- a/Mailman/Handlers/ToUsenet.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# Copyright (C) 1998-2007 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
-# USA.
-
-"""Move the message to the mail->news queue."""
-
-import logging
-
-from Mailman.configuration import config
-from Mailman.queue import Switchboard
-
-COMMASPACE = ', '
-
-log = logging.getLogger('mailman.error')
-
-
-
-def process(mlist, msg, msgdata):
- # short circuits
- if not mlist.gateway_to_news or \
- msgdata.get('isdigest') or \
- msgdata.get('fromusenet'):
- return
- # sanity checks
- error = []
- if not mlist.linked_newsgroup:
- error.append('no newsgroup')
- if not mlist.nntp_host:
- error.append('no NNTP host')
- if error:
- log.error('NNTP gateway improperly configured: %s',
- COMMASPACE.join(error))
- return
- # Put the message in the news runner's queue
- newsq = Switchboard(config.NEWSQUEUE_DIR)
- newsq.enqueue(msg, msgdata, listname=mlist.fqdn_listname)
diff --git a/Mailman/Handlers/__init__.py b/Mailman/Handlers/__init__.py
deleted file mode 100644
index e69de29bb..000000000
--- a/Mailman/Handlers/__init__.py
+++ /dev/null