#! @PYTHON@ # # Copyright (C) 1998-2013 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. """Synchronize a mailing list's membership with a flat file. This script is useful if you have a Mailman mailing list and a sendmail :include: style list of addresses (also as is used in Majordomo). For every address in the file that does not appear in the mailing list, the address is added. For every address in the mailing list that does not appear in the file, the address is removed. Other options control what happens when an address is added or removed. Usage: %(PROGRAM)s [options] -f file listname Where `options' are: --no-change -n Don't actually make the changes. Instead, print out what would be done to the list. --welcome-msg[=] -w[=] Sets whether or not to send the newly added members a welcome message, overriding whatever the list's `send_welcome_msg' setting is. With -w=yes or -w, the welcome message is sent. With -w=no, no message is sent. --goodbye-msg[=] -g[=] Sets whether or not to send the goodbye message to removed members, overriding whatever the list's `send_goodbye_msg' setting is. With -g=yes or -g, the goodbye message is sent. With -g=no, no message is sent. --digest[=] -d[=] Selects whether to make newly added members receive messages in digests. With -d=yes or -d, they become digest members. With -d=no (or if no -d option given) they are added as regular members. --notifyadmin[=] -a[=] Specifies whether the admin should be notified for each subscription or unsubscription. If you're adding a lot of addresses, you definitely want to turn this off! With -a=yes or -a, the admin is notified. With -a=no, the admin is not notified. With no -a option, the default for the list is used. --file -f This option is required. It specifies the flat file to synchronize against. Email addresses must appear one per line. If filename is `-' then stdin is used. --help -h Print this message. listname Required. This specifies the list to synchronize. """ import sys import paths # Import this /after/ paths so that the sys.path is properly hacked import email.Utils from Mailman import MailList from Mailman import Errors from Mailman import Utils from Mailman.UserDesc import UserDesc from Mailman.i18n import _ PROGRAM = sys.argv[0] def usage(code, msg=''): if code: fd = sys.stderr else: fd = sys.stdout print >> fd, _(__doc__) if msg: print >> fd, msg sys.exit(code) def yesno(opt): i = opt.find('=') yesno = opt[i+1:].lower() if yesno in ('y', 'yes'): return 1 elif yesno in ('n', 'no'): return 0 else: usage(1, _('Bad choice: %(yesno)s')) # no return def main(): dryrun = 0 digest = 0 welcome = None goodbye = None filename = None listname = None notifyadmin = None # TBD: can't use getopt with this command line syntax, which is broken and # should be changed to be getopt compatible. i = 1 while i < len(sys.argv): opt = sys.argv[i] if opt in ('-h', '--help'): usage(0) elif opt in ('-n', '--no-change'): dryrun = 1 i += 1 print _('Dry run mode') elif opt in ('-d', '--digest'): digest = 1 i += 1 elif opt.startswith('-d=') or opt.startswith('--digest='): digest = yesno(opt) i += 1 elif opt in ('-w', '--welcome-msg'): welcome = 1 i += 1 elif opt.startswith('-w=') or opt.startswith('--welcome-msg='): welcome = yesno(opt) i += 1 elif opt in ('-g', '--goodbye-msg'): goodbye = 1 i += 1 elif opt.startswith('-g=') or opt.startswith('--goodbye-msg='): goodbye = yesno(opt) i += 1 elif opt in ('-f', '--file'): if filename is not None: usage(1, _('Only one -f switch allowed')) try: filename = sys.argv[i+1] except IndexError: usage(1, _('No argument to -f given')) i += 2 elif opt in ('-a', '--notifyadmin'): notifyadmin = 1 i += 1 elif opt.startswith('-a=') or opt.startswith('--notifyadmin='): notifyadmin = yesno(opt) i += 1 elif opt[0] == '-': usage(1, _('Illegal option: %(opt)s')) else: try: listname = sys.argv[i].lower() i += 1 except IndexError: usage(1, _('No listname given')) break if listname is None or filename is None: usage(1, _('Must have a listname and a filename')) # read the list of addresses to sync to from the file if filename == '-': filemembers = sys.stdin.readlines() else: try: fp = open(filename) except IOError, (code, msg): usage(1, _('Cannot read address file: %(filename)s: %(msg)s')) try: filemembers = fp.readlines() finally: fp.close() # strip out lines we don't care about, they are comments (# in first # non-whitespace) or are blank for i in range(len(filemembers)-1, -1, -1): addr = filemembers[i].strip() if addr == '' or addr[:1] == '#': del filemembers[i] print _('Ignore : %(addr)30s') # first filter out any invalid addresses filemembers = email.Utils.getaddresses(filemembers) invalid = 0 for name, addr in filemembers: try: Utils.ValidateEmail(addr) except Errors.EmailAddressError: print _('Invalid : %(addr)30s') invalid = 1 if invalid: print _('You must fix the preceding invalid addresses first.') sys.exit(1) # get the locked list object try: mlist = MailList.MailList(listname) except Errors.MMListError, e: print _('No such list: %(listname)s') sys.exit(1) try: # Get the list of addresses currently subscribed addrs = {} needsadding = {} matches = {} for addr in mlist.getMemberCPAddresses(mlist.getMembers()): addrs[addr.lower()] = addr for name, addr in filemembers: # Any address found in the file that is also in the list can be # ignored. If not found in the list, it must be added later. laddr = addr.lower() if addrs.has_key(laddr): del addrs[laddr] matches[laddr] = 1 elif not matches.has_key(laddr): needsadding[laddr] = (name, addr) if not needsadding and not addrs: print _('Nothing to do.') sys.exit(0) enc = sys.getdefaultencoding() # addrs contains now all the addresses that need removing for laddr, (name, addr) in needsadding.items(): pw = Utils.MakeRandomPassword() # should not already be subscribed, otherwise our test above is # broken. Bogosity is if the address is listed in the file more # than once. Second and subsequent ones trigger an # MMAlreadyAMember error. Just catch it and go on. userdesc = UserDesc(addr, name, pw, digest) try: if not dryrun: mlist.ApprovedAddMember(userdesc, welcome, notifyadmin) # Avoid UnicodeError if name can't be decoded name = unicode(name, errors='replace').encode(enc, 'replace') s = email.Utils.formataddr((name, addr)).encode(enc, 'replace') print _('Added : %(s)s') except Errors.MMAlreadyAMember: pass except Errors.MembershipIsBanned, pattern: print ('%s:' % addr), _('Banned address (matched %(pattern)s)') for laddr, addr in addrs.items(): # Should be a member, otherwise our test above is broken name = mlist.getMemberName(laddr) or '' if not dryrun: try: mlist.ApprovedDeleteMember(addr, admin_notif=notifyadmin, userack=goodbye) except Errors.NotAMemberError: # This can happen if the address is illegal (i.e. can't be # parsed by email.Utils.parseaddr()) but for legacy # reasons is in the database. Use a lower level remove to # get rid of this member's entry mlist.removeMember(addr) # Avoid UnicodeError if name can't be decoded name = unicode(name, errors='replace').encode(enc, 'replace') s = email.Utils.formataddr((name, addr)).encode(enc, 'replace') print _('Removed: %(s)s') mlist.Save() finally: mlist.Unlock() if __name__ == '__main__': main()