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