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