# Copyright (C) 2005 by Stefan Schlott # Copyright (C) 2005 by Tilburg University, http://www.uvt.nl/. # Copyright (C) 2001-2003 by the Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. """Posting moderation filter, if appropriate takes care of decrypting using list key """ import re from email.Parser import Parser from email.MIMEMessage import MIMEMessage from email.MIMEText import MIMEText from Mailman import mm_cfg from Mailman import GPGUtils from Mailman import Utils from Mailman import Message from Mailman import Errors from Mailman.i18n import _ from Mailman.Handlers import Hold from Mailman.Logging.Syslog import syslog def isAdministrativeMail(mlist, msg, msgdata): result = False if msgdata.get('torequest') or msgdata.get('toleave') \ or msgdata.get('tojoin') or msgdata.get('toconfirm'): result = True return result def enforceEncryptPolicy(mlist, msg, msgdata): result = True if msgdata.get('toowner') or msgdata.get('toleave') \ or msgdata.get('tojoin') or msgdata.get('toconfirm'): result = False if msgdata.get('torequest'): # This could be more sophisticated: # Parse message, enforce if commands containing passwords are used # These would be: password, subscribe, unsubscribe, who result = False return result def decryptGpg(mlist, msg, msgdata): # should return (encrypted (bool), signed (bool), key_ids), msg should be replaced with decrypted msg encrypted = False signed = False key_ids = () plaintext = None ciphertext = None is_pgpmime = False # Check: Is inline pgp? if msg.get_content_type()=='application/pgp' or msg.get_param('x-action')=='pgp-encrypted': ciphertext = msg.get_payload() is_pgpmime = False # Check: Is pgp/mime? if msg.get_content_type()=='multipart/encrypted' and msg.get_param('protocol')=='application/pgp-encrypted': if msg.is_multipart(): for submsg in msg.get_payload(): if submsg.get_content_type()=='application/octet-stream': is_pgpmime = True ciphertext = submsg.get_payload() else: ciphertext = msg.get_payload() # Some clients send text/plain messages containing PGP-encrypted data :-( if not msg.is_multipart() and (ciphertext==None) and \ (len(msg.get_payload())>10): firstline = msg.get_payload().splitlines()[0] if firstline=='-----BEGIN PGP MESSAGE-----': syslog('gpg','Encrypted message detected, although MIME type is %s',msg.get_content_type()) is_pgpmime = False ciphertext = msg.get_payload() # Ciphertext present? Decode if ciphertext: gh = GPGUtils.GPGHelper(mlist) (plaintext,key_ids) = gh.decryptMessage(ciphertext) if plaintext is None: syslog('gpg','Unable to decrypt GPG data') raise Errors.RejectMessage, "Unable to decrypt mail!" else: encrypted = True if key_ids: signed = True if not encrypted: return (encrypted, signed, key_ids) # Check decryption result # Check transfer type parser = Parser() tmpmsg = parser.parsestr(plaintext) if msg.get_content_type()=='application/pgp': msg.set_type("text/plain") msg.del_param("x-action") for i in ('Content-Type','Content-Disposition','Content-Transfer-Encoding'): if tmpmsg.has_key(i): if msg.has_key(i): msg.replace_header(i,tmpmsg.get(i)) else: msg.add_header(i,tmpmsg.get(i)) if tmpmsg.is_multipart(): msg.set_payload(None) for i in tmpmsg.get_payload(): msg.attach(i) else: tmppayload = tmpmsg.get_payload() msg.set_payload(tmppayload) if not is_pgpmime: mailclient = '' if msg.has_key('User-Agent'): mailclient = msg.get('User-Agent').lower() # Content-Transfer-Encoding and charset are not standardized... if mailclient.startswith('mutt'): msg.set_param('charset','utf-8') if msg.has_key('Content-Transfer-Encoding'): msg.replace_header('Content-Transfer-Encoding','utf-8') else: msg.add_header('Content-Transfer-Encoding','utf-8') else: # Just a wild guess... msg.set_param('charset','iso-8859-1') if msg.has_key('Content-Transfer-Encoding'): msg.replace_header('Content-Transfer-Encoding','8bit') else: msg.add_header('Content-Transfer-Encoding','8bit') return (encrypted, signed, key_ids) 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 # Deal with encrypted messages encrypted = False signed = False key_ids = '' # no, yes, force (0,1,2) if mlist.gpg_postings_allowed!=0: # if msg is encrypted, we should decryp (encrypted, signed, key_ids) = decryptGpg(mlist, msg, msgdata) if mlist.gpg_postings_allowed==2 and not encrypted: syslog('gpg','Throwing RejectMessage exception: Message has to be encrypted') raise Errors.RejectMessage, "Message has to be encrypted!" # Are we dealing with a secure list, and is the message properly signed? signedByMember = False # legal values are: # 0 = "No" # 1 = "Yes" # 2 = "Force" if mlist.gpg_post_sign!=0 and not signed: # PGP signature matters, we have not checked while decrypting gh = GPGUtils.GPGHelper(mlist) payload = '' signature = '' if msg.get_content_type()=='multipart/signed' and msg.get_param('protocol')=='application/pgp-signature' and msg.is_multipart(): # handle detached signatures, these look like: # # Content-Type: multipart/signed; micalg=pgp-sha1; protocol="application/pgp-signature"; boundary="x0ZPnva+gsdVsg/k" # Content-Disposition: inline # # # --x0ZPnva+gsdVsg/k # Content-Type: text/plain; charset=us-ascii # Content-Disposition: inline # # hello # # --x0ZPnva+gsdVsg/k # Content-Type: application/pgp-signature; name="signature.asc" # Content-Description: Digital signature # Content-Disposition: inline # # -----BEGIN PGP SIGNATURE----- # Version: GnuPG v1.2.5 (GNU/Linux) # # iD8DBQFCQDTGPSnqOAwU/4wRAsoZAKDtN6Pn1dXjC/DAQhqOLHNI6VfNigCfaDPs # FRJlhlGvyhkpx4soGR+CLxE= # =AmS5 # -----END PGP SIGNATURE----- # # --x0ZPnva+gsdVsg/k-- # # for verification, use payload INCLUDING MIME header: # # 'Content-Type: text/plain; charset=us-ascii # Content-Disposition: inline # # hello # ' # Thanks Wessel Dankers for hint. for submsg in msg.get_payload(): if submsg.get_content_type()=='application/pgp-signature': signature = submsg.get_payload() else: # we assume exactly 2 parts: one payload, one signature # # yes, including headers payload = submsg.as_string() elif msg.get_content_type()=='text/plain' and not msg.is_multipart(): # handle inline signature; message looks like e.g. # # Content-Type: text/plain; charset=iso-8859-1 # Content-Disposition: inline # Content-Transfer-Encoding: 8bit # MIME-Version: 1.0 # # -----BEGIN PGP SIGNED MESSAGE----- # Hash: SHA1 # # blah blah # # -----BEGIN PGP SIGNATURE----- # Version: GnuPG v1.4.0 (GNU/Linux) # # iD8DBQFCPtWXW5ql+IAeqTIRAirPAK.... # -----END PGP SIGNATURE----- signature = None payload = msg.get_payload() syslog('gpg', "gonna verify payload '%s' with signature '%s'", payload, signature) key_ids = gh.verifyMessage(payload, signature) if mlist.gpg_post_sign!=0: if not key_ids: syslog('gpg','No valid signatures on message') if mlist.gpg_post_sign==1: # unsigned means: hold syslog('gpg','gpg_post_sign is Yes, gonna Hold') Hold.hold_for_approval(mlist, msg, msgdata, Hold.NonGPGSignedPost) return else: # mlist.gpg_post_sign==2 # 'Force' do_discard(mlist, msg) for user in mlist.getMembers(): syslog('gpg','Checking signature: listmember %s',user) for key_id in key_ids: syslog('gpg','Checking signature: key_id %s',key_id) try: ks=mlist.getGPGKeyIDs(user) except: ks=None if ks: for k in mlist.getGPGKeyIDs(user): syslog('gpg','Checking signature: keyid of listmember is %s',k) if k==key_id: signedByMember = True break # done dealing with most of gpg stuff # Is the poster a member or not? for sender in msg.get_senders(): if mlist.isMember(sender): break else: sender = None if sender: # If posts need to be PGP signed, process signature. if mlist.gpg_post_sign!=0: if mlist.gpg_post_sign==1: if signedByMember==True: syslog('gpg','Message properly signed: distribute') return else: Hold.hold_for_approval(mlist, msg, msgdata, Hold.WrongGPGSignedPost) elif mlist.gpg_post_sign==2: if signedByMember==True: syslog('gpg','Message properly signed: distribute') return else: do_discard(mlist, msg) # If the member's moderation flag is on, then perform the moderation # action. if mlist.getMemberOption(sender, mm_cfg.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() 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