Print more informations.
[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 0
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 details, members = get_list_info(userdesc, perms, mlist)
418 members.sort()
419 members = map(lambda member: (get_name(member), member), members)
420 return (details, members, mlist.owner)
421
422
423 #-------------------------------------------------------------------------------
424 # users procedures for [ trombi.php ]
425 #
426
427 def get_members_limit(userdesc, perms, mlist, page, nb_per_page):
428 """ Get a range of members of the list.
429 @mlist
430 """
431 members = get_members(userdesc, perms, mlist)[1]
432 i = int(page) * int(nb_per_page)
433 return (len(members), members[i:i+int(nb_per_page)])
434
435 def get_owners(userdesc, perms, mlist):
436 """ Get the owners of the list.
437 @mlist
438 """
439 details, members, owners = get_members(userdesc, perms, mlist)
440 return (details, owners)
441
442
443 #-------------------------------------------------------------------------------
444 # owners procedures [ admin.php ]
445 #
446
447 def replace_email(userdesc, perms, mlist, from_email, to_email):
448 """ Replace the address of a member by another one.
449 @mlist
450 @edit
451 @admin
452 """
453 mlist.ApprovedChangeMemberAddress(from_email.lower(), to_email.lower(), 0)
454 return 1
455
456 def mass_subscribe(userdesc, perms, mlist, users):
457 """ Add a list of users to the list.
458 @mlist
459 @edit
460 @admin
461 """
462 members = mlist.getRegularMemberKeys()
463 added = []
464 for user in users:
465 email, name = to_forlife(user)
466 if ( email is None ) or ( email in members ):
467 continue
468 userd = UserDesc(email, name, None, 0)
469 mlist.ApprovedAddMember(userd)
470 added.append( (quote(userd.fullname), userd.address) )
471 return added
472
473 def mass_unsubscribe(userdesc, perms, mlist, users):
474 """ Remove a list of users from the list.
475 @mlist
476 @edit
477 @admin
478 """
479 map(lambda user: mlist.ApprovedDeleteMember(user), users)
480 return users
481
482 def add_owner(userdesc, perms, mlist, user):
483 """ Add a owner to the list.
484 @mlist
485 @edit
486 @admin
487 """
488 email = to_forlife(user)[0]
489 if email is None:
490 return 0
491 if email not in mlist.owner:
492 mlist.owner.append(email)
493 return True
494
495 def del_owner(userdesc, perms, mlist, user):
496 """ Remove a owner of the list.
497 @mlist
498 @edit
499 @admin
500 """
501 if len(mlist.owner) < 2:
502 return 0
503 mlist.owner.remove(user)
504 return True
505
506 #-------------------------------------------------------------------------------
507 # owners procedures [ admin.php ]
508 #
509
510 def get_pending_ops(userdesc, perms, mlist):
511 """ Get the list of operation waiting for an action from the owners.
512 @mlist
513 @lock
514 @admin
515 """
516 subs = []
517 seen = []
518 dosave = False
519 for id in mlist.GetSubscriptionIds():
520 time, addr, fullname, passwd, digest, lang = mlist.GetRecord(id)
521 if addr in seen:
522 mlist.HandleRequest(id, mm_cfg.DISCARD)
523 dosave = True
524 continue
525 seen.append(addr)
526 try:
527 login = re.match("^[^.]*\.[^.]*\.\d\d\d\d$", addr.split('@')[0]).group()
528 subs.append({'id': id, 'name': quote(fullname), 'addr': addr, 'login': login })
529 except:
530 subs.append({'id': id, 'name': quote(fullname), 'addr': addr })
531
532 helds = []
533 for id in mlist.GetHeldMessageIds():
534 ptime, sender, subject, reason, filename, msgdata = mlist.GetRecord(id)
535 fpath = os.path.join(mm_cfg.DATA_DIR, filename)
536 try:
537 size = os.path.getsize(fpath)
538 except OSError, e:
539 if e.errno <> errno.ENOENT: raise
540 continue
541 try:
542 msg = readMessage(fpath)
543 fromX = msg.has_key("X-Org-Mail")
544 except:
545 pass
546 helds.append({
547 'id' : id,
548 'sender': quote(sender, True),
549 'size' : size,
550 'subj' : quote(subject, True),
551 'stamp' : ptime,
552 'fromx' : fromX
553 })
554 if dosave:
555 mlist.Save()
556 return (subs, helds)
557
558 def handle_request(userdesc, perms, mlist, id, value, comment):
559 """ Handle a moderation request.
560 @mlist
561 @edit
562 @admin
563 """
564 mlist.HandleRequest(int(id), int(value), comment)
565 return 1
566
567 def get_pending_sub(userdesc, perms, mlist, id):
568 """ Get informations about a given subscription moderation.
569 @mlist
570 @lock
571 @admin
572 """
573 sub = 0
574 id = int(id)
575 if id in mlist.GetSubscriptionIds():
576 time, addr, fullname, passwd, digest, lang = mlist.GetRecord(id)
577 try:
578 login = re.match("^[^.]*\.[^.]*\.\d\d\d\d$", addr.split('@')[0]).group()
579 sub = {'id': id, 'name': quote(fullname), 'addr': addr, 'login': login }
580 except:
581 sub = {'id': id, 'name': quote(fullname), 'addr': addr }
582 return sub
583
584 def get_pending_mail(userdesc, perms, mlist, id, raw=0):
585 """ Get informations about a given mail moderation.
586 @mlist
587 @lock
588 @admin
589 """
590 ptime, sender, subject, reason, filename, msgdata = mlist.GetRecord(int(id))
591 fpath = os.path.join(mm_cfg.DATA_DIR, filename)
592 size = os.path.getsize(fpath)
593 msg = readMessage(fpath)
594
595 if raw:
596 return quote(str(msg))
597 results_plain = []
598 results_html = []
599 for part in typed_subpart_iterator(msg, 'text', 'plain'):
600 c = part.get_payload()
601 if c is not None: results_plain.append (c)
602 results_plain = map(lambda x: quote(x), results_plain)
603 for part in typed_subpart_iterator(msg, 'text', 'html'):
604 c = part.get_payload()
605 if c is not None: results_html.append (c)
606 results_html = map(lambda x: quote(x), results_html)
607 return {'id' : id,
608 'sender': quote(sender, True),
609 'size' : size,
610 'subj' : quote(subject, True),
611 'stamp' : ptime,
612 'parts_plain' : results_plain,
613 'parts_html': results_html }
614
615 #-------------------------------------------------------------------------------
616 # owner options [ options.php ]
617 #
618
619 owner_opts = ['accept_these_nonmembers', 'admin_notify_mchanges', 'description', \
620 'default_member_moderation', 'generic_nonmember_action', 'info', \
621 'subject_prefix', 'goodbye_msg', 'send_goodbye_msg', 'subscribe_policy', \
622 'welcome_msg']
623
624 def get_owner_options(userdesc, perms, mlist):
625 """ Get the owner options of a list.
626 @mlist
627 @admin
628 """
629 return get_options(userdesc, perms, mlist, owner_opts)
630
631 def set_owner_options(userdesc, perms, mlist, values):
632 """ Set the owner options of a list.
633 @mlist
634 @edit
635 @admin
636 """
637 return set_options(userdesc, perms, mlist, owner_opts, values)
638
639 def add_to_wl(userdesc, perms, mlist, addr):
640 """ Add addr to the whitelist
641 @mlist
642 @edit
643 @admin
644 """
645 mlist.accept_these_nonmembers.append(addr)
646 return 1
647
648 def del_from_wl(userdesc, perms, mlist, addr):
649 """ Remove an address from the whitelist
650 @mlist
651 @edit
652 @admin
653 """
654 mlist.accept_these_nonmembers.remove(addr)
655 return 1
656
657 def get_bogo_level(userdesc, perms, mlist):
658 """ Compute bogo level from the filtering rules set up on the list.
659 @mlist
660 @admin
661 """
662 if len(mlist.header_filter_rules) == 0:
663 return 0
664
665 unsurelevel = 0
666 filterlevel = 0
667 filterbase = 0
668
669 # The first rule filters Unsure mails
670 if mlist.header_filter_rules[0][0] == 'X-Spam-Flag: Unsure, tests=bogofilter':
671 unsurelevel = 1
672 filterbase = 1
673
674 # Check the other rules:
675 # - we have 2 rules: this is level 2 (drop > 0.999999, moderate Yes)
676 # - we have only one rule with HOLD directive : this is level 1 (moderate spams)
677 # - we have only one rule with DISCARD directive : this is level 3 (drop spams)
678 try:
679 action = mlist.header_filter_rules[filterbase + 1][1]
680 filterlevel = 2
681 except:
682 action = mlist.header_filter_rules[filterbase][1]
683 if action == mm_cfg.HOLD:
684 filterlevel = 1
685 elif action == mm_cfg.DISCARD:
686 filterlevel = 3
687 return (filterlevel << 1) + unsurelevel
688
689 def set_bogo_level(userdesc, perms, mlist, level):
690 """ Set filter to the specified level.
691 @mlist
692 @edit
693 @admin
694 """
695 hfr = []
696
697 # The level is a combination of a spam filtering level and unsure filtering level
698 # - the unsure filtering level is only 1 bit (1 = HOLD unsures, 0 = Accept unsures)
699 # - the spam filtering level is a number growing with filtering strength
700 # (0 = no filtering, 1 = moderate spam, 2 = drop 0.999999 and moderate others, 3 = drop spams)
701 bogolevel = int(level)
702 filterlevel = bogolevel >> 1
703 unsurelevel = bogolevel & 1
704
705 # Set up unusre filtering
706 if unsurelevel == 1:
707 hfr.append(('X-Spam-Flag: Unsure, tests=bogofilter', mm_cfg.HOLD, False))
708
709 # Set up spam filtering
710 if filterlevel is 1:
711 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.HOLD, False))
712 elif filterlevel is 2:
713 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter, spamicity=(0\.999999|1\.000000)', mm_cfg.DISCARD, False))
714 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.HOLD, False))
715 elif filterlevel is 3:
716 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.DISCARD, False))
717
718 # save configuration
719 if mlist.header_filter_rules != hfr:
720 mlist.header_filter_rules = hfr
721 return 1
722
723 #-------------------------------------------------------------------------------
724 # admin procedures [ soptions.php ]
725 #
726
727 admin_opts = [ 'advertised', 'archive', \
728 'max_message_size', 'msg_footer', 'msg_header']
729
730 def get_admin_options(userdesc, perms, mlist):
731 """ Get administrator options.
732 @mlist
733 @root
734 """
735 return get_options(userdesc, perms, mlist, admin_opts)
736
737 def set_admin_options(userdesc, perms, mlist, values):
738 """ Set administrator options.
739 @mlist
740 @edit
741 @root
742 """
743 return set_options(userdesc, perms, mlist, admin_opts, values)
744
745 #-------------------------------------------------------------------------------
746 # admin procedures [ check.php ]
747 #
748
749 check_opts = {
750 'acceptable_aliases' : '',
751 'admin_immed_notify' : True,
752 'administrivia' : True,
753 'anonymous_list' : False,
754 'autorespond_admin' : False,
755 'autorespond_postings' : False,
756 'autorespond_requests' : False,
757 'available_languages' : ['fr'],
758 'ban_list' : [],
759 'bounce_matching_headers' : '',
760 'bounce_processing' : False,
761 'convert_html_to_plaintext' : False,
762 'digestable' : False,
763 'digest_is_default' : False,
764 'discard_these_nonmembers' : [],
765 'emergency' : False,
766 'encode_ascii_prefixes' : 2,
767 'filter_content' : False,
768 'first_strip_reply_to' : False,
769 'forward_auto_discards' : True,
770 'hold_these_nonmembers' : [],
771 'host_name' : 'listes.polytechnique.org',
772 'include_list_post_header' : False,
773 'include_rfc2369_headers' : False,
774 'max_num_recipients' : 0,
775 'new_member_options' : 256,
776 'nondigestable' : True,
777 'obscure_addresses' : True,
778 'preferred_language' : 'fr',
779 'reject_these_nonmembers' : [],
780 'reply_goes_to_list' : 0,
781 'reply_to_address' : '',
782 'require_explicit_destination' : False,
783 'send_reminders' : 0,
784 'send_welcome_msg' : True,
785 'topics_enabled' : False,
786 'umbrella_list' : False,
787 'unsubscribe_policy' : 0,
788 }
789
790 def check_options_runner(userdesc, perms, mlist, listname, correct):
791 options = { }
792 for (k, v) in check_opts.iteritems():
793 if mlist.__dict__[k] != v:
794 options[k] = v, mlist.__dict__[k]
795 if correct: mlist.__dict__[k] = v
796 if mlist.real_name.lower() != listname:
797 options['real_name'] = listname, mlist.real_name
798 if correct: mlist.real_name = listname
799 return 1
800
801
802 def check_options(userdesc, perms, vhost, listname, correct=False):
803 """ Check the list.
804 @root
805 """
806 listname = listname.lower()
807 mlist = MailList.MailList(vhost + VHOST_SEP + listname, lock=0)
808 if correct:
809 return list_call_locked(check_options_runner, userdesc, perms, mlist, True, listname, True)
810 else:
811 return check_options_runner(userdesc, perms, mlist, listname, False)
812
813 #-------------------------------------------------------------------------------
814 # super-admin procedures
815 #
816
817 def get_all_lists(userdesc, perms, vhost):
818 """ Get all the list for the given vhost
819 @root
820 """
821 prefix = vhost.lower()+VHOST_SEP
822 names = Utils.list_names()
823 names.sort()
824 result = []
825 for name in names:
826 if not name.startswith(prefix):
827 continue
828 result.append(name.replace(prefix, ''))
829 return result
830
831 def get_all_user_lists(userdesc, perms, vhost, email):
832 """ Get all the lists for the given user
833 @root
834 """
835 names = Utils.list_names()
836 names.sort()
837 result = []
838 for name in names:
839 try:
840 mlist = MailList.MailList(name, lock=0)
841 ismember = email in mlist.getRegularMemberKeys()
842 isowner = email in mlist.owner
843 if not ismember and not isowner:
844 continue
845 host = mlist.internal_name().split(VHOST_SEP)[0].lower()
846 result.append({ 'list': mlist.real_name,
847 'addr': mlist.real_name.lower() + '@' + host,
848 'host': host,
849 'own' : isowner,
850 'sub' : ismember
851 })
852 except Exception, e:
853 continue
854 return result
855
856 def change_user_email(userdesc, perms, vhost, from_email, to_email):
857 """ Change the email of a user
858 @root
859 """
860 from_email = from_email.lower()
861 to_email = to_email.lower()
862 for list in Utils.list_names():
863 try:
864 mlist = MailList.MailList(list, lock=0)
865 except:
866 continue
867 try:
868 mlist.Lock()
869 mlist.ApprovedChangeMemberAddress(from_email, to_email, 0)
870 mlist.Save()
871 mlist.Unlock()
872 except:
873 mlist.Unlock()
874 return 1
875
876
877 def create_list(userdesc, perms, vhost, listname, desc, advertise, modlevel, inslevel, owners, members):
878 """ Create a new list.
879 @root
880 """
881 name = vhost.lower() + VHOST_SEP + listname.lower();
882 if Utils.list_exists(name):
883 return 0
884
885 owner = []
886 for o in owners:
887 email = to_forlife(o)[0]
888 if email is not None:
889 owner.append(email)
890 if len(owner) is 0:
891 return 0
892
893 mlist = MailList.MailList()
894 try:
895 oldmask = os.umask(002)
896 pw = sha.new('foobar').hexdigest()
897
898 try:
899 mlist.Create(name, owner[0], pw)
900 finally:
901 os.umask(oldmask)
902
903 mlist.real_name = listname
904 mlist.host_name = 'listes.polytechnique.org'
905 mlist.description = desc
906
907 mlist.advertised = int(advertise) is 0
908 mlist.default_member_moderation = int(modlevel) is 2
909 mlist.generic_nonmember_action = int(modlevel) > 0
910 mlist.subscribe_policy = 2 * (int(inslevel) is 1)
911 mlist.admin_notify_mchanges = (mlist.subscribe_policy or mlist.generic_nonmember_action or mlist.default_member_moderation or not mlist.advertised)
912
913 mlist.owner = owner
914
915 mlist.subject_prefix = '['+listname+'] '
916 mlist.max_message_size = 0
917
918 inverted_listname = listname.lower() + '_' + vhost.lower()
919 mlist.msg_footer = "_______________________________________________\n" \
920 + "Liste de diffusion %(real_name)s\n" \
921 + "http://listes.polytechnique.org/members/" + inverted_listname
922
923 mlist.header_filter_rules = []
924 mlist.header_filter_rules.append(('X-Spam-Flag: Unsure, tests=bogofilter', mm_cfg.HOLD, False))
925 mlist.header_filter_rules.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.HOLD, False))
926
927 if ON_CREATE_CMD != '':
928 try: os.system(ON_CREATE_CMD + ' ' + name)
929 except: pass
930
931 check_options_runner(userdesc, perms, mlist, listname.lower(), True)
932 mass_subscribe(userdesc, perms, mlist, members)
933 mlist.Save()
934 finally:
935 mlist.Unlock()
936
937 # avoid the "-1 mail to moderate" bug
938 mlist = MailList.MailList(name)
939 try:
940 mlist._UpdateRecords()
941 mlist.Save()
942 finally:
943 mlist.Unlock()
944 return 1
945
946 def delete_list(userdesc, perms, mlist, del_archives=0):
947 """ Delete the list.
948 @mlist
949 @admin
950 """
951 lname = mlist.internal_name()
952 # remove the list
953 REMOVABLES = [ os.path.join('lists', lname), ]
954 # remove stalled locks
955 for filename in os.listdir(mm_cfg.LOCK_DIR):
956 fn_lname = filename.split('.')[0]
957 if fn_lname == lname:
958 REMOVABLES.append(os.path.join(mm_cfg.LOCK_DIR, filename))
959 # remove archives ?
960 if del_archives:
961 REMOVABLES.extend([
962 os.path.join('archives', 'private', lname),
963 os.path.join('archives', 'private', lname+'.mbox'),
964 os.path.join('archives', 'public', lname),
965 os.path.join('archives', 'public', lname+'.mbox')
966 ])
967 map(lambda dir: remove_it(lname, os.path.join(mm_cfg.VAR_PREFIX, dir)), REMOVABLES)
968 return 1
969
970 def kill(userdesc, perms, vhost, alias, del_from_promo):
971 """ Remove a user from all the lists.
972 """
973 exclude = []
974 if not del_from_promo:
975 exclude.append(PLATAL_DOMAIN + VHOST_SEP + 'promo' + alias[-4:])
976 for list in Utils.list_names():
977 if list in exclude:
978 continue
979 try:
980 mlist = MailList.MailList(list, lock=0)
981 except:
982 continue
983 try:
984 mlist.Lock()
985 mlist.ApprovedDeleteMember(alias+'@'+PLATAL_DOMAIN, None, 0, 0)
986 mlist.Save()
987 mlist.Unlock()
988 except:
989 mlist.Unlock()
990 return 1
991
992
993 #-------------------------------------------------------------------------------
994 # server
995 #
996 class FastXMLRPCServer(SocketServer.ThreadingMixIn, SimpleXMLRPCServer):
997 allow_reuse_address = True
998
999 ################################################################################
1000 #
1001 # INIT
1002 #
1003 #-------------------------------------------------------------------------------
1004 # use Mailman user and group (not root)
1005 # fork in background if asked to
1006 #
1007
1008 uid = getpwnam(mm_cfg.MAILMAN_USER)[2]
1009 gid = getgrnam(mm_cfg.MAILMAN_GROUP)[2]
1010
1011 if not os.getuid():
1012 os.setregid(gid, gid)
1013 os.setreuid(uid, uid)
1014
1015 signal.signal(signal.SIGHUP, signal.SIG_IGN)
1016
1017 if ( os.getuid() is not uid ) or ( os.getgid() is not gid):
1018 sys.exit(0)
1019
1020 opts, args = getopt.getopt(sys.argv[1:], 'f')
1021 for o, a in opts:
1022 if o == '-f' and os.fork():
1023 sys.exit(0)
1024
1025 i18n.set_language('fr')
1026 mysql = connectDB()
1027 lock = Lock()
1028
1029 #-------------------------------------------------------------------------------
1030 # server
1031 #
1032 server = FastXMLRPCServer((SRV_HOST, SRV_PORT), BasicAuthXMLRPCRequestHandler)
1033
1034 # index.php
1035 server.register_function(get_lists)
1036 server.register_function(subscribe)
1037 server.register_function(unsubscribe)
1038 # members.php
1039 server.register_function(get_members)
1040 # trombi.php
1041 server.register_function(get_members_limit)
1042 server.register_function(get_owners)
1043 # admin.php
1044 server.register_function(replace_email)
1045 server.register_function(mass_subscribe)
1046 server.register_function(mass_unsubscribe)
1047 server.register_function(add_owner)
1048 server.register_function(del_owner)
1049 # moderate.php
1050 server.register_function(get_pending_ops)
1051 server.register_function(handle_request)
1052 server.register_function(get_pending_sub)
1053 server.register_function(get_pending_mail)
1054 # options.php
1055 server.register_function(get_owner_options)
1056 server.register_function(set_owner_options)
1057 server.register_function(add_to_wl)
1058 server.register_function(del_from_wl)
1059 server.register_function(get_bogo_level)
1060 server.register_function(set_bogo_level)
1061 # soptions.php
1062 server.register_function(get_admin_options)
1063 server.register_function(set_admin_options)
1064 # check.php
1065 server.register_function(check_options)
1066 # create + del
1067 server.register_function(get_all_lists)
1068 server.register_function(get_all_user_lists)
1069 server.register_function(change_user_email)
1070 server.register_function(create_list)
1071 server.register_function(delete_list)
1072 # utilisateurs.php
1073 server.register_function(kill)
1074
1075 server.serve_forever()
1076
1077 # vim:set et sw=4 sts=4 sws=4: