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