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