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