def addNewMember(self, member, **kws): # assert self.__mlist.Locked() # Make sure this address isn't already a member if self.isMember(member): raise Errors.MMAlreadyAMember, member # Parse the keywords digest = 0 password = Utils.MakeRandomPassword() language = self.__mlist.preferred_language realname = None if kws.has_key('digest'): digest = kws['digest'] del kws['digest'] if kws.has_key('password'): password = kws['password'] del kws['password'] if kws.has_key('language'): language = kws['language'] del kws['language'] if kws.has_key('realname'): realname = kws['realname'] del kws['realname'] # Assert that no other keywords are present if kws: raise ValueError, kws.keys() # If the localpart has uppercase letters in it, then the value in the # members (or digest_members) dict is the case preserved address. # Otherwise the value is 0. Note that the case of the domain part is # of course ignored. if Utils.LCDomain(member) == member.lower(): value = 0 else: value = member member = member.lower() if digest: digest = 'Y' else: digest = 'N' # All we need to do here is add the address. # and Set the member's default set of options if self.__mlist.new_member_options: options = self.__mlist.new_member_options else: options = 0 query = "INSERT INTO %s " \ + "(address, user_options, password, lang, " \ + "digest, delivery_status,listname) values " \ + "('%s',%s,'%s','%s','%s','%s','%s')" query = query %( self._table, self.escape(member), options, md5_new(password).hexdigest(), language, digest, MemberAdaptor.ENABLED,self.__mlist.internal_name()) if mm_cfg.MYSQL_MEMBER_DB_VERBOSE: syslog('mysql',query) mm_cfg.cursor.execute(query) mm_cfg.connection.commit() if realname: self.setMemberName(member, realname)
def addNewMember(self, member, **kws): assert self.__mlist.Locked() # Make sure this address isn't already a member if self.isMember(member): raise Errors.MMAlreadyAMember(member) # Parse the keywords digest = 0 password = Utils.MakeRandomPassword() language = self.__mlist.preferred_language realname = None if 'digest' in kws: digest = kws['digest'] del kws['digest'] if 'password' in kws: password = kws['password'] del kws['password'] if 'language' in kws: language = kws['language'] del kws['language'] if 'realname' in kws: realname = kws['realname'] del kws['realname'] # Assert that no other keywords are present if kws: raise ValueError(list(kws.keys())) # If the localpart has uppercase letters in it, then the value in the # members (or digest_members) dict is the case preserved address. # Otherwise the value is 0. Note that the case of the domain part is # of course ignored. if Utils.LCDomain(member) == member.lower(): value = 0 else: value = member member = member.lower() if digest: self.__mlist.digest_members[member] = value else: self.__mlist.members[member] = value self.setMemberPassword(member, password) self.setMemberLanguage(member, language) if realname: self.setMemberName(member, realname) # Set the member's default set of options if self.__mlist.new_member_options: self.__mlist.user_options[member] = self.__mlist.new_member_options
def UpdateOldVars(l, stored_state): """Transform old variable values into new ones, deleting old ones. stored_state is last snapshot from file, as opposed to from InitVars().""" def PreferStored(oldname, newname, newdefault=uniqueval, l=l, state=stored_state): """Use specified old value if new value is not in stored state. If the old attr does not exist, and no newdefault is specified, the new attr is *not* created - so either specify a default or be positive that the old attr exists - or don't depend on the new attr. """ if hasattr(l, oldname): if not state.has_key(newname): setattr(l, newname, getattr(l, oldname)) delattr(l, oldname) if not hasattr(l, newname) and newdefault is not uniqueval: setattr(l, newname, newdefault) def recode(mlist, f, t): """If the character set for a list's preferred_language has changed, attempt to recode old string values into the new character set. mlist is the list, f is the old charset and t is the new charset. """ for x in dir(mlist): if x.startswith('_'): continue nv = doitem(getattr(mlist, x), f, t) if nv: setattr(mlist, x, nv) def doitem(v, f, t): """Recursively process lists, tuples and dictionary values and convert strings as needed. Return either the updated item or None if no change.""" changed = False if isinstance(v, str): return convert(v, f, t) elif isinstance(v, list): for i in range(len(v)): nv = doitem(v[i], f, t) if nv: changed = True v[i] = nv if changed: return v else: return None elif isinstance(v, tuple): nt = () for i in range(len(v)): nv = doitem(v[i], f, t) if nv: changed = True nt += (nv, ) else: nt += (v[i], ) if changed: return nt else: return None elif isinstance(v, dict): for k, ov in v.items(): nv = doitem(ov, f, t) if nv: changed = True v[k] = nv if changed: return v else: return None else: return None def convert(s, f, t): """This does the actual character set conversion of the string s from charset f to charset t.""" try: u = unicode(s, f) is_f = True except ValueError: is_f = False try: unicode(s, t) is_t = True except ValueError: is_t = False if is_f and not is_t: return u.encode(t, 'replace') else: return None # Migrate to 2.1b3, baw 17-Aug-2001 if hasattr(l, 'dont_respond_to_post_requests'): oldval = getattr(l, 'dont_respond_to_post_requests') if not hasattr(l, 'respond_to_post_requests'): l.respond_to_post_requests = not oldval del l.dont_respond_to_post_requests # Migrate to 2.1b3, baw 13-Oct-2001 # Basic defaults for new variables if not hasattr(l, 'default_member_moderation'): l.default_member_moderation = mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION if not hasattr(l, 'accept_these_nonmembers'): l.accept_these_nonmembers = [] if not hasattr(l, 'hold_these_nonmembers'): l.hold_these_nonmembers = [] if not hasattr(l, 'reject_these_nonmembers'): l.reject_these_nonmembers = [] if not hasattr(l, 'discard_these_nonmembers'): l.discard_these_nonmembers = [] if not hasattr(l, 'forward_auto_discards'): l.forward_auto_discards = mm_cfg.DEFAULT_FORWARD_AUTO_DISCARDS if not hasattr(l, 'generic_nonmember_action'): l.generic_nonmember_action = mm_cfg.DEFAULT_GENERIC_NONMEMBER_ACTION # Now convert what we can... Note that the interaction between the # MM2.0.x attributes `moderated', `member_posting_only', and `posters' is # so confusing, it makes my brain really ache. Which is why they go away # in MM2.1. I think the best we can do semantically is the following: # # - If moderated == yes, then any sender who's address is not on the # posters attribute would get held for approval. If the sender was on # the posters list, then we'd defer judgement to a later step # - If member_posting_only == yes, then members could post without holds, # and if there were any addresses added to posters, they could also post # without holds. # - If member_posting_only == no, then what happens depends on the value # of the posters attribute: # o If posters was empty, then anybody can post without their # message being held for approval # o If posters was non-empty, then /only/ those addresses could post # without approval, i.e. members not on posters would have their # messages held for approval. # # How to translate this mess to MM2.1 values? I'm sure I got this wrong # before, but here's how we're going to do it, as of MM2.1b3. # # - We'll control member moderation through their Moderate flag, and # non-member moderation through the generic_nonmember_action, # hold_these_nonmembers, and accept_these_nonmembers. # - If moderated == yes then we need to troll through the addresses on # posters, and any non-members would get added to # accept_these_nonmembers. /Then/ we need to troll through the # membership and any member on posters would get their Moderate flag # unset, while members not on posters would get their Moderate flag set. # Then generic_nonmember_action gets set to 1 (hold) so nonmembers get # moderated, and default_member_moderation will be set to 1 (hold) so # new members will also get held for moderation. We'll stop here. # - We only get to here if moderated == no. # - If member_posting_only == yes, then we'll turn off the Moderate flag # for members. We troll through the posters attribute and add all those # addresses to accept_these_nonmembers. We'll also set # generic_nonmember_action to 1 and default_member_moderation to 0. # We'll stop here. # - We only get to here if member_posting_only == no # - If posters is empty, then anybody could post without being held for # approval, so we'll set generic_nonmember_action to 0 (accept), and # we'll turn off the Moderate flag for all members. We'll also turn off # default_member_moderation so new members can post without approval. # We'll stop here. # - We only get here if posters is non-empty. # - This means that /only/ the addresses on posters got to post without # being held for approval. So first, we troll through posters and add # all non-members to accept_these_nonmembers. Then we troll through the # membership and if their address is on posters, we'll clear their # Moderate flag, otherwise we'll set it. We'll turn on # default_member_moderation so new members get moderated. We'll set # generic_nonmember_action to 1 (hold) so all other non-members will get # moderated. And I think we're finally done. # # SIGH. if hasattr(l, 'moderated'): # We'll assume we're converting all these attributes at once if l.moderated: #syslog('debug', 'Case 1') for addr in l.posters: if not l.isMember(addr): l.accept_these_nonmembers.append(addr) for member in l.getMembers(): l.setMemberOption( member, mm_cfg.Moderate, # reset for explicitly named members member not in l.posters) l.generic_nonmember_action = 1 l.default_member_moderation = 1 elif l.member_posting_only: #syslog('debug', 'Case 2') for addr in l.posters: if not l.isMember(addr): l.accept_these_nonmembers.append(addr) for member in l.getMembers(): l.setMemberOption(member, mm_cfg.Moderate, 0) l.generic_nonmember_action = 1 l.default_member_moderation = 0 elif not l.posters: #syslog('debug', 'Case 3') for member in l.getMembers(): l.setMemberOption(member, mm_cfg.Moderate, 0) l.generic_nonmember_action = 0 l.default_member_moderation = 0 else: #syslog('debug', 'Case 4') for addr in l.posters: if not l.isMember(addr): l.accept_these_nonmembers.append(addr) for member in l.getMembers(): l.setMemberOption( member, mm_cfg.Moderate, # reset for explicitly named members member not in l.posters) l.generic_nonmember_action = 1 l.default_member_moderation = 1 # Now get rid of the old attributes del l.moderated del l.posters del l.member_posting_only if hasattr(l, 'forbidden_posters'): # For each of the posters on this list, if they are members, toggle on # their moderation flag. If they are not members, then add them to # hold_these_nonmembers. forbiddens = l.forbidden_posters for addr in forbiddens: if l.isMember(addr): l.setMemberOption(addr, mm_cfg.Moderate, 1) else: l.hold_these_nonmembers.append(addr) del l.forbidden_posters # Migrate to 1.0b6, klm 10/22/1998: PreferStored('reminders_to_admins', 'umbrella_list', mm_cfg.DEFAULT_UMBRELLA_LIST) # Migrate up to 1.0b5: PreferStored('auto_subscribe', 'open_subscribe') PreferStored('closed', 'private_roster') PreferStored('mimimum_post_count_before_removal', 'mimimum_post_count_before_bounce_action') PreferStored('bad_posters', 'forbidden_posters') PreferStored('automatically_remove', 'automatic_bounce_action') if hasattr(l, "open_subscribe"): if l.open_subscribe: if mm_cfg.ALLOW_OPEN_SUBSCRIBE: l.subscribe_policy = 0 else: l.subscribe_policy = 1 else: l.subscribe_policy = 2 # admin approval delattr(l, "open_subscribe") if not hasattr(l, "administrivia"): setattr(l, "administrivia", mm_cfg.DEFAULT_ADMINISTRIVIA) if not hasattr(l, "admin_member_chunksize"): setattr(l, "admin_member_chunksize", mm_cfg.DEFAULT_ADMIN_MEMBER_CHUNKSIZE) # # this attribute was added then deleted, so there are a number of # cases to take care of # if hasattr(l, "posters_includes_members"): if l.posters_includes_members: if l.posters: l.member_posting_only = 1 else: if l.posters: l.member_posting_only = 0 delattr(l, "posters_includes_members") elif l.data_version <= 10 and l.posters: # make sure everyone gets the behavior the list used to have, but only # for really old versions of Mailman (1.0b5 or before). Any newer # version of Mailman should not get this attribute whacked. l.member_posting_only = 0 # # transfer the list data type for holding members and digest members # to the dict data type starting file format version 11 # if type(l.members) is ListType: members = {} for m in l.members: members[m] = 1 l.members = members if type(l.digest_members) is ListType: dmembers = {} for dm in l.digest_members: dmembers[dm] = 1 l.digest_members = dmembers # # set admin_notify_mchanges # if not hasattr(l, "admin_notify_mchanges"): setattr(l, "admin_notify_mchanges", mm_cfg.DEFAULT_ADMIN_NOTIFY_MCHANGES) # # Convert the members and digest_members addresses so that the keys of # both these are always lowercased, but if there is a case difference, the # value contains the case preserved value # for k in l.members.keys(): if k.lower() <> k: l.members[k.lower()] = Utils.LCDomain(k) del l.members[k] elif type(l.members[k]) == StringType and k == l.members[k].lower(): # already converted pass else: l.members[k] = 0 for k in l.digest_members.keys(): if k.lower() <> k: l.digest_members[k.lower()] = Utils.LCDomain(k) del l.digest_members[k] elif type(l.digest_members[k]) == StringType and \ k == l.digest_members[k].lower(): # already converted pass else: l.digest_members[k] = 0 # # Convert pre 2.2 topics regexps which were compiled in verbose mode # to a non-verbose equivalent. # if stored_state['data_version'] < 106 and stored_state.has_key('topics'): l.topics = [] for name, pattern, description, emptyflag in stored_state['topics']: pattern = Utils.strip_verbose_pattern(pattern) l.topics.append((name, pattern, description, emptyflag)) # # Romanian and Russian had their character sets changed in 2.1.19 # to utf-8. If there are any strings in the old encoding, try to recode # them. # if stored_state['data_version'] < 108: if l.preferred_language == 'ro': if Utils.GetCharSet('ro') == 'utf-8': recode(l, 'iso-8859-2', 'utf-8') if l.preferred_language == 'ru': if Utils.GetCharSet('ru') == 'utf-8': recode(l, 'koi8-r', 'utf-8') # # from_is_list was called author_is_list in 2.1.16rc2 (only). PreferStored('author_is_list', 'from_is_list', mm_cfg.DEFAULT_FROM_IS_LIST)
def main(): global _ doc = Document() doc.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE) method = Utils.GetRequestMethod() if method.lower() not in ('get', 'post'): title = _('CGI script error') doc.SetTitle(title) doc.AddItem(Header(2, title)) doc.addError(_('Invalid request method: %(method)s')) doc.AddItem('<hr>') doc.AddItem(MailmanLogo()) print('Status: 405 Method Not Allowed') print(doc.Format()) return parts = Utils.GetPathPieces() lenparts = parts and len(parts) if not parts or lenparts < 1: title = _('CGI script error') doc.SetTitle(title) doc.AddItem(Header(2, title)) doc.addError(_('Invalid options to CGI script.')) doc.AddItem('<hr>') doc.AddItem(MailmanLogo()) print(doc.Format()) return # get the list and user's name listname = parts[0].lower() # open list try: mlist = MailList.MailList(listname, lock=0) except Errors.MMListError as e: # Avoid cross-site scripting attacks safelistname = Utils.websafe(listname) title = _('CGI script error') doc.SetTitle(title) doc.AddItem(Header(2, title)) doc.addError(_('No such list <em>%(safelistname)s</em>')) doc.AddItem('<hr>') doc.AddItem(MailmanLogo()) # Send this with a 404 status. print('Status: 404 Not Found') print(doc.Format()) syslog('error', 'options: No such list "%s": %s\n', listname, e) return # The total contents of the user's response cgidata = cgi.FieldStorage(keep_blank_values=1) # CSRF check safe_params = [ 'displang-button', 'language', 'email', 'password', 'login', 'login-unsub', 'login-remind', 'VARHELP', 'UserOptions' ] try: params = list(cgidata.keys()) except TypeError: # Someone crafted a POST with a bad Content-Type:. doc.AddItem(Header(2, _("Error"))) doc.AddItem(Bold(_('Invalid options to CGI script.'))) # Send this with a 400 status. print('Status: 400 Bad Request') print(doc.Format()) return if set(params) - set(safe_params): csrf_checked = csrf_check(mlist, cgidata.getfirst('csrf_token')) else: csrf_checked = True # if password is present, void cookie to force password authentication. if cgidata.getfirst('password'): os.environ['HTTP_COOKIE'] = '' csrf_checked = True # Set the language for the page. If we're coming from the listinfo cgi, # we might have a 'language' key in the cgi data. That was an explicit # preference to view the page in, so we should honor that here. If that's # not available, use the list's default language. language = cgidata.getfirst('language') if not Utils.IsLanguage(language): language = mlist.preferred_language i18n.set_language(language) doc.set_language(language) if lenparts < 2: user = cgidata.getfirst('email') if not user: # If we're coming from the listinfo page and we left the email # address field blank, it's not an error. Likewise if we're # coming from anywhere else. Only issue the error if we came # via one of our buttons. if (cgidata.getfirst('login') or cgidata.getfirst('login-unsub') or cgidata.getfirst('login-remind')): doc.addError(_('No address given')) loginpage(mlist, doc, None, language) print(doc.Format()) return else: user = Utils.LCDomain(Utils.UnobscureEmail(SLASH.join(parts[1:]))) # If a user submits a form or URL with post data or query fragments # with multiple occurrences of the same variable, we can get a list # here. Be as careful as possible. if isinstance(user, list) or isinstance(user, tuple): if len(user) == 0: user = '' else: user = user[-1] # Avoid cross-site scripting attacks safeuser = Utils.websafe(user) try: Utils.ValidateEmail(user) except Errors.EmailAddressError: doc.addError(_('Illegal Email Address: %(safeuser)s')) loginpage(mlist, doc, None, language) print(doc.Format()) return # Sanity check the user, but only give the "no such member" error when # using public rosters, otherwise, we'll leak membership information. if not mlist.isMember(user) and mlist.private_roster == 0: doc.addError(_('No such member: %(safeuser)s.')) loginpage(mlist, doc, None, language) print(doc.Format()) return # Find the case preserved email address (the one the user subscribed with) lcuser = user.lower() try: cpuser = mlist.getMemberCPAddress(lcuser) except Errors.NotAMemberError: # This happens if the user isn't a member but we've got private rosters cpuser = None if lcuser == cpuser: cpuser = None # And now we know the user making the request, so set things up to for the # user's stored preferred language, overridden by any form settings for # their new language preference. userlang = cgidata.getfirst('language') if not Utils.IsLanguage(userlang): userlang = mlist.getMemberLanguage(user) doc.set_language(userlang) i18n.set_language(userlang) # Are we processing an unsubscription request from the login screen? msgc = _('If you are a list member, a confirmation email has been sent.') msga = _("""If you are a list member, your unsubscription request has been forwarded to the list administrator for approval.""") if 'login-unsub' in cgidata: # Because they can't supply a password for unsubscribing, we'll need # to do the confirmation dance. if mlist.isMember(user): # We must acquire the list lock in order to pend a request. try: mlist.Lock() # If unsubs require admin approval, then this request has to # be held. Otherwise, send a confirmation. if mlist.unsubscribe_policy: mlist.HoldUnsubscription(user) doc.addError(msga, tag='') else: ip = os.environ.get( 'HTTP_FORWARDED_FOR', os.environ.get( 'HTTP_X_FORWARDED_FOR', os.environ.get('REMOTE_ADDR', 'unidentified origin'))) mlist.ConfirmUnsubscription(user, userlang, remote=ip) doc.addError(msgc, tag='') mlist.Save() finally: mlist.Unlock() else: # Not a member if mlist.private_roster == 0: # Public rosters doc.addError(_('No such member: %(safeuser)s.')) else: syslog('mischief', 'Unsub attempt of non-member w/ private rosters: %s', user) if mlist.unsubscribe_policy: doc.addError(msga, tag='') else: doc.addError(msgc, tag='') loginpage(mlist, doc, user, language) print(doc.Format()) return # Are we processing a password reminder from the login screen? msg = _("""If you are a list member, your password has been emailed to you.""") if 'login-remind' in cgidata: if mlist.isMember(user): mlist.MailUserPassword(user) doc.addError(msg, tag='') else: # Not a member if mlist.private_roster == 0: # Public rosters doc.addError(_('No such member: %(safeuser)s.')) else: syslog( 'mischief', 'Reminder attempt of non-member w/ private rosters: %s', user) doc.addError(msg, tag='') loginpage(mlist, doc, user, language) print(doc.Format()) return # Get the password from the form. password = cgidata.getfirst('password', '').strip() # Check authentication. We need to know if the credentials match the user # or the site admin, because they are the only ones who are allowed to # change things globally. Specifically, the list admin may not change # values globally. if mm_cfg.ALLOW_SITE_ADMIN_COOKIES: user_or_siteadmin_context = (mm_cfg.AuthUser, mm_cfg.AuthSiteAdmin) else: # Site and list admins are treated equal so that list admin can pass # site admin test. :-( user_or_siteadmin_context = (mm_cfg.AuthUser, ) is_user_or_siteadmin = mlist.WebAuthenticate(user_or_siteadmin_context, password, user) # Authenticate, possibly using the password supplied in the login page if not is_user_or_siteadmin and \ not mlist.WebAuthenticate((mm_cfg.AuthListAdmin, mm_cfg.AuthSiteAdmin), password, user): # Not authenticated, so throw up the login page again. If they tried # to authenticate via cgi (instead of cookie), then print an error # message. if 'password' in cgidata: doc.addError(_('Authentication failed.')) remote = os.environ.get( 'HTTP_FORWARDED_FOR', os.environ.get( 'HTTP_X_FORWARDED_FOR', os.environ.get('REMOTE_ADDR', 'unidentified origin'))) syslog( 'security', 'Authorization failed (private): user=%s: list=%s: remote=%s', user, listname, remote) # So as not to allow membership leakage, prompt for the email # address and the password here. if mlist.private_roster != 0: syslog('mischief', 'Login failure with private rosters: %s from %s', user, remote) user = None # give an HTTP 401 for authentication failure print('Status: 401 Unauthorized') loginpage(mlist, doc, user, language) print(doc.Format()) return # From here on out, the user is okay to view and modify their membership # options. The first set of checks does not require the list to be # locked. # However, if a form is submitted for a user who has been asynchronously # unsubscribed, uncaught NotAMemberError exceptions can be thrown. if not mlist.isMember(user): loginpage(mlist, doc, user, language) print(doc.Format()) return # Before going further, get the result of CSRF check and do nothing # if it has failed. if csrf_checked == False: doc.addError( _('The form lifetime has expired. (request forgery check)')) options_page(mlist, doc, user, cpuser, userlang) print(doc.Format()) return # See if this is VARHELP on topics. varhelp = None if 'VARHELP' in cgidata: varhelp = cgidata['VARHELP'].value elif os.environ.get('QUERY_STRING'): # POST methods, even if their actions have a query string, don't get # put into FieldStorage's keys :-( qs = cgi.parse_qs(os.environ['QUERY_STRING']).get('VARHELP') if qs and type(qs) == list: varhelp = qs[0] if varhelp: # Sanitize the topic name. varhelp = re.sub('<.*', '', varhelp) topic_details(mlist, doc, user, cpuser, userlang, varhelp) return if 'logout' in cgidata: print(mlist.ZapCookie(mm_cfg.AuthUser, user)) loginpage(mlist, doc, user, language) print(doc.Format()) return if 'emailpw' in cgidata: mlist.MailUserPassword(user) options_page(mlist, doc, user, cpuser, userlang, _('A reminder of your password has been emailed to you.')) print(doc.Format()) return if 'othersubs' in cgidata: # Only the user or site administrator can view all subscriptions. if not is_user_or_siteadmin: doc.addError( _("""The list administrator may not view the other subscriptions for this user."""), _('Note: ')) options_page(mlist, doc, user, cpuser, userlang) print(doc.Format()) return hostname = mlist.host_name title = _('List subscriptions for %(safeuser)s on %(hostname)s') doc.SetTitle(title) doc.AddItem(Header(2, title)) doc.AddItem( _('''Click on a link to visit your options page for the requested mailing list.''')) # Troll through all the mailing lists that match host_name and see if # the user is a member. If so, add it to the list. onlists = [] for gmlist in lists_of_member(mlist, user) + [mlist]: extra = '' url = gmlist.GetOptionsURL(user) link = Link(url, gmlist.real_name) if gmlist.getDeliveryStatus(user) != MemberAdaptor.ENABLED: extra += ', ' + _('nomail') if user in gmlist.getDigestMemberKeys(): extra += ', ' + _('digest') link = HTMLFormatObject(link, 0) + extra onlists.append((gmlist.real_name, link)) onlists.sort() items = OrderedList(*[link for name, link in onlists]) doc.AddItem(items) print(doc.Format()) return if 'change-of-address' in cgidata: # We could be changing the user's full name, email address, or both. # Watch out for non-ASCII characters in the member's name. membername = cgidata.getfirst('fullname') # Canonicalize the member's name membername = Utils.canonstr(membername, language) newaddr = cgidata.getfirst('new-address') confirmaddr = cgidata.getfirst('confirm-address') oldname = mlist.getMemberName(user) set_address = set_membername = 0 # See if the user wants to change their email address globally. The # list admin is /not/ allowed to make global changes. globally = cgidata.getfirst('changeaddr-globally') if globally and not is_user_or_siteadmin: doc.addError( _("""The list administrator may not change the names or addresses for this user's other subscriptions. However, the subscription for this mailing list has been changed."""), _('Note: ')) globally = False # We will change the member's name under the following conditions: # - membername has a value # - membername has no value, but they /used/ to have a membername if membername and membername != oldname: # Setting it to a new value set_membername = 1 if not membername and oldname: # Unsetting it set_membername = 1 # We will change the user's address if both newaddr and confirmaddr # are non-blank, have the same value, and aren't the currently # subscribed email address (when compared case-sensitively). If both # are blank, but membername is set, we ignore it, otherwise we print # an error. msg = '' if newaddr and confirmaddr: if newaddr != confirmaddr: options_page(mlist, doc, user, cpuser, userlang, _('Addresses did not match!')) print(doc.Format()) return if newaddr == cpuser: options_page(mlist, doc, user, cpuser, userlang, _('You are already using that email address')) print(doc.Format()) return # If they're requesting to subscribe an address which is already a # member, and they're /not/ doing it globally, then refuse. # Otherwise, we'll agree to do it globally (with a warning # message) and let ApprovedChangeMemberAddress() handle already a # member issues. if mlist.isMember(newaddr): safenewaddr = Utils.websafe(newaddr) if globally: listname = mlist.real_name msg += _("""\ The new address you requested %(newaddr)s is already a member of the %(listname)s mailing list, however you have also requested a global change of address. Upon confirmation, any other mailing list containing the address %(safeuser)s will be changed. """) # Don't return else: options_page( mlist, doc, user, cpuser, userlang, _('The new address is already a member: %(newaddr)s')) print(doc.Format()) return set_address = 1 elif (newaddr or confirmaddr) and not set_membername: options_page(mlist, doc, user, cpuser, userlang, _('Addresses may not be blank')) print(doc.Format()) return # Standard sigterm handler. def sigterm_handler(signum, frame, mlist=mlist): mlist.Unlock() sys.exit(0) signal.signal(signal.SIGTERM, sigterm_handler) if set_address: if cpuser is None: cpuser = user # Register the pending change after the list is locked msg += _('A confirmation message has been sent to %(newaddr)s. ') mlist.Lock() try: try: mlist.ChangeMemberAddress(cpuser, newaddr, globally) mlist.Save() finally: mlist.Unlock() except Errors.MMBadEmailError: msg = _('Bad email address provided') except Errors.MMHostileAddress: msg = _('Illegal email address provided') except Errors.MMAlreadyAMember: msg = _('%(newaddr)s is already a member of the list.') except Errors.MembershipIsBanned: owneraddr = mlist.GetOwnerEmail() msg = _("""%(newaddr)s is banned from this list. If you think this restriction is erroneous, please contact the list owners at %(owneraddr)s.""") if set_membername: mlist.Lock() try: mlist.ChangeMemberName(user, membername, globally) mlist.Save() finally: mlist.Unlock() msg += _('Member name successfully changed. ') options_page(mlist, doc, user, cpuser, userlang, msg) print(doc.Format()) return if 'changepw' in cgidata: # Is this list admin and is list admin allowed to change passwords. if not (is_user_or_siteadmin or mm_cfg.OWNERS_CAN_CHANGE_MEMBER_PASSWORDS): doc.addError( _("""The list administrator may not change the password for a user.""")) options_page(mlist, doc, user, cpuser, userlang) print(doc.Format()) return newpw = cgidata.getfirst('newpw', '').strip() confirmpw = cgidata.getfirst('confpw', '').strip() if not newpw or not confirmpw: options_page(mlist, doc, user, cpuser, userlang, _('Passwords may not be blank')) print(doc.Format()) return if newpw != confirmpw: options_page(mlist, doc, user, cpuser, userlang, _('Passwords did not match!')) print(doc.Format()) return # See if the user wants to change their passwords globally, however # the list admin is /not/ allowed to change passwords globally. pw_globally = cgidata.getfirst('pw-globally') if pw_globally and not is_user_or_siteadmin: doc.addError( _("""The list administrator may not change the password for this user's other subscriptions. However, the password for this mailing list has been changed."""), _('Note: ')) pw_globally = False mlists = [mlist] if pw_globally: mlists.extend(lists_of_member(mlist, user)) for gmlist in mlists: change_password(gmlist, user, newpw, confirmpw) # Regenerate the cookie so a re-authorization isn't necessary print(mlist.MakeCookie(mm_cfg.AuthUser, user)) options_page(mlist, doc, user, cpuser, userlang, _('Password successfully changed.')) print(doc.Format()) return if 'unsub' in cgidata: # Was the confirming check box turned on? if not cgidata.getfirst('unsubconfirm'): options_page( mlist, doc, user, cpuser, userlang, _('''You must confirm your unsubscription request by turning on the checkbox below the <em>Unsubscribe</em> button. You have not been unsubscribed!''')) print(doc.Format()) return # Standard signal handler def sigterm_handler(signum, frame, mlist=mlist): mlist.Unlock() sys.exit(0) # Okay, zap them. Leave them sitting at the list's listinfo page. We # must own the list lock, and we want to make sure the user (BAW: and # list admin?) is informed of the removal. signal.signal(signal.SIGTERM, sigterm_handler) mlist.Lock() needapproval = False try: _ = D_ try: mlist.DeleteMember(user, _('via the member options page'), userack=1) except Errors.MMNeedApproval: needapproval = True except Errors.NotAMemberError: # MAS This except should really be in the outer try so we # don't save the list redundantly, but except and finally in # the same try requires Python >= 2.5. # Setting a switch and making the Save() conditional doesn't # seem worth it as the Save() won't change anything. pass mlist.Save() finally: _ = i18n._ mlist.Unlock() # Now throw up some results page, with appropriate links. We can't # drop them back into their options page, because that's gone now! fqdn_listname = mlist.GetListEmail() owneraddr = mlist.GetOwnerEmail() url = mlist.GetScriptURL('listinfo', absolute=1) title = _('Unsubscription results') doc.SetTitle(title) doc.AddItem(Header(2, title)) if needapproval: doc.AddItem( _("""Your unsubscription request has been received and forwarded on to the list moderators for approval. You will receive notification once the list moderators have made their decision.""")) else: doc.AddItem( _("""You have been successfully unsubscribed from the mailing list %(fqdn_listname)s. If you were receiving digest deliveries you may get one more digest. If you have any questions about your unsubscription, please contact the list owners at %(owneraddr)s.""")) doc.AddItem(mlist.GetMailmanFooter()) print(doc.Format()) return if 'options-submit' in cgidata: # Digest action flags digestwarn = 0 cantdigest = 0 mustdigest = 0 newvals = [] # First figure out which options have changed. The item names come # from FormatOptionButton() in HTMLFormatter.py for item, flag in ( ('digest', mm_cfg.Digests), ('mime', mm_cfg.DisableMime), ('dontreceive', mm_cfg.DontReceiveOwnPosts), ('ackposts', mm_cfg.AcknowledgePosts), ('disablemail', mm_cfg.DisableDelivery), ('conceal', mm_cfg.ConcealSubscription), ('remind', mm_cfg.SuppressPasswordReminder), ('rcvtopic', mm_cfg.ReceiveNonmatchingTopics), ('nodupes', mm_cfg.DontReceiveDuplicates), ): try: newval = int(cgidata.getfirst(item)) except (TypeError, ValueError): newval = None # Skip this option if there was a problem or it wasn't changed. # Note that delivery status is handled separate from the options # flags. if newval is None: continue elif flag == mm_cfg.DisableDelivery: status = mlist.getDeliveryStatus(user) # Here, newval == 0 means enable, newval == 1 means disable if not newval and status != MemberAdaptor.ENABLED: newval = MemberAdaptor.ENABLED elif newval and status == MemberAdaptor.ENABLED: newval = MemberAdaptor.BYUSER else: continue elif newval == mlist.getMemberOption(user, flag): continue # Should we warn about one more digest? if flag == mm_cfg.Digests and \ newval == 0 and mlist.getMemberOption(user, flag): digestwarn = 1 newvals.append((flag, newval)) # The user language is handled a little differently if userlang not in mlist.GetAvailableLanguages(): newvals.append((SETLANGUAGE, mlist.preferred_language)) else: newvals.append((SETLANGUAGE, userlang)) # Process user selected topics, but don't make the changes to the # MailList object; we must do that down below when the list is # locked. topicnames = cgidata.getvalue('usertopic') if topicnames: # Some topics were selected. topicnames can actually be a string # or a list of strings depending on whether more than one topic # was selected or not. if not isinstance(topicnames, list): # Assume it was a bare string, so listify it topicnames = [topicnames] # unquote the topic names topicnames = [urllib.parse.unquote_plus(n) for n in topicnames] # The standard sigterm handler (see above) def sigterm_handler(signum, frame, mlist=mlist): mlist.Unlock() sys.exit(0) # Now, lock the list and perform the changes mlist.Lock() try: signal.signal(signal.SIGTERM, sigterm_handler) # `values' is a tuple of flags and the web values for flag, newval in newvals: # Handle language settings differently if flag == SETLANGUAGE: mlist.setMemberLanguage(user, newval) # Handle delivery status separately elif flag == mm_cfg.DisableDelivery: mlist.setDeliveryStatus(user, newval) else: try: mlist.setMemberOption(user, flag, newval) except Errors.CantDigestError: cantdigest = 1 except Errors.MustDigestError: mustdigest = 1 # Set the topics information. mlist.setMemberTopics(user, topicnames) mlist.Save() finally: mlist.Unlock() # A bag of attributes for the global options class Global: enable = None remind = None nodupes = None mime = None def __bool__(self): return len(list(self.__dict__.keys())) > 0 globalopts = Global() # The enable/disable option and the password remind option may have # their global flags sets. if cgidata.getfirst('deliver-globally'): # Yes, this is inefficient, but the list is so small it shouldn't # make much of a difference. for flag, newval in newvals: if flag == mm_cfg.DisableDelivery: globalopts.enable = newval break if cgidata.getfirst('remind-globally'): for flag, newval in newvals: if flag == mm_cfg.SuppressPasswordReminder: globalopts.remind = newval break if cgidata.getfirst('nodupes-globally'): for flag, newval in newvals: if flag == mm_cfg.DontReceiveDuplicates: globalopts.nodupes = newval break if cgidata.getfirst('mime-globally'): for flag, newval in newvals: if flag == mm_cfg.DisableMime: globalopts.mime = newval break # Change options globally, but only if this is the user or site admin, # /not/ if this is the list admin. if globalopts: if not is_user_or_siteadmin: doc.addError( _("""The list administrator may not change the options for this user's other subscriptions. However the options for this mailing list subscription has been changed."""), _('Note: ')) else: for gmlist in lists_of_member(mlist, user): global_options(gmlist, user, globalopts) # Now print the results if cantdigest: msg = _('''The list administrator has disabled digest delivery for this list, so your delivery option has not been set. However your other options have been set successfully.''') elif mustdigest: msg = _('''The list administrator has disabled non-digest delivery for this list, so your delivery option has not been set. However your other options have been set successfully.''') else: msg = _('You have successfully set your options.') if digestwarn: msg += _('You may get one last digest.') options_page(mlist, doc, user, cpuser, userlang, msg) print(doc.Format()) return if mlist.isMember(user): options_page(mlist, doc, user, cpuser, userlang) else: loginpage(mlist, doc, user, userlang) print(doc.Format())
def UpdateOldVars(l, stored_state): """Transform old variable values into new ones, deleting old ones. stored_state is last snapshot from file, as opposed to from InitVars().""" def PreferStored(oldname, newname, newdefault=uniqueval, l=l, state=stored_state): """Use specified old value if new value is not in stored state. If the old attr does not exist, and no newdefault is specified, the new attr is *not* created - so either specify a default or be positive that the old attr exists - or don't depend on the new attr. """ if hasattr(l, oldname): if not state.has_key(newname): setattr(l, newname, getattr(l, oldname)) delattr(l, oldname) if not hasattr(l, newname) and newdefault is not uniqueval: setattr(l, newname, newdefault) # Migrate to 2.1b3, baw 17-Aug-2001 if hasattr(l, 'dont_respond_to_post_requests'): oldval = getattr(l, 'dont_respond_to_post_requests') if not hasattr(l, 'respond_to_post_requests'): l.respond_to_post_requests = not oldval del l.dont_respond_to_post_requests # Migrate to 2.1b3, baw 13-Oct-2001 # Basic defaults for new variables if not hasattr(l, 'default_member_moderation'): l.default_member_moderation = mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION if not hasattr(l, 'accept_these_nonmembers'): l.accept_these_nonmembers = [] if not hasattr(l, 'hold_these_nonmembers'): l.hold_these_nonmembers = [] if not hasattr(l, 'reject_these_nonmembers'): l.reject_these_nonmembers = [] if not hasattr(l, 'discard_these_nonmembers'): l.discard_these_nonmembers = [] if not hasattr(l, 'forward_auto_discards'): l.forward_auto_discards = mm_cfg.DEFAULT_FORWARD_AUTO_DISCARDS if not hasattr(l, 'generic_nonmember_action'): l.generic_nonmember_action = mm_cfg.DEFAULT_GENERIC_NONMEMBER_ACTION # Now convert what we can... Note that the interaction between the # MM2.0.x attributes `moderated', `member_posting_only', and `posters' is # so confusing, it makes my brain really ache. Which is why they go away # in MM2.1. I think the best we can do semantically is the following: # # - If moderated == yes, then any sender who's address is not on the # posters attribute would get held for approval. If the sender was on # the posters list, then we'd defer judgement to a later step # - If member_posting_only == yes, then members could post without holds, # and if there were any addresses added to posters, they could also post # without holds. # - If member_posting_only == no, then what happens depends on the value # of the posters attribute: # o If posters was empty, then anybody can post without their # message being held for approval # o If posters was non-empty, then /only/ those addresses could post # without approval, i.e. members not on posters would have their # messages held for approval. # # How to translate this mess to MM2.1 values? I'm sure I got this wrong # before, but here's how we're going to do it, as of MM2.1b3. # # - We'll control member moderation through their Moderate flag, and # non-member moderation through the generic_nonmember_action, # hold_these_nonmembers, and accept_these_nonmembers. # - If moderated == yes then we need to troll through the addresses on # posters, and any non-members would get added to # accept_these_nonmembers. /Then/ we need to troll through the # membership and any member on posters would get their Moderate flag # unset, while members not on posters would get their Moderate flag set. # Then generic_nonmember_action gets set to 1 (hold) so nonmembers get # moderated, and default_member_moderation will be set to 1 (hold) so # new members will also get held for moderation. We'll stop here. # - We only get to here if moderated == no. # - If member_posting_only == yes, then we'll turn off the Moderate flag # for members. We troll through the posters attribute and add all those # addresses to accept_these_nonmembers. We'll also set # generic_nonmember_action to 1 and default_member_moderation to 0. # We'll stop here. # - We only get to here if member_posting_only == no # - If posters is empty, then anybody could post without being held for # approval, so we'll set generic_nonmember_action to 0 (accept), and # we'll turn off the Moderate flag for all members. We'll also turn off # default_member_moderation so new members can post without approval. # We'll stop here. # - We only get here if posters is non-empty. # - This means that /only/ the addresses on posters got to post without # being held for approval. So first, we troll through posters and add # all non-members to accept_these_nonmembers. Then we troll through the # membership and if their address is on posters, we'll clear their # Moderate flag, otherwise we'll set it. We'll turn on # default_member_moderation so new members get moderated. We'll set # generic_nonmember_action to 1 (hold) so all other non-members will get # moderated. And I think we're finally done. # # SIGH. if hasattr(l, 'moderated'): # We'll assume we're converting all these attributes at once if l.moderated: #syslog('debug', 'Case 1') for addr in l.posters: if not l.isMember(addr): l.accept_these_nonmembers.append(addr) for member in l.getMembers(): l.setMemberOption(member, mm_cfg.Moderate, # reset for explicitly named members member not in l.posters) l.generic_nonmember_action = 1 l.default_member_moderation = 1 elif l.member_posting_only: #syslog('debug', 'Case 2') for addr in l.posters: if not l.isMember(addr): l.accept_these_nonmembers.append(addr) for member in l.getMembers(): l.setMemberOption(member, mm_cfg.Moderate, 0) l.generic_nonmember_action = 1 l.default_member_moderation = 0 elif not l.posters: #syslog('debug', 'Case 3') for member in l.getMembers(): l.setMemberOption(member, mm_cfg.Moderate, 0) l.generic_nonmember_action = 0 l.default_member_moderation = 0 else: #syslog('debug', 'Case 4') for addr in l.posters: if not l.isMember(addr): l.accept_these_nonmembers.append(addr) for member in l.getMembers(): l.setMemberOption(member, mm_cfg.Moderate, # reset for explicitly named members member not in l.posters) l.generic_nonmember_action = 1 l.default_member_moderation = 1 # Now get rid of the old attributes del l.moderated del l.posters del l.member_posting_only if hasattr(l, 'forbidden_posters'): # For each of the posters on this list, if they are members, toggle on # their moderation flag. If they are not members, then add them to # hold_these_nonmembers. forbiddens = l.forbidden_posters for addr in forbiddens: if l.isMember(addr): l.setMemberOption(addr, mm_cfg.Moderate, 1) else: l.hold_these_nonmembers.append(addr) del l.forbidden_posters # Migrate to 1.0b6, klm 10/22/1998: PreferStored('reminders_to_admins', 'umbrella_list', mm_cfg.DEFAULT_UMBRELLA_LIST) # Migrate up to 1.0b5: PreferStored('auto_subscribe', 'open_subscribe') PreferStored('closed', 'private_roster') PreferStored('mimimum_post_count_before_removal', 'mimimum_post_count_before_bounce_action') PreferStored('bad_posters', 'forbidden_posters') PreferStored('automatically_remove', 'automatic_bounce_action') if hasattr(l, "open_subscribe"): if l.open_subscribe: if mm_cfg.ALLOW_OPEN_SUBSCRIBE: l.subscribe_policy = 0 else: l.subscribe_policy = 1 else: l.subscribe_policy = 2 # admin approval delattr(l, "open_subscribe") if not hasattr(l, "administrivia"): setattr(l, "administrivia", mm_cfg.DEFAULT_ADMINISTRIVIA) if not hasattr(l, "admin_member_chunksize"): setattr(l, "admin_member_chunksize", mm_cfg.DEFAULT_ADMIN_MEMBER_CHUNKSIZE) # # this attribute was added then deleted, so there are a number of # cases to take care of # if hasattr(l, "posters_includes_members"): if l.posters_includes_members: if l.posters: l.member_posting_only = 1 else: if l.posters: l.member_posting_only = 0 delattr(l, "posters_includes_members") elif l.data_version <= 10 and l.posters: # make sure everyone gets the behavior the list used to have, but only # for really old versions of Mailman (1.0b5 or before). Any newer # version of Mailman should not get this attribute whacked. l.member_posting_only = 0 # # transfer the list data type for holding members and digest members # to the dict data type starting file format version 11 # if type(l.members) is ListType: members = {} for m in l.members: members[m] = 1 l.members = members if type(l.digest_members) is ListType: dmembers = {} for dm in l.digest_members: dmembers[dm] = 1 l.digest_members = dmembers # # set admin_notify_mchanges # if not hasattr(l, "admin_notify_mchanges"): setattr(l, "admin_notify_mchanges", mm_cfg.DEFAULT_ADMIN_NOTIFY_MCHANGES) # # Convert the members and digest_members addresses so that the keys of # both these are always lowercased, but if there is a case difference, the # value contains the case preserved value # for k in l.members.keys(): if k.lower() <> k: l.members[k.lower()] = Utils.LCDomain(k) del l.members[k] elif type(l.members[k]) == StringType and k == l.members[k].lower(): # already converted pass else: l.members[k] = 0 for k in l.digest_members.keys(): if k.lower() <> k: l.digest_members[k.lower()] = Utils.LCDomain(k) del l.digest_members[k] elif type(l.digest_members[k]) == StringType and \ k == l.digest_members[k].lower(): # already converted pass else: l.digest_members[k] = 0
if lenparts < 2: user = cgidata.getvalue('email') if not user: # If we're coming from the listinfo page and we left the email # address field blank, it's not an error. Likewise if we're # coming from anywhere else. Only issue the error if we came # via one of our buttons. if (cgidata.getvalue('login') or cgidata.getvalue('login-unsub') or cgidata.getvalue('login-remind')): doc.addError(_('No address given')) loginpage(mlist, doc, None, language) print doc.Format() return else: user = Utils.LCDomain(Utils.UnobscureEmail(SLASH.join(parts[1:]))) # Avoid cross-site scripting attacks safeuser = Utils.websafe(user) try: Utils.ValidateEmail(user) except Errors.EmailAddressError: doc.addError(_('Illegal Email Address: %(safeuser)s')) loginpage(mlist, doc, None, language) print doc.Format() return # Sanity check the user, but only give the "no such member" error when # using public rosters, otherwise, we'll leak membership information. if not mlist.isMember(user) and mlist.private_roster == 0: doc.addError(_('No such member: %(safeuser)s.')) loginpage(mlist, doc, None, language)