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