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