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