Merge branch 'platal-0.10.0'
[platal.git] / bin / lists.rpc.py
CommitLineData
0337d704 1#!/usr/bin/env python
2#***************************************************************************
8d84c630 3#* Copyright (C) 2004-2009 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
d36b2def 321def set_options(userdesc, perms, mlist, opts, vals):
c1bae0aa
FB
322 for (k, v) in vals.iteritems():
323 if k not in opts:
324 continue
325 if k == 'default_member_moderation':
326 for member in mlist.getMembers():
327 mlist.setMemberOption(member, mm_cfg.Moderate, int(v))
328 t = type(mlist.__dict__[k])
329 if t is bool: mlist.__dict__[k] = bool(v)
330 elif t is int: mlist.__dict__[k] = int(v)
331 elif t is str: mlist.__dict__[k] = Utils.uncanonstr(v, 'fr')
332 else: mlist.__dict__[k] = v
333 return 1
0337d704 334
335#-------------------------------------------------------------------------------
336# users procedures for [ index.php ]
337#
338
ae6c293b 339def get_lists(userdesc, perms, vhost, email=None):
c1bae0aa
FB
340 """ List available lists for the given vhost
341 """
0337d704 342 if email is None:
343 udesc = userdesc
344 else:
cf5e8ef1 345 udesc = UserDesc(email.lower(), email.lower(), None, 0)
0337d704 346 prefix = vhost.lower()+VHOST_SEP
347 names = Utils.list_names()
348 names.sort()
349 result = []
350 for name in names:
351 if not name.startswith(prefix):
352 continue
353 try:
ae6c293b 354 mlist = MailList.MailList(name, lock=0)
0337d704 355 except:
356 continue
357 try:
ae6c293b 358 details = get_list_info(udesc, perms, mlist, (email is None and vhost == PLATAL_DOMAIN))[0]
0337d704 359 result.append(details)
849baea6
FB
360 except Exception, e:
361 sys.stderr.write('Can\'t get list %s: %s\n' % (name, str(e)))
0337d704 362 continue
363 return result
364
c1bae0aa
FB
365def subscribe(userdesc, perms, mlist):
366 """ Subscribe to a list.
367 @mlist
368 @edit
369 """
370 if ( mlist.subscribe_policy in (0, 1) ) or userdesc.address in mlist.owner:
371 mlist.ApprovedAddMember(userdesc)
372 result = 2
373 else:
374 result = 1
375 try:
376 mlist.AddMember(userdesc)
377 except Errors.MMNeedApproval:
378 pass
0337d704 379 return result
380
c1bae0aa
FB
381def unsubscribe(userdesc, perms, mlist):
382 """ Unsubscribe from a list
383 @mlist
384 @edit
385 """
386 mlist.ApprovedDeleteMember(userdesc.address)
387 return 1
0337d704 388
389#-------------------------------------------------------------------------------
390# users procedures for [ index.php ]
391#
392
79a9ca23 393def get_name(member):
394 try:
395 return quote(mlist.getMemberName(member))
396 except:
79a9ca23 397 return ''
398
c1bae0aa
FB
399def get_members(userdesc, perms, mlist):
400 """ List the members of a list.
401 @mlist
402 """
403 details, members = get_list_info(userdesc, perms, mlist)
404 members.sort()
405 members = map(lambda member: (get_name(member), member), members)
406 return (details, members, mlist.owner)
407
0337d704 408
409#-------------------------------------------------------------------------------
410# users procedures for [ trombi.php ]
411#
412
c1bae0aa
FB
413def get_members_limit(userdesc, perms, mlist, page, nb_per_page):
414 """ Get a range of members of the list.
415 @mlist
416 """
417 members = get_members(userdesc, perms, mlist)[1]
0337d704 418 i = int(page) * int(nb_per_page)
419 return (len(members), members[i:i+int(nb_per_page)])
420
c1bae0aa
FB
421def get_owners(userdesc, perms, mlist):
422 """ Get the owners of the list.
423 @mlist
424 """
425 details, members, owners = get_members(userdesc, perms, mlist)
ae6c293b 426 return (details, owners)
0337d704 427
c1bae0aa 428
0337d704 429#-------------------------------------------------------------------------------
430# owners procedures [ admin.php ]
431#
432
c1bae0aa
FB
433def replace_email(userdesc, perms, mlist, from_email, to_email):
434 """ Replace the address of a member by another one.
435 @mlist
436 @edit
437 @admin
438 """
439 mlist.ApprovedChangeMemberAddress(from_email.lower(), to_email.lower(), 0)
440 return 1
ae6c293b 441
c1bae0aa
FB
442def mass_subscribe(userdesc, perms, mlist, users):
443 """ Add a list of users to the list.
444 @mlist
445 @edit
446 @admin
447 """
448 members = mlist.getRegularMemberKeys()
449 added = []
c1bae0aa
FB
450 for user in users:
451 email, name = to_forlife(user)
452 if ( email is None ) or ( email in members ):
453 continue
454 userd = UserDesc(email, name, None, 0)
455 mlist.ApprovedAddMember(userd)
456 added.append( (quote(userd.fullname), userd.address) )
0337d704 457 return added
458
c1bae0aa
FB
459def mass_unsubscribe(userdesc, perms, mlist, users):
460 """ Remove a list of users from the list.
461 @mlist
462 @edit
463 @admin
464 """
465 map(lambda user: mlist.ApprovedDeleteMember(user), users)
0337d704 466 return users
467
c1bae0aa
FB
468def add_owner(userdesc, perms, mlist, user):
469 """ Add a owner to the list.
470 @mlist
471 @edit
472 @admin
473 """
474 email = to_forlife(user)[0]
475 if email is None:
0337d704 476 return 0
c1bae0aa
FB
477 if email not in mlist.owner:
478 mlist.owner.append(email)
0337d704 479 return True
480
c1bae0aa
FB
481def del_owner(userdesc, perms, mlist, user):
482 """ Remove a owner of the list.
483 @mlist
484 @edit
485 @admin
486 """
487 if len(mlist.owner) < 2:
0337d704 488 return 0
c1bae0aa 489 mlist.owner.remove(user)
0337d704 490 return True
491
492#-------------------------------------------------------------------------------
493# owners procedures [ admin.php ]
494#
495
c1bae0aa
FB
496def get_pending_ops(userdesc, perms, mlist):
497 """ Get the list of operation waiting for an action from the owners.
498 @mlist
499 @lock
500 @admin
501 """
502 subs = []
503 seen = []
504 dosave = False
505 for id in mlist.GetSubscriptionIds():
506 time, addr, fullname, passwd, digest, lang = mlist.GetRecord(id)
507 if addr in seen:
508 mlist.HandleRequest(id, mm_cfg.DISCARD)
509 dosave = True
510 continue
511 seen.append(addr)
512 try:
513 login = re.match("^[^.]*\.[^.]*\.\d\d\d\d$", addr.split('@')[0]).group()
514 subs.append({'id': id, 'name': quote(fullname), 'addr': addr, 'login': login })
515 except:
516 subs.append({'id': id, 'name': quote(fullname), 'addr': addr })
0337d704 517
c1bae0aa
FB
518 helds = []
519 for id in mlist.GetHeldMessageIds():
520 ptime, sender, subject, reason, filename, msgdata = mlist.GetRecord(id)
0337d704 521 fpath = os.path.join(mm_cfg.DATA_DIR, filename)
c1bae0aa
FB
522 try:
523 size = os.path.getsize(fpath)
524 except OSError, e:
525 if e.errno <> errno.ENOENT: raise
526 continue
527 try:
528 msg = readMessage(fpath)
529 fromX = msg.has_key("X-Org-Mail")
530 except:
531 pass
532 helds.append({
533 'id' : id,
0337d704 534 'sender': quote(sender, True),
535 'size' : size,
536 'subj' : quote(subject, True),
537 'stamp' : ptime,
c1bae0aa
FB
538 'fromx' : fromX
539 })
540 if dosave:
541 mlist.Save()
542 return (subs, helds)
543
544def handle_request(userdesc, perms, mlist, id, value, comment):
545 """ Handle a moderation request.
546 @mlist
547 @edit
548 @admin
549 """
550 mlist.HandleRequest(int(id), int(value), comment)
551 return 1
552
553def get_pending_sub(userdesc, perms, mlist, id):
554 """ Get informations about a given subscription moderation.
555 @mlist
556 @lock
557 @admin
558 """
559 sub = 0
560 id = int(id)
561 if id in mlist.GetSubscriptionIds():
562 time, addr, fullname, passwd, digest, lang = mlist.GetRecord(id)
563 try:
564 login = re.match("^[^.]*\.[^.]*\.\d\d\d\d$", addr.split('@')[0]).group()
565 sub = {'id': id, 'name': quote(fullname), 'addr': addr, 'login': login }
566 except:
567 sub = {'id': id, 'name': quote(fullname), 'addr': addr }
568 return sub
569
570def get_pending_mail(userdesc, perms, mlist, id, raw=0):
571 """ Get informations about a given mail moderation.
572 @mlist
573 @lock
574 @admin
575 """
576 ptime, sender, subject, reason, filename, msgdata = mlist.GetRecord(int(id))
577 fpath = os.path.join(mm_cfg.DATA_DIR, filename)
578 size = os.path.getsize(fpath)
579 msg = readMessage(fpath)
580
581 if raw:
582 return quote(str(msg))
583 results_plain = []
584 results_html = []
585 for part in typed_subpart_iterator(msg, 'text', 'plain'):
586 c = part.get_payload()
587 if c is not None: results_plain.append (c)
588 results_plain = map(lambda x: quote(x), results_plain)
589 for part in typed_subpart_iterator(msg, 'text', 'html'):
590 c = part.get_payload()
591 if c is not None: results_html.append (c)
592 results_html = map(lambda x: quote(x), results_html)
593 return {'id' : id,
594 'sender': quote(sender, True),
595 'size' : size,
596 'subj' : quote(subject, True),
597 'stamp' : ptime,
598 'parts_plain' : results_plain,
599 'parts_html': results_html }
0337d704 600
601#-------------------------------------------------------------------------------
602# owner options [ options.php ]
603#
604
605owner_opts = ['accept_these_nonmembers', 'admin_notify_mchanges', 'description', \
606 'default_member_moderation', 'generic_nonmember_action', 'info', \
607 'subject_prefix', 'goodbye_msg', 'send_goodbye_msg', 'subscribe_policy', \
608 'welcome_msg']
609
c1bae0aa
FB
610def get_owner_options(userdesc, perms, mlist):
611 """ Get the owner options of a list.
612 @mlist
613 @admin
614 """
615 return get_options(userdesc, perms, mlist, owner_opts)
0337d704 616
c1bae0aa
FB
617def set_owner_options(userdesc, perms, mlist, values):
618 """ Set the owner options of a list.
619 @mlist
620 @edit
621 @admin
622 """
623 return set_options(userdesc, perms, mlist, owner_opts, values)
0337d704 624
c1bae0aa
FB
625def add_to_wl(userdesc, perms, mlist, addr):
626 """ Add addr to the whitelist
627 @mlist
628 @edit
629 @admin
630 """
631 mlist.accept_these_nonmembers.append(addr)
632 return 1
0337d704 633
c1bae0aa
FB
634def del_from_wl(userdesc, perms, mlist, addr):
635 """ Remove an address from the whitelist
636 @mlist
637 @edit
638 @admin
639 """
640 mlist.accept_these_nonmembers.remove(addr)
641 return 1
0337d704 642
c1bae0aa
FB
643def get_bogo_level(userdesc, perms, mlist):
644 """ Compute bogo level from the filtering rules set up on the list.
645 @mlist
646 @admin
647 """
648 if len(mlist.header_filter_rules) == 0:
0337d704 649 return 0
c638d8c8 650
c1bae0aa
FB
651 unsurelevel = 0
652 filterlevel = 0
653 filterbase = 0
c638d8c8 654
c1bae0aa
FB
655 # The first rule filters Unsure mails
656 if mlist.header_filter_rules[0][0] == 'X-Spam-Flag: Unsure, tests=bogofilter':
657 unsurelevel = 1
658 filterbase = 1
c638d8c8 659
c1bae0aa
FB
660 # Check the other rules:
661 # - we have 2 rules: this is level 2 (drop > 0.999999, moderate Yes)
662 # - we have only one rule with HOLD directive : this is level 1 (moderate spams)
663 # - we have only one rule with DISCARD directive : this is level 3 (drop spams)
664 try:
665 action = mlist.header_filter_rules[filterbase + 1][1]
666 filterlevel = 2
0337d704 667 except:
c1bae0aa
FB
668 action = mlist.header_filter_rules[filterbase][1]
669 if action == mm_cfg.HOLD:
670 filterlevel = 1
671 elif action == mm_cfg.DISCARD:
672 filterlevel = 3
673 return (filterlevel << 1) + unsurelevel
0337d704 674
d36b2def 675def set_bogo_level(userdesc, perms, mlist, level):
c1bae0aa
FB
676 """ Set filter to the specified level.
677 @mlist
678 @edit
679 @admin
680 """
681 hfr = []
682
683 # The level is a combination of a spam filtering level and unsure filtering level
684 # - the unsure filtering level is only 1 bit (1 = HOLD unsures, 0 = Accept unsures)
685 # - the spam filtering level is a number growing with filtering strength
686 # (0 = no filtering, 1 = moderate spam, 2 = drop 0.999999 and moderate others, 3 = drop spams)
687 bogolevel = int(level)
688 filterlevel = bogolevel >> 1
689 unsurelevel = bogolevel & 1
690
691 # Set up unusre filtering
692 if unsurelevel == 1:
693 hfr.append(('X-Spam-Flag: Unsure, tests=bogofilter', mm_cfg.HOLD, False))
694
695 # Set up spam filtering
696 if filterlevel is 1:
697 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.HOLD, False))
698 elif filterlevel is 2:
699 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter, spamicity=(0\.999999|1\.000000)', mm_cfg.DISCARD, False))
700 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.HOLD, False))
701 elif filterlevel is 3:
702 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.DISCARD, False))
703
704 # save configuration
705 if mlist.header_filter_rules != hfr:
706 mlist.header_filter_rules = hfr
707 return 1
0337d704 708
709#-------------------------------------------------------------------------------
710# admin procedures [ soptions.php ]
711#
712
713admin_opts = [ 'advertised', 'archive', \
714 'max_message_size', 'msg_footer', 'msg_header']
715
c1bae0aa
FB
716def get_admin_options(userdesc, perms, mlist):
717 """ Get administrator options.
718 @mlist
719 @root
720 """
721 return get_options(userdesc, perms, mlist, admin_opts)
0337d704 722
c1bae0aa
FB
723def set_admin_options(userdesc, perms, mlist, values):
724 """ Set administrator options.
725 @mlist
726 @edit
727 @root
728 """
729 return set_options(userdesc, perms, mlist, admin_opts, values)
0337d704 730
731#-------------------------------------------------------------------------------
732# admin procedures [ check.php ]
733#
734
735check_opts = {
736 'acceptable_aliases' : '',
737 'admin_immed_notify' : True,
738 'administrivia' : True,
739 'anonymous_list' : False,
740 'autorespond_admin' : False,
741 'autorespond_postings' : False,
742 'autorespond_requests' : False,
743 'available_languages' : ['fr'],
744 'ban_list' : [],
745 'bounce_matching_headers' : '',
746 'bounce_processing' : False,
747 'convert_html_to_plaintext' : False,
748 'digestable' : False,
749 'digest_is_default' : False,
750 'discard_these_nonmembers' : [],
751 'emergency' : False,
752 'encode_ascii_prefixes' : 2,
753 'filter_content' : False,
754 'first_strip_reply_to' : False,
755 'forward_auto_discards' : True,
756 'hold_these_nonmembers' : [],
757 'host_name' : 'listes.polytechnique.org',
758 'include_list_post_header' : False,
759 'include_rfc2369_headers' : False,
760 'max_num_recipients' : 0,
761 'new_member_options' : 256,
762 'nondigestable' : True,
763 'obscure_addresses' : True,
764 'preferred_language' : 'fr',
765 'reject_these_nonmembers' : [],
766 'reply_goes_to_list' : 0,
767 'reply_to_address' : '',
768 'require_explicit_destination' : False,
769 'send_reminders' : 0,
770 'send_welcome_msg' : True,
771 'topics_enabled' : False,
772 'umbrella_list' : False,
773 'unsubscribe_policy' : 0,
774}
775
c1bae0aa
FB
776def check_options_runner(userdesc, perms, mlist, listname, correct):
777 options = { }
778 for (k, v) in check_opts.iteritems():
779 if mlist.__dict__[k] != v:
780 options[k] = v, mlist.__dict__[k]
781 if correct: mlist.__dict__[k] = v
782 if mlist.real_name.lower() != listname:
783 options['real_name'] = listname, mlist.real_name
784 if correct: mlist.real_name = listname
95bb095d 785 return 1
c1bae0aa
FB
786
787
ae6c293b 788def check_options(userdesc, perms, vhost, listname, correct=False):
c1bae0aa
FB
789 """ Check the list.
790 @root
791 """
88f7a3f1 792 listname = listname.lower()
c1bae0aa
FB
793 mlist = MailList.MailList(vhost + VHOST_SEP + listname, lock=0)
794 if correct:
795 return list_call_locked(check_options_runner, userdesc, perms, mlist, True, listname, True)
796 else:
797 return check_options_runner(userdesc, perms, mlist, listname, False)
0337d704 798
799#-------------------------------------------------------------------------------
800# super-admin procedures
801#
802
ae6c293b 803def get_all_lists(userdesc, perms, vhost):
c1bae0aa
FB
804 """ Get all the list for the given vhost
805 """
0337d704 806 prefix = vhost.lower()+VHOST_SEP
807 names = Utils.list_names()
808 names.sort()
809 result = []
810 for name in names:
811 if not name.startswith(prefix):
812 continue
ae6c293b 813 result.append(name.replace(prefix, ''))
0337d704 814 return result
815
ae6c293b 816def create_list(userdesc, perms, vhost, listname, desc, advertise, modlevel, inslevel, owners, members):
c1bae0aa
FB
817 """ Create a new list.
818 @root
819 """
820 name = vhost.lower() + VHOST_SEP + listname.lower();
0337d704 821 if Utils.list_exists(name):
822 return 0
ae6c293b 823
0337d704 824 owner = []
825 for o in owners:
826 email = to_forlife(o)[0]
827 if email is not None:
828 owner.append(email)
829 if len(owner) is 0:
830 return 0
831
832 mlist = MailList.MailList()
833 try:
834 oldmask = os.umask(002)
835 pw = sha.new('foobar').hexdigest()
ae6c293b 836
0337d704 837 try:
838 mlist.Create(name, owner[0], pw)
839 finally:
840 os.umask(oldmask)
841
842 mlist.real_name = listname
843 mlist.host_name = 'listes.polytechnique.org'
844 mlist.description = desc
845
846 mlist.advertised = int(advertise) is 0
847 mlist.default_member_moderation = int(modlevel) is 2
848 mlist.generic_nonmember_action = int(modlevel) > 0
849 mlist.subscribe_policy = 2 * (int(inslevel) is 1)
850 mlist.admin_notify_mchanges = (mlist.subscribe_policy or mlist.generic_nonmember_action or mlist.default_member_moderation or not mlist.advertised)
ae6c293b 851
0337d704 852 mlist.owner = owner
ae6c293b 853
0337d704 854 mlist.subject_prefix = '['+listname+'] '
855 mlist.max_message_size = 0
856
e480db84 857 inverted_listname = listname.lower() + '_' + vhost.lower()
0337d704 858 mlist.msg_footer = "_______________________________________________\n" \
31f2df6a 859 + "Liste de diffusion %(real_name)s\n" \
860 + "http://listes.polytechnique.org/members/" + inverted_listname
ae6c293b 861
0337d704 862 mlist.header_filter_rules = []
c638d8c8 863 mlist.header_filter_rules.append(('X-Spam-Flag: Unsure, tests=bogofilter', mm_cfg.HOLD, False))
0337d704 864 mlist.header_filter_rules.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.HOLD, False))
0337d704 865
98c79ede 866 if ON_CREATE_CMD != '':
867 try: os.system(ON_CREATE_CMD + ' ' + name)
868 except: pass
869
95bb095d 870 check_options_runner(userdesc, perms, mlist, listname.lower(), True)
c1bae0aa 871 mass_subscribe(userdesc, perms, mlist, members)
0337d704 872 mlist.Save()
c1bae0aa
FB
873 finally:
874 mlist.Unlock()
95bb095d
FB
875
876 # avoid the "-1 mail to moderate" bug
877 mlist = MailList.MailList(name)
878 mlist._UpdateRecords()
879 mlist.Save()
880 return 1
c1bae0aa
FB
881
882def delete_list(userdesc, perms, mlist, del_archives=0):
883 """ Delete the list.
884 @mlist
885 @admin
886 """
887 lname = mlist.internal_name()
888 # remove the list
889 REMOVABLES = [ os.path.join('lists', lname), ]
890 # remove stalled locks
891 for filename in os.listdir(mm_cfg.LOCK_DIR):
892 fn_lname = filename.split('.')[0]
893 if fn_lname == lname:
894 REMOVABLES.append(os.path.join(mm_cfg.LOCK_DIR, filename))
895 # remove archives ?
896 if del_archives:
897 REMOVABLES.extend([
898 os.path.join('archives', 'private', lname),
899 os.path.join('archives', 'private', lname+'.mbox'),
900 os.path.join('archives', 'public', lname),
901 os.path.join('archives', 'public', lname+'.mbox')
902 ])
903 map(lambda dir: remove_it(lname, os.path.join(mm_cfg.VAR_PREFIX, dir)), REMOVABLES)
904 return 1
0337d704 905
ae6c293b 906def kill(userdesc, perms, vhost, alias, del_from_promo):
c1bae0aa
FB
907 """ Remove a user from all the lists.
908 """
0337d704 909 exclude = []
910 if not del_from_promo:
c1bae0aa 911 exclude.append(PLATAL_DOMAIN + VHOST_SEP + 'promo' + alias[-4:])
0337d704 912 for list in Utils.list_names():
c1bae0aa
FB
913 if list in exclude:
914 continue
0337d704 915 try:
ae6c293b 916 mlist = MailList.MailList(list, lock=0)
0337d704 917 except:
918 continue
919 try:
920 mlist.Lock()
ae6c293b 921 mlist.ApprovedDeleteMember(alias+'@'+PLATAL_DOMAIN, None, 0, 0)
0337d704 922 mlist.Save()
923 mlist.Unlock()
924 except:
925 mlist.Unlock()
926 return 1
927
928
929#-------------------------------------------------------------------------------
930# server
931#
932class FastXMLRPCServer(SocketServer.ThreadingMixIn, SimpleXMLRPCServer):
ae6c293b 933 allow_reuse_address = True
0337d704 934
935################################################################################
936#
ae6c293b 937# INIT
0337d704 938#
939#-------------------------------------------------------------------------------
940# use Mailman user and group (not root)
941# fork in background if asked to
942#
943
944uid = getpwnam(mm_cfg.MAILMAN_USER)[2]
945gid = getgrnam(mm_cfg.MAILMAN_GROUP)[2]
946
947if not os.getuid():
ae6c293b 948 os.setregid(gid, gid)
949 os.setreuid(uid, uid)
0337d704 950
951signal.signal(signal.SIGHUP, signal.SIG_IGN)
952
953if ( os.getuid() is not uid ) or ( os.getgid() is not gid):
954 sys.exit(0)
955
956opts, args = getopt.getopt(sys.argv[1:], 'f')
957for o, a in opts:
958 if o == '-f' and os.fork():
959 sys.exit(0)
960
961i18n.set_language('fr')
962mysql = connectDB()
963lock = Lock()
964
965#-------------------------------------------------------------------------------
966# server
967#
1fec3393 968server = FastXMLRPCServer((SRV_HOST, SRV_PORT), BasicAuthXMLRPCRequestHandler)
0337d704 969
970# index.php
971server.register_function(get_lists)
972server.register_function(subscribe)
973server.register_function(unsubscribe)
974# members.php
975server.register_function(get_members)
976# trombi.php
977server.register_function(get_members_limit)
978server.register_function(get_owners)
979# admin.php
c4d57bd8 980server.register_function(replace_email)
0337d704 981server.register_function(mass_subscribe)
982server.register_function(mass_unsubscribe)
983server.register_function(add_owner)
984server.register_function(del_owner)
985# moderate.php
986server.register_function(get_pending_ops)
987server.register_function(handle_request)
4b0d9ef3 988server.register_function(get_pending_sub)
0337d704 989server.register_function(get_pending_mail)
990# options.php
991server.register_function(get_owner_options)
992server.register_function(set_owner_options)
993server.register_function(add_to_wl)
994server.register_function(del_from_wl)
995server.register_function(get_bogo_level)
996server.register_function(set_bogo_level)
997# soptions.php
998server.register_function(get_admin_options)
999server.register_function(set_admin_options)
1000# check.php
1001server.register_function(check_options)
1002# create + del
1003server.register_function(get_all_lists)
1004server.register_function(create_list)
1005server.register_function(delete_list)
1006# utilisateurs.php
1007server.register_function(kill)
1008
1009server.serve_forever()
1010
c638d8c8 1011# vim:set et sw=4 sts=4 sws=4: