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