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