#!/usr/bin/python ### ### This object represents a collection of Mailing Lists. It ### contains methods for dealing with all lists (such as searching ### through them) and also for retrieving information about a ### particular list (via listname). ### ### It is used by Python CGI and shell scripts on our web server to ### allow them to make changes to Mailman subscribers such as ### subscribing, unsubscribing, changing address, etc. It basically ### provides an abstraction layer, centralizing how those changes are ### "really" done - namely be emailing admin commands to Mailman. ### ### In depends on list subscription files being refreshed hourly from ### the list server (either by email query to Mailman and then saving ### the results, or by doing the query/save on the list server and ### then copying the files over to the web server; see the ### review_and_copy util script). All of these files reside in one ### directory, one file per mailing list, named after the list, and ### the contents of each file are the list's subscribers. ### ### NOTE: This was originally written in 1999 and never intended for ### public consumption. In all likelihood it's inefficient and even ### embarrassingly poorly coded. However cleaning it up is not a ### project I have time for when it's basically working code that's ### "good enough". It's provided because maybe it might be useful to ### you or someone else, not because it's particularly good code. ### ### Anthony R. Thompson, November 1999, updated 2010 for Listserv ### to Mailman conversion project ### Contact: put @ between art and sigilservices.com ### ### Copyright (C) 1998-2010 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. http://www.fsf.org/licensing/licenses/gpl.html ### import os, sys, string, re import EMail, PropertyFile from stat import * from email.Utils import parseaddr class MailmanLists: __instance = None # storage for the instance reference ### ### Implement singleton pattern; based on code found at: ### http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52558 ### def __init__(self): # Check whether we already have an instance if MailmanLists.__instance is None: # Create and remember instance MailmanLists.__instance = MailmanLists.__impl() # Store instance reference as the only member in the handle self.__dict__['_MailmanLists__instance'] = MailmanLists.__instance def __getattr__(self, attr): return getattr(self.__instance, attr) def __setattr__(self, attr, value): return setattr(self.__instance, attr, value) class __impl: ### ### Initialize this class, setting a number of instance variables ### def __init__(self): ### ### Use other modules ### self.email = EMail.EMail() self.propfile = PropertyFile.PropertyFile() ### ### Variables for the whole collection of lists ### self.listadmin = 'listadmin@someplace.org' # INSTALL self.listserv = 'mailman-admin-cmd@lists.someplace.org' # INSTALL self.listdir = '/path/to/list/files/dir' # INSTALL self.listprefix = '^abc-' # INSTALL self.listend = '\.list$' self.listpass = 'LISTPASS' # INSTALL; override w/ set_list_password self.listnames = [] self.listfiles = {} self.listmembers = {} self.listmembernames = {} self.listpasswords = {} # for overridden lists only self.defaultsubpass = 'DEFAULTPASS' # INSTALL ### ### The public mailing lists; if a list isn't here, it's ### assumed to be a private, members-only list ### self.public_lists = self.propfile.contents_as_list( self.listdir + '/public-lists.properties') # one list per line ### ### Lists which require someone to be added manually ### self.manual_add_lists = self.propfile.contents_as_list( self.listdir + '/manual-add-lists.properties') # one per line ### ### E-mail addresses which may not subscribe to our lists ### self.banned_users = self.propfile.contents_as_list( self.listdir + '/banned-users.properties') # one per line ### ### Use the list directory and prefix to get list of lists ### allfiles = os.listdir(self.listdir) listpreobj = re.compile(self.listprefix) listendobj = re.compile(self.listend) for file in allfiles: filepath = self.listdir + '/' + file if (os.path.isfile(filepath)): # if it's a file if (listpreobj.search(file)): if not (listendobj.search(file)): continue listname = listendobj.sub('', file) self.listnames.append(listname) self.listfiles[listname] = filepath ### ### Read list members into memory; example subscriber file line: ### John Smith ### Test Subscriber <10oOnL@hotmail.com> ### for listname in self.listnames: listfilepath = self.listfiles[listname] listfile = open(listfilepath,'r') self.listmembers[listname] = [] self.listmembernames[listname] = {} for line in listfile.readlines(): memname, member = parseaddr(line) self.listmembers[listname].append(string.lower(member)) self.listmembernames[listname][member] = memname def get_listserv(self): return self.listserv ### ### Return a list of all mailing list names ### def get_all_list_names(self): return self.listnames def valid_list_name(self, listname): return (listname in self.listnames) ### ### Given a mailing list name, return its corresponding file path ### def get_list_file_loc(self, listname): return self.listfiles[listname] ### ### Return a list of all mailing list members ### def get_list_members(self, listname): return self.listmembers[listname] ### ### For a given list, return a dictionary of subscriber email ### addresses and their corresponding subscription names ### def get_list_members_and_names(self, listname): return self.listmembernames[listname] ### ### Return whether a given address is a member of a given list ### def member_is_on_list(self, memaddr, listname): return (string.lower(memaddr) in self.listmembers[listname]) ### ### Return whether a given address is on ANY lists (true/false) ### def member_is_on_lists(self, memaddr): return ( len(self.lists_user_is_on(memaddr)) > 0) ### ### Return the list of public lists ### def get_public_lists(self): return self.public_lists ### ### Return the list of the private, members-only lists; assumes ### that if it's not a public list from get_public_lists(), it's ### a private list. ### def get_private_lists(self): private_lists = [] public_lists = self.get_public_lists() for list_name in self.get_all_list_names(): if list_name not in public_lists: private_lists.append(list_name) return private_lists ### ### Return whether a given list is public or private (members only) ### def list_is_public(self, listname): return (listname in self.public_lists) ### ### Return the list of manual addition only lists ### def get_manual_add_lists(self): return self.manual_add_lists ### ### Return whether a given list is manual add only (ultra-private) ### def list_is_manual_add(self, listname): return (listname in self.manual_add_lists) ### ### Return the list of banned users ### def get_banned_users(self): return self.banned_users ### ### Return whether a given address is banned ### def address_is_banned(self, address): return (address in self.banned_users) ### ### Set a password for a list which is different than the default ### def set_list_password(self, listname, password): self.listpasswords[listname] = password ### ### Get the password for a list; unless overridden, return default ### def get_list_password(self, listname): if self.listpasswords.has_key(listname): return self.listpasswords[listname] else: return self.listpass ### ### Get the default list password ### def get_default_list_password(self): return self.listpass ### ### Send a given command to the Listserver ### def send_listserv_command(self, command, \ desc='Mailman Command ADMINSUBJECTPASSWD', fromaddr=''): # INSTALL if (fromaddr == ''): fromaddr = self.listadmin self.email.send_mail(command, desc, self.listserv, fromaddr) ### ### Send a who (aka review) command to the Listserver ### def who(self, listname, whofrom=''): if (whofrom == ''): whofrom = self.listadmin pw = self.get_list_password(listname) cmd = "who %s %s" % (listname, pw) self.send_listserv_command(cmd, fromaddr=whofrom) ### ### Add someone to a given mailing list; if they are a member, ### also set their password for the list to their website one ### def add_member_to_list(self, listname, email, subname='Subscriber', options=''): if (re.search('DIGEST', options, re.IGNORECASE)): digest = 'digest' else: digest = 'nodigest' pw = self.get_list_password(listname) cmd = "subscribe %s %s %s %s %s" % (listname, pw, email, \ digest, subname) cmd += "\npassword %s %s %s %s" % (listname, pw, email, \ self.defaultsubpass) # Our code normally checks whether someone is a member # and, if so, whether they've created an account on our # website; if so, it uses the password for their Mailman # list subscription(s), if not, it uses a default password # for them which is shared by all subscribers. Yes, # that's probably not smart, but it's something that's # necessary with 70+ lists. The point is, you need to # have a password strategy for your subscribers! I am # leaving this code in, commented, to show you our logic. #import Members #members = Members.Members() #memid = members.get_id_by_email(email) #if (memid <> ''): # import MemberLogins # memlogins = MemberLogins.MemberLogins() # memloginid = memlogins.get_member_login_id_by_member_id(memid) # mlpass = memlogins.get_password(memloginid) # cmd += "\npassword %s %s %s %s" % (listname, pw, email,mlpass) #else: # cmd += "\npassword %s %s %s %s" % (listname, pw, email, \ # self.defaultsubpass) self.send_listserv_command(cmd) ### ### Remove someone from a given mailing list ### def delete_member_from_list(self, listname, email): pw = self.get_list_password(listname) cmd = "unsubscribe %s %s %s" % (listname, pw, email) self.send_listserv_command(cmd) ### ### Remove someone from one or more mailing lists ### def delete_member_from_lists(self, lists, email): cmds = [] for listname in lists: pw = self.get_list_password(listname) if email in self.get_list_members(listname): cmds.append("unsubscribe %s %s %s" % (listname, pw, email)) if (len(cmds) > 0): self.send_listserv_command("\n".join(cmds)) ### ### Given an e-mail address, remove it from ### all the mailing lists it is subscribed to. ### def delete_member_from_all_lists(self, email): pw = self.get_default_list_password() cmd = "unsubscribe all %s %s" % (pw, email) self.send_listserv_command(cmd) ### ### Given an e-mail address, remove it from ### the private mailing lists it is subscribed to. ### def delete_member_from_private_lists(self, email): cmds = [] for listname in self.get_private_lists(): pw = self.get_list_password(listname) if email in self.get_list_members(listname): cmds.append("unsubscribe %s %s %s" % (listname, pw, email)) if (len(cmds) > 0): self.send_listserv_command("\n".join(cmds)) ### ### Change someone's subscription address on a mailing list ### def change_member_on_list(self, listname, oldaddr, newaddr): pw = self.get_list_password(listname) cmd = "change %s %s %s %s" % (listname, pw, oldaddr, newaddr) self.send_listserv_command(cmd) # Translate some of the old Listserv options to new Mailman ones def listserv_to_mailman_option(self, lsvopt): if (lsvopt == 'digest'): mmopt = 'digest plain' elif (lsvopt == 'nodigest'): mmopt = 'digest off' elif (lsvopt == 'mime'): mmopt = 'digest mime' elif (lsvopt == 'nomime'): mmopt = 'digest plain' elif (lsvopt == 'mail'): mmopt = 'delivery on' elif (lsvopt == 'nomail'): mmopt = 'delivery off' elif (lsvopt == 'review'): mmopt = 'mod on' elif (lsvopt == 'noreview'): mmopt = 'mod off' elif (lsvopt == 'nopost'): mmopt = 'mod on' elif (lsvopt == 'post'): mmopt = 'mod off' elif (lsvopt == 'ack'): mmopt = 'ack on' elif (lsvopt == 'noack'): mmopt = 'ack off' # always want off elif (lsvopt == 'repro'): mmopt = 'myposts on' # always on elif (lsvopt == 'norepro'): mmopt = 'myposts off' elif (lsvopt == 'conceal'): mmopt = 'hide on' elif (lsvopt == 'noconceal'): mmopt = 'hide off' # always off else: mmopt = lsvopt return mmopt ### ### Set someone as a particular option on a mailing list. We ### only allow setting mail/nomail and digest/nodigest ### def set_member_option_on_list(self, listname, email, lsvopt): pw = self.get_list_password(listname) mmopt = self.listserv_to_mailman_option(lsvopt) cmd = "set %s %s %s %s" % (listname, pw, email, mmopt) self.send_listserv_command(cmd) ### ### Given an e-mail address, set on option on all the mailing ### lists it is subscribed to. Used to just call ### set_member_option_on_list function repeatedly but that ### was inefficient (generating an email for each list) and ### caused server timeouts, so now this function batches up ### all the SET commands and sends one big email with one ### command per line. ### def set_member_option_on_all_lists(self, email, lsvopt): pw = self.get_default_list_password() mmopt = self.listserv_to_mailman_option(lsvopt) cmd = "set all %s %s %s" % (pw, email, mmopt) self.send_listserv_command(cmd) ### ### Given an e-mail address, an option, and and list of ### e-mail lists to act on, set the option all the mailing ### lists it is subscribed to. ### def set_member_option_on_lists(self, email, lsvopt, lists): if (len(lists) == 1): if (string.lower(lists[0]) == 'all'): self.set_member_option_on_all_lists(email, lsvopt) else: self.set_member_option_on_list(lists[0], email, lsvopt) else: mmopt = self.listserv_to_mailman_option(lsvopt) cmds = [] for listname in lists: pw = self.get_list_password(listname) cmds.append("set %s %s %s %s" % (listname,pw,email,mmopt)) self.send_listserv_command("\n".join(cmds)) ### ### Return a list of mailing lists a user is on ### def lists_user_is_on(self,email): subbed = [] for list in self.get_all_list_names(): if self.member_is_on_list(email,list): subbed.append(list) return subbed ### ### Change someone's subscription on all lists they're on. ### Like set_member_option_on_all_lists, this used to call ### change_member_on_list once for each list, but that was ### inefficient and caused the script to timeout and be ### killed, producing a server error. So now it batches up ### all the CHANGE commands and sends one big email with ### one command per line. ### def change_member_on_all_lists(self, oldaddr, newaddr): pw = self.get_default_list_password() cmd = "change all %s %s %s" % (pw, oldaddr, newaddr) self.send_listserv_command(cmd) ### ### Get unique addresses on all, public, or private lists ### def get_unique_subscribers(self, list_type): subscribers = [] if (list_type == 'public'): lists = self.get_public_lists() elif (list_type == 'private'): lists = self.get_private_lists() else: lists = self.get_all_list_names() for list_name in lists: for subscriber in self.get_list_members(list_name): lcsubscriber = string.lower(subscriber) if (lcsubscriber not in subscribers): subscribers.append(lcsubscriber) return subscribers ### ### Add or change the Listserv password associated with an email ### The person will get a confirmation OK e-mail UNLESS the old ### password is given, in which case it will be blissfully silent ### def add_or_change_user_password(self, email, password, oldpass=''): pw = self.get_default_list_password() cmd = "password all %s %s %s" % (pw, email, password) self.send_listserv_command(cmd) ### ### Make it so that the only subscribers to the given list ### are those email addresses in the "sync_subscribers" list; ### remove all others. Returns a tuple, the first element of ### which is the list of those who were added, and the second ### is the lost of those removed. Then the caller can use ### the added and removed lists to send out notices, if nec. ### def sync_subscribers(self, listname, sync_subscribers): added = [] removed = [] # This next section is a bit ugly but is here because it's # pretty inefficient to have to look through the # subscribers one address at a time with a "not in", # versus a simple dictionary lookup; but it does make the # code uglier :( cur_subs_dict = {} for cur_sub_email in self.get_list_members(listname): cur_subs_dict[cur_sub_email] = 1 # already lower sync_subs_dict = {} for sync_email in sync_subscribers: sync_subs_dict[sync_email.lower()] = 1 # Now take care of the adds (those who were given to us as # must be on, but who aren't currently on - add them). # Some parts of this are commented out because normally we # get the name from our membership database, which you # won't have; you'll need some other way of getting names, # maybe change sync_subscribers to email->name dictionary #import Members #members = Members.Members() for must_be_on_addr in sync_subs_dict.keys(): if (not cur_subs_dict.has_key(must_be_on_addr)): #memid = members.get_id_by_email(must_be_on_addr) #subname = members.get_preferred_name(memid) subname = 'List Subscriber' # see comment above self.add_member_to_list(listname, must_be_on_addr, subname) added.append(must_be_on_addr) # could add throttling, time.sleep(5), here if nec. # Now take care of the removals (those who were not in the # list given to us of those that must be on, but who are # currently on anyway - remove them) for cur_sub_addr in cur_subs_dict.keys(): if (not sync_subs_dict.has_key(cur_sub_addr)): self.delete_member_from_list(listname, cur_sub_addr) removed.append(cur_sub_addr) # could add throttling, time.sleep(5), here if nec. return (added, removed) if __name__ == '__main__': ml = MailmanLists() #simple test code would go here print "Done."