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