2 #***************************************************************************
3 #* Copyright (C) 2003-2013 Polytechnique.org *
4 #* http://opensource.polytechnique.org/ *
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. *
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. *
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 #***************************************************************************
22 import base64
, MySQLdb
, os
, getopt
, sys
, sha
, signal
, re
, shutil
, ConfigParser
23 import MySQLdb
.converters
28 sys
.path
.append('/usr/lib/mailman/bin')
30 from pwd
import getpwnam
31 from grp
import getgrnam
33 from SimpleXMLRPCServer
import SimpleXMLRPCServer
34 from SimpleXMLRPCServer
import SimpleXMLRPCRequestHandler
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
48 class AuthFailed(Exception): pass
50 ################################################################################
54 #------------------------------------------------
56 config
= ConfigParser
.ConfigParser()
57 config
.read(os
.path
.dirname(__file__
)+'/../configs/platal.ini')
58 config
.read(os
.path
.dirname(__file__
)+'/../configs/platal.conf')
60 def get_config(sec
, val
, default
=None):
62 return config
.get(sec
, val
)[1:-1]
63 except ConfigParser
.NoOptionError
, e
:
65 sys
.stderr
.write('%s\n' % str
(e
))
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')
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
)
80 VHOST_SEP
= get_config('Lists', 'vhost_sep', '_')
81 ON_CREATE_CMD
= get_config('Lists', 'on_create', '')
83 SRV_HOST
= get_config('Lists', 'rpchost', 'localhost')
84 SRV_PORT
= int(get_config('Lists', 'rpcport', '4949'))
86 ################################################################################
90 #------------------------------------------------
91 # Manage Basic authentication
94 class BasicAuthXMLRPCRequestHandler(SimpleXMLRPCRequestHandler
):
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().
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
105 def _get_function(self
, method
):
107 # check to see if a matching function has been registered
108 return self
.server
.funcs
[method
]
110 raise Exception('method "%s" is not supported' % method
)
112 def is_rpc_path_valid(self
):
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
)
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:
126 # Call super.do_POST() to do the actual work
127 SimpleXMLRPCRequestHandler
.do_POST(self
)
129 self
.send_response(401)
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))
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'
141 %
(PLATAL_DOMAIN
, uid
, md5
))
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'""" \
152 userdesc
= UserDesc(forlife
, name
, None, 0)
153 return (userdesc
, perms
, vhost
)
155 print >> sys
.stderr
, "no user found for uid: %s, passwd: %s" %
(uid
, md5
)
158 ################################################################################
162 #-------------------------------------------------------------------------------
167 db
= MySQLdb
.connect(
176 def mysql_fetchone(query
):
181 if int(mysql
.rowcount
) > 0:
182 ret
= mysql
.fetchone()
187 def is_admin_on(userdesc
, perms
, mlist
):
188 return ( perms
== 'admin' ) or ( userdesc
.address
in mlist
.owner
)
191 def quote(s
, is_header
=False):
193 h
= Utils
.oneline(s
, 'iso-8859-1')
196 h
= str('').join(re
.split('[\x00-\x08\x0B-\x1f]+', h
))
197 return Utils
.uquote(h
.replace('&', '&').replace('>', '>').replace('<', '<'))
199 def to_forlife(email
):
201 mbox
, fqdn
= email
.split('@')
205 if ( fqdn
== PLATAL_DOMAIN
) or ( fqdn
== PLATAL_DOMAIN2
):
206 res
= mysql_fetchone("""SELECT CONCAT(s1.email, '@%s'), a.full_name
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'
212 %
(PLATAL_DOMAIN
, mbox
))
217 return (email
.lower(), mbox
)
220 # see /usr/lib/mailman/bin/rmlist
222 def remove_it(listname
, filename
):
223 if os
.path
.islink(filename
) or os
.path
.isfile(filename
):
225 elif os
.path
.isdir(filename
):
226 shutil
.rmtree(filename
)
232 def has_annotation(method
, name
):
233 """ Check if the method contains the given annoation.
235 return method
.__doc__
and method
.__doc__
.find("@%s" % name
) > -1
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
249 print >> sys
.stderr
, "calling method: %s" % method
250 if has_annotation(method
, "root") and perms
!= "admin":
252 if has_annotation(method
, "mlist"):
253 listname
= str(arg
[0])
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
):
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
)
261 return method(userdesc
, perms
, mlist
, *arg
)
263 return method(userdesc
, perms
, vhost
, *arg
)
265 sys
.stderr
.write('Exception in dispatcher %s\n' % str
(e
))
269 def list_call_locked(method
, userdesc
, perms
, mlist
, edit
, *arg
):
270 """Call the given method after locking the mlist.
274 ret
= method(userdesc
, perms
, mlist
, *arg
)
280 traceback
.print_exc(file=sys
.stderr
)
281 sys
.stderr
.write('Exception in locked call %s: %s\n' %
(method
.__name__
, str(e
)))
284 # TODO: use finally when switching to python 2.5
286 #-------------------------------------------------------------------------------
290 def is_subscription_pending(userdesc
, perms
, mlist
):
291 for id in mlist
.GetSubscriptionIds():
292 if userdesc
.address
== mlist
.GetRecord(id)[1]:
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'):
302 if not is_member
and (mlist
.subscribe_policy
> 1):
303 is_pending
= list_call_locked(is_subscription_pending
, userdesc
, perms
, mlist
, False)
307 host
= mlist
.internal_name().split(VHOST_SEP
)[0].lower()
309 'list' : mlist
.real_name
,
310 'addr' : mlist
.real_name
.lower() + '@' + 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
,
319 'nbsub': len(members
)
321 return (details
, members
)
324 def get_options(userdesc
, perms
, mlist
, opts
):
325 """ Get the options of a list.
330 for (k
, v
) in mlist
.__dict__
.iteritems():
333 options
[k
] = quote(v
)
335 details
= get_list_info(userdesc
, perms
, mlist
)[0]
336 return (details
, options
)
338 def set_options(userdesc
, perms
, mlist
, opts
, vals
):
339 for (k
, v
) in vals
.iteritems():
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
352 #-------------------------------------------------------------------------------
353 # users procedures for [ index.php ]
356 def get_lists(userdesc
, perms
, vhost
, email
=None):
357 """ List available lists for the given vhost
362 udesc
= UserDesc(email
.lower(), email
.lower(), None, 0)
363 prefix
= vhost
.lower()+VHOST_SEP
364 names
= Utils
.list_names()
368 if not name
.startswith(prefix
):
371 mlist
= MailList
.MailList(name
, lock
=0)
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])
379 sys
.stderr
.write('Can\'t get list %s: %s\n' %
(name
, str(e
)))
383 def subscribe(userdesc
, perms
, mlist
):
384 """ Subscribe to a list.
388 if ( mlist
.subscribe_policy
in (0, 1) ) or userdesc
.address
in mlist
.owner
:
389 mlist
.ApprovedAddMember(userdesc
)
394 mlist
.AddMember(userdesc
)
395 except Errors
.MMNeedApproval
:
399 def unsubscribe(userdesc
, perms
, mlist
):
400 """ Unsubscribe from a list
404 mlist
.ApprovedDeleteMember(userdesc
.address
)
407 #-------------------------------------------------------------------------------
408 # users procedures for [ index.php ]
411 def get_name(member
):
413 return quote(mlist
.getMemberName(member
))
417 def get_members(userdesc
, perms
, mlist
):
418 """ List the members of a list.
421 infos
= get_list_info(userdesc
, perms
, mlist
)
423 # Do not return None, this is not serializable
425 details
, members
= infos
427 members
= map(lambda member
: (get_name(member
), member
), members
)
428 return (details
, members
, mlist
.owner
)
431 #-------------------------------------------------------------------------------
432 # users procedures for [ trombi.php ]
435 def get_members_limit(userdesc
, perms
, mlist
, page
, nb_per_page
):
436 """ Get a range of members of the list.
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
)])
443 def get_owners(userdesc
, perms
, mlist
):
444 """ Get the owners of the list.
447 details
, members
, owners
= get_members(userdesc
, perms
, mlist
)
448 return (details
, owners
)
451 #-------------------------------------------------------------------------------
452 # owners procedures [ admin.php ]
455 def replace_email(userdesc
, perms
, mlist
, from_email
, to_email
):
456 """ Replace the address of a member by another one.
461 mlist
.ApprovedChangeMemberAddress(from_email
.lower(), to_email
.lower(), 0)
464 def mass_subscribe(userdesc
, perms
, mlist
, users
):
465 """ Add a list of users to the list.
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()
477 email
, name
= to_forlife(user
)
478 if ( email
is None ) or ( email
in members
):
480 userd
= UserDesc(email
, name
, None, 0)
481 mlist
.ApprovedAddMember(userd
)
482 added
.append( (quote(userd
.fullname
), userd
.address
) )
485 def mass_unsubscribe(userdesc
, perms
, mlist
, users
):
486 """ Remove a list of users from the list.
491 map(lambda user
: mlist
.ApprovedDeleteMember(user
), users
)
494 def add_owner(userdesc
, perms
, mlist
, user
):
495 """ Add a owner to the list.
500 email
= to_forlife(user
)[0]
503 if email
not in mlist
.owner
:
504 mlist
.owner
.append(email
)
507 def del_owner(userdesc
, perms
, mlist
, user
):
508 """ Remove a owner of the list.
513 if len(mlist
.owner
) < 2:
515 mlist
.owner
.remove(user
)
518 #-------------------------------------------------------------------------------
519 # owners procedures [ admin.php ]
522 def get_pending_ops(userdesc
, perms
, mlist
):
523 """ Get the list of operation waiting for an action from the owners.
531 for id in mlist
.GetSubscriptionIds():
532 time
, addr
, fullname
, passwd
, digest
, lang
= mlist
.GetRecord(id)
534 mlist
.HandleRequest(id, mm_cfg
.DISCARD
)
539 login
= re
.match("^[^.]*\.[^.]*\.\d\d\d\d$", addr
.split('@')[0]).group()
540 subs
.append({'id': id, 'name': quote(fullname
), 'addr': addr
, 'login': login
})
542 subs
.append({'id': id, 'name': quote(fullname
), 'addr': addr
})
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
)
549 size
= os
.path
.getsize(fpath
)
551 if e
.errno
<> errno
.ENOENT
: raise
554 msg
= readMessage(fpath
)
555 fromX
= msg
.has_key("X-Org-Mail")
560 'sender': quote(sender
, True),
562 'subj' : quote(subject
, True),
570 def handle_request(userdesc
, perms
, mlist
, id, value
, comment
):
571 """ Handle a moderation request.
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
)
584 def get_pending_sub(userdesc
, perms
, mlist
, id):
585 """ Get informations about a given subscription moderation.
592 if id in mlist
.GetSubscriptionIds():
593 time
, addr
, fullname
, passwd
, digest
, lang
= mlist
.GetRecord(id)
595 login
= re
.match("^[^.]*\.[^.]*\.\d\d\d\d$", addr
.split('@')[0]).group()
596 sub
= {'id': id, 'name': quote(fullname
), 'addr': addr
, 'login': login
}
598 sub
= {'id': id, 'name': quote(fullname
), 'addr': addr
}
601 def get_pending_mail(userdesc
, perms
, mlist
, id, raw
=0):
602 """ Get informations about a given mail moderation.
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
)
613 return quote(str(msg
))
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
)
625 'sender': quote(sender
, True),
627 'subj' : quote(subject
, True),
629 'parts_plain' : results_plain
,
630 'parts_html': results_html
}
632 #-------------------------------------------------------------------------------
633 # owner options [ options.php ]
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', \
641 def get_owner_options(userdesc
, perms
, mlist
):
642 """ Get the owner options of a list.
646 return get_options(userdesc
, perms
, mlist
, owner_opts
)
648 def set_owner_options(userdesc
, perms
, mlist
, values
):
649 """ Set the owner options of a list.
654 return set_options(userdesc
, perms
, mlist
, owner_opts
, values
)
656 def add_to_wl(userdesc
, perms
, mlist
, addr
):
657 """ Add addr to the whitelist
662 mlist
.accept_these_nonmembers
.append(addr
)
665 def del_from_wl(userdesc
, perms
, mlist
, addr
):
666 """ Remove an address from the whitelist
671 mlist
.accept_these_nonmembers
.remove(addr
)
674 def get_bogo_level(userdesc
, perms
, mlist
):
675 """ Compute bogo level from the filtering rules set up on the list.
679 if len(mlist
.header_filter_rules
) == 0:
686 # The first rule filters Unsure mails
687 if mlist
.header_filter_rules
[0][0] == 'X-Spam-Flag: Unsure, tests=bogofilter':
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)
696 action
= mlist
.header_filter_rules
[filterbase
+ 1][1]
699 action
= mlist
.header_filter_rules
[filterbase
][1]
700 if action
== mm_cfg
.HOLD
:
702 elif action
== mm_cfg
.DISCARD
:
704 return (filterlevel
<< 1) + unsurelevel
706 def set_bogo_level(userdesc
, perms
, mlist
, level
):
707 """ Set filter to the specified level.
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
722 # Set up unusre filtering
724 hfr
.append(('X-Spam-Flag: Unsure, tests=bogofilter', mm_cfg
.HOLD
, False))
726 # Set up spam filtering
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))
736 if mlist
.header_filter_rules
!= hfr
:
737 mlist
.header_filter_rules
= hfr
740 #-------------------------------------------------------------------------------
741 # admin procedures [ soptions.php ]
744 admin_opts
= [ 'advertised', 'archive', \
745 'max_message_size', 'msg_footer', 'msg_header']
747 def get_admin_options(userdesc
, perms
, mlist
):
748 """ Get administrator options.
752 return get_options(userdesc
, perms
, mlist
, admin_opts
)
754 def set_admin_options(userdesc
, perms
, mlist
, values
):
755 """ Set administrator options.
760 return set_options(userdesc
, perms
, mlist
, admin_opts
, values
)
762 #-------------------------------------------------------------------------------
763 # admin procedures [ check.php ]
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'],
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' : [],
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,
807 def check_options_runner(userdesc
, perms
, mlist
, listname
, correct
):
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
819 def check_options(userdesc
, perms
, vhost
, listname
, correct
=False):
823 listname
= listname
.lower()
824 mlist
= MailList
.MailList(vhost
+ VHOST_SEP
+ listname
, lock
=0)
826 return list_call_locked(check_options_runner
, userdesc
, perms
, mlist
, True, listname
, True)
828 return check_options_runner(userdesc
, perms
, mlist
, listname
, False)
830 #-------------------------------------------------------------------------------
831 # super-admin procedures
834 def get_all_lists(userdesc
, perms
, vhost
):
835 """ Get all the list for the given vhost
838 prefix
= vhost
.lower()+VHOST_SEP
839 names
= Utils
.list_names()
843 if not name
.startswith(prefix
):
845 result
.append(name
.replace(prefix
, ''))
848 def get_all_user_lists(userdesc
, perms
, vhost
, email
):
849 """ Get all the lists for the given user
852 names
= Utils
.list_names()
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
:
862 host
= mlist
.internal_name().split(VHOST_SEP
)[0].lower()
863 result
.append({ 'list': mlist
.real_name
,
864 'addr': mlist
.real_name
.lower() + '@' + host
,
873 def change_user_email(userdesc
, perms
, vhost
, from_email
, to_email
):
874 """ Change the email of a user
877 from_email
= from_email
.lower()
878 to_email
= to_email
.lower()
879 for list in Utils
.list_names():
881 mlist
= MailList
.MailList(list, lock
=0)
886 mlist
.ApprovedChangeMemberAddress(from_email
, to_email
, 0)
894 def create_list(userdesc
, perms
, vhost
, listname
, desc
, advertise
, modlevel
, inslevel
, owners
, members
):
895 """ Create a new list.
898 name
= vhost
.lower() + VHOST_SEP
+ listname
.lower();
899 if Utils
.list_exists(name
):
900 print >> sys
.stderr
, "List ", name
, " already exists"
905 email
= to_forlife(o
)
906 print >> sys
.stderr
, "owner in list", o
, email
908 if email
is not None:
911 print >> sys
.stderr
, "No owner found in ", owners
914 mlist
= MailList
.MailList()
916 oldmask
= os
.umask(002)
917 pw
= sha
.new('foobar').hexdigest()
920 mlist
.Create(name
, owner
[0], pw
)
924 mlist
.real_name
= listname
925 mlist
.host_name
= 'listes.polytechnique.org'
926 mlist
.description
= desc
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
)
936 mlist
.subject_prefix
= '['+listname
+'] '
937 mlist
.max_message_size
= 0
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
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))
948 if ON_CREATE_CMD
!= '':
949 try: os
.system(ON_CREATE_CMD
+ ' ' + name
)
952 check_options_runner(userdesc
, perms
, mlist
, listname
.lower(), True)
953 mass_subscribe(userdesc
, perms
, mlist
, members
)
958 # avoid the "-1 mail to moderate" bug
959 mlist
= MailList
.MailList(name
)
961 mlist
._UpdateRecords()
967 def delete_list(userdesc
, perms
, mlist
, del_archives
=0):
972 lname
= mlist
.internal_name()
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
))
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')
988 map(lambda dir: remove_it(lname
, os
.path
.join(mm_cfg
.VAR_PREFIX
, dir)), REMOVABLES
)
991 def kill(userdesc
, perms
, vhost
, alias
, del_from_promo
):
992 """ Remove a user from all the lists.
995 if not del_from_promo
:
996 exclude
.append(PLATAL_DOMAIN
+ VHOST_SEP
+ 'promo' + alias
[-4:])
997 for list in Utils
.list_names():
1001 mlist
= MailList
.MailList(list, lock
=0)
1006 mlist
.ApprovedDeleteMember(alias
+'@'+PLATAL_DOMAIN
, None, 0, 0)
1014 #-------------------------------------------------------------------------------
1017 class FastXMLRPCServer(SocketServer
.ThreadingMixIn
, SimpleXMLRPCServer
):
1018 allow_reuse_address
= True
1020 ################################################################################
1024 #-------------------------------------------------------------------------------
1025 # use Mailman user and group (not root)
1026 # fork in background if asked to
1029 uid
= getpwnam(mm_cfg
.MAILMAN_USER
)[2]
1030 gid
= getgrnam(mm_cfg
.MAILMAN_GROUP
)[2]
1033 os
.setregid(gid
, gid
)
1034 os
.setreuid(uid
, uid
)
1036 signal
.signal(signal
.SIGHUP
, signal
.SIG_IGN
)
1038 if ( os
.getuid() is not uid
) or ( os
.getgid() is not gid
):
1041 opts
, args
= getopt
.getopt(sys
.argv
[1:], 'f')
1043 if o
== '-f' and os
.fork():
1046 i18n
.set_language('fr')
1050 #-------------------------------------------------------------------------------
1053 server
= FastXMLRPCServer((SRV_HOST
, SRV_PORT
), BasicAuthXMLRPCRequestHandler
)
1056 server
.register_function(get_lists
)
1057 server
.register_function(subscribe
)
1058 server
.register_function(unsubscribe
)
1060 server
.register_function(get_members
)
1062 server
.register_function(get_members_limit
)
1063 server
.register_function(get_owners
)
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
)
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
)
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
)
1083 server
.register_function(get_admin_options
)
1084 server
.register_function(set_admin_options
)
1086 server
.register_function(check_options
)
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
)
1094 server
.register_function(kill
)
1096 server
.serve_forever()
1098 # vim:set et sw=4 sts=4 sws=4: