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