Xnet users should login with their curretn email address.
[platal.git] / bin / lists.rpc.py
CommitLineData
0337d704 1#!/usr/bin/env python
2#***************************************************************************
12262f13 3#* Copyright (C) 2003-2011 Polytechnique.org *
0337d704 4#* http://opensource.polytechnique.org/ *
5#* *
6#* This program is free software; you can redistribute it and/or modify *
7#* it under the terms of the GNU General Public License as published by *
8#* the Free Software Foundation; either version 2 of the License, or *
9#* (at your option) any later version. *
10#* *
11#* This program is distributed in the hope that it will be useful, *
12#* but WITHOUT ANY WARRANTY; without even the implied warranty of *
13#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14#* GNU General Public License for more details. *
15#* *
16#* You should have received a copy of the GNU General Public License *
17#* along with this program; if not, write to the Free Software *
18#* Foundation, Inc., *
19#* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
20#***************************************************************************
21
22import base64, MySQLdb, os, getopt, sys, sha, signal, re, shutil, ConfigParser
23import MySQLdb.converters
24import SocketServer
eb07e9c4
RB
25import errno
26import traceback
0337d704 27
28sys.path.append('/usr/lib/mailman/bin')
29
30from pwd import getpwnam
31from grp import getgrnam
32
33from SimpleXMLRPCServer import SimpleXMLRPCServer
34from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler
35
36import paths
37from Mailman import MailList
38from Mailman import Utils
39from Mailman import Message
40from Mailman import Errors
41from Mailman import mm_cfg
42from Mailman import i18n
43from Mailman.UserDesc import UserDesc
44from Mailman.ListAdmin import readMessage
45from email.Iterators import typed_subpart_iterator
46from threading import Lock
47
48class AuthFailed(Exception): pass
49
50################################################################################
51#
52# CONFIG
53#
54#------------------------------------------------
55
56config = ConfigParser.ConfigParser()
78dd3eb2 57config.read(os.path.dirname(__file__)+'/../configs/platal.ini')
0337d704 58config.read(os.path.dirname(__file__)+'/../configs/platal.conf')
59
ae6c293b 60def get_config(sec, val, default=None):
0337d704 61 try:
62 return config.get(sec, val)[1:-1]
63 except ConfigParser.NoOptionError, e:
64 if default is None:
849baea6 65 sys.stderr.write('%s\n' % str(e))
0337d704 66 sys.exit(1)
67 else:
68 return default
69
0337d704 70MYSQL_USER = get_config('Core', 'dbuser')
71MYSQL_PASS = get_config('Core', 'dbpwd')
7660a7c7 72MYSQL_DB = get_config('Core', 'dbdb')
0337d704 73
74PLATAL_DOMAIN = get_config('Mail', 'domain')
75PLATAL_DOMAIN2 = get_config('Mail', 'domain2', '')
849baea6 76sys.stderr.write('PLATAL_DOMAIN = %s\n' % PLATAL_DOMAIN )
3e3d853e 77sys.stderr.write("MYSQL_DB = %s\n" % MYSQL_DB)
0337d704 78
0337d704 79VHOST_SEP = get_config('Lists', 'vhost_sep', '_')
80ON_CREATE_CMD = get_config('Lists', 'on_create', '')
81
1fec3393
FB
82SRV_HOST = get_config('Lists', 'rpchost', 'localhost')
83SRV_PORT = int(get_config('Lists', 'rpcport', '4949'))
84
0337d704 85################################################################################
86#
87# CLASSES
88#
89#------------------------------------------------
90# Manage Basic authentication
91#
92
93class BasicAuthXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
94
95 """XMLRPC Request Handler
96 This request handler is used to provide BASIC HTTP user authentication.
97 It first overloads the do_POST() function, authenticates the user, then
98 calls the super.do_POST().
99
100 Moreover, we override _dispatch, so that we call functions with as first
101 argument a UserDesc taken from the database, containing name, email and perms
102 """
103
c1bae0aa
FB
104 def _get_function(self, method):
105 try:
106 # check to see if a matching function has been registered
107 return self.server.funcs[method]
108 except:
109 raise Exception('method "%s" is not supported' % method)
110
c6aeb88a
FB
111 def is_rpc_path_valid(self):
112 return True
c1bae0aa 113
ae6c293b 114 def _dispatch(self, method, params):
c1bae0aa 115 return list_call_dispatcher(self._get_function(method), self.data[0], self.data[1], self.data[2], *params)
0337d704 116
117 def do_POST(self):
118 try:
119 _, auth = self.headers["authorization"].split()
120 uid, md5 = base64.decodestring(auth).strip().split(':')
121 vhost = self.path.split('/')[1].lower()
ae6c293b 122 self.data = self.getUser(uid, md5, vhost)
0337d704 123 if self.data is None:
124 raise AuthFailed
125 # Call super.do_POST() to do the actual work
126 SimpleXMLRPCRequestHandler.do_POST(self)
127 except:
128 self.send_response(401)
129 self.end_headers()
130
131 def getUser(self, uid, md5, vhost):
337a6acf 132 res = mysql_fetchone ("""SELECT a.full_name, IF(s.email IS NULL, a.email, CONCAT(s.email, '@%s')),
2ca5f031
FB
133 IF (a.is_admin, 'admin',
134 IF(FIND_IN_SET('lists', at.perms) OR FIND_IN_SET('lists', a.user_perms), 'lists', NULL))
7660a7c7 135 FROM accounts AS a
2ca5f031 136 INNER JOIN account_types AS at ON (at.type = a.type)
337a6acf 137 LEFT JOIN email_source_account AS s ON (s.uid = a.uid AND s.type = 'forlife')
7660a7c7
FB
138 WHERE a.uid = '%s' AND a.password = '%s' AND a.state = 'active'
139 LIMIT 1""" \
2ca5f031 140 % (PLATAL_DOMAIN, uid, md5))
0337d704 141 if res:
ae6c293b 142 name, forlife, perms = res
9b12102b 143 if vhost != PLATAL_DOMAIN and perms != 'admin':
2ca5f031 144 res = mysql_fetchone ("""SELECT m.uid, IF(m.perms = 'admin', 'admin', 'lists')
7660a7c7
FB
145 FROM group_members AS m
146 INNER JOIN groups AS g ON (m.asso_id = g.id)
2ca5f031 147 WHERE uid = '%s' AND mail_domain = '%s'""" \
7660a7c7
FB
148 % (uid, vhost))
149 if res:
2ca5f031
FB
150 _, perms = res
151 userdesc = UserDesc(forlife, name, None, 0)
ae6c293b 152 return (userdesc, perms, vhost)
0337d704 153 else:
168ca63b 154 print >> sys.stderr, "no user found for uid: %s, passwd: %s" % (uid, md5)
0337d704 155 return None
ae6c293b 156
0337d704 157################################################################################
158#
159# XML RPC STUFF
160#
161#-------------------------------------------------------------------------------
162# helpers
163#
164
165def connectDB():
166 db = MySQLdb.connect(
7660a7c7 167 db=MYSQL_DB,
0337d704 168 user=MYSQL_USER,
169 passwd=MYSQL_PASS,
6bd94db9 170 unix_socket='/var/run/mysqld/mysqld.sock')
0337d704 171 db.ping()
31ddf875 172 db.autocommit(True)
0337d704 173 return db.cursor()
174
175def mysql_fetchone(query):
176 ret = None
177 try:
178 lock.acquire()
179 mysql.execute(query)
180 if int(mysql.rowcount) > 0:
181 ret = mysql.fetchone()
182 finally:
183 lock.release()
184 return ret
185
ae6c293b 186def is_admin_on(userdesc, perms, mlist):
0337d704 187 return ( perms == 'admin' ) or ( userdesc.address in mlist.owner )
188
189
ae6c293b 190def quote(s, is_header=False):
0337d704 191 if is_header:
ae6c293b 192 h = Utils.oneline(s, 'iso-8859-1')
0337d704 193 else:
194 h = s
ea626742 195 h = str('').join(re.split('[\x00-\x08\x0B-\x1f]+', h))
25112e7a 196 return Utils.uquote(h.replace('&', '&amp;').replace('>', '&gt;').replace('<', '&lt;'))
0337d704 197
198def to_forlife(email):
199 try:
ae6c293b 200 mbox, fqdn = email.split('@')
0337d704 201 except:
202 mbox = email
203 fqdn = PLATAL_DOMAIN
204 if ( fqdn == PLATAL_DOMAIN ) or ( fqdn == PLATAL_DOMAIN2 ):
94b64368 205 res = mysql_fetchone("""SELECT CONCAT(s1.email, '@%s'), a.full_name
7660a7c7 206 FROM accounts AS a
337a6acf
SJ
207 INNER JOIN email_source_account AS s1 ON (a.uid = s1.uid AND s1.type = 'forlife')
208 INNER JOIN email_source_account AS s2 ON (a.uid = s2.uid AND s2.email = '%s')
7660a7c7
FB
209 WHERE a.state = 'active'
210 LIMIT 1""" \
211 % (PLATAL_DOMAIN, mbox))
0337d704 212 if res:
213 return res
214 else:
ae6c293b 215 return (None, None)
cf5e8ef1 216 return (email.lower(), mbox)
0337d704 217
218##
219# see /usr/lib/mailman/bin/rmlist
220##
221def remove_it(listname, filename):
222 if os.path.islink(filename) or os.path.isfile(filename):
223 os.unlink(filename)
224 elif os.path.isdir(filename):
225 shutil.rmtree(filename)
ae6c293b 226
c1bae0aa
FB
227##
228# Call dispatcher
229##
230
231def has_annotation(method, name):
232 """ Check if the method contains the given annoation.
233 """
234 return method.__doc__ and method.__doc__.find("@%s" % name) > -1
235
236def list_call_dispatcher(method, userdesc, perms, vhost, *arg):
237 """Dispatch the call to the right handler.
238 This function checks the options of the called method the set the environment of the call.
239 The dispatcher uses method annotation (special tokens in the documentation of the method) to
240 guess the requested environment:
241 @mlist: the handler requires a mlist object instead of the vhost/listname couple
242 @lock: the handler requires the mlist to be locked (@mlist MUST be specified)
243 @edit: the handler edit the mlist (@mlist MUST be specified)
244 @admin: the handler requires admin rights on the list (@mlist MUST be specified)
245 @root: the handler requires site admin rights
246 """
247 try:
168ca63b 248 print >> sys.stderr, "calling method: %s" % method
c1bae0aa
FB
249 if has_annotation(method, "root") and perms != "admin":
250 return 0
251 if has_annotation(method, "mlist"):
079b7d91 252 listname = str(arg[0])
c1bae0aa
FB
253 arg = arg[1:]
254 mlist = MailList.MailList(vhost + VHOST_SEP + listname.lower(), lock=0)
255 if has_annotation(method, "admin") and not is_admin_on(userdesc, perms, mlist):
256 return 0
257 if has_annotation(method, "edit") or has_annotation(method, "lock"):
258 return list_call_locked(method, userdesc, perms, mlist, has_annotation(method, "edit"), *arg)
259 else:
260 return method(userdesc, perms, mlist, *arg)
261 else:
262 return method(userdesc, perms, vhost, *arg)
263 except Exception, e:
fc240413 264 sys.stderr.write('Exception in dispatcher %s\n' % str(e))
c1bae0aa
FB
265 raise e
266 return 0
267
268def list_call_locked(method, userdesc, perms, mlist, edit, *arg):
269 """Call the given method after locking the mlist.
270 """
271 try:
272 mlist.Lock()
273 ret = method(userdesc, perms, mlist, *arg)
274 if edit:
275 mlist.Save()
276 mlist.Unlock()
277 return ret
fc240413 278 except Exception, e:
eb07e9c4 279 traceback.print_exc(file=sys.stderr)
fc240413 280 sys.stderr.write('Exception in locked call %s: %s\n' % (method.__name__, str(e)))
c1bae0aa
FB
281 mlist.Unlock()
282 return 0
283 # TODO: use finally when switching to python 2.5
284
0337d704 285#-------------------------------------------------------------------------------
286# helpers on lists
287#
288
849baea6 289def is_subscription_pending(userdesc, perms, mlist):
c1bae0aa
FB
290 for id in mlist.GetSubscriptionIds():
291 if userdesc.address == mlist.GetRecord(id)[1]:
292 return True
293 return False
294
ae6c293b 295def get_list_info(userdesc, perms, mlist, front_page=0):
0337d704 296 members = mlist.getRegularMemberKeys()
297 is_member = userdesc.address in members
ae6c293b 298 is_owner = userdesc.address in mlist.owner
2ca5f031 299 if (mlist.advertised and perms in ('lists', 'admin')) or is_member or is_owner or (not front_page and perms == 'admin'):
0337d704 300 is_pending = False
301 if not is_member and (mlist.subscribe_policy > 1):
849baea6 302 is_pending = list_call_locked(is_subscription_pending, userdesc, perms, mlist, False)
c1bae0aa 303 if is_pending is 0:
5f0ac25e 304 return None
0337d704 305
306 host = mlist.internal_name().split(VHOST_SEP)[0].lower()
307 details = {
308 'list' : mlist.real_name,
309 'addr' : mlist.real_name.lower() + '@' + host,
310 'host' : host,
311 'desc' : quote(mlist.description),
312 'info' : quote(mlist.info),
313 'diff' : (mlist.default_member_moderation>0) + (mlist.generic_nonmember_action>0),
314 'ins' : mlist.subscribe_policy > 1,
ae6c293b 315 'priv' : 1-mlist.advertised,
0337d704 316 'sub' : 2*is_member + is_pending,
317 'own' : is_owner,
318 'nbsub': len(members)
319 }
ae6c293b 320 return (details, members)
ae525b0e 321 return None
0337d704 322
c1bae0aa
FB
323def get_options(userdesc, perms, mlist, opts):
324 """ Get the options of a list.
325 @mlist
326 @admin
327 """
328 options = { }
329 for (k, v) in mlist.__dict__.iteritems():
330 if k in opts:
331 if type(v) is str:
332 options[k] = quote(v)
333 else: options[k] = v
334 details = get_list_info(userdesc, perms, mlist)[0]
335 return (details, options)
336
d36b2def 337def set_options(userdesc, perms, mlist, opts, vals):
c1bae0aa
FB
338 for (k, v) in vals.iteritems():
339 if k not in opts:
340 continue
341 if k == 'default_member_moderation':
342 for member in mlist.getMembers():
343 mlist.setMemberOption(member, mm_cfg.Moderate, int(v))
344 t = type(mlist.__dict__[k])
345 if t is bool: mlist.__dict__[k] = bool(v)
346 elif t is int: mlist.__dict__[k] = int(v)
347 elif t is str: mlist.__dict__[k] = Utils.uncanonstr(v, 'fr')
348 else: mlist.__dict__[k] = v
349 return 1
0337d704 350
351#-------------------------------------------------------------------------------
352# users procedures for [ index.php ]
353#
354
ae6c293b 355def get_lists(userdesc, perms, vhost, email=None):
c1bae0aa
FB
356 """ List available lists for the given vhost
357 """
0337d704 358 if email is None:
359 udesc = userdesc
360 else:
cf5e8ef1 361 udesc = UserDesc(email.lower(), email.lower(), None, 0)
0337d704 362 prefix = vhost.lower()+VHOST_SEP
363 names = Utils.list_names()
364 names.sort()
365 result = []
366 for name in names:
367 if not name.startswith(prefix):
368 continue
369 try:
ae6c293b 370 mlist = MailList.MailList(name, lock=0)
0337d704 371 except:
372 continue
373 try:
ae525b0e
FB
374 details = get_list_info(udesc, perms, mlist, (email is None and vhost == PLATAL_DOMAIN))
375 if details is not None:
376 result.append(details[0])
849baea6
FB
377 except Exception, e:
378 sys.stderr.write('Can\'t get list %s: %s\n' % (name, str(e)))
0337d704 379 continue
380 return result
381
c1bae0aa
FB
382def subscribe(userdesc, perms, mlist):
383 """ Subscribe to a list.
384 @mlist
385 @edit
386 """
387 if ( mlist.subscribe_policy in (0, 1) ) or userdesc.address in mlist.owner:
388 mlist.ApprovedAddMember(userdesc)
389 result = 2
390 else:
391 result = 1
392 try:
393 mlist.AddMember(userdesc)
394 except Errors.MMNeedApproval:
395 pass
0337d704 396 return result
397
c1bae0aa 398def unsubscribe(userdesc, perms, mlist):
61d4544b 399 """ Unsubscribe from a list
c1bae0aa
FB
400 @mlist
401 @edit
402 """
403 mlist.ApprovedDeleteMember(userdesc.address)
404 return 1
0337d704 405
406#-------------------------------------------------------------------------------
407# users procedures for [ index.php ]
408#
409
79a9ca23 410def get_name(member):
411 try:
412 return quote(mlist.getMemberName(member))
413 except:
79a9ca23 414 return ''
415
c1bae0aa
FB
416def get_members(userdesc, perms, mlist):
417 """ List the members of a list.
418 @mlist
419 """
5f0ac25e
FB
420 infos = get_list_info(userdesc, perms, mlist)
421 if infos is None:
89cf6882
FB
422 # Do not return None, this is not serializable
423 return 0
5f0ac25e 424 details, members = infos
c1bae0aa
FB
425 members.sort()
426 members = map(lambda member: (get_name(member), member), members)
427 return (details, members, mlist.owner)
428
0337d704 429
430#-------------------------------------------------------------------------------
431# users procedures for [ trombi.php ]
432#
433
c1bae0aa
FB
434def get_members_limit(userdesc, perms, mlist, page, nb_per_page):
435 """ Get a range of members of the list.
436 @mlist
437 """
438 members = get_members(userdesc, perms, mlist)[1]
0337d704 439 i = int(page) * int(nb_per_page)
440 return (len(members), members[i:i+int(nb_per_page)])
441
c1bae0aa
FB
442def get_owners(userdesc, perms, mlist):
443 """ Get the owners of the list.
444 @mlist
445 """
446 details, members, owners = get_members(userdesc, perms, mlist)
ae6c293b 447 return (details, owners)
0337d704 448
c1bae0aa 449
0337d704 450#-------------------------------------------------------------------------------
451# owners procedures [ admin.php ]
452#
453
c1bae0aa
FB
454def replace_email(userdesc, perms, mlist, from_email, to_email):
455 """ Replace the address of a member by another one.
456 @mlist
457 @edit
458 @admin
459 """
460 mlist.ApprovedChangeMemberAddress(from_email.lower(), to_email.lower(), 0)
461 return 1
ae6c293b 462
c1bae0aa
FB
463def mass_subscribe(userdesc, perms, mlist, users):
464 """ Add a list of users to the list.
465 @mlist
466 @edit
467 @admin
468 """
6876570a
FB
469 if not isinstance(users, list):
470 raise Exception("userlist must be a list")
c1bae0aa
FB
471 members = mlist.getRegularMemberKeys()
472 added = []
c1bae0aa
FB
473 for user in users:
474 email, name = to_forlife(user)
475 if ( email is None ) or ( email in members ):
476 continue
477 userd = UserDesc(email, name, None, 0)
478 mlist.ApprovedAddMember(userd)
479 added.append( (quote(userd.fullname), userd.address) )
0337d704 480 return added
481
c1bae0aa
FB
482def mass_unsubscribe(userdesc, perms, mlist, users):
483 """ Remove a list of users from the list.
484 @mlist
485 @edit
486 @admin
487 """
488 map(lambda user: mlist.ApprovedDeleteMember(user), users)
0337d704 489 return users
490
c1bae0aa
FB
491def add_owner(userdesc, perms, mlist, user):
492 """ Add a owner to the list.
493 @mlist
494 @edit
495 @admin
496 """
497 email = to_forlife(user)[0]
498 if email is None:
0337d704 499 return 0
c1bae0aa
FB
500 if email not in mlist.owner:
501 mlist.owner.append(email)
0337d704 502 return True
503
c1bae0aa
FB
504def del_owner(userdesc, perms, mlist, user):
505 """ Remove a owner of the list.
506 @mlist
507 @edit
508 @admin
509 """
510 if len(mlist.owner) < 2:
0337d704 511 return 0
c1bae0aa 512 mlist.owner.remove(user)
0337d704 513 return True
514
515#-------------------------------------------------------------------------------
516# owners procedures [ admin.php ]
517#
518
c1bae0aa
FB
519def get_pending_ops(userdesc, perms, mlist):
520 """ Get the list of operation waiting for an action from the owners.
521 @mlist
522 @lock
523 @admin
524 """
525 subs = []
526 seen = []
527 dosave = False
528 for id in mlist.GetSubscriptionIds():
529 time, addr, fullname, passwd, digest, lang = mlist.GetRecord(id)
530 if addr in seen:
531 mlist.HandleRequest(id, mm_cfg.DISCARD)
532 dosave = True
533 continue
534 seen.append(addr)
535 try:
536 login = re.match("^[^.]*\.[^.]*\.\d\d\d\d$", addr.split('@')[0]).group()
537 subs.append({'id': id, 'name': quote(fullname), 'addr': addr, 'login': login })
538 except:
539 subs.append({'id': id, 'name': quote(fullname), 'addr': addr })
0337d704 540
c1bae0aa
FB
541 helds = []
542 for id in mlist.GetHeldMessageIds():
543 ptime, sender, subject, reason, filename, msgdata = mlist.GetRecord(id)
0337d704 544 fpath = os.path.join(mm_cfg.DATA_DIR, filename)
c1bae0aa
FB
545 try:
546 size = os.path.getsize(fpath)
547 except OSError, e:
548 if e.errno <> errno.ENOENT: raise
549 continue
550 try:
551 msg = readMessage(fpath)
552 fromX = msg.has_key("X-Org-Mail")
553 except:
554 pass
555 helds.append({
556 'id' : id,
0337d704 557 'sender': quote(sender, True),
558 'size' : size,
559 'subj' : quote(subject, True),
560 'stamp' : ptime,
c1bae0aa
FB
561 'fromx' : fromX
562 })
563 if dosave:
564 mlist.Save()
565 return (subs, helds)
566
567def handle_request(userdesc, perms, mlist, id, value, comment):
568 """ Handle a moderation request.
569 @mlist
570 @edit
571 @admin
572 """
eb07e9c4
RB
573 # Force encoding to mailman's default for french, since this is what
574 # Mailman will use internally
575 # LC_DESCRIPTIONS is a dict of lang => (name, charset, direction) tuples.
576 encoding = mm_cfg.LC_DESCRIPTIONS['fr'][1]
9896b9cf 577 comment = comment.encode(encoding, 'replace')
c1bae0aa
FB
578 mlist.HandleRequest(int(id), int(value), comment)
579 return 1
580
581def get_pending_sub(userdesc, perms, mlist, id):
582 """ Get informations about a given subscription moderation.
583 @mlist
584 @lock
585 @admin
586 """
587 sub = 0
588 id = int(id)
589 if id in mlist.GetSubscriptionIds():
590 time, addr, fullname, passwd, digest, lang = mlist.GetRecord(id)
591 try:
592 login = re.match("^[^.]*\.[^.]*\.\d\d\d\d$", addr.split('@')[0]).group()
593 sub = {'id': id, 'name': quote(fullname), 'addr': addr, 'login': login }
594 except:
595 sub = {'id': id, 'name': quote(fullname), 'addr': addr }
596 return sub
597
598def get_pending_mail(userdesc, perms, mlist, id, raw=0):
599 """ Get informations about a given mail moderation.
600 @mlist
601 @lock
602 @admin
603 """
604 ptime, sender, subject, reason, filename, msgdata = mlist.GetRecord(int(id))
605 fpath = os.path.join(mm_cfg.DATA_DIR, filename)
606 size = os.path.getsize(fpath)
607 msg = readMessage(fpath)
608
609 if raw:
610 return quote(str(msg))
611 results_plain = []
612 results_html = []
613 for part in typed_subpart_iterator(msg, 'text', 'plain'):
614 c = part.get_payload()
615 if c is not None: results_plain.append (c)
616 results_plain = map(lambda x: quote(x), results_plain)
617 for part in typed_subpart_iterator(msg, 'text', 'html'):
618 c = part.get_payload()
619 if c is not None: results_html.append (c)
620 results_html = map(lambda x: quote(x), results_html)
621 return {'id' : id,
622 'sender': quote(sender, True),
623 'size' : size,
624 'subj' : quote(subject, True),
625 'stamp' : ptime,
626 'parts_plain' : results_plain,
627 'parts_html': results_html }
0337d704 628
629#-------------------------------------------------------------------------------
630# owner options [ options.php ]
631#
632
633owner_opts = ['accept_these_nonmembers', 'admin_notify_mchanges', 'description', \
634 'default_member_moderation', 'generic_nonmember_action', 'info', \
635 'subject_prefix', 'goodbye_msg', 'send_goodbye_msg', 'subscribe_policy', \
636 'welcome_msg']
637
c1bae0aa
FB
638def get_owner_options(userdesc, perms, mlist):
639 """ Get the owner options of a list.
640 @mlist
641 @admin
642 """
643 return get_options(userdesc, perms, mlist, owner_opts)
0337d704 644
c1bae0aa
FB
645def set_owner_options(userdesc, perms, mlist, values):
646 """ Set the owner options of a list.
647 @mlist
648 @edit
649 @admin
650 """
651 return set_options(userdesc, perms, mlist, owner_opts, values)
0337d704 652
c1bae0aa
FB
653def add_to_wl(userdesc, perms, mlist, addr):
654 """ Add addr to the whitelist
655 @mlist
656 @edit
657 @admin
658 """
659 mlist.accept_these_nonmembers.append(addr)
660 return 1
0337d704 661
c1bae0aa
FB
662def del_from_wl(userdesc, perms, mlist, addr):
663 """ Remove an address from the whitelist
664 @mlist
665 @edit
666 @admin
667 """
668 mlist.accept_these_nonmembers.remove(addr)
669 return 1
0337d704 670
c1bae0aa
FB
671def get_bogo_level(userdesc, perms, mlist):
672 """ Compute bogo level from the filtering rules set up on the list.
673 @mlist
674 @admin
675 """
676 if len(mlist.header_filter_rules) == 0:
0337d704 677 return 0
c638d8c8 678
c1bae0aa
FB
679 unsurelevel = 0
680 filterlevel = 0
681 filterbase = 0
c638d8c8 682
c1bae0aa
FB
683 # The first rule filters Unsure mails
684 if mlist.header_filter_rules[0][0] == 'X-Spam-Flag: Unsure, tests=bogofilter':
685 unsurelevel = 1
686 filterbase = 1
c638d8c8 687
c1bae0aa
FB
688 # Check the other rules:
689 # - we have 2 rules: this is level 2 (drop > 0.999999, moderate Yes)
690 # - we have only one rule with HOLD directive : this is level 1 (moderate spams)
691 # - we have only one rule with DISCARD directive : this is level 3 (drop spams)
692 try:
693 action = mlist.header_filter_rules[filterbase + 1][1]
694 filterlevel = 2
0337d704 695 except:
c1bae0aa
FB
696 action = mlist.header_filter_rules[filterbase][1]
697 if action == mm_cfg.HOLD:
698 filterlevel = 1
699 elif action == mm_cfg.DISCARD:
700 filterlevel = 3
701 return (filterlevel << 1) + unsurelevel
0337d704 702
d36b2def 703def set_bogo_level(userdesc, perms, mlist, level):
c1bae0aa
FB
704 """ Set filter to the specified level.
705 @mlist
706 @edit
707 @admin
708 """
709 hfr = []
710
711 # The level is a combination of a spam filtering level and unsure filtering level
712 # - the unsure filtering level is only 1 bit (1 = HOLD unsures, 0 = Accept unsures)
713 # - the spam filtering level is a number growing with filtering strength
714 # (0 = no filtering, 1 = moderate spam, 2 = drop 0.999999 and moderate others, 3 = drop spams)
715 bogolevel = int(level)
716 filterlevel = bogolevel >> 1
717 unsurelevel = bogolevel & 1
718
719 # Set up unusre filtering
720 if unsurelevel == 1:
721 hfr.append(('X-Spam-Flag: Unsure, tests=bogofilter', mm_cfg.HOLD, False))
722
723 # Set up spam filtering
724 if filterlevel is 1:
725 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.HOLD, False))
726 elif filterlevel is 2:
727 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter, spamicity=(0\.999999|1\.000000)', mm_cfg.DISCARD, False))
728 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.HOLD, False))
729 elif filterlevel is 3:
730 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.DISCARD, False))
731
732 # save configuration
733 if mlist.header_filter_rules != hfr:
734 mlist.header_filter_rules = hfr
735 return 1
0337d704 736
737#-------------------------------------------------------------------------------
738# admin procedures [ soptions.php ]
739#
740
741admin_opts = [ 'advertised', 'archive', \
742 'max_message_size', 'msg_footer', 'msg_header']
743
c1bae0aa
FB
744def get_admin_options(userdesc, perms, mlist):
745 """ Get administrator options.
746 @mlist
747 @root
748 """
749 return get_options(userdesc, perms, mlist, admin_opts)
0337d704 750
c1bae0aa
FB
751def set_admin_options(userdesc, perms, mlist, values):
752 """ Set administrator options.
753 @mlist
754 @edit
755 @root
756 """
757 return set_options(userdesc, perms, mlist, admin_opts, values)
0337d704 758
759#-------------------------------------------------------------------------------
760# admin procedures [ check.php ]
761#
762
763check_opts = {
764 'acceptable_aliases' : '',
765 'admin_immed_notify' : True,
766 'administrivia' : True,
767 'anonymous_list' : False,
768 'autorespond_admin' : False,
769 'autorespond_postings' : False,
770 'autorespond_requests' : False,
771 'available_languages' : ['fr'],
772 'ban_list' : [],
773 'bounce_matching_headers' : '',
774 'bounce_processing' : False,
775 'convert_html_to_plaintext' : False,
776 'digestable' : False,
777 'digest_is_default' : False,
778 'discard_these_nonmembers' : [],
779 'emergency' : False,
780 'encode_ascii_prefixes' : 2,
781 'filter_content' : False,
782 'first_strip_reply_to' : False,
783 'forward_auto_discards' : True,
784 'hold_these_nonmembers' : [],
785 'host_name' : 'listes.polytechnique.org',
786 'include_list_post_header' : False,
787 'include_rfc2369_headers' : False,
788 'max_num_recipients' : 0,
789 'new_member_options' : 256,
790 'nondigestable' : True,
791 'obscure_addresses' : True,
792 'preferred_language' : 'fr',
793 'reject_these_nonmembers' : [],
794 'reply_goes_to_list' : 0,
795 'reply_to_address' : '',
796 'require_explicit_destination' : False,
797 'send_reminders' : 0,
798 'send_welcome_msg' : True,
799 'topics_enabled' : False,
800 'umbrella_list' : False,
801 'unsubscribe_policy' : 0,
802}
803
c1bae0aa
FB
804def check_options_runner(userdesc, perms, mlist, listname, correct):
805 options = { }
806 for (k, v) in check_opts.iteritems():
807 if mlist.__dict__[k] != v:
808 options[k] = v, mlist.__dict__[k]
809 if correct: mlist.__dict__[k] = v
810 if mlist.real_name.lower() != listname:
811 options['real_name'] = listname, mlist.real_name
812 if correct: mlist.real_name = listname
95bb095d 813 return 1
c1bae0aa
FB
814
815
ae6c293b 816def check_options(userdesc, perms, vhost, listname, correct=False):
c1bae0aa
FB
817 """ Check the list.
818 @root
819 """
88f7a3f1 820 listname = listname.lower()
c1bae0aa
FB
821 mlist = MailList.MailList(vhost + VHOST_SEP + listname, lock=0)
822 if correct:
823 return list_call_locked(check_options_runner, userdesc, perms, mlist, True, listname, True)
824 else:
825 return check_options_runner(userdesc, perms, mlist, listname, False)
0337d704 826
827#-------------------------------------------------------------------------------
828# super-admin procedures
829#
830
ae6c293b 831def get_all_lists(userdesc, perms, vhost):
c1bae0aa 832 """ Get all the list for the given vhost
d1c13dbf 833 @root
c1bae0aa 834 """
0337d704 835 prefix = vhost.lower()+VHOST_SEP
836 names = Utils.list_names()
837 names.sort()
838 result = []
839 for name in names:
840 if not name.startswith(prefix):
841 continue
ae6c293b 842 result.append(name.replace(prefix, ''))
0337d704 843 return result
844
d1c13dbf
FB
845def get_all_user_lists(userdesc, perms, vhost, email):
846 """ Get all the lists for the given user
847 @root
848 """
849 names = Utils.list_names()
850 names.sort()
851 result = []
852 for name in names:
853 try:
854 mlist = MailList.MailList(name, lock=0)
855 ismember = email in mlist.getRegularMemberKeys()
856 isowner = email in mlist.owner
857 if not ismember and not isowner:
858 continue
859 host = mlist.internal_name().split(VHOST_SEP)[0].lower()
860 result.append({ 'list': mlist.real_name,
861 'addr': mlist.real_name.lower() + '@' + host,
862 'host': host,
863 'own' : isowner,
864 'sub' : ismember
865 })
866 except Exception, e:
867 continue
868 return result
869
a8a11a70
FB
870def change_user_email(userdesc, perms, vhost, from_email, to_email):
871 """ Change the email of a user
872 @root
873 """
874 from_email = from_email.lower()
875 to_email = to_email.lower()
876 for list in Utils.list_names():
877 try:
878 mlist = MailList.MailList(list, lock=0)
879 except:
880 continue
881 try:
882 mlist.Lock()
883 mlist.ApprovedChangeMemberAddress(from_email, to_email, 0)
884 mlist.Save()
885 mlist.Unlock()
886 except:
887 mlist.Unlock()
888 return 1
889
890
ae6c293b 891def create_list(userdesc, perms, vhost, listname, desc, advertise, modlevel, inslevel, owners, members):
c1bae0aa
FB
892 """ Create a new list.
893 @root
894 """
895 name = vhost.lower() + VHOST_SEP + listname.lower();
0337d704 896 if Utils.list_exists(name):
42567b67 897 print >> sys.stderr, "List ", name, " already exists"
0337d704 898 return 0
ae6c293b 899
0337d704 900 owner = []
901 for o in owners:
42567b67
FB
902 email = to_forlife(o)
903 print >> sys.stderr, "owner in list", o, email
904 email = email[0]
0337d704 905 if email is not None:
906 owner.append(email)
907 if len(owner) is 0:
42567b67 908 print >> sys.stderr, "No owner found in ", owners
0337d704 909 return 0
910
911 mlist = MailList.MailList()
912 try:
913 oldmask = os.umask(002)
914 pw = sha.new('foobar').hexdigest()
ae6c293b 915
0337d704 916 try:
917 mlist.Create(name, owner[0], pw)
918 finally:
919 os.umask(oldmask)
920
921 mlist.real_name = listname
922 mlist.host_name = 'listes.polytechnique.org'
923 mlist.description = desc
924
925 mlist.advertised = int(advertise) is 0
926 mlist.default_member_moderation = int(modlevel) is 2
927 mlist.generic_nonmember_action = int(modlevel) > 0
928 mlist.subscribe_policy = 2 * (int(inslevel) is 1)
929 mlist.admin_notify_mchanges = (mlist.subscribe_policy or mlist.generic_nonmember_action or mlist.default_member_moderation or not mlist.advertised)
ae6c293b 930
0337d704 931 mlist.owner = owner
ae6c293b 932
0337d704 933 mlist.subject_prefix = '['+listname+'] '
934 mlist.max_message_size = 0
935
e480db84 936 inverted_listname = listname.lower() + '_' + vhost.lower()
0337d704 937 mlist.msg_footer = "_______________________________________________\n" \
31f2df6a 938 + "Liste de diffusion %(real_name)s\n" \
939 + "http://listes.polytechnique.org/members/" + inverted_listname
ae6c293b 940
0337d704 941 mlist.header_filter_rules = []
c638d8c8 942 mlist.header_filter_rules.append(('X-Spam-Flag: Unsure, tests=bogofilter', mm_cfg.HOLD, False))
0337d704 943 mlist.header_filter_rules.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.HOLD, False))
0337d704 944
98c79ede 945 if ON_CREATE_CMD != '':
946 try: os.system(ON_CREATE_CMD + ' ' + name)
947 except: pass
948
95bb095d 949 check_options_runner(userdesc, perms, mlist, listname.lower(), True)
c1bae0aa 950 mass_subscribe(userdesc, perms, mlist, members)
0337d704 951 mlist.Save()
c1bae0aa
FB
952 finally:
953 mlist.Unlock()
95bb095d
FB
954
955 # avoid the "-1 mail to moderate" bug
956 mlist = MailList.MailList(name)
57b04c90
FB
957 try:
958 mlist._UpdateRecords()
959 mlist.Save()
960 finally:
961 mlist.Unlock()
95bb095d 962 return 1
c1bae0aa
FB
963
964def delete_list(userdesc, perms, mlist, del_archives=0):
965 """ Delete the list.
966 @mlist
967 @admin
968 """
969 lname = mlist.internal_name()
970 # remove the list
971 REMOVABLES = [ os.path.join('lists', lname), ]
972 # remove stalled locks
973 for filename in os.listdir(mm_cfg.LOCK_DIR):
974 fn_lname = filename.split('.')[0]
975 if fn_lname == lname:
976 REMOVABLES.append(os.path.join(mm_cfg.LOCK_DIR, filename))
977 # remove archives ?
978 if del_archives:
979 REMOVABLES.extend([
980 os.path.join('archives', 'private', lname),
981 os.path.join('archives', 'private', lname+'.mbox'),
982 os.path.join('archives', 'public', lname),
983 os.path.join('archives', 'public', lname+'.mbox')
984 ])
985 map(lambda dir: remove_it(lname, os.path.join(mm_cfg.VAR_PREFIX, dir)), REMOVABLES)
986 return 1
0337d704 987
ae6c293b 988def kill(userdesc, perms, vhost, alias, del_from_promo):
c1bae0aa
FB
989 """ Remove a user from all the lists.
990 """
0337d704 991 exclude = []
992 if not del_from_promo:
c1bae0aa 993 exclude.append(PLATAL_DOMAIN + VHOST_SEP + 'promo' + alias[-4:])
0337d704 994 for list in Utils.list_names():
c1bae0aa
FB
995 if list in exclude:
996 continue
0337d704 997 try:
ae6c293b 998 mlist = MailList.MailList(list, lock=0)
0337d704 999 except:
1000 continue
1001 try:
1002 mlist.Lock()
ae6c293b 1003 mlist.ApprovedDeleteMember(alias+'@'+PLATAL_DOMAIN, None, 0, 0)
0337d704 1004 mlist.Save()
1005 mlist.Unlock()
1006 except:
1007 mlist.Unlock()
1008 return 1
1009
1010
1011#-------------------------------------------------------------------------------
1012# server
1013#
1014class FastXMLRPCServer(SocketServer.ThreadingMixIn, SimpleXMLRPCServer):
ae6c293b 1015 allow_reuse_address = True
0337d704 1016
1017################################################################################
1018#
ae6c293b 1019# INIT
0337d704 1020#
1021#-------------------------------------------------------------------------------
1022# use Mailman user and group (not root)
1023# fork in background if asked to
1024#
1025
1026uid = getpwnam(mm_cfg.MAILMAN_USER)[2]
1027gid = getgrnam(mm_cfg.MAILMAN_GROUP)[2]
1028
1029if not os.getuid():
ae6c293b 1030 os.setregid(gid, gid)
1031 os.setreuid(uid, uid)
0337d704 1032
1033signal.signal(signal.SIGHUP, signal.SIG_IGN)
1034
1035if ( os.getuid() is not uid ) or ( os.getgid() is not gid):
1036 sys.exit(0)
1037
1038opts, args = getopt.getopt(sys.argv[1:], 'f')
1039for o, a in opts:
1040 if o == '-f' and os.fork():
1041 sys.exit(0)
1042
1043i18n.set_language('fr')
1044mysql = connectDB()
1045lock = Lock()
1046
1047#-------------------------------------------------------------------------------
1048# server
1049#
1fec3393 1050server = FastXMLRPCServer((SRV_HOST, SRV_PORT), BasicAuthXMLRPCRequestHandler)
0337d704 1051
1052# index.php
1053server.register_function(get_lists)
1054server.register_function(subscribe)
1055server.register_function(unsubscribe)
1056# members.php
1057server.register_function(get_members)
1058# trombi.php
1059server.register_function(get_members_limit)
1060server.register_function(get_owners)
1061# admin.php
c4d57bd8 1062server.register_function(replace_email)
0337d704 1063server.register_function(mass_subscribe)
1064server.register_function(mass_unsubscribe)
1065server.register_function(add_owner)
1066server.register_function(del_owner)
1067# moderate.php
1068server.register_function(get_pending_ops)
1069server.register_function(handle_request)
4b0d9ef3 1070server.register_function(get_pending_sub)
0337d704 1071server.register_function(get_pending_mail)
1072# options.php
1073server.register_function(get_owner_options)
1074server.register_function(set_owner_options)
1075server.register_function(add_to_wl)
1076server.register_function(del_from_wl)
1077server.register_function(get_bogo_level)
1078server.register_function(set_bogo_level)
1079# soptions.php
1080server.register_function(get_admin_options)
1081server.register_function(set_admin_options)
1082# check.php
1083server.register_function(check_options)
1084# create + del
1085server.register_function(get_all_lists)
d1c13dbf 1086server.register_function(get_all_user_lists)
a8a11a70 1087server.register_function(change_user_email)
0337d704 1088server.register_function(create_list)
1089server.register_function(delete_list)
1090# utilisateurs.php
1091server.register_function(kill)
1092
1093server.serve_forever()
1094
c638d8c8 1095# vim:set et sw=4 sts=4 sws=4: