aboutsummaryrefslogtreecommitdiffstats
path: root/bin/sync_members
diff options
context:
space:
mode:
Diffstat (limited to 'bin/sync_members')
-rwxr-xr-xbin/sync_members286
1 files changed, 286 insertions, 0 deletions
diff --git a/bin/sync_members b/bin/sync_members
new file mode 100755
index 00000000..1bf9a454
--- /dev/null
+++ b/bin/sync_members
@@ -0,0 +1,286 @@
+#! @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.
+
+"""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[=<yes|no>]
+ -w[=<yes|no>]
+ 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[=<yes|no>]
+ -g[=<yes|no>]
+ 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[=<yes|no>]
+ -d[=<yes|no>]
+ 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[=<yes|no>]
+ -a[=<yes|no>]
+ 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 <filename | ->
+ -f <filename | ->
+ 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)
+
+ # 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)
+ s = email.Utils.formataddr((name, addr)).encode(enc, 'replace')
+ print _('Added : %(s)s')
+ except Errors.MMAlreadyAMember:
+ pass
+
+ for laddr, addr in addrs.items():
+ # Should be a member, otherwise our test above is broken
+ 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)
+ name = mlist.getMemberName(laddr) or ''
+ enc = sys.getdefaultencoding()
+ s = email.Utils.formataddr((name, addr)).encode(enc, 'replace')
+ print _('Removed: %(s)s')
+
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+
+
+if __name__ == '__main__':
+ main()