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