def _create(self, team_name): """Create a new mailing list.""" # Create the mailing list and set the defaults. mlist = MailList() try: # Use a fake list admin password; Mailman will never be # administered from its web u/i. Nor will the mailing list # require an owner that's different from the site owner. Also by # default, only English is supported. try: mlist.Create(team_name, mm_cfg.SITE_LIST_OWNER, ' no password ') # Additional hard coded list defaults. # - Personalize regular delivery so that we can VERP these. # - Turn off RFC 2369 headers; we'll do them differently # - enable $-string substitutions in headers/footers mlist.personalize = 1 mlist.include_rfc2369_headers = False mlist.use_dollar_strings = True mlist.held_message_ids = {} mlist.Save() # Now create the archive directory for MHonArc. path = os.path.join(mm_cfg.VAR_PREFIX, 'mhonarc', team_name) os.makedirs(path) # We have to use a bare except here because of the legacy string # exceptions that Mailman can raise. except: log_exception('List creation error for team: %s', team_name) return False else: return True finally: mlist.Unlock()
def _apply_list_defaults(self, team_name, list_defaults): """Apply mailing list defaults and tie the new list into the MTA.""" mlist = MailList(team_name) try: for key, value in list_defaults.items(): setattr(mlist, key, value) # Do MTA specific creation steps. if mm_cfg.MTA: modname = 'Mailman.MTA.' + mm_cfg.MTA __import__(modname) sys.modules[modname].create(mlist, quiet=True) mlist.Save() finally: mlist.Unlock()
def main(): for x in list_names(): ml = MailList(x, True) try: changed = False for o in ml.owner: if not o in ALLOWED_OWNERS: print 'Removing %s from %s' % (o, x) ml.owner.remove(o) changed = True if not ml.owner: ml.owner.append(ALLOWED_OWNERS[0]) changed = True if changed: ml.Save() finally: ml.Unlock()
def main(): for x in list_names(): ml = MailList(x, True) try: changed = False if ml.host_name != settings.LISTS_MAILDOMAIN: print 'Updating host_name of %s' % x ml.host_name = settings.LISTS_MAILDOMAIN changed = True if ml.from_is_list != 1: print 'Updating from_is_list of %s' % x ml.from_is_list = 1 changed = True if changed: print 'Saving %s' % x ml.Save() finally: ml.Unlock()
def _resynchronize(self, actions, statuses): """Process resynchronization actions. actions is a sequence of 2-tuples specifying what needs to be resynchronized. The tuple is of the form (listname, current-status). statuses is a dictionary mapping team names to one of the strings 'success' or 'failure'. """ syslog('xmlrpc', 'resynchronizing: %s', COMMASPACE.join(sorted(name for (name, status) in actions))) for name, status in actions: # There's no way to really know whether the original action # succeeded or not, however, it's unlikely that an action would # fail leaving the mailing list in a usable state. Therefore, if # the list is loadable and lockable, we'll say it succeeded. try: mlist = MailList(name) except Errors.MMUnknownListError: # The list doesn't exist on the Mailman side, so if its status # is CONSTRUCTING, we can create it now. if status == 'constructing': if self._create(name): statuses[name] = ('resynchronize', 'success') else: statuses[name] = ('resynchronize', 'failure') else: # Any other condition leading to an unknown list is a # failure state. statuses[name] = ('resynchronize', 'failure') except: # Any other exception is also a failure. statuses[name] = ('resynchronize', 'failure') log_exception('Mailing list does not load: %s', name) else: # The list loaded just fine, so it successfully # resynchronized. Be sure to unlock it! mlist.Unlock() statuses[name] = ('resynchronize', 'success')
def _modify(self, actions, statuses): """Process mailing list modification actions. actions is a sequence of (team_name, modifications) tuples where the team_name is the name of the mailing list to create and modifications is a dictionary of values to set on the mailing list. statuses is a dictionary mapping team names to one of the strings 'success' or 'failure'. """ for team_name, modifications in actions: # First, validate the modification keywords. list_settings = {} for key in attrmap: if key in modifications: list_settings[attrmap[key]] = modifications[key] del modifications[key] if modifications: statuses[team_name] = ('modify', 'failure') syslog('xmlrpc', 'Unexpected modify settings: %s', COMMASPACE.join(modifications)) continue try: mlist = MailList(team_name) try: for key, value in list_settings.items(): setattr(mlist, key, value) mlist.Save() finally: mlist.Unlock() # We have to use a bare except here because of the legacy string # exceptions that Mailman can raise. except: log_exception('List modification error for team: %s', team_name) statuses[team_name] = ('modify', 'failure') else: statuses[team_name] = ('modify', 'success')
def main(): url = 'https://%s/mailman/' % settings.MAILDOMAIN for x in list_names(): ml = MailList(x, True) try: changed = False if ml.host_name != settings.LISTS_MAILDOMAIN: print('Updating host_name of %s (was %s)' % (x, ml.host_name)) ml.host_name = settings.LISTS_MAILDOMAIN changed = True if ml.from_is_list != 1: print('Updating from_is_list of %s' % x) ml.from_is_list = 1 changed = True if ml.web_page_url != url: print('Updating url_host of %s (was %s)' % (x, ml.web_page_url)) ml.web_page_url = url changed = True if changed: print('Saving %s' % x) # ml.Save() finally: ml.Unlock()
def registerBounce(self, member, msg, weight=1.0, day=None, sibling=False): if not self.isMember(member): # check regular_include_lists, only one level if not self.regular_include_lists or sibling: return from Mailman.MailList import MailList for listaddr in self.regular_include_lists: listname, hostname = listaddr.split('@') listname = listname.lower() if listname == self.internal_name(): syslog('error', 'Bouncer: %s: Include list self reference', listname) continue try: siblist = None try: siblist = MailList(listname) except MMUnknownListError: syslog('error', 'Bouncer: %s: Include list "%s" not found.', self.real_name, listname) continue siblist.registerBounce(member, msg, weight, day, sibling=True) siblist.Save() finally: if siblist and siblist.Locked(): siblist.Unlock() return info = self.getBounceInfo(member) first_today = True if day is None: # Use today's date day = time.localtime()[:3] if not isinstance(info, _BounceInfo): # This is the first bounce we've seen from this member info = _BounceInfo(member, weight, day, self.bounce_you_are_disabled_warnings) # setBounceInfo is now called below after check phase. syslog('bounce', '%s: %s bounce score: %s', self.internal_name(), member, info.score) # Continue to the check phase below elif self.getDeliveryStatus(member) <> MemberAdaptor.ENABLED: # The user is already disabled, so we can just ignore subsequent # bounces. These are likely due to residual messages that were # sent before disabling the member, but took a while to bounce. syslog('bounce', '%s: %s residual bounce received', self.internal_name(), member) return elif info.date == day: # We've already scored any bounces for this day, so ignore it. first_today = False syslog('bounce', '%s: %s already scored a bounce for date %s', self.internal_name(), member, time.strftime('%d-%b-%Y', day + (0, 0, 0, 0, 1, 0))) # Continue to check phase below else: # See if this member's bounce information is stale. now = Utils.midnight(day) lastbounce = Utils.midnight(info.date) if lastbounce + self.bounce_info_stale_after < now: # Information is stale, so simply reset it info.reset(weight, day, self.bounce_you_are_disabled_warnings) syslog('bounce', '%s: %s has stale bounce info, resetting', self.internal_name(), member) else: # Nope, the information isn't stale, so add to the bounce # score and take any necessary action. info.score += weight info.date = day syslog('bounce', '%s: %s current bounce score: %s', self.internal_name(), member, info.score) # Continue to the check phase below # # Now that we've adjusted the bounce score for this bounce, let's # check to see if the disable-by-bounce threshold has been reached. if info.score >= self.bounce_score_threshold: if mm_cfg.VERP_PROBES: syslog('bounce', 'sending %s list probe to: %s (score %s >= %s)', self.internal_name(), member, info.score, self.bounce_score_threshold) self.sendProbe(member, msg) info.reset(0, info.date, info.noticesleft) else: self.disableBouncingMember(member, info, msg) elif self.bounce_notify_owner_on_bounce_increment and first_today: self.__sendAdminBounceNotice(member, msg, did=_('bounce score incremented')) # We've set/changed bounce info above. We now need to tell the # MemberAdaptor to set/update it. We do it here in case the # MemberAdaptor stores bounce info externally to the list object to # be sure updated information is stored, but we have to be sure the # member wasn't removed. if self.isMember(member): self.setBounceInfo(member, info)
def _check_held_messages(self): """See if any held messages have been accepted or rejected.""" try: dispositions = self._proxy.getMessageDispositions() except (xmlrpclib.ProtocolError, socket.error) as error: log_exception('Cannot talk to Launchpad:\n%s', error) return except xmlrpclib.Fault as error: log_exception('Launchpad exception: %s', error) return if dispositions: syslog('xmlrpc', 'Received dispositions for these message-ids: %s', COMMASPACE.join(dispositions)) else: return # For each message that has been acted upon in Launchpad, handle the # message in here in Mailman. We need to resort the dispositions so # that we can handle all of them for a particular mailing list at the # same time. by_list = {} for message_id, (team_name, action) in dispositions.items(): accepts, declines, discards = by_list.setdefault( team_name, ([], [], [])) if action == 'accept': accepts.append(message_id) elif action == 'decline': declines.append(message_id) elif action == 'discard': discards.append(message_id) else: syslog('xmlrpc', 'Skipping invalid disposition "%s" for message-id: %s', action, message_id) # Now cycle through the dispositions for every mailing list. for team_name in by_list: try: mlist = MailList(team_name) except Errors.MMUnknownListError: log_exception('Skipping dispositions for unknown list: %s', team_name) continue try: accepts, declines, discards = by_list[team_name] for message_id in accepts: request_id = mlist.held_message_ids.pop(message_id, None) if request_id is None: syslog('xmlrpc', 'Missing accepted message-id: %s', message_id) else: mlist.HandleRequest(request_id, mm_cfg.APPROVE) syslog('xmlrpc', 'Approved: %s', message_id) for message_id in declines: request_id = mlist.held_message_ids.pop(message_id, None) if request_id is None: syslog('xmlrpc', 'Missing declined message-id: %s', message_id) else: mlist.HandleRequest(request_id, mm_cfg.REJECT) syslog('xmlrpc', 'Rejected: %s', message_id) for message_id in discards: request_id = mlist.held_message_ids.pop(message_id, None) if request_id is None: syslog('xmlrpc', 'Missing declined message-id: %s', message_id) else: mlist.HandleRequest(request_id, mm_cfg.DISCARD) syslog('xmlrpc', 'Discarded: %s', message_id) mlist.Save() finally: mlist.Unlock()
def _update_list_subscriptions(self, list_name, subscription_info): """Update the subscription information for a single mailing list. :param list_name: The name of the mailing list to update. :type list_name: string :param subscription_info: The mailing list's new subscription information. :type subscription_info: a list of 4-tuples containing the address, real name, flags, and status of each person in the list's subscribers. """ ## syslog('xmlrpc', '%s subinfo: %s', list_name, subscription_info) # Start with an unlocked list. mlist = MailList(list_name, lock=False) # Create a mapping of email address to the member's real name, # flags, and status. Note that flags is currently unused. member_map = dict( (address, (realname, flags, status)) for address, realname, flags, status in subscription_info) # In Mailman parlance, the 'member key' is the lower-cased # address. We need a mapping from the member key to # case-preserved address for all future members of the list. key_to_case_preserved = dict( (address.lower(), address) for address in member_map) # The following modifications to membership may be made: # - Current members whose address is changing case # - New members who need to be added to the mailing list # - Old members who need to be removed from the mailing list # - Current members whose settings are being changed # # Start by getting the case-folded membership sets of all current # and future members. current_members = set(mlist.getMembers()) future_members = set(key_to_case_preserved) # Additions are all those addresses in future_members who are not # in current_members. additions = future_members - current_members # Deletions are all those addresses in current_members who are not # in future_members. deletions = current_members - future_members # Any address in both current and future members is either # changing case, updating settings, or both. updates = current_members & future_members # If there's nothing to do, just skip this list. if not additions and not deletions and not updates: # Why did we get here? This list should not have shown up in # getMembershipInformation(). syslog('xmlrpc', 'Strange subscription information for list: %s', list_name) return # Lock the list and make the modifications. Don't worry if the list # couldn't be locked, we'll just log that and try again the next time # through the loop. Eventually any existing lock will expire anyway. try: mlist.Lock(2) except TimeOutError: syslog('xmlrpc', 'Could not lock the list to update subscriptions: %s', list_name) return try: # Handle additions first. if len(additions) > 0: syslog('xmlrpc', 'Adding to %s: %s', list_name, additions) for address in additions: # When adding the new member, be sure to use the # case-preserved email address. original_address = key_to_case_preserved[address] realname, flags, status = member_map[original_address] mlist.addNewMember(original_address, realname=realname) mlist.setDeliveryStatus(original_address, status) # Handle deletions next. if len(deletions) > 0: syslog('xmlrpc', 'Removing from %s: %s', list_name, deletions) for address in deletions: mlist.removeMember(address) # Updates can be either a settings update, a change in the # case of the subscribed address, or both. found_updates = [] for address in updates: # See if the case is changing. current_address = mlist.getMemberCPAddress(address) future_address = key_to_case_preserved[address] if current_address != future_address: mlist.changeMemberAddress(address, future_address) found_updates.append('%s -> %s' % (address, future_address)) # flags are ignored for now. realname, flags, status = member_map[future_address] if realname != mlist.getMemberName(address): mlist.setMemberName(address, realname) found_updates.append('%s new name: %s' % (address, realname)) if status != mlist.getDeliveryStatus(address): mlist.setDeliveryStatus(address, status) found_updates.append('%s new status: %s' % (address, status)) if len(found_updates) > 0: syslog('xmlrpc', 'Membership updates for %s: %s', list_name, found_updates) # We're done, so flush the changes for this mailing list. mlist.Save() finally: mlist.Unlock()
def fixHostnames(self): """Fix up the host names in Mailman and the LP database.""" # These can't be done at module global scope. from Mailman import Utils from Mailman import mm_cfg from Mailman.MailList import MailList # Grab a couple of useful components. email_address_set = getUtility(IEmailAddressSet) mailing_list_set = getUtility(IMailingListSet) # Clean things up per mailing list. for list_name in Utils.list_names(): # Skip the site list. if list_name == mm_cfg.MAILMAN_SITE_LIST: continue # The first thing to clean up is the mailing list pickles. There # are things like host names in some attributes that need to be # converted. The following opens a locked list. mailing_list = MailList(list_name) try: mailing_list.host_name = mm_cfg.DEFAULT_EMAIL_HOST mailing_list.web_page_url = ( mm_cfg.DEFAULT_URL_PATTERN % mm_cfg.DEFAULT_URL_HOST) mailing_list.Save() finally: mailing_list.Unlock() # Patch up the email address for the list in the Launchpad # database. lp_mailing_list = mailing_list_set.get(list_name) if lp_mailing_list is None: # We found a mailing list in Mailman that does not exist in # the Launchpad database. This can happen if we rsync'd the # Mailman directories after the lists were created, but we # copied the LP database /before/ the lists were created. # If we don't delete the Mailman lists, we won't be able to # create the mailing lists on staging. self.logger.error('No LP mailing list for: %s', list_name) self.deleteMailmanList(list_name) continue # Clean up the team email addresses corresponding to their mailing # lists. Note that teams can have two email addresses if they # have a different contact address. team = getUtility(IPersonSet).getByName(list_name) mlist_addresses = email_address_set.getByPerson(team) if mlist_addresses.count() == 0: self.logger.error('No LP email address for: %s', list_name) else: # Teams can have both a mailing list and a contact address. old_address = '%s@%s' % (list_name, self.options.hostname) for email_address in mlist_addresses: if email_address.email == old_address: new_address = lp_mailing_list.address removeSecurityProxy(email_address).email = new_address self.logger.info('%s -> %s', old_address, new_address) break else: self.logger.error('No change to LP email address for: %s', list_name)