Add get_all_user_lists() to MM RPC.
[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 create_list(userdesc, perms, vhost, listname, desc, advertise, modlevel, inslevel, owners, members):
853 """ Create a new list.
854 @root
855 """
856 name = vhost.lower() + VHOST_SEP + listname.lower();
857 if Utils.list_exists(name):
858 return 0
859
860 owner = []
861 for o in owners:
862 email = to_forlife(o)[0]
863 if email is not None:
864 owner.append(email)
865 if len(owner) is 0:
866 return 0
867
868 mlist = MailList.MailList()
869 try:
870 oldmask = os.umask(002)
871 pw = sha.new('foobar').hexdigest()
872
873 try:
874 mlist.Create(name, owner[0], pw)
875 finally:
876 os.umask(oldmask)
877
878 mlist.real_name = listname
879 mlist.host_name = 'listes.polytechnique.org'
880 mlist.description = desc
881
882 mlist.advertised = int(advertise) is 0
883 mlist.default_member_moderation = int(modlevel) is 2
884 mlist.generic_nonmember_action = int(modlevel) > 0
885 mlist.subscribe_policy = 2 * (int(inslevel) is 1)
886 mlist.admin_notify_mchanges = (mlist.subscribe_policy or mlist.generic_nonmember_action or mlist.default_member_moderation or not mlist.advertised)
887
888 mlist.owner = owner
889
890 mlist.subject_prefix = '['+listname+'] '
891 mlist.max_message_size = 0
892
893 inverted_listname = listname.lower() + '_' + vhost.lower()
894 mlist.msg_footer = "_______________________________________________\n" \
895 + "Liste de diffusion %(real_name)s\n" \
896 + "http://listes.polytechnique.org/members/" + inverted_listname
897
898 mlist.header_filter_rules = []
899 mlist.header_filter_rules.append(('X-Spam-Flag: Unsure, tests=bogofilter', mm_cfg.HOLD, False))
900 mlist.header_filter_rules.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.HOLD, False))
901
902 if ON_CREATE_CMD != '':
903 try: os.system(ON_CREATE_CMD + ' ' + name)
904 except: pass
905
906 check_options_runner(userdesc, perms, mlist, listname.lower(), True)
907 mass_subscribe(userdesc, perms, mlist, members)
908 mlist.Save()
909 finally:
910 mlist.Unlock()
911
912 # avoid the "-1 mail to moderate" bug
913 mlist = MailList.MailList(name)
914 try:
915 mlist._UpdateRecords()
916 mlist.Save()
917 finally:
918 mlist.Unlock()
919 return 1
920
921 def delete_list(userdesc, perms, mlist, del_archives=0):
922 """ Delete the list.
923 @mlist
924 @admin
925 """
926 lname = mlist.internal_name()
927 # remove the list
928 REMOVABLES = [ os.path.join('lists', lname), ]
929 # remove stalled locks
930 for filename in os.listdir(mm_cfg.LOCK_DIR):
931 fn_lname = filename.split('.')[0]
932 if fn_lname == lname:
933 REMOVABLES.append(os.path.join(mm_cfg.LOCK_DIR, filename))
934 # remove archives ?
935 if del_archives:
936 REMOVABLES.extend([
937 os.path.join('archives', 'private', lname),
938 os.path.join('archives', 'private', lname+'.mbox'),
939 os.path.join('archives', 'public', lname),
940 os.path.join('archives', 'public', lname+'.mbox')
941 ])
942 map(lambda dir: remove_it(lname, os.path.join(mm_cfg.VAR_PREFIX, dir)), REMOVABLES)
943 return 1
944
945 def kill(userdesc, perms, vhost, alias, del_from_promo):
946 """ Remove a user from all the lists.
947 """
948 exclude = []
949 if not del_from_promo:
950 exclude.append(PLATAL_DOMAIN + VHOST_SEP + 'promo' + alias[-4:])
951 for list in Utils.list_names():
952 if list in exclude:
953 continue
954 try:
955 mlist = MailList.MailList(list, lock=0)
956 except:
957 continue
958 try:
959 mlist.Lock()
960 mlist.ApprovedDeleteMember(alias+'@'+PLATAL_DOMAIN, None, 0, 0)
961 mlist.Save()
962 mlist.Unlock()
963 except:
964 mlist.Unlock()
965 return 1
966
967
968 #-------------------------------------------------------------------------------
969 # server
970 #
971 class FastXMLRPCServer(SocketServer.ThreadingMixIn, SimpleXMLRPCServer):
972 allow_reuse_address = True
973
974 ################################################################################
975 #
976 # INIT
977 #
978 #-------------------------------------------------------------------------------
979 # use Mailman user and group (not root)
980 # fork in background if asked to
981 #
982
983 uid = getpwnam(mm_cfg.MAILMAN_USER)[2]
984 gid = getgrnam(mm_cfg.MAILMAN_GROUP)[2]
985
986 if not os.getuid():
987 os.setregid(gid, gid)
988 os.setreuid(uid, uid)
989
990 signal.signal(signal.SIGHUP, signal.SIG_IGN)
991
992 if ( os.getuid() is not uid ) or ( os.getgid() is not gid):
993 sys.exit(0)
994
995 opts, args = getopt.getopt(sys.argv[1:], 'f')
996 for o, a in opts:
997 if o == '-f' and os.fork():
998 sys.exit(0)
999
1000 i18n.set_language('fr')
1001 mysql = connectDB()
1002 lock = Lock()
1003
1004 #-------------------------------------------------------------------------------
1005 # server
1006 #
1007 server = FastXMLRPCServer((SRV_HOST, SRV_PORT), BasicAuthXMLRPCRequestHandler)
1008
1009 # index.php
1010 server.register_function(get_lists)
1011 server.register_function(subscribe)
1012 server.register_function(unsubscribe)
1013 # members.php
1014 server.register_function(get_members)
1015 # trombi.php
1016 server.register_function(get_members_limit)
1017 server.register_function(get_owners)
1018 # admin.php
1019 server.register_function(replace_email)
1020 server.register_function(mass_subscribe)
1021 server.register_function(mass_unsubscribe)
1022 server.register_function(add_owner)
1023 server.register_function(del_owner)
1024 # moderate.php
1025 server.register_function(get_pending_ops)
1026 server.register_function(handle_request)
1027 server.register_function(get_pending_sub)
1028 server.register_function(get_pending_mail)
1029 # options.php
1030 server.register_function(get_owner_options)
1031 server.register_function(set_owner_options)
1032 server.register_function(add_to_wl)
1033 server.register_function(del_from_wl)
1034 server.register_function(get_bogo_level)
1035 server.register_function(set_bogo_level)
1036 # soptions.php
1037 server.register_function(get_admin_options)
1038 server.register_function(set_admin_options)
1039 # check.php
1040 server.register_function(check_options)
1041 # create + del
1042 server.register_function(get_all_lists)
1043 server.register_function(get_all_user_lists)
1044 server.register_function(create_list)
1045 server.register_function(delete_list)
1046 # utilisateurs.php
1047 server.register_function(kill)
1048
1049 server.serve_forever()
1050
1051 # vim:set et sw=4 sts=4 sws=4: