Moving to GitHub.
[platal.git] / bin / lists.rpc.py
CommitLineData
0337d704 1#!/usr/bin/env python
2#***************************************************************************
c441aabe 3#* Copyright (C) 2003-2014 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 """
d55f85b2
SJ
470 if isinstance(users, dict):
471 users = users.values()
6876570a
FB
472 if not isinstance(users, list):
473 raise Exception("userlist must be a list")
c1bae0aa
FB
474 members = mlist.getRegularMemberKeys()
475 added = []
c1bae0aa
FB
476 for user in users:
477 email, name = to_forlife(user)
478 if ( email is None ) or ( email in members ):
479 continue
480 userd = UserDesc(email, name, None, 0)
481 mlist.ApprovedAddMember(userd)
482 added.append( (quote(userd.fullname), userd.address) )
0337d704 483 return added
484
c1bae0aa
FB
485def mass_unsubscribe(userdesc, perms, mlist, users):
486 """ Remove a list of users from the list.
487 @mlist
488 @edit
489 @admin
490 """
491 map(lambda user: mlist.ApprovedDeleteMember(user), users)
0337d704 492 return users
493
c1bae0aa
FB
494def add_owner(userdesc, perms, mlist, user):
495 """ Add a owner to the list.
496 @mlist
497 @edit
498 @admin
499 """
500 email = to_forlife(user)[0]
501 if email is None:
0337d704 502 return 0
c1bae0aa
FB
503 if email not in mlist.owner:
504 mlist.owner.append(email)
0337d704 505 return True
506
c1bae0aa
FB
507def del_owner(userdesc, perms, mlist, user):
508 """ Remove a owner of the list.
509 @mlist
510 @edit
511 @admin
512 """
513 if len(mlist.owner) < 2:
0337d704 514 return 0
c1bae0aa 515 mlist.owner.remove(user)
0337d704 516 return True
517
518#-------------------------------------------------------------------------------
519# owners procedures [ admin.php ]
520#
521
c1bae0aa
FB
522def get_pending_ops(userdesc, perms, mlist):
523 """ Get the list of operation waiting for an action from the owners.
524 @mlist
525 @lock
526 @admin
527 """
528 subs = []
529 seen = []
530 dosave = False
531 for id in mlist.GetSubscriptionIds():
532 time, addr, fullname, passwd, digest, lang = mlist.GetRecord(id)
533 if addr in seen:
534 mlist.HandleRequest(id, mm_cfg.DISCARD)
535 dosave = True
536 continue
537 seen.append(addr)
538 try:
539 login = re.match("^[^.]*\.[^.]*\.\d\d\d\d$", addr.split('@')[0]).group()
540 subs.append({'id': id, 'name': quote(fullname), 'addr': addr, 'login': login })
541 except:
542 subs.append({'id': id, 'name': quote(fullname), 'addr': addr })
0337d704 543
c1bae0aa
FB
544 helds = []
545 for id in mlist.GetHeldMessageIds():
546 ptime, sender, subject, reason, filename, msgdata = mlist.GetRecord(id)
0337d704 547 fpath = os.path.join(mm_cfg.DATA_DIR, filename)
c1bae0aa
FB
548 try:
549 size = os.path.getsize(fpath)
550 except OSError, e:
551 if e.errno <> errno.ENOENT: raise
552 continue
553 try:
554 msg = readMessage(fpath)
555 fromX = msg.has_key("X-Org-Mail")
556 except:
557 pass
558 helds.append({
559 'id' : id,
0337d704 560 'sender': quote(sender, True),
561 'size' : size,
562 'subj' : quote(subject, True),
563 'stamp' : ptime,
c1bae0aa
FB
564 'fromx' : fromX
565 })
566 if dosave:
567 mlist.Save()
568 return (subs, helds)
569
570def handle_request(userdesc, perms, mlist, id, value, comment):
571 """ Handle a moderation request.
572 @mlist
573 @edit
574 @admin
575 """
eb07e9c4
RB
576 # Force encoding to mailman's default for french, since this is what
577 # Mailman will use internally
578 # LC_DESCRIPTIONS is a dict of lang => (name, charset, direction) tuples.
579 encoding = mm_cfg.LC_DESCRIPTIONS['fr'][1]
9896b9cf 580 comment = comment.encode(encoding, 'replace')
c1bae0aa
FB
581 mlist.HandleRequest(int(id), int(value), comment)
582 return 1
583
584def get_pending_sub(userdesc, perms, mlist, id):
585 """ Get informations about a given subscription moderation.
586 @mlist
587 @lock
588 @admin
589 """
590 sub = 0
591 id = int(id)
592 if id in mlist.GetSubscriptionIds():
593 time, addr, fullname, passwd, digest, lang = mlist.GetRecord(id)
594 try:
595 login = re.match("^[^.]*\.[^.]*\.\d\d\d\d$", addr.split('@')[0]).group()
596 sub = {'id': id, 'name': quote(fullname), 'addr': addr, 'login': login }
597 except:
598 sub = {'id': id, 'name': quote(fullname), 'addr': addr }
599 return sub
600
601def get_pending_mail(userdesc, perms, mlist, id, raw=0):
602 """ Get informations about a given mail moderation.
603 @mlist
604 @lock
605 @admin
606 """
607 ptime, sender, subject, reason, filename, msgdata = mlist.GetRecord(int(id))
608 fpath = os.path.join(mm_cfg.DATA_DIR, filename)
609 size = os.path.getsize(fpath)
610 msg = readMessage(fpath)
611
612 if raw:
613 return quote(str(msg))
614 results_plain = []
615 results_html = []
616 for part in typed_subpart_iterator(msg, 'text', 'plain'):
617 c = part.get_payload()
618 if c is not None: results_plain.append (c)
619 results_plain = map(lambda x: quote(x), results_plain)
620 for part in typed_subpart_iterator(msg, 'text', 'html'):
621 c = part.get_payload()
622 if c is not None: results_html.append (c)
623 results_html = map(lambda x: quote(x), results_html)
624 return {'id' : id,
625 'sender': quote(sender, True),
626 'size' : size,
627 'subj' : quote(subject, True),
628 'stamp' : ptime,
629 'parts_plain' : results_plain,
630 'parts_html': results_html }
0337d704 631
632#-------------------------------------------------------------------------------
633# owner options [ options.php ]
634#
635
636owner_opts = ['accept_these_nonmembers', 'admin_notify_mchanges', 'description', \
637 'default_member_moderation', 'generic_nonmember_action', 'info', \
638 'subject_prefix', 'goodbye_msg', 'send_goodbye_msg', 'subscribe_policy', \
639 'welcome_msg']
640
c1bae0aa
FB
641def get_owner_options(userdesc, perms, mlist):
642 """ Get the owner options of a list.
643 @mlist
644 @admin
645 """
646 return get_options(userdesc, perms, mlist, owner_opts)
0337d704 647
c1bae0aa
FB
648def set_owner_options(userdesc, perms, mlist, values):
649 """ Set the owner options of a list.
650 @mlist
651 @edit
652 @admin
653 """
654 return set_options(userdesc, perms, mlist, owner_opts, values)
0337d704 655
c1bae0aa
FB
656def add_to_wl(userdesc, perms, mlist, addr):
657 """ Add addr to the whitelist
658 @mlist
659 @edit
660 @admin
661 """
662 mlist.accept_these_nonmembers.append(addr)
663 return 1
0337d704 664
c1bae0aa
FB
665def del_from_wl(userdesc, perms, mlist, addr):
666 """ Remove an address from the whitelist
667 @mlist
668 @edit
669 @admin
670 """
671 mlist.accept_these_nonmembers.remove(addr)
672 return 1
0337d704 673
c1bae0aa
FB
674def get_bogo_level(userdesc, perms, mlist):
675 """ Compute bogo level from the filtering rules set up on the list.
676 @mlist
677 @admin
678 """
679 if len(mlist.header_filter_rules) == 0:
0337d704 680 return 0
c638d8c8 681
c1bae0aa
FB
682 unsurelevel = 0
683 filterlevel = 0
684 filterbase = 0
c638d8c8 685
c1bae0aa
FB
686 # The first rule filters Unsure mails
687 if mlist.header_filter_rules[0][0] == 'X-Spam-Flag: Unsure, tests=bogofilter':
688 unsurelevel = 1
689 filterbase = 1
c638d8c8 690
c1bae0aa
FB
691 # Check the other rules:
692 # - we have 2 rules: this is level 2 (drop > 0.999999, moderate Yes)
693 # - we have only one rule with HOLD directive : this is level 1 (moderate spams)
694 # - we have only one rule with DISCARD directive : this is level 3 (drop spams)
695 try:
696 action = mlist.header_filter_rules[filterbase + 1][1]
697 filterlevel = 2
0337d704 698 except:
c1bae0aa
FB
699 action = mlist.header_filter_rules[filterbase][1]
700 if action == mm_cfg.HOLD:
701 filterlevel = 1
702 elif action == mm_cfg.DISCARD:
703 filterlevel = 3
704 return (filterlevel << 1) + unsurelevel
0337d704 705
d36b2def 706def set_bogo_level(userdesc, perms, mlist, level):
c1bae0aa
FB
707 """ Set filter to the specified level.
708 @mlist
709 @edit
710 @admin
711 """
712 hfr = []
713
714 # The level is a combination of a spam filtering level and unsure filtering level
715 # - the unsure filtering level is only 1 bit (1 = HOLD unsures, 0 = Accept unsures)
716 # - the spam filtering level is a number growing with filtering strength
717 # (0 = no filtering, 1 = moderate spam, 2 = drop 0.999999 and moderate others, 3 = drop spams)
718 bogolevel = int(level)
719 filterlevel = bogolevel >> 1
720 unsurelevel = bogolevel & 1
721
722 # Set up unusre filtering
723 if unsurelevel == 1:
724 hfr.append(('X-Spam-Flag: Unsure, tests=bogofilter', mm_cfg.HOLD, False))
725
726 # Set up spam filtering
727 if filterlevel is 1:
728 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.HOLD, False))
729 elif filterlevel is 2:
730 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter, spamicity=(0\.999999|1\.000000)', mm_cfg.DISCARD, False))
731 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.HOLD, False))
732 elif filterlevel is 3:
733 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.DISCARD, False))
734
735 # save configuration
736 if mlist.header_filter_rules != hfr:
737 mlist.header_filter_rules = hfr
738 return 1
0337d704 739
740#-------------------------------------------------------------------------------
741# admin procedures [ soptions.php ]
742#
743
744admin_opts = [ 'advertised', 'archive', \
745 'max_message_size', 'msg_footer', 'msg_header']
746
c1bae0aa
FB
747def get_admin_options(userdesc, perms, mlist):
748 """ Get administrator options.
749 @mlist
750 @root
751 """
752 return get_options(userdesc, perms, mlist, admin_opts)
0337d704 753
c1bae0aa
FB
754def set_admin_options(userdesc, perms, mlist, values):
755 """ Set administrator options.
756 @mlist
757 @edit
758 @root
759 """
760 return set_options(userdesc, perms, mlist, admin_opts, values)
0337d704 761
762#-------------------------------------------------------------------------------
763# admin procedures [ check.php ]
764#
765
766check_opts = {
767 'acceptable_aliases' : '',
768 'admin_immed_notify' : True,
769 'administrivia' : True,
770 'anonymous_list' : False,
771 'autorespond_admin' : False,
772 'autorespond_postings' : False,
773 'autorespond_requests' : False,
774 'available_languages' : ['fr'],
775 'ban_list' : [],
776 'bounce_matching_headers' : '',
777 'bounce_processing' : False,
778 'convert_html_to_plaintext' : False,
779 'digestable' : False,
780 'digest_is_default' : False,
781 'discard_these_nonmembers' : [],
782 'emergency' : False,
783 'encode_ascii_prefixes' : 2,
784 'filter_content' : False,
785 'first_strip_reply_to' : False,
786 'forward_auto_discards' : True,
787 'hold_these_nonmembers' : [],
788 'host_name' : 'listes.polytechnique.org',
789 'include_list_post_header' : False,
790 'include_rfc2369_headers' : False,
791 'max_num_recipients' : 0,
792 'new_member_options' : 256,
793 'nondigestable' : True,
794 'obscure_addresses' : True,
795 'preferred_language' : 'fr',
796 'reject_these_nonmembers' : [],
797 'reply_goes_to_list' : 0,
798 'reply_to_address' : '',
799 'require_explicit_destination' : False,
800 'send_reminders' : 0,
801 'send_welcome_msg' : True,
802 'topics_enabled' : False,
803 'umbrella_list' : False,
804 'unsubscribe_policy' : 0,
805}
806
c1bae0aa
FB
807def check_options_runner(userdesc, perms, mlist, listname, correct):
808 options = { }
809 for (k, v) in check_opts.iteritems():
810 if mlist.__dict__[k] != v:
811 options[k] = v, mlist.__dict__[k]
812 if correct: mlist.__dict__[k] = v
813 if mlist.real_name.lower() != listname:
814 options['real_name'] = listname, mlist.real_name
815 if correct: mlist.real_name = listname
95bb095d 816 return 1
c1bae0aa
FB
817
818
ae6c293b 819def check_options(userdesc, perms, vhost, listname, correct=False):
c1bae0aa
FB
820 """ Check the list.
821 @root
822 """
88f7a3f1 823 listname = listname.lower()
c1bae0aa
FB
824 mlist = MailList.MailList(vhost + VHOST_SEP + listname, lock=0)
825 if correct:
826 return list_call_locked(check_options_runner, userdesc, perms, mlist, True, listname, True)
827 else:
828 return check_options_runner(userdesc, perms, mlist, listname, False)
0337d704 829
830#-------------------------------------------------------------------------------
831# super-admin procedures
832#
833
ae6c293b 834def get_all_lists(userdesc, perms, vhost):
c1bae0aa 835 """ Get all the list for the given vhost
d1c13dbf 836 @root
c1bae0aa 837 """
0337d704 838 prefix = vhost.lower()+VHOST_SEP
839 names = Utils.list_names()
840 names.sort()
841 result = []
842 for name in names:
843 if not name.startswith(prefix):
844 continue
ae6c293b 845 result.append(name.replace(prefix, ''))
0337d704 846 return result
847
d1c13dbf
FB
848def get_all_user_lists(userdesc, perms, vhost, email):
849 """ Get all the lists for the given user
850 @root
851 """
852 names = Utils.list_names()
853 names.sort()
854 result = []
855 for name in names:
856 try:
857 mlist = MailList.MailList(name, lock=0)
858 ismember = email in mlist.getRegularMemberKeys()
859 isowner = email in mlist.owner
860 if not ismember and not isowner:
861 continue
862 host = mlist.internal_name().split(VHOST_SEP)[0].lower()
863 result.append({ 'list': mlist.real_name,
864 'addr': mlist.real_name.lower() + '@' + host,
865 'host': host,
866 'own' : isowner,
867 'sub' : ismember
868 })
869 except Exception, e:
870 continue
871 return result
872
a8a11a70
FB
873def change_user_email(userdesc, perms, vhost, from_email, to_email):
874 """ Change the email of a user
875 @root
876 """
877 from_email = from_email.lower()
878 to_email = to_email.lower()
879 for list in Utils.list_names():
880 try:
881 mlist = MailList.MailList(list, lock=0)
882 except:
883 continue
884 try:
885 mlist.Lock()
886 mlist.ApprovedChangeMemberAddress(from_email, to_email, 0)
887 mlist.Save()
888 mlist.Unlock()
889 except:
890 mlist.Unlock()
891 return 1
892
893
ae6c293b 894def create_list(userdesc, perms, vhost, listname, desc, advertise, modlevel, inslevel, owners, members):
c1bae0aa
FB
895 """ Create a new list.
896 @root
897 """
898 name = vhost.lower() + VHOST_SEP + listname.lower();
0337d704 899 if Utils.list_exists(name):
42567b67 900 print >> sys.stderr, "List ", name, " already exists"
0337d704 901 return 0
ae6c293b 902
0337d704 903 owner = []
904 for o in owners:
42567b67
FB
905 email = to_forlife(o)
906 print >> sys.stderr, "owner in list", o, email
907 email = email[0]
0337d704 908 if email is not None:
909 owner.append(email)
910 if len(owner) is 0:
42567b67 911 print >> sys.stderr, "No owner found in ", owners
0337d704 912 return 0
913
914 mlist = MailList.MailList()
915 try:
916 oldmask = os.umask(002)
917 pw = sha.new('foobar').hexdigest()
ae6c293b 918
0337d704 919 try:
920 mlist.Create(name, owner[0], pw)
921 finally:
922 os.umask(oldmask)
923
924 mlist.real_name = listname
925 mlist.host_name = 'listes.polytechnique.org'
926 mlist.description = desc
927
928 mlist.advertised = int(advertise) is 0
929 mlist.default_member_moderation = int(modlevel) is 2
930 mlist.generic_nonmember_action = int(modlevel) > 0
931 mlist.subscribe_policy = 2 * (int(inslevel) is 1)
932 mlist.admin_notify_mchanges = (mlist.subscribe_policy or mlist.generic_nonmember_action or mlist.default_member_moderation or not mlist.advertised)
ae6c293b 933
0337d704 934 mlist.owner = owner
ae6c293b 935
0337d704 936 mlist.subject_prefix = '['+listname+'] '
937 mlist.max_message_size = 0
938
e480db84 939 inverted_listname = listname.lower() + '_' + vhost.lower()
0337d704 940 mlist.msg_footer = "_______________________________________________\n" \
31f2df6a 941 + "Liste de diffusion %(real_name)s\n" \
942 + "http://listes.polytechnique.org/members/" + inverted_listname
ae6c293b 943
0337d704 944 mlist.header_filter_rules = []
c638d8c8 945 mlist.header_filter_rules.append(('X-Spam-Flag: Unsure, tests=bogofilter', mm_cfg.HOLD, False))
0337d704 946 mlist.header_filter_rules.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.HOLD, False))
0337d704 947
98c79ede 948 if ON_CREATE_CMD != '':
949 try: os.system(ON_CREATE_CMD + ' ' + name)
950 except: pass
951
95bb095d 952 check_options_runner(userdesc, perms, mlist, listname.lower(), True)
c1bae0aa 953 mass_subscribe(userdesc, perms, mlist, members)
0337d704 954 mlist.Save()
c1bae0aa
FB
955 finally:
956 mlist.Unlock()
95bb095d
FB
957
958 # avoid the "-1 mail to moderate" bug
959 mlist = MailList.MailList(name)
57b04c90
FB
960 try:
961 mlist._UpdateRecords()
962 mlist.Save()
963 finally:
964 mlist.Unlock()
95bb095d 965 return 1
c1bae0aa
FB
966
967def delete_list(userdesc, perms, mlist, del_archives=0):
968 """ Delete the list.
969 @mlist
970 @admin
971 """
972 lname = mlist.internal_name()
973 # remove the list
974 REMOVABLES = [ os.path.join('lists', lname), ]
975 # remove stalled locks
976 for filename in os.listdir(mm_cfg.LOCK_DIR):
977 fn_lname = filename.split('.')[0]
978 if fn_lname == lname:
979 REMOVABLES.append(os.path.join(mm_cfg.LOCK_DIR, filename))
980 # remove archives ?
981 if del_archives:
982 REMOVABLES.extend([
983 os.path.join('archives', 'private', lname),
984 os.path.join('archives', 'private', lname+'.mbox'),
985 os.path.join('archives', 'public', lname),
986 os.path.join('archives', 'public', lname+'.mbox')
987 ])
988 map(lambda dir: remove_it(lname, os.path.join(mm_cfg.VAR_PREFIX, dir)), REMOVABLES)
989 return 1
0337d704 990
ae6c293b 991def kill(userdesc, perms, vhost, alias, del_from_promo):
c1bae0aa
FB
992 """ Remove a user from all the lists.
993 """
0337d704 994 exclude = []
995 if not del_from_promo:
c1bae0aa 996 exclude.append(PLATAL_DOMAIN + VHOST_SEP + 'promo' + alias[-4:])
0337d704 997 for list in Utils.list_names():
c1bae0aa
FB
998 if list in exclude:
999 continue
0337d704 1000 try:
ae6c293b 1001 mlist = MailList.MailList(list, lock=0)
0337d704 1002 except:
1003 continue
1004 try:
1005 mlist.Lock()
ae6c293b 1006 mlist.ApprovedDeleteMember(alias+'@'+PLATAL_DOMAIN, None, 0, 0)
0337d704 1007 mlist.Save()
1008 mlist.Unlock()
1009 except:
1010 mlist.Unlock()
1011 return 1
1012
1013
1014#-------------------------------------------------------------------------------
1015# server
1016#
1017class FastXMLRPCServer(SocketServer.ThreadingMixIn, SimpleXMLRPCServer):
ae6c293b 1018 allow_reuse_address = True
0337d704 1019
1020################################################################################
1021#
ae6c293b 1022# INIT
0337d704 1023#
1024#-------------------------------------------------------------------------------
1025# use Mailman user and group (not root)
1026# fork in background if asked to
1027#
1028
1029uid = getpwnam(mm_cfg.MAILMAN_USER)[2]
1030gid = getgrnam(mm_cfg.MAILMAN_GROUP)[2]
1031
1032if not os.getuid():
ae6c293b 1033 os.setregid(gid, gid)
1034 os.setreuid(uid, uid)
0337d704 1035
1036signal.signal(signal.SIGHUP, signal.SIG_IGN)
1037
1038if ( os.getuid() is not uid ) or ( os.getgid() is not gid):
1039 sys.exit(0)
1040
1041opts, args = getopt.getopt(sys.argv[1:], 'f')
1042for o, a in opts:
1043 if o == '-f' and os.fork():
1044 sys.exit(0)
1045
1046i18n.set_language('fr')
1047mysql = connectDB()
1048lock = Lock()
1049
1050#-------------------------------------------------------------------------------
1051# server
1052#
1fec3393 1053server = FastXMLRPCServer((SRV_HOST, SRV_PORT), BasicAuthXMLRPCRequestHandler)
0337d704 1054
1055# index.php
1056server.register_function(get_lists)
1057server.register_function(subscribe)
1058server.register_function(unsubscribe)
1059# members.php
1060server.register_function(get_members)
1061# trombi.php
1062server.register_function(get_members_limit)
1063server.register_function(get_owners)
1064# admin.php
c4d57bd8 1065server.register_function(replace_email)
0337d704 1066server.register_function(mass_subscribe)
1067server.register_function(mass_unsubscribe)
1068server.register_function(add_owner)
1069server.register_function(del_owner)
1070# moderate.php
1071server.register_function(get_pending_ops)
1072server.register_function(handle_request)
4b0d9ef3 1073server.register_function(get_pending_sub)
0337d704 1074server.register_function(get_pending_mail)
1075# options.php
1076server.register_function(get_owner_options)
1077server.register_function(set_owner_options)
1078server.register_function(add_to_wl)
1079server.register_function(del_from_wl)
1080server.register_function(get_bogo_level)
1081server.register_function(set_bogo_level)
1082# soptions.php
1083server.register_function(get_admin_options)
1084server.register_function(set_admin_options)
1085# check.php
1086server.register_function(check_options)
1087# create + del
1088server.register_function(get_all_lists)
d1c13dbf 1089server.register_function(get_all_user_lists)
a8a11a70 1090server.register_function(change_user_email)
0337d704 1091server.register_function(create_list)
1092server.register_function(delete_list)
1093# utilisateurs.php
1094server.register_function(kill)
1095
1096server.serve_forever()
1097
c638d8c8 1098# vim:set et sw=4 sts=4 sws=4: