#! @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[=<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)
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
if isinstance(name, str):
name = unicode(name, errors='replace')
name = name.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
if isinstance(name, str):
name = unicode(name, errors='replace')
name = name.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()