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