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