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