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