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