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