--- /usr/local/mm21b5-clean/Mailman/MailList.py Sun Nov 24 06:36:47 2002 +++ /usr/local/mailman/Mailman/MailList.py Tue Nov 26 02:20:08 2002 @@ -56,6 +56,7 @@ from Mailman.ListAdmin import ListAdmin from Mailman.SecurityManager import SecurityManager from Mailman.TopicMgr import TopicMgr +from Mailman.SecureList import SecureList # gui components package from Mailman import Gui @@ -76,7 +77,7 @@ # Use mixins here just to avoid having any one chunk be too large. class MailList(HTMLFormatter, Deliverer, ListAdmin, Archiver, Digester, SecurityManager, Bouncer, GatewayManager, - Autoresponder, TopicMgr): + Autoresponder, TopicMgr, SecureList): # # A MailList object's basic Python object model support --- /usr/local/mm21b5-clean/Mailman/Defaults.py Sun Nov 24 06:36:46 2002 +++ /usr/local/mailman/Mailman/Defaults.py Tue Nov 26 02:20:09 2002 @@ -369,7 +369,7 @@ # consecutive sessions. SMTP_MAX_SESSIONS_PER_CONNECTION = 0 -# Maximum number of simultaneous subthreads that will be used for SMTP +# Maximum number of simulatenous subthreads that will be used for SMTP # delivery. After the recipients list is chunked according to SMTP_MAX_RCPTS, # each chunk is handed off to the smptd by a separate such thread. If your # Python interpreter was not built for threads, this feature is disabled. You @@ -633,6 +633,7 @@ ('NewsRunner', 1), # outgoing messages to the nntpd ('OutgoingRunner', 1), # outgoing messages to the smtpd ('VirginRunner', 1), # internally crafted (virgin birth) messages + ('SecureRunner', 1), # message dealt with by the secure runner ] # Set this to true to use the `Maildir' delivery option. If you change this @@ -740,6 +741,7 @@ ADMIN_CATEGORIES = [ # First column 'general', 'passwords', 'language', 'members', 'nondigest', 'digest', + 'secure', # Second column 'privacy', 'bounce', 'archive', 'gateway', 'autoreply', 'contentfilter', 'topics', @@ -1115,6 +1117,7 @@ ReceiveNonmatchingTopics = 64 Moderate = 128 DontReceiveDuplicates = 256 +ReceivePGPAsAttachment = 512 #Receive PGP Messages as body or attachment # A mapping between short option tags and their flag OPTINFO = {'hide' : ConcealSubscription, @@ -1123,7 +1126,8 @@ 'notmetoo': DontReceiveOwnPosts, 'digest' : 0, 'plain' : DisableMime, - 'nodupes' : DontReceiveDuplicates + 'nodupes' : DontReceiveDuplicates, + 'pgpattach' : ReceivePGPAsAttachment } # Authentication contexts. @@ -1176,6 +1180,7 @@ VIRGINQUEUE_DIR = os.path.join(QUEUE_DIR, 'virgin') BADQUEUE_DIR = os.path.join(QUEUE_DIR, 'bad') MAILDIR_DIR = os.path.join(QUEUE_DIR, 'maildir') +SECUREQUEUE_DIR = os.path.join(QUEUE_DIR, 'secure') # Other useful files PIDFILE = os.path.join(DATA_DIR, 'master-qrunner.pid') @@ -1219,3 +1224,23 @@ add_language('sv', _('Swedish'), 'iso-8859-1') del _ + +####### +#Settings for Secure Lists +####### +PGP_TMP='/tmp' +PGP_CMD='gpg' +PGP_RINGDIR=os.path.join(PREFIX,"pgp_rings") +PGP_IMPORT_ADMIN_ALERT= \ +"""While importing list keys, no valid PGP data was found. The list must +be manually enabled by turning on delivery to non-digest members, \n%s, +and providing valid PGP key data using the Secure List option page,\n%s""" +PGP_IMPORT_USER_ALERT=""" +The list delivery has been disabled because it is misconfigured. The list +adminstator(s) have been notified and the message(s) currently in the queue +will remain there.""" +PGP_MSG_REJECT= \ +"""This list does not accept unencrypted messages. Please encrypt your +messages before you send them to the list""" +PGP_MSG_ACCEPT= """This message was received unencrypted. Please remind + the sender to encrypt messages before sending them to the list\n\n""" --- /usr/local/mm21b5-clean/Mailman/HTMLFormatter.py Sun Nov 24 06:36:46 2002 +++ /usr/local/mailman/Mailman/HTMLFormatter.py Tue Nov 26 02:20:09 2002 @@ -118,6 +118,7 @@ mm_cfg.SuppressPasswordReminder : 'remind', mm_cfg.ReceiveNonmatchingTopics : 'rcvtopic', mm_cfg.DontReceiveDuplicates : 'nodupes', + mm_cfg.ReceivePGPAsAttachment : 'pgpattach', }[option] return '' % ( name, value, checked) --- /usr/local/mm21b5-clean/Mailman/Queue/SecureRunner.py Sun Nov 24 06:38:38 2002 +++ /usr/local/mailman/Mailman/Queue/SecureRunner.py Tue Nov 26 02:20:09 2002 @@ -0,0 +1,151 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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. + +"""Secure queue runner.""" + + +import sys +import os +import copy +from cStringIO import StringIO + +from Mailman import mm_cfg +from Mailman import Errors +from Mailman import LockFile +from Mailman.i18n import _ +from Mailman.Queue.IncomingRunner import IncomingRunner +from Mailman.Logging.Syslog import syslog + +from Mailman import PGPClass +from Mailman import PGPErrors +from Mailman import Message + +class SecureRunner(IncomingRunner): + QDIR = mm_cfg.SECUREQUEUE_DIR + def _dispose(self, mlist, msg, msgdata): + # Try to get the list lock. + try: + mlist.Lock(timeout=mm_cfg.LIST_LOCK_TIMEOUT) + except LockFile.TimeOutError: + # Oh well, try again later + return 1 + + try: + #if the ring has changed, delete the old ring and import the + #new one + list_key = os.path.join(mm_cfg.PGP_RINGDIR, mlist.real_name) + if mlist.ring_changed: + if not os.path.exists(mm_cfg.PGP_RINGDIR): + try: + os.mkdir(mm_cfg.PGP_RINGDIR) + except OSError: + syslog('error', """Unable to create directory for the + PGP keyrings""") + return 1 #may need to update this for 2.1 + if os.path.exists(list_key): + try: + os.remove(list_key) + except OSError: + syslog('error', 'Unable to remove old keyring') + return 1 #may need to update this for 2.1 + + pgp = PGPClass.GPGMail({'no-default-keyring': 1, + 'keyring': list_key, + 'homedir': mm_cfg.PGP_RINGDIR, + 'ignore-time-conflict': 1, + }) + + try: + pgp.ImportPublicKey(mlist.pgp_ring) + mlist.ring_changed = 0 + except PGPErrors.NoValidDataFoundError, e: + #list are already disabled just return 1 + if mlist.IsListDisabled(): + return 1 + + mlist.DisableList() + mlist.PGPFailedNotifyAdminsAndUsers() + + syslog('error', 'Key import failed. List Disabled.') + return 1 + + if not mlist.IsEncryptedMessage(str(msg._payload)): + if mlist.reject_unencrypted_mail: + #send rejection message to sender + recips = [msg.get_sender()] + msg = Message.UserNotification( + recips, mlist.GetOwnerEmail(), + _('Message to list %s Rejected' %mlist.real_name), + mlist.reject_message ) + msg.send(mlist) + return 0 + else: + msg._payload = mlist.accept_message + "\n\n" + msg._payload + pgp = PGPClass.GPGMail({'no-default-keyring': 1, + 'keyring': list_key, + 'homedir': mm_cfg.PGP_RINGDIR, + 'ignore-time-conflict': 1 + }) + try: + mlist.RegularizeMessage(msg) + msg._payload = \ + pgp.EncryptString(msg._payload, [mlist.GetListEmail()]) + except PGPErrors.PublicKeyNotFoundError, e: + #list are already disabled just return 1 + if mlist.IsListDisabled(): + return 1 + + mlist.DisableList() + mlist.PGPFailedNotifyAdminsAndUsers() + + syslog('error', 'No Public Key Found. List Disabled.') + return 1 + + #PGP keys are okay. We can move on. + attach_status = body_status = 0 + body_recips = mlist.PGPBodyRecipients() + attach_recips = mlist.PGPAttachRecipients() + + if mlist.suppres_subject: + msg = mlist.SuppressSubject(msg) + + if body_recips: + body_msg = mlist.CreateBodyMail(msg) + body_msgdata = copy.deepcopy(msgdata) + body_msgdata.update({'recips': body_recips}) + body_status = \ + self.send_to_pipeline(mlist, body_msg, body_msgdata) + + if attach_recips: + attachmsg = mlist.CreateAttachMail(msg) + attach_msgdata = copy.deepcopy(msgdata) + attach_msgdata.update({'recips':attach_recips}) + attach_status = \ + self.send_to_pipeline(mlist, attachmsg, attach_msgdata) + + return attach_status + body_status + finally: + mlist.Save() #If there is a list error we save the list changes + #Must come up with a better way to deal with this + mlist.Unlock() + + def send_to_pipeline(self, mlist, msg, msgdata): + pipeline = self._get_pipeline(mlist, msg, msgdata) + status = self._dopipeline(mlist, msg, msgdata, pipeline) + if status: + msgdata['pipeline'] = pipeline + return status + --- /usr/local/mm21b5-clean/Mailman/Cgi/admin.py Sun Nov 24 06:36:48 2002 +++ /usr/local/mailman/Mailman/Cgi/admin.py Tue Nov 26 02:20:09 2002 @@ -49,7 +49,7 @@ i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE) NL = '\n' -OPTCOLUMNS = 11 +OPTCOLUMNS = 12 @@ -891,6 +891,7 @@ _('ack'), _('not metoo'), _('nodupes'), _('digest'), _('plain'), + _('pgpattach'), _('language'))]) rowindex = usertable.GetCurrentRowIndex() for i in range(OPTCOLUMNS): @@ -962,6 +963,15 @@ value = 'off' checked = 0 cells.append(Center(CheckBox('%s_plain' % addr, value, checked))) + + if mlist.getMemberOption(addr, mm_cfg.OPTINFO['pgpattach']): + value = 'on' + checked = 1 + else: + value = 'off' + checked = 0 + cells.append(Center(CheckBox('%s_pgpattach' % addr, value, checked))) + # User's preferred language langpref = mlist.getMemberLanguage(addr) langs = mlist.GetAvailableLanguages() @@ -1390,7 +1400,8 @@ mlist.setDeliveryStatus(user, MemberAdaptor.BYADMIN) else: mlist.setDeliveryStatus(user, MemberAdaptor.ENABLED) - for opt in ('hide', 'ack', 'notmetoo', 'nodupes', 'plain'): + for opt in ('hide', 'ack', 'notmetoo', 'nodupes', 'plain', + 'pgpattach'): opt_code = mm_cfg.OPTINFO[opt] if cgidata.has_key('%s_%s' % (user, opt)): mlist.setMemberOption(user, opt_code, 1) --- /usr/local/mm21b5-clean/Mailman/Cgi/listinfo.py Sun Nov 24 06:36:48 2002 +++ /usr/local/mailman/Mailman/Cgi/listinfo.py Tue Nov 26 02:20:09 2002 @@ -167,6 +167,11 @@ mlist.FormatUndigestButton() replacements[''] = '' replacements[''] = '' + if mlist.secure: + replacements[''] = mlist.FormatPgpRingBox() + else: + replacements[''] = "" + replacements[''] = \ mlist.FormatPlainDigestsButton() replacements[''] = mlist.FormatMimeDigestsButton() --- /usr/local/mm21b5-clean/Mailman/Cgi/options.py Sun Nov 24 06:36:48 2002 +++ /usr/local/mailman/Mailman/Cgi/options.py Tue Nov 26 02:20:09 2002 @@ -455,6 +455,7 @@ ('remind', mm_cfg.SuppressPasswordReminder), ('rcvtopic', mm_cfg.ReceiveNonmatchingTopics), ('nodupes', mm_cfg.DontReceiveDuplicates), + ('pgpattach', mm_cfg.ReceivePGPAsAttachment), ): try: newval = int(cgidata.getvalue(item)) @@ -656,6 +657,10 @@ mlist.FormatOptionButton(mm_cfg.DontReceiveDuplicates, 1, user)) replacements[''] = ( mlist.FormatOptionButton(mm_cfg.DontReceiveDuplicates, 0, user)) + replacements[''] = ( + mlist.FormatOptionButton(mm_cfg.ReceivePGPAsAttachment, 0, user)) + replacements[''] = ( + mlist.FormatOptionButton(mm_cfg.ReceivePGPAsAttachment, 1, user)) replacements[''] = ( mlist.FormatButton('unsub', _('Unsubscribe')) + '
' + CheckBox('unsubconfirm', 1, checked=0).Format() + --- /usr/local/mm21b5-clean/Mailman/Gui/SecureList.py Sun Nov 24 06:38:38 2002 +++ /usr/local/mailman/Mailman/Gui/SecureList.py Tue Nov 26 02:20:09 2002 @@ -0,0 +1,93 @@ +# Copyright (C) 2001,2002 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. + +"""MailList mixin class managing the Secure List options. +""" + +from Mailman import mm_cfg +from Mailman import Utils +from Mailman import Errors +from Mailman.i18n import _ +from Mailman.Gui.GUIBase import GUIBase + +WIDTH = mm_cfg.TEXTFIELDWIDTH + + +class SecureList(GUIBase): + def GetConfigCategory(self): + return 'secure', _('Secure List options') + + def GetConfigInfo(self, mlist, category, subcat): + if category <> 'secure': + return None + return[ + _('Secure List options.'), + + ('secure', mm_cfg.Toggle, ('No', 'Yes'), 0, + _('Will the list use PGP?'), + _('''If the list uses PGP you can control if messages sent to the + list unencrypted are rejected (and the sender notified) or encrypted + with list's public key.''')), + + ('reject_unencrypted_mail', mm_cfg.Toggle, ('No', 'Yes'), 0, + _('If messages come in unencrypted should we reject them?'), + _('''If message come to the list and they are not encrypted + do we reject them and send notification back to the user or + do we accept them and encrypt them with the list\'s key?''')), + + ('suppres_subject', mm_cfg.Toggle, ('No', 'Yes'), 0, + _('Remove the subject line from all email?'), + _('''When yes, the subject line will be removed from all emails + to the list, leaving only the subject prefix.''')), + + ('pgp_ring', mm_cfg.Text, (7, WIDTH), 0, + _('All the public keys for the list'), + _('''All the public keys you want your list users to be aware of + should be placed here in a single keyring. You must include a + public key for the mailing list entry address if you want + incoming unencrypted message to be automatically encrypted.''')), + + ('accept_message', mm_cfg.Text, (5, WIDTH), 0, + _('Reminder text added to unencrypted mail.'), + _('''If we accept unencrypted messages, we can add a reminder + message to it before it is encrypted and sent out.''')), + + ('reject_message', mm_cfg.Text, (5, WIDTH), 0, + _('Rejected messages will have this message.'), + _('''If we reject unencrypted messages the message body will be + replaced with this message''')), + + ('_default_secure_settings', mm_cfg.Toggle, ('No', 'Yes'), 0, + _('Reset the list settings to the PGP defaults?'), + _('''The default setting for a list using PGP are: unencrypted + messages are rejected, digests are not allowed, no + reminders, the list is not advertised, subscribing requires + a list admin's approval, only list members can see the list + members, no archives are kept, and the list admin is notified of + any changes to list membership.''')), + ] + + def _setValue(self, mlist, property, val, doc): + # Watch for the special, immediate action attributes + if property == '_default_secure_settings' and val: + mlist.TightenList() + else: + GUIBase._setValue(self, mlist, property, val, doc) + + def getValue(self, mlist, kind, varname, params): + if varname <> 'pgp_ring': + mlist.ring_changed = 1 + --- /usr/local/mm21b5-clean/Mailman/Gui/__init__.py Sun Nov 24 06:36:48 2002 +++ /usr/local/mailman/Mailman/Gui/__init__.py Tue Nov 26 02:20:09 2002 @@ -23,6 +23,7 @@ from NonDigest import NonDigest from Passwords import Passwords from Privacy import Privacy +from SecureList import SecureList from Topics import Topics from Usenet import Usenet from Language import Language --- /usr/local/mm21b5-clean/Mailman/Commands/cmd_set.py Sun Nov 24 06:36:49 2002 +++ /usr/local/mailman/Mailman/Commands/cmd_set.py Tue Nov 26 02:20:09 2002 @@ -34,6 +34,7 @@ settings. """) +global DETAILS DETAILS = _(""" set help Show this detailed help. @@ -95,6 +96,15 @@ reminder for this mailing list. """) +PGP_DETAILS=""" + set pgpattach on + set pgpattach off + Use 'set pgpattach on' to receive encryped messages from a secure + mailing list as a pgp-mime attachment instead of in the body of the + emails. This will allow you to receive PGP messages in the format you + like, independent of how it was sent. +""" + _ = i18n._ STOP = 1 @@ -113,18 +123,27 @@ def process(self, res, args): if not args: + if res.mlist.IsListSecure(): + global DETAILS + DETAILS += PGP_DETAILS res.results.append(_(DETAILS)) return STOP subcmd = args.pop(0) methname = 'set_' + subcmd method = getattr(self, methname, None) if method is None: + if res.mlist.IsListSecure(): + global DETAILS + DETAILS += PGP_DETAILS res.results.append(_('Bad set command: %(subcmd)s')) res.results.append(_(DETAILS)) return STOP return method(res, args) def set_help(self, res, args=1): + if res.mlist.IsListSecure(): + global DETAILS + DETAILS += PGP_DETAILS res.results.append(_(DETAILS)) if args: return STOP @@ -201,6 +220,10 @@ # sense is reversed onoff = (not opt) and _('on') or _('off') res.results.append(_(' reminders %(onoff)s')) + opt = mlist.getMemberOption(address, mm_cfg.ReceivePGPAsAttachment) + # sense NOT is reversed + onoff = (opt) and _('on') or _('off') + res.results.append(_(' pgpattach %(onoff)s')) def set_authenticate(self, res, args): mlist = res.mlist @@ -344,7 +367,17 @@ not status) res.results.append(_('reminder option set')) - + def set_pgpattach(self, res, args): + mlist = res.mlist + if len(args) <> 1: + return self._usage(res) + status = self._status(res, args[0]) + if status < 0: + return STOP + # sense is NOT reversed + mlist.setMemberOption(self.__address, mm_cfg.ReceivePGPAsAttachment, + status) + res.results.append(_('pgpattach option set')) def process(res, args): # We need to keep some state between set commands --- /usr/local/mm21b5-clean/Mailman/SecureList.py Sun Nov 24 06:38:38 2002 +++ /usr/local/mailman/Mailman/SecureList.py Tue Nov 26 02:20:09 2002 @@ -0,0 +1,306 @@ +# -*- python -*- + +# Copyright (C) 1998,1999,2000,2001,2002 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. + +"Class for the added functionality of Secure Mailing Lists" + +import string, copy, re, os +import email, mimetools +import Message, Utils, mm_cfg +import PGPClass, PGPErrors +import SecureListHTMLFormat +from i18n import _ + +#This class hold all functions dealing with secure lists +class SecureList (PGPClass.GPGMail, + SecureListHTMLFormat.SecureListHTMLFormat): + def __init__ (self): + try: + type (self.secure) + except: + self.secure=0 + + self.suppres_subject = 0 + self.ring_changed = 0 + self.reject_message = '' + self.accept_message = '' + self.reject_unencrypted_mail = 1 + self.pgp_ring = '' + self.import_admin_alert = '' + self.import_user_alert = '' + + def TightenList (self): + self.digestable = 0 + self.send_reminders = 0 + self.advertised = 0 + self.subscribe_policy = 2 #"require approval" + self.private_roster = 1 #"List members only" + self.member_posting_only = 1 + self.archive = 0 + self.admin_notify_mchanges = 1 #admin gets notices of (un)subscribes + self.reject_unencrypted_mail = 1 + self.accept_message = mm_cfg.PGP_MSG_ACCEPT + self.reject_message = mm_cfg.PGP_MSG_REJECT + + non_digest = os.path.join(self.GetScriptURL('admin'), 'nondigest') + pgp = os.path.join(self.GetScriptURL('admin'), 'pgp') + + self.import_admin_alert = mm_cfg.PGP_IMPORT_ADMIN_ALERT %(non_digest, + pgp) + self.import_user_alert = mm_cfg.PGP_IMPORT_USER_ALERT + + ################### + #If PGP fails, either with no PGP data OR if the public key not found on + #the key ring, send notification emails to the list admins and users + ################### + def PGPFailedNotifyAdminsAndUsers(self): + msg = Message.UserNotification( + self.owner, self.GetOwnerEmail(), + _('List %s Disabled' %self.real_name), + self.import_admin_alert) + msg.send(self) + + msg = Message.UserNotification( + self.getRegularMemberKeys(), self.GetListEmail(), + _('List %s Disabled' %self.real_name), + self.import_user_alert) + msg.send(self) + + ################## + #Suppress Subject + #Remove the subject + ################## + def SuppressSubject(self, msg): + new_headers = [] + for h in msg._headers: + if string.lower(h[0][:7]) == 'subject': + new_headers.append(("Subject", '')) + else: + new_headers.append(h) + msg._headers = new_headers + return msg + + ################### + #Regularize Message + #Change the Line Feeds to Control Return/Line Feeds + ################### + def RegularizeMessage(self, msg): + msg._payload = email.Utils.fix_eols(msg._payload) + + ################### + #Create a Body Mail + #Create a deep copy of the message object passed in & modifies it + #so the PGP is the body of the message + ################### + def CreateBodyMail(self, msg): + new_msg = copy.deepcopy(msg) + new_msg._payload = self.ExtractMessage(str(msg._payload)) + "\n" + new_msg._headers = self.CreateBodyHeaders(msg) + + return new_msg + + ################### + #Create the Header for the message with PGP Message in the body + ################### + def CreateBodyHeaders(self, msg): + new_headers = [] + Content_Length = str(len(msg._payload)) + Lines = str(str(msg._payload).count('\n')) + Content_Type = "TEXT/PLAIN; charset=%s" \ + %Utils.GetCharSet(self.preferred_language) + for h in msg._headers: + if string.lower(h[0][:8]) == 'content-': + if string.lower(h[0][:14]) == 'content-length': + new_headers.append(("Content-Length", Content_Length)) + if string.lower(h[0][:5]) == 'lines': + new_headers.append(("Lines", Lines)) + if string.lower(h[0][:12]) == 'content-type': + new_headers.append(("Content-Type", Content_Type)) + #throw away boundary line + elif string.find(string.lower(h[0]),"boundary") is not -1: + continue + else: + new_headers.append(h) + return new_headers + + ################### + #Create an Attachment Mail + #Creates a deep copy of the message object passed in & modifies it, + #so the PGP Message is an attachment to the mail + ################### + def CreateAttachMail(self, msg): + new_msg = copy.deepcopy(msg) + boundary = mimetools.choose_boundary() + + new_msg._payload = self.CreateAttachPayload(msg, boundary) + new_msg._headers = self.CreateAttachHeaders(msg, boundary) + + #this is an ugly hack to dump the text repr of the msg back + #into email module to create stardardized object + text_msg = '' + + for k, v in new_msg._headers: + text_msg += k + ": " + v + "\n" + text_msg += "\n" + new_msg._payload + new_msg = email.message_from_string(text_msg, Message.Message) + return new_msg + + ############## + #Create Payload for PGP Attachment Mail + ############## + def CreateAttachPayload(self, msg, boundary): + msg_payload = self.ExtractMessage(str(msg._payload)) + new_payload = """ +--%s +Content-Type: application/pgp-encrypted +Content-Disposition: inline; filename="msg.asc" + +Version: 1 + +--%s +Content-Type: application/octet-stream +Content-Disposition: inline + +%s + +--%s-- +""" % (boundary, boundary, msg_payload, boundary) + return new_payload + + ############## + #Create Header for PGP Attachment Mail + ############## + def CreateAttachHeaders(self, msg, boundary): + new_headers = [] + Content_Length = str(len(msg._payload)) + Lines = str(str(msg._payload).count('\n')) + Content_Type = "multipart/encrypted; "+ \ + 'protocol="application/pgp-encrypted";' + \ + '\n\tboundary="%s"' %boundary + for h in msg._headers: + if string.lower(h[0][:8]) == 'content-': + if string.lower(h[0][:14]) == 'content-length': + new_headers.append(("Content-Length", Content_Length)) + if string.lower(h[0][:5]) == 'lines': + new_headers.append(("Lines", Lines)) + if string.lower(h[0][:12]) == 'content-type': + new_headers.append(("Content-Type", Content_Type)) + #throw away boundary line + elif string.find(string.lower(h[0]),"boundary") is not -1: + continue + else: + new_headers.append(h) + return new_headers + + ############# + #Create list of users who want to recieve PGP Block in the body + ############# + def PGPBodyRecipients (self): + def fil_pgp_body (x, s=self, v=mm_cfg.ReceivePGPAsAttachment): + return not s.getMemberOption(x,v) + return filter(fil_pgp_body,self.getRegularMemberKeys()) + + ############# + #Create list of users who want to recieve PGP Block as an attachment + ############# + def PGPAttachRecipients(self): + def fil_pgp_attach (x, s=self, v=mm_cfg.ReceivePGPAsAttachment): + return s.getMemberOption(x,v) + return filter(fil_pgp_attach,self.getRegularMemberKeys()) + + ############# + #Check if the list is disabled + ############# + def IsListDisabled(self): + if not self.nondigestable and not self.digestable: + return 1 + else: + return 0 + + ################ + #Disable the list by turning off delivery to the digest and regular members + ################ + def DisableList(self): + self.nondigestable = 0 + self.digestable = 0 + + ################ + #Enable the list by turning on delivery to regular members + ################ + def EnableList(self): + self.nondigestable = 1 + + ############################ + #Checks whether the list handles secure traffic + ########################### + def IsListSecure(self): + try: + if self.secure: return 1 + else: return 0 + except: + return 0 + + ############################# + #This function checks for an even number of PGP Messege Beginning & Endings + ############################## + def IsEncryptedMessage(self, unknown_string): + num_enc = string.count(unknown_string, PGPClass.BEGIN_PGP_MSG) + if num_enc == 0: + return None + #If the number of BEGIN_PGP_MSG & END_PGP_MSG are not the same + if num_enc != string.count(unknown_string, PGPClass.END_PGP_MSG): + return None + else: + return 1 + + ############################# + #This function checks for an even number of PGP Pub Key Beginning & Endings + ############################### + def IsEncryptedPubRing(self, unknown_string): + num_enc = string.count(unknown_string, PGPClass.BEGIN_PGP_PUB_RING) + if num_enc == 0: + return None + if num_enc != string.count(unknown_string, PGPClass.END_PGP_PUB_RING): + return None + else: + return 1 + + ############################# + #This function will extract only the first PGP Message + ############################### + def ExtractMessage(self, combined_string): + if string.count(combined_string, PGPClass.BEGIN_PGP_MSG) == 0: + raise PGPErrors.NoMessageFoundError("No Message beginning was found") + + msg = string.split(combined_string, PGPClass.BEGIN_PGP_MSG) + msg = string.split(msg[1], PGPClass.END_PGP_MSG) + msg = PGPClass.BEGIN_PGP_MSG + msg[0] + PGPClass.END_PGP_MSG + return msg + + ############################# + #This function will extract only the first PGP keyring + ############################### + def ExtractPublicKey(self, combined_string): + if string.count(combined_string, PGPClass.BEGIN_PGP_PUB_RING) == 0: + raise PGPErrors.NoKeyFoundError("No PGP key information was found") + + key = string.split(combined_string, PGPClass.BEGIN_PGP_PUB_RING) + key = string.split(key[1], PGPClass.END_PGP_PUB_RING) + key = PGPClass.BEGIN_PGP_PUB_RING + key[0] + PGPClass.END_PGP_PUB_RING + return key + + --- /usr/local/mm21b5-clean/Mailman/SecureListHTMLFormat.py Sun Nov 24 06:38:38 2002 +++ /usr/local/mailman/Mailman/SecureListHTMLFormat.py Tue Nov 26 02:20:09 2002 @@ -0,0 +1,26 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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. + +"Class for Formatting Options for all PGP modications to the Web UI" + +from Mailman.htmlformat import * +from Mailman.i18n import _ + +class SecureListHTMLFormat: + def FormatPgpRingBox(self): + text = _('All public keys related to the list are on this keyring.

') + ta = TextArea('ring', self.pgp_ring, 10, 68, wrap=None) + return text + '

' + '
' + ta.Format() + '
' --- /usr/local/mm21b5-clean/Mailman/PGP.py Sun Nov 24 06:38:38 2002 +++ /usr/local/mailman/Mailman/PGP.py Tue Nov 26 02:20:09 2002 @@ -0,0 +1,299 @@ +#COPYRIGHT: +# +#Copyright (C) 2001 Frank J. Tobin, ftobin@neverending.org +#LICENSE: +# +#This library is free software; you can redistribute it and/or +#modify it under the terms of the GNU Lesser General Public +#License as published by the Free Software Foundation; either +# +#This library 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 +#Lesser General Public License for more details. +# +#You should have received a copy of the GNU Lesser General Public +#License along with this library; if not, write to the Free Software +#Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +#or see http://www.gnu.org/copyleft/lesser.html + +"The direct calls to gpg or the pgp executable" + +import os +import sys +import fcntl, FCNTL + +from Mailman import mm_cfg + +# "standard" filehandles attached to processes +_stds = [ 'stdin', 'stdout', 'stderr' ] + +# the permissions each type of file handle needs to be opened with +_fd_modes = { 'stdin': 'w', + 'stdout': 'r', + 'stderr': 'r', + 'passphrase': 'w', + 'command': 'w', + 'logger': 'r', + 'status': 'r' + } + +# correlation between handle names and the arguments we'll pass +_fd_options = { 'passphrase': '--passphrase-fd', + 'logger': '--logger-fd', + 'status': '--status-fd', + 'command': '--command-fd' } + +class GnuPG: + """Class instances represent GnuPG. + + Instance attributes of a GnuPG object are: + + * call -- string to call GnuPG with. Defaults to "gpg" + + * passphrase -- Since it is a common operation + to pass in a passphrase to GnuPG, + and working with the passphrase filehandle mechanism directly + can be mundane, if set, the passphrase attribute + works in a special manner. If the passphrase attribute is set, + and no passphrase file object is sent in to run(), + then the GnuPG instance will take care of sending the passphrase to + GnuPG, the executable instead of having the user send it in manually. + + * options -- Object of type GnuPGInterface.Options. + Attribute-setting in options determines + the command-line options used when calling GnuPG. + """ + def __init__(self): + self.call = mm_cfg.PGP_CMD + self.passphrase = None + self.options = Options() + + def run(self, gnupg_commands, args=None, create_fhs=None, attach_fhs=None): + if args == None: args = [] + if create_fhs == None: create_fhs = [] + if attach_fhs == None: attach_fhs = {} + + for std in _stds: + if not attach_fhs.has_key(std) \ + and std not in create_fhs: + attach_fhs.setdefault(std, getattr(sys, std)) + + handle_passphrase = 0 + + if self.passphrase != None \ + and not attach_fhs.has_key('passphrase') \ + and 'passphrase' not in create_fhs: + handle_passphrase = 1 + create_fhs.append('passphrase') + + process = self._attach_fork_exec(gnupg_commands, args, + create_fhs, attach_fhs) + + if handle_passphrase: + passphrase_fh = process.handles['passphrase'] + passphrase_fh.write( self.passphrase ) + passphrase_fh.close() + del process.handles['passphrase'] + + return process + + def _attach_fork_exec(self, gnupg_commands, args, create_fhs, attach_fhs): + """This is like run(), but without the passphrase-helping + (note that run() calls this).""" + + process = Process() + + for fh_name in create_fhs + attach_fhs.keys(): + if not _fd_modes.has_key(fh_name): + raise KeyError, \ + "unrecognized filehandle name '%s'; must be one of %s" \ + % (fh_name, _fd_modes.keys()) + + for fh_name in create_fhs: + # make sure the user doesn't specify a filehandle + # to be created *and* attached + if attach_fhs.has_key(fh_name): + raise ValueError, \ + "cannot have filehandle '%s' in both create_fhs and attach_fhs" \ + % fh_name + + pipe = os.pipe() + # fix by drt@un.bewaff.net noting + # that since pipes are unidirectional on some systems, + # so we have to 'turn the pipe around' + # if we are writing + if _fd_modes[fh_name] == 'w': pipe = (pipe[1], pipe[0]) + process._pipes[fh_name] = Pipe(pipe[0], pipe[1], 0) + + for fh_name, fh in attach_fhs.items(): + process._pipes[fh_name] = Pipe(fh.fileno(), fh.fileno(), 1) + + process.pid = os.fork() + + if process.pid == 0: self._as_child(process, gnupg_commands, args) + return self._as_parent(process) + + def _as_parent(self, process): + """Stuff run after forking in parent""" + for k, p in process._pipes.items(): + if not p.direct: + os.close(p.child) + process.handles[k] = os.fdopen(p.parent, _fd_modes[k]) + + # user doesn't need these + del process._pipes + + return process + + + def _as_child(self, process, gnupg_commands, args): + """Stuff run after forking in child""" + # child + for std in _stds: + p = process._pipes[std] + os.dup2( p.child, getattr(sys, "__%s__" % std).fileno() ) + + for k, p in process._pipes.items(): + if p.direct and k not in _stds: + # we want the fh to stay open after execing + fcntl.fcntl( p.child, FCNTL.F_SETFD, 0 ) + + fd_args = [] + + for k, p in process._pipes.items(): + # set command-line options for non-standard fds + if k not in _stds: + fd_args.extend([ _fd_options[k], "%d" % p.child ]) + + if not p.direct: os.close(p.parent) + + command = [ self.call ] + fd_args + self.options.get_args() \ + + gnupg_commands + args + + os.execvp( command[0], command ) + +class Pipe: + """simple struct holding stuff about pipes we use""" + def __init__(self, parent, child, direct): + self.parent = parent + self.child = child + self.direct = direct + +class Options: + def __init__(self): + # booleans + self.armor = 0 + self.no_greeting = 0 + self.verbose = 0 + self.no_verbose = 0 + self.quiet = 0 + self.batch = 0 + self.always_trust = 0 + self.rfc1991 = 0 + self.openpgp = 0 + self.force_v3_sigs = 0 + self.no_options = 0 + self.textmode = 0 + + # meta-option booleans + self.meta_pgp_5_compatible = 0 + self.meta_pgp_2_compatible = 0 + self.meta_interactive = 1 + + # strings + self.homedir = None + self.default_key = None + self.comment = None + self.compress_algo = None + self.options = None + + # lists + self.encrypt_to = [] + self.recipients = [] + + # miscellaneous arguments + self.extra_args = [] + + def get_args( self ): + """Generate a list of GnuPG arguments based upon attributes.""" + + return self.get_meta_args() + self.get_standard_args() + self.extra_args + + def get_standard_args( self ): + """Generate a list of standard, non-meta or extra arguments""" + args = [] + if self.homedir != None: args.extend( [ '--homedir', self.homedir ] ) + if self.options != None: args.extend( [ '--options', self.options ] ) + if self.comment != None: args.extend( [ '--comment', self.comment ] ) + if self.compress_algo != None: args.extend( [ '--compress-algo', self.compress_algo ] ) + if self.default_key != None: args.extend( [ '--default-key', self.default_key ] ) + + if self.no_options: args.append( '--no-options' ) + if self.armor: args.append( '--armor' ) + if self.textmode: args.append( '--textmode' ) + if self.no_greeting: args.append( '--no-greeting' ) + if self.verbose: args.append( '--verbose' ) + if self.no_verbose: args.append( '--no-verbose' ) + if self.quiet: args.append( '--quiet' ) + if self.batch: args.append( '--batch' ) + if self.always_trust: args.append( '--always-trust' ) + if self.force_v3_sigs: args.append( '--force-v3-sigs' ) + if self.rfc1991: args.append( '--rfc1991' ) + if self.openpgp: args.append( '--openpgp' ) + + for r in self.recipients: args.extend( [ '--recipient', r ] ) + for r in self.encrypt_to: args.extend( [ '--encrypt-to', r ] ) + + return args + + def get_meta_args( self ): + """Get a list of generated meta-arguments""" + args = [] + + if self.meta_pgp_5_compatible: args.extend( [ '--compress-algo', '1', + '--force-v3-sigs' + ] ) + if self.meta_pgp_2_compatible: args.append( '--rfc1991' ) + if not self.meta_interactive: args.extend( [ '--batch', '--no-tty' ] ) + + return args + + +class Process: + """Objects of this class encompass properties of a GnuPG + process spawned by GnuPG.run(). + + # gnupg is a GnuPG object + process = gnupg.run( [ '--decrypt' ], stdout = 1 ) + out = process.handles['stdout'].read() + ... + os.waitpid( process.pid, 0 ) + + Data Attributes + + handles -- This is a map of filehandle-names to + the file handles, if any, that were requested via run() and hence + are connected to the running GnuPG process. Valid names + of this map are only those handles that were requested. + + pid -- The PID of the spawned GnuPG process. + Useful to know, since once should call + os.waitpid() to clean up the process, especially + if multiple calls are made to run(). + """ + + def __init__(self): + self._pipes = {} + self.handles = {} + self.pid = None + self._waited = None + + def wait(self): + """Wait on the process to exit, allowing for child cleanup. + Will raise an IOError if the process exits non-zero.""" + + e = os.waitpid(self.pid, 0)[1] + if e != 0: + raise IOError, "GnuPG exited non-zero, with code %d" % (e << 8) + --- /usr/local/mm21b5-clean/Mailman/PGPClass.py Sun Nov 24 06:38:38 2002 +++ /usr/local/mailman/Mailman/PGPClass.py Tue Nov 26 02:20:09 2002 @@ -0,0 +1,116 @@ +# -*- python -*- + +# Copyright (C) 1998,1999,2000,2001,2002 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. + +"Class abstraction of PGP" + +import sys +import string +import re +import os + +import PGP +import PGPErrors + +BEGIN_PGP_MSG = "-----BEGIN PGP MESSAGE-----" +END_PGP_MSG = "-----END PGP MESSAGE-----" + +BEGIN_PGP_PUB_RING = "-----BEGIN PGP PUBLIC KEY BLOCK-----" +END_PGP_PUB_RING = "-----END PGP PUBLIC KEY BLOCK-----" + +class GPGMail(PGP.GnuPG): + + #################### + #This function is called a initialization time. + #It justs calls PGP.GnuPG.__init__(self) and self.SetOptions + #################### + def __init__(self, options={}): + PGP.GnuPG.__init__(self) + self.SetOptions(options) + + #################### + #This function is called at initialization time. It sets the options for + #the GPG execution + #################### + def SetOptions(self, options = {}): + self.options.armor = 1 + self.options.meta_interactive = 0 + self.options.extra_args.append('--no-secmem-warning') + + if options.has_key('homedir'): self.options.homedir = \ + (options.get('homedir')) + if options.has_key('no-default-keyring'): \ + self.options.extra_args.append('--no-default-key') + if options.has_key('ignore-time-conflict'): \ + self.options.extra_args.append('--ignore-time-conflict') + if options.has_key('keyring'): + self.options.extra_args.append('--keyring') + self.options.extra_args.append(options.get('keyring')) + + ############################### + #This function will encrypt a string to a list of recipients and + #will either return the PGP Message, raise PGPErrors.PublicKeyNotFoundError + #or one of the PGPErrors. Will raise UnknownGPGError when things are really + #wrong. + ############################### + def EncryptString(self, uncrypted_string, recipients): + self.options.recipients = recipients + self.options.always_trust = 1 + + proc = self.run(['--encrypt'], + create_fhs=['stdin', 'stdout', 'stderr']) + + proc.handles['stdin'].write(uncrypted_string) + proc.handles['stdin'].close() + output = proc.handles['stdout'].read() + proc.handles['stdout'].close() + error = proc.handles['stderr'].read() + proc.handles['stdout'].close() + + try: + proc.wait() + except IOError: + result = re.search("public key not found", error) + if result: + raise PGPErrors.PublicKeyNotFoundError(error) + else: + raise PGPErrors.UnknownGPGError(error) + + return output + + ############################# + #This Function adds a public key ring to the lists' keyring. + #It returns nothing if successful and raises an error if not. + ############################### + def ImportPublicKey(self, ring): + self.options.verbose = 1 + + proc = self.run(['--import'], create_fhs=['stdin', 'stdout', 'stderr']) + + proc.handles['stdin'].write(ring) + proc.handles['stdin'].close() + output = proc.handles['stdout'].read() + proc.handles['stdout'].close() + error = proc.handles['stderr'].read() + proc.handles['stderr'].close() + + try: + proc.wait() + except IOError: + raise PGPErrors.NoValidDataFoundError(error) + else: + return output --- /usr/local/mm21b5-clean/Mailman/PGPErrors.py Sun Nov 24 06:38:38 2002 +++ /usr/local/mailman/Mailman/PGPErrors.py Tue Nov 26 02:20:09 2002 @@ -0,0 +1,74 @@ +# -*- python -*- + +# Copyright (C) 1998,1999,2000,2001,2002 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. + +import sys + +"Error classes raised by PGPClass" + +######################## +#This is the base Error class. +######################## +class Error (Exception): + pass + +######################### +#This Error is raised when the Public Key is not found. +#This is should usually be called when trying to encrypt text to a +#recipient that we don't have the public key for. +######################### +class PublicKeyNotFoundError (Error): + def __init__(self, value): + self.ErrorMessage = value + + +######################## +#This is the Generic GPG error. +#This is just here so we can catch errors that are none of the above. +######################## +class UnknownGPGError (Error): + def __init__(self, value): + self.ErrorMessage = value + +######################### +#This Error Should raised when no valid public key data is found. +#This should be called when AddPublicRing is not giving valid data. +######################### +class NoValidDataFoundError (Error): + def __init__(self, value): + self.ErrorMessage = value + + +######################### +#This Error is raised when no Message is found in a text body +#This should be called when ExtractMessage is given a string that doesn't +#contain a message. +######################### +class NoMessageFoundError (Error): + def __init__(self, value): + self.ErrorMessage = value + +######################### +#This Error Should raised when no Key is found in a text body +#This should be called when ExtractPublicKey is given a string that doesn't +#contain a key. +######################### +class NoKeyFoundError (Error): + def __init__(self, value): + self.ErrorMessage = value + + --- /usr/local/mm21b5-clean/scripts/post Sun Nov 24 06:36:49 2002 +++ /usr/local/mailman/scripts/post Tue Nov 26 02:20:09 2002 @@ -29,6 +29,7 @@ import sys import paths +from Mailman import MailList from Mailman import mm_cfg from Mailman import Utils from Mailman.i18n import _ @@ -58,10 +59,24 @@ # some MTAs have a hard limit to the time a filter prog can run. Postfix # is a good example; if the limit is hit, the proc is SIGKILL'd giving us # no chance to save the message. - inq = get_switchboard(mm_cfg.INQUEUE_DIR) - inq.enqueue(sys.stdin.read(), - listname=listname, - tolist=1, _plaintext=1) + try: + mlist = MailList.MailList(listname, lock=0) + except Errors.MMListError, e: + sys.stderr.write('Mailman error: post got bad listname: %s\n%s' % + (listname, e)) + sys.exit(1) + + #Redirect Message to a secure list to the secure runner + if mlist.IsListSecure(): + inq = get_switchboard(mm_cfg.SECUREQUEUE_DIR) + inq.enqueue(sys.stdin.read(), + listname=listname, + tosecure=1, _plaintext=1) + else: + inq = get_switchboard(mm_cfg.INQUEUE_DIR) + inq.enqueue(sys.stdin.read(), + listname=listname, + tolist=1, _plaintext=1) --- /usr/local/mm21b5-clean/templates/en/listinfo.html Sun Nov 24 06:36:53 2002 +++ /usr/local/mailman/templates/en/listinfo.html Tue Nov 26 02:20:09 2002 @@ -138,6 +138,11 @@ + + + + + --- /usr/local/mm21b5-clean/templates/en/options.html Sun Nov 24 06:36:53 2002 +++ /usr/local/mailman/templates/en/options.html Tue Nov 26 02:20:09 2002 @@ -303,6 +303,20 @@ Set globally + + + PGP Body or PGP Attachment

+ + If the list handles encrypted traffic, do you want to receive the + PGP data in the body of the message or as a PGP-MIME attachment? +
+ + + Body
+ Attachment

+ + +