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