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