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