Add account consultation statistics to weekly crons.
[platal.git] / bin / lists.rpc.py
1 #!/usr/bin/env python
2 #***************************************************************************
3 #* Copyright (C) 2003-2011 Polytechnique.org *
4 #* http://opensource.polytechnique.org/ *
5 #* *
6 #* This program is free software; you can redistribute it and/or modify *
7 #* it under the terms of the GNU General Public License as published by *
8 #* the Free Software Foundation; either version 2 of the License, or *
9 #* (at your option) any later version. *
10 #* *
11 #* This program is distributed in the hope that it will be useful, *
12 #* but WITHOUT ANY WARRANTY; without even the implied warranty of *
13 #* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14 #* GNU General Public License for more details. *
15 #* *
16 #* You should have received a copy of the GNU General Public License *
17 #* along with this program; if not, write to the Free Software *
18 #* Foundation, Inc., *
19 #* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
20 #***************************************************************************
21
22 import base64, MySQLdb, os, getopt, sys, sha, signal, re, shutil, ConfigParser
23 import MySQLdb.converters
24 import SocketServer
25 import errno
26 import traceback
27
28 sys.path.append('/usr/lib/mailman/bin')
29
30 from pwd import getpwnam
31 from grp import getgrnam
32
33 from SimpleXMLRPCServer import SimpleXMLRPCServer
34 from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler
35
36 import paths
37 from Mailman import MailList
38 from Mailman import Utils
39 from Mailman import Message
40 from Mailman import Errors
41 from Mailman import mm_cfg
42 from Mailman import i18n
43 from Mailman.UserDesc import UserDesc
44 from Mailman.ListAdmin import readMessage
45 from email.Iterators import typed_subpart_iterator
46 from threading import Lock
47
48 class AuthFailed(Exception): pass
49
50 ################################################################################
51 #
52 # CONFIG
53 #
54 #------------------------------------------------
55
56 config = ConfigParser.ConfigParser()
57 config.read(os.path.dirname(__file__)+'/../configs/platal.ini')
58 config.read(os.path.dirname(__file__)+'/../configs/platal.conf')
59
60 def get_config(sec, val, default=None):
61 try:
62 return config.get(sec, val)[1:-1]
63 except ConfigParser.NoOptionError, e:
64 if default is None:
65 sys.stderr.write('%s\n' % str(e))
66 sys.exit(1)
67 else:
68 return default
69
70 MYSQL_USER = get_config('Core', 'dbuser')
71 MYSQL_PASS = get_config('Core', 'dbpwd')
72 MYSQL_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 not isinstance(users, list):
471 raise Exception("userlist must be a list")
472 members = mlist.getRegularMemberKeys()
473 added = []
474 for user in users:
475 email, name = to_forlife(user)
476 if ( email is None ) or ( email in members ):
477 continue
478 userd = UserDesc(email, name, None, 0)
479 mlist.ApprovedAddMember(userd)
480 added.append( (quote(userd.fullname), userd.address) )
481 return added
482
483 def mass_unsubscribe(userdesc, perms, mlist, users):
484 """ Remove a list of users from the list.
485 @mlist
486 @edit
487 @admin
488 """
489 map(lambda user: mlist.ApprovedDeleteMember(user), users)
490 return users
491
492 def add_owner(userdesc, perms, mlist, user):
493 """ Add a owner to the list.
494 @mlist
495 @edit
496 @admin
497 """
498 email = to_forlife(user)[0]
499 if email is None:
500 return 0
501 if email not in mlist.owner:
502 mlist.owner.append(email)
503 return True
504
505 def del_owner(userdesc, perms, mlist, user):
506 """ Remove a owner of the list.
507 @mlist
508 @edit
509 @admin
510 """
511 if len(mlist.owner) < 2:
512 return 0
513 mlist.owner.remove(user)
514 return True
515
516 #-------------------------------------------------------------------------------
517 # owners procedures [ admin.php ]
518 #
519
520 def get_pending_ops(userdesc, perms, mlist):
521 """ Get the list of operation waiting for an action from the owners.
522 @mlist
523 @lock
524 @admin
525 """
526 subs = []
527 seen = []
528 dosave = False
529 for id in mlist.GetSubscriptionIds():
530 time, addr, fullname, passwd, digest, lang = mlist.GetRecord(id)
531 if addr in seen:
532 mlist.HandleRequest(id, mm_cfg.DISCARD)
533 dosave = True
534 continue
535 seen.append(addr)
536 try:
537 login = re.match("^[^.]*\.[^.]*\.\d\d\d\d$", addr.split('@')[0]).group()
538 subs.append({'id': id, 'name': quote(fullname), 'addr': addr, 'login': login })
539 except:
540 subs.append({'id': id, 'name': quote(fullname), 'addr': addr })
541
542 helds = []
543 for id in mlist.GetHeldMessageIds():
544 ptime, sender, subject, reason, filename, msgdata = mlist.GetRecord(id)
545 fpath = os.path.join(mm_cfg.DATA_DIR, filename)
546 try:
547 size = os.path.getsize(fpath)
548 except OSError, e:
549 if e.errno <> errno.ENOENT: raise
550 continue
551 try:
552 msg = readMessage(fpath)
553 fromX = msg.has_key("X-Org-Mail")
554 except:
555 pass
556 helds.append({
557 'id' : id,
558 'sender': quote(sender, True),
559 'size' : size,
560 'subj' : quote(subject, True),
561 'stamp' : ptime,
562 'fromx' : fromX
563 })
564 if dosave:
565 mlist.Save()
566 return (subs, helds)
567
568 def handle_request(userdesc, perms, mlist, id, value, comment):
569 """ Handle a moderation request.
570 @mlist
571 @edit
572 @admin
573 """
574 # Force encoding to mailman's default for french, since this is what
575 # Mailman will use internally
576 # LC_DESCRIPTIONS is a dict of lang => (name, charset, direction) tuples.
577 encoding = mm_cfg.LC_DESCRIPTIONS['fr'][1]
578 comment = comment.encode(encoding, 'replace')
579 mlist.HandleRequest(int(id), int(value), comment)
580 return 1
581
582 def get_pending_sub(userdesc, perms, mlist, id):
583 """ Get informations about a given subscription moderation.
584 @mlist
585 @lock
586 @admin
587 """
588 sub = 0
589 id = int(id)
590 if id in mlist.GetSubscriptionIds():
591 time, addr, fullname, passwd, digest, lang = mlist.GetRecord(id)
592 try:
593 login = re.match("^[^.]*\.[^.]*\.\d\d\d\d$", addr.split('@')[0]).group()
594 sub = {'id': id, 'name': quote(fullname), 'addr': addr, 'login': login }
595 except:
596 sub = {'id': id, 'name': quote(fullname), 'addr': addr }
597 return sub
598
599 def get_pending_mail(userdesc, perms, mlist, id, raw=0):
600 """ Get informations about a given mail moderation.
601 @mlist
602 @lock
603 @admin
604 """
605 ptime, sender, subject, reason, filename, msgdata = mlist.GetRecord(int(id))
606 fpath = os.path.join(mm_cfg.DATA_DIR, filename)
607 size = os.path.getsize(fpath)
608 msg = readMessage(fpath)
609
610 if raw:
611 return quote(str(msg))
612 results_plain = []
613 results_html = []
614 for part in typed_subpart_iterator(msg, 'text', 'plain'):
615 c = part.get_payload()
616 if c is not None: results_plain.append (c)
617 results_plain = map(lambda x: quote(x), results_plain)
618 for part in typed_subpart_iterator(msg, 'text', 'html'):
619 c = part.get_payload()
620 if c is not None: results_html.append (c)
621 results_html = map(lambda x: quote(x), results_html)
622 return {'id' : id,
623 'sender': quote(sender, True),
624 'size' : size,
625 'subj' : quote(subject, True),
626 'stamp' : ptime,
627 'parts_plain' : results_plain,
628 'parts_html': results_html }
629
630 #-------------------------------------------------------------------------------
631 # owner options [ options.php ]
632 #
633
634 owner_opts = ['accept_these_nonmembers', 'admin_notify_mchanges', 'description', \
635 'default_member_moderation', 'generic_nonmember_action', 'info', \
636 'subject_prefix', 'goodbye_msg', 'send_goodbye_msg', 'subscribe_policy', \
637 'welcome_msg']
638
639 def get_owner_options(userdesc, perms, mlist):
640 """ Get the owner options of a list.
641 @mlist
642 @admin
643 """
644 return get_options(userdesc, perms, mlist, owner_opts)
645
646 def set_owner_options(userdesc, perms, mlist, values):
647 """ Set the owner options of a list.
648 @mlist
649 @edit
650 @admin
651 """
652 return set_options(userdesc, perms, mlist, owner_opts, values)
653
654 def add_to_wl(userdesc, perms, mlist, addr):
655 """ Add addr to the whitelist
656 @mlist
657 @edit
658 @admin
659 """
660 mlist.accept_these_nonmembers.append(addr)
661 return 1
662
663 def del_from_wl(userdesc, perms, mlist, addr):
664 """ Remove an address from the whitelist
665 @mlist
666 @edit
667 @admin
668 """
669 mlist.accept_these_nonmembers.remove(addr)
670 return 1
671
672 def get_bogo_level(userdesc, perms, mlist):
673 """ Compute bogo level from the filtering rules set up on the list.
674 @mlist
675 @admin
676 """
677 if len(mlist.header_filter_rules) == 0:
678 return 0
679
680 unsurelevel = 0
681 filterlevel = 0
682 filterbase = 0
683
684 # The first rule filters Unsure mails
685 if mlist.header_filter_rules[0][0] == 'X-Spam-Flag: Unsure, tests=bogofilter':
686 unsurelevel = 1
687 filterbase = 1
688
689 # Check the other rules:
690 # - we have 2 rules: this is level 2 (drop > 0.999999, moderate Yes)
691 # - we have only one rule with HOLD directive : this is level 1 (moderate spams)
692 # - we have only one rule with DISCARD directive : this is level 3 (drop spams)
693 try:
694 action = mlist.header_filter_rules[filterbase + 1][1]
695 filterlevel = 2
696 except:
697 action = mlist.header_filter_rules[filterbase][1]
698 if action == mm_cfg.HOLD:
699 filterlevel = 1
700 elif action == mm_cfg.DISCARD:
701 filterlevel = 3
702 return (filterlevel << 1) + unsurelevel
703
704 def set_bogo_level(userdesc, perms, mlist, level):
705 """ Set filter to the specified level.
706 @mlist
707 @edit
708 @admin
709 """
710 hfr = []
711
712 # The level is a combination of a spam filtering level and unsure filtering level
713 # - the unsure filtering level is only 1 bit (1 = HOLD unsures, 0 = Accept unsures)
714 # - the spam filtering level is a number growing with filtering strength
715 # (0 = no filtering, 1 = moderate spam, 2 = drop 0.999999 and moderate others, 3 = drop spams)
716 bogolevel = int(level)
717 filterlevel = bogolevel >> 1
718 unsurelevel = bogolevel & 1
719
720 # Set up unusre filtering
721 if unsurelevel == 1:
722 hfr.append(('X-Spam-Flag: Unsure, tests=bogofilter', mm_cfg.HOLD, False))
723
724 # Set up spam filtering
725 if filterlevel is 1:
726 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.HOLD, False))
727 elif filterlevel is 2:
728 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter, spamicity=(0\.999999|1\.000000)', mm_cfg.DISCARD, False))
729 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.HOLD, False))
730 elif filterlevel is 3:
731 hfr.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.DISCARD, False))
732
733 # save configuration
734 if mlist.header_filter_rules != hfr:
735 mlist.header_filter_rules = hfr
736 return 1
737
738 #-------------------------------------------------------------------------------
739 # admin procedures [ soptions.php ]
740 #
741
742 admin_opts = [ 'advertised', 'archive', \
743 'max_message_size', 'msg_footer', 'msg_header']
744
745 def get_admin_options(userdesc, perms, mlist):
746 """ Get administrator options.
747 @mlist
748 @root
749 """
750 return get_options(userdesc, perms, mlist, admin_opts)
751
752 def set_admin_options(userdesc, perms, mlist, values):
753 """ Set administrator options.
754 @mlist
755 @edit
756 @root
757 """
758 return set_options(userdesc, perms, mlist, admin_opts, values)
759
760 #-------------------------------------------------------------------------------
761 # admin procedures [ check.php ]
762 #
763
764 check_opts = {
765 'acceptable_aliases' : '',
766 'admin_immed_notify' : True,
767 'administrivia' : True,
768 'anonymous_list' : False,
769 'autorespond_admin' : False,
770 'autorespond_postings' : False,
771 'autorespond_requests' : False,
772 'available_languages' : ['fr'],
773 'ban_list' : [],
774 'bounce_matching_headers' : '',
775 'bounce_processing' : False,
776 'convert_html_to_plaintext' : False,
777 'digestable' : False,
778 'digest_is_default' : False,
779 'discard_these_nonmembers' : [],
780 'emergency' : False,
781 'encode_ascii_prefixes' : 2,
782 'filter_content' : False,
783 'first_strip_reply_to' : False,
784 'forward_auto_discards' : True,
785 'hold_these_nonmembers' : [],
786 'host_name' : 'listes.polytechnique.org',
787 'include_list_post_header' : False,
788 'include_rfc2369_headers' : False,
789 'max_num_recipients' : 0,
790 'new_member_options' : 256,
791 'nondigestable' : True,
792 'obscure_addresses' : True,
793 'preferred_language' : 'fr',
794 'reject_these_nonmembers' : [],
795 'reply_goes_to_list' : 0,
796 'reply_to_address' : '',
797 'require_explicit_destination' : False,
798 'send_reminders' : 0,
799 'send_welcome_msg' : True,
800 'topics_enabled' : False,
801 'umbrella_list' : False,
802 'unsubscribe_policy' : 0,
803 }
804
805 def check_options_runner(userdesc, perms, mlist, listname, correct):
806 options = { }
807 for (k, v) in check_opts.iteritems():
808 if mlist.__dict__[k] != v:
809 options[k] = v, mlist.__dict__[k]
810 if correct: mlist.__dict__[k] = v
811 if mlist.real_name.lower() != listname:
812 options['real_name'] = listname, mlist.real_name
813 if correct: mlist.real_name = listname
814 return 1
815
816
817 def check_options(userdesc, perms, vhost, listname, correct=False):
818 """ Check the list.
819 @root
820 """
821 listname = listname.lower()
822 mlist = MailList.MailList(vhost + VHOST_SEP + listname, lock=0)
823 if correct:
824 return list_call_locked(check_options_runner, userdesc, perms, mlist, True, listname, True)
825 else:
826 return check_options_runner(userdesc, perms, mlist, listname, False)
827
828 #-------------------------------------------------------------------------------
829 # super-admin procedures
830 #
831
832 def get_all_lists(userdesc, perms, vhost):
833 """ Get all the list for the given vhost
834 @root
835 """
836 prefix = vhost.lower()+VHOST_SEP
837 names = Utils.list_names()
838 names.sort()
839 result = []
840 for name in names:
841 if not name.startswith(prefix):
842 continue
843 result.append(name.replace(prefix, ''))
844 return result
845
846 def get_all_user_lists(userdesc, perms, vhost, email):
847 """ Get all the lists for the given user
848 @root
849 """
850 names = Utils.list_names()
851 names.sort()
852 result = []
853 for name in names:
854 try:
855 mlist = MailList.MailList(name, lock=0)
856 ismember = email in mlist.getRegularMemberKeys()
857 isowner = email in mlist.owner
858 if not ismember and not isowner:
859 continue
860 host = mlist.internal_name().split(VHOST_SEP)[0].lower()
861 result.append({ 'list': mlist.real_name,
862 'addr': mlist.real_name.lower() + '@' + host,
863 'host': host,
864 'own' : isowner,
865 'sub' : ismember
866 })
867 except Exception, e:
868 continue
869 return result
870
871 def change_user_email(userdesc, perms, vhost, from_email, to_email):
872 """ Change the email of a user
873 @root
874 """
875 from_email = from_email.lower()
876 to_email = to_email.lower()
877 for list in Utils.list_names():
878 try:
879 mlist = MailList.MailList(list, lock=0)
880 except:
881 continue
882 try:
883 mlist.Lock()
884 mlist.ApprovedChangeMemberAddress(from_email, to_email, 0)
885 mlist.Save()
886 mlist.Unlock()
887 except:
888 mlist.Unlock()
889 return 1
890
891
892 def create_list(userdesc, perms, vhost, listname, desc, advertise, modlevel, inslevel, owners, members):
893 """ Create a new list.
894 @root
895 """
896 name = vhost.lower() + VHOST_SEP + listname.lower();
897 if Utils.list_exists(name):
898 print >> sys.stderr, "List ", name, " already exists"
899 return 0
900
901 owner = []
902 for o in owners:
903 email = to_forlife(o)
904 print >> sys.stderr, "owner in list", o, email
905 email = email[0]
906 if email is not None:
907 owner.append(email)
908 if len(owner) is 0:
909 print >> sys.stderr, "No owner found in ", owners
910 return 0
911
912 mlist = MailList.MailList()
913 try:
914 oldmask = os.umask(002)
915 pw = sha.new('foobar').hexdigest()
916
917 try:
918 mlist.Create(name, owner[0], pw)
919 finally:
920 os.umask(oldmask)
921
922 mlist.real_name = listname
923 mlist.host_name = 'listes.polytechnique.org'
924 mlist.description = desc
925
926 mlist.advertised = int(advertise) is 0
927 mlist.default_member_moderation = int(modlevel) is 2
928 mlist.generic_nonmember_action = int(modlevel) > 0
929 mlist.subscribe_policy = 2 * (int(inslevel) is 1)
930 mlist.admin_notify_mchanges = (mlist.subscribe_policy or mlist.generic_nonmember_action or mlist.default_member_moderation or not mlist.advertised)
931
932 mlist.owner = owner
933
934 mlist.subject_prefix = '['+listname+'] '
935 mlist.max_message_size = 0
936
937 inverted_listname = listname.lower() + '_' + vhost.lower()
938 mlist.msg_footer = "_______________________________________________\n" \
939 + "Liste de diffusion %(real_name)s\n" \
940 + "http://listes.polytechnique.org/members/" + inverted_listname
941
942 mlist.header_filter_rules = []
943 mlist.header_filter_rules.append(('X-Spam-Flag: Unsure, tests=bogofilter', mm_cfg.HOLD, False))
944 mlist.header_filter_rules.append(('X-Spam-Flag: Yes, tests=bogofilter', mm_cfg.HOLD, False))
945
946 if ON_CREATE_CMD != '':
947 try: os.system(ON_CREATE_CMD + ' ' + name)
948 except: pass
949
950 check_options_runner(userdesc, perms, mlist, listname.lower(), True)
951 mass_subscribe(userdesc, perms, mlist, members)
952 mlist.Save()
953 finally:
954 mlist.Unlock()
955
956 # avoid the "-1 mail to moderate" bug
957 mlist = MailList.MailList(name)
958 try:
959 mlist._UpdateRecords()
960 mlist.Save()
961 finally:
962 mlist.Unlock()
963 return 1
964
965 def delete_list(userdesc, perms, mlist, del_archives=0):
966 """ Delete the list.
967 @mlist
968 @admin
969 """
970 lname = mlist.internal_name()
971 # remove the list
972 REMOVABLES = [ os.path.join('lists', lname), ]
973 # remove stalled locks
974 for filename in os.listdir(mm_cfg.LOCK_DIR):
975 fn_lname = filename.split('.')[0]
976 if fn_lname == lname:
977 REMOVABLES.append(os.path.join(mm_cfg.LOCK_DIR, filename))
978 # remove archives ?
979 if del_archives:
980 REMOVABLES.extend([
981 os.path.join('archives', 'private', lname),
982 os.path.join('archives', 'private', lname+'.mbox'),
983 os.path.join('archives', 'public', lname),
984 os.path.join('archives', 'public', lname+'.mbox')
985 ])
986 map(lambda dir: remove_it(lname, os.path.join(mm_cfg.VAR_PREFIX, dir)), REMOVABLES)
987 return 1
988
989 def kill(userdesc, perms, vhost, alias, del_from_promo):
990 """ Remove a user from all the lists.
991 """
992 exclude = []
993 if not del_from_promo:
994 exclude.append(PLATAL_DOMAIN + VHOST_SEP + 'promo' + alias[-4:])
995 for list in Utils.list_names():
996 if list in exclude:
997 continue
998 try:
999 mlist = MailList.MailList(list, lock=0)
1000 except:
1001 continue
1002 try:
1003 mlist.Lock()
1004 mlist.ApprovedDeleteMember(alias+'@'+PLATAL_DOMAIN, None, 0, 0)
1005 mlist.Save()
1006 mlist.Unlock()
1007 except:
1008 mlist.Unlock()
1009 return 1
1010
1011
1012 #-------------------------------------------------------------------------------
1013 # server
1014 #
1015 class FastXMLRPCServer(SocketServer.ThreadingMixIn, SimpleXMLRPCServer):
1016 allow_reuse_address = True
1017
1018 ################################################################################
1019 #
1020 # INIT
1021 #
1022 #-------------------------------------------------------------------------------
1023 # use Mailman user and group (not root)
1024 # fork in background if asked to
1025 #
1026
1027 uid = getpwnam(mm_cfg.MAILMAN_USER)[2]
1028 gid = getgrnam(mm_cfg.MAILMAN_GROUP)[2]
1029
1030 if not os.getuid():
1031 os.setregid(gid, gid)
1032 os.setreuid(uid, uid)
1033
1034 signal.signal(signal.SIGHUP, signal.SIG_IGN)
1035
1036 if ( os.getuid() is not uid ) or ( os.getgid() is not gid):
1037 sys.exit(0)
1038
1039 opts, args = getopt.getopt(sys.argv[1:], 'f')
1040 for o, a in opts:
1041 if o == '-f' and os.fork():
1042 sys.exit(0)
1043
1044 i18n.set_language('fr')
1045 mysql = connectDB()
1046 lock = Lock()
1047
1048 #-------------------------------------------------------------------------------
1049 # server
1050 #
1051 server = FastXMLRPCServer((SRV_HOST, SRV_PORT), BasicAuthXMLRPCRequestHandler)
1052
1053 # index.php
1054 server.register_function(get_lists)
1055 server.register_function(subscribe)
1056 server.register_function(unsubscribe)
1057 # members.php
1058 server.register_function(get_members)
1059 # trombi.php
1060 server.register_function(get_members_limit)
1061 server.register_function(get_owners)
1062 # admin.php
1063 server.register_function(replace_email)
1064 server.register_function(mass_subscribe)
1065 server.register_function(mass_unsubscribe)
1066 server.register_function(add_owner)
1067 server.register_function(del_owner)
1068 # moderate.php
1069 server.register_function(get_pending_ops)
1070 server.register_function(handle_request)
1071 server.register_function(get_pending_sub)
1072 server.register_function(get_pending_mail)
1073 # options.php
1074 server.register_function(get_owner_options)
1075 server.register_function(set_owner_options)
1076 server.register_function(add_to_wl)
1077 server.register_function(del_from_wl)
1078 server.register_function(get_bogo_level)
1079 server.register_function(set_bogo_level)
1080 # soptions.php
1081 server.register_function(get_admin_options)
1082 server.register_function(set_admin_options)
1083 # check.php
1084 server.register_function(check_options)
1085 # create + del
1086 server.register_function(get_all_lists)
1087 server.register_function(get_all_user_lists)
1088 server.register_function(change_user_email)
1089 server.register_function(create_list)
1090 server.register_function(delete_list)
1091 # utilisateurs.php
1092 server.register_function(kill)
1093
1094 server.serve_forever()
1095
1096 # vim:set et sw=4 sts=4 sws=4: