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