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