def decorate(mlist, uri, extradict=None): """Expand the decoration template.""" if uri is None: return '' # Get the decorator template. loader = getUtility(ITemplateLoader) template_uri = expand(uri, dict( listname=mlist.fqdn_listname, language=mlist.preferred_language.code, )) template = loader.get(template_uri) # Create a dictionary which includes the default set of interpolation # variables allowed in headers and footers. These will be augmented by # any key/value pairs in the extradict. substitutions = dict( fqdn_listname = mlist.fqdn_listname, list_name = mlist.list_name, host_name = mlist.mail_host, display_name = mlist.display_name, listinfo_uri = mlist.script_url('listinfo'), list_requests = mlist.request_address, description = mlist.description, info = mlist.info, ) if extradict is not None: substitutions.update(extradict) text = expand(template, substitutions) # Turn any \r\n line endings into just \n return re.sub(r' *\r?\n', r'\n', text)
def send_welcome_message(mlist, address, language, delivery_mode, text=''): """Send a welcome message to a subscriber. Prepending to the standard welcome message template is the mailing list's welcome message, if there is one. :param mlist: the mailing list :type mlist: IMailingList :param address: The address to respond to :type address: string :param language: the language of the response :type language: ILanguage :param delivery_mode: the type of delivery the subscriber is getting :type delivery_mode: DeliveryMode """ if mlist.welcome_message_uri: try: uri = expand(mlist.welcome_message_uri, dict( listname=mlist.fqdn_listname, language=language.code, )) welcome_message = getUtility(ITemplateLoader).get(uri) except URLError: log.exception('Welcome message URI not found ({0}): {1}'.format( mlist.fqdn_listname, mlist.welcome_message_uri)) welcome = '' else: welcome = wrap(welcome_message) else: welcome = '' # Find the IMember object which is subscribed to the mailing list, because # from there, we can get the member's options url. member = mlist.members.get_member(address) user_name = member.user.display_name options_url = member.options_url # Get the text from the template. text = expand(welcome, dict( fqdn_listname=mlist.fqdn_listname, list_name=mlist.display_name, listinfo_uri=mlist.script_url('listinfo'), list_requests=mlist.request_address, user_name=user_name, user_address=address, user_options_uri=options_url, )) if delivery_mode is not DeliveryMode.regular: digmode = _(' (Digest mode)') else: digmode = '' msg = UserNotification( formataddr((user_name, address)), mlist.request_address, _('Welcome to the "$mlist.display_name" mailing list${digmode}'), text, language) msg['X-No-Archive'] = 'yes' msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def send_welcome_message(mlist, member, language, text=''): """Send a welcome message to a subscriber. Prepending to the standard welcome message template is the mailing list's welcome message, if there is one. :param mlist: The mailing list. :type mlist: IMailingList :param member: The member to send the welcome message to. :param address: IMember :param language: The language of the response. :type language: ILanguage """ welcome_message = _get_message(mlist.welcome_message_uri, mlist, language) options_url = member.options_url # Get the text from the template. display_name = ('' if member.user is None else member.user.display_name) text = expand(welcome_message, dict( fqdn_listname=mlist.fqdn_listname, list_name=mlist.display_name, listinfo_uri=mlist.script_url('listinfo'), list_requests=mlist.request_address, user_name=display_name, user_address=member.address.email, user_options_uri=options_url, )) digmode = ('' if member.delivery_mode is DeliveryMode.regular else _(' (Digest mode)')) msg = UserNotification( formataddr((display_name, member.address.email)), mlist.request_address, _('Welcome to the "$mlist.display_name" mailing list${digmode}'), text, language) msg['X-No-Archive'] = 'yes' msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def list_url(self, mlist): """See `IArchiver`.""" # XXX What about private MHonArc archives? return expand( self.base_url, dict(listname=mlist.fqdn_listname, hostname=mlist.domain.url_host, fqdn_listname=mlist.fqdn_listname), )
def get(self, store, name, context, **kws): """See `ITemplateManager`.""" template = store.query(Template).filter( Template.name == name, Template.context == context).one_or_none() if template is None: return None actual_uri = expand(template.uri, None, kws) cache_mgr = getUtility(ICacheManager) contents = cache_mgr.get(actual_uri) if contents is None: # It's likely that the cached contents have expired. auth = {} if template.username is not None: auth['auth'] = (template.username, template.password) try: contents = protocols.get(actual_uri, **auth) except HTTPError as error: # 404/NotFound errors are interpreted as missing templates, # for which we'll return the default (i.e. the empty string). # All other exceptions get passed up the chain. if error.response.status_code != 404: raise log.exception('Cannot retrieve template at {} ({})'.format( actual_uri, auth.get('auth', '<no authorization>'))) return '' # We don't need to cache mailman: contents since those are already # on the file system. if urlparse(actual_uri).scheme != 'mailman': cache_mgr.add(actual_uri, contents) return contents
def __init__(self, mlist, volume, digest_number): self._mlist = mlist self._charset = mlist.preferred_language.charset # This will be used in the Subject, so use $-strings. self._digest_id = _( '$mlist.display_name Digest, Vol $volume, Issue $digest_number') self._subject = Header(self._digest_id, self._charset, header_name='Subject') self._message = self._make_message() self._digest_part = self._make_digest_part() self._message['From'] = mlist.request_address self._message['Subject'] = self._subject self._message['To'] = mlist.posting_address self._message['Reply-To'] = mlist.posting_address self._message['Date'] = formatdate(localtime=True) self._message['Message-ID'] = make_msgid() # In the rfc1153 digest, the masthead contains the digest boilerplate # plus any digest header. In the MIME digests, the masthead and # digest header are separate MIME subobjects. In either case, it's # the first thing in the digest, and we can calculate it now, so go # ahead and add it now. template = getUtility(ITemplateLoader).get( 'list:member:digest:masthead', mlist) self._masthead = wrap(expand(template, mlist, dict( # For backward compatibility. got_list_email=mlist.posting_address, got_request_email=mlist.request_address, got_owner_email=mlist.owner_address, ))) # Set things up for the table of contents. self._header = decorate('list:member:digest:header', mlist) self._toc = StringIO() print(_("Today's Topics:\n"), file=self._toc)
def __init__(self, name, slice=None): """Create a runner. :param slice: The slice number for this runner. This is passed directly to the underlying `ISwitchboard` object. This is ignored for runners that don't manage a queue. :type slice: int or None """ # Grab the configuration section. self.name = name section = getattr(config, 'runner.' + name) substitutions = config.paths substitutions['name'] = name numslices = int(section.instances) # Check whether the runner is queue runner or not; non-queue runner # should not have queue_directory or switchboard instance. if self.is_queue_runner: self.queue_directory = expand(section.path, substitutions) self.switchboard = Switchboard( name, self.queue_directory, slice, numslices, True) else: self.queue_directory = None self.switchboard= None self.sleep_time = as_timedelta(section.sleep_time) # sleep_time is a timedelta; turn it into a float for time.sleep(). self.sleep_float = (86400 * self.sleep_time.days + self.sleep_time.seconds + self.sleep_time.microseconds / 1.0e6) self.max_restarts = int(section.max_restarts) self.start = as_boolean(section.start) self._stop = False self.status = 0
def send_rejection(mlist, request, recip, comment, origmsg=None, lang=None): # As this message is going to the requester, try to set the language to # his/her language choice, if they are a member. Otherwise use the list's # preferred language. display_name = mlist.display_name # noqa: F841 if lang is None: member = mlist.members.get_member(recip) lang = (mlist.preferred_language if member is None else member.preferred_language) template = getUtility(ITemplateLoader).get('list:user:notice:refuse', mlist) text = wrap( expand( template, mlist, dict( language=lang.code, reason=comment, # For backward compatibility. request=request, adminaddr=mlist.owner_address, ))) with _.using(lang.code): # add in original message, but not wrap/filled if origmsg: text = NL.join([ text, '---------- ' + _('Original Message') + ' ----------', str(origmsg) ]) subject = _('Request to mailing list "$display_name" rejected') msg = UserNotification(recip, mlist.bounces_address, subject, text, lang) msg.send(mlist)
def _get_sender(self, mlist, msg, msgdata): """Return the recipient's address VERP encoded in the sender. :param mlist: The mailing list being delivered to. :type mlist: `IMailingList` :param msg: The original message being delivered. :type msg: `Message` :param msgdata: Additional message metadata for this delivery. :type msgdata: dictionary """ sender = super()._get_sender(mlist, msg, msgdata) if msgdata.get('verp', False): log.debug('VERPing %s', msg.get('message-id')) recipient = msgdata['recipient'] sender_mailbox, sender_domain = split_email(sender) # Encode the recipient's address for VERP. recipient_mailbox, recipient_domain = split_email(recipient) if recipient_domain is None: # The recipient address is not fully-qualified. We can't # deliver it to this person, nor can we craft a valid verp # header. I don't think there's much we can do except ignore # this recipient. log.info('Skipping VERP delivery to unqual recip: %s', recipient) return sender return '{0}@{1}'.format( expand( config.mta.verp_format, mlist, dict(bounces=sender_mailbox, local=recipient_mailbox, domain=DOT.join(recipient_domain))), DOT.join(sender_domain)) else: return sender
def _step_get_moderator_approval(self): # Here's the next step in the workflow, assuming the moderator # approves of the subscription. If they don't, the workflow and # subscription request will just be thrown away. self._set_token(TokenOwner.moderator) self.push('subscribe_from_restored') self.save() log.info('{}: held subscription request from {}'.format( self.mlist.fqdn_listname, self.address.email)) # Possibly send a notification to the list moderators. if self.mlist.admin_immed_notify: subject = _('New subscription request to $self.mlist.display_name ' 'from $self.address.email') username = formataddr( (self.subscriber.display_name, self.address.email)) template = getUtility(ITemplateLoader).get( 'list:admin:action:subscribe', self.mlist) text = wrap(expand(template, self.mlist, dict(member=username, ))) # This message should appear to come from the <list>-owner so as # to avoid any useless bounce processing. msg = UserNotification(self.mlist.owner_address, self.mlist.owner_address, subject, text, self.mlist.preferred_language) msg.send(self.mlist) # The workflow must stop running here. raise StopIteration
def _get_sender(self, mlist, msg, msgdata): """Return the recipient's address VERP encoded in the sender. :param mlist: The mailing list being delivered to. :type mlist: `IMailingList` :param msg: The original message being delivered. :type msg: `Message` :param msgdata: Additional message metadata for this delivery. :type msgdata: dictionary """ sender = super(VERPMixin, self)._get_sender(mlist, msg, msgdata) if msgdata.get("verp", False): log.debug("VERPing %s", msg.get("message-id")) recipient = msgdata["recipient"] sender_mailbox, sender_domain = split_email(sender) # Encode the recipient's address for VERP. recipient_mailbox, recipient_domain = split_email(recipient) if recipient_domain is None: # The recipient address is not fully-qualified. We can't # deliver it to this person, nor can we craft a valid verp # header. I don't think there's much we can do except ignore # this recipient. log.info("Skipping VERP delivery to unqual recip: %s", recipient) return sender return "{0}@{1}".format( expand( config.mta.verp_format, dict(bounces=sender_mailbox, local=recipient_mailbox, domain=DOT.join(recipient_domain)), ), DOT.join(sender_domain), ) else: return sender
def __init__(self, name, slice=None): """Create a runner. :param slice: The slice number for this runner. This is passed directly to the underlying `ISwitchboard` object. This is ignored for runners that don't manage a queue. :type slice: int or None """ # Grab the configuration section. self.name = name section = getattr(config, 'runner.' + name) substitutions = config.paths substitutions['name'] = name numslices = int(section.instances) # Check whether the runner is queue runner or not; non-queue runner # should not have queue_directory or switchboard instance. if self.is_queue_runner: self.queue_directory = expand(section.path, None, substitutions) self.switchboard = Switchboard(name, self.queue_directory, slice, numslices, True) else: self.queue_directory = None self.switchboard = None self.sleep_time = as_timedelta(section.sleep_time) # sleep_time is a timedelta; turn it into a float for time.sleep(). self.sleep_float = (86400 * self.sleep_time.days + self.sleep_time.seconds + self.sleep_time.microseconds / 1.0e6) self.max_restarts = int(section.max_restarts) self.start = as_boolean(section.start) self._stop = False self.status = 0
def initialize(self, debug=None): """See `IDatabase`.""" # Calculate the engine url. url = expand(config.database.url, config.paths) self._prepare(url) log.debug('Database url: %s', url) # XXX By design of SQLite, database file creation does not honor # umask. See their ticket #1193: # http://www.sqlite.org/cvstrac/tktview?tn=1193,31 # # This sucks for us because the mailman.db file /must/ be group # writable, however even though we guarantee our umask is 002 here, it # still gets created without the necessary g+w permission, due to # SQLite's policy. This should only affect SQLite engines because its # the only one that creates a little file on the local file system. # This kludges around their bug by "touch"ing the database file before # SQLite has any chance to create it, thus honoring the umask and # ensuring the right permissions. We only try to do this for SQLite # engines, and yes, we could have chmod'd the file after the fact, but # half dozen and all... self.url = url self.engine = create_engine(url) session = sessionmaker(bind=self.engine) self.store = session() self.store.commit()
def initialize(self, debug=None): """See `IDatabase`.""" # Calculate the engine url. url = expand(config.database.url, None, config.paths) self._prepare(url) log.debug('Database url: %s', url) # XXX By design of SQLite, database file creation does not honor # umask. See their ticket #1193: # http://www.sqlite.org/cvstrac/tktview?tn=1193,31 # # This sucks for us because the mailman.db file /must/ be group # writable, however even though we guarantee our umask is 002 here, it # still gets created without the necessary g+w permission, due to # SQLite's policy. This should only affect SQLite engines because its # the only one that creates a little file on the local file system. # This kludges around their bug by "touch"ing the database file before # SQLite has any chance to create it, thus honoring the umask and # ensuring the right permissions. We only try to do this for SQLite # engines, and yes, we could have chmod'd the file after the fact, but # half dozen and all... self.url = url self.engine = create_engine(url, isolation_level='READ UNCOMMITTED', pool_pre_ping=True) session = sessionmaker(bind=self.engine) self.store = session() self.store.commit()
def hold_unsubscription(mlist, email): data = dict(email=email) requestsdb = IListRequests(mlist) request_id = requestsdb.hold_request(RequestType.unsubscription, email, data) vlog.info('%s: held unsubscription request from %s', mlist.fqdn_listname, email) # Possibly notify the administrator of the hold if mlist.admin_immed_notify: subject = _( 'New unsubscription request from $mlist.display_name by $email') template = getUtility(ITemplateLoader).get( 'list:admin:action:unsubscribe', mlist) text = wrap( expand( template, mlist, dict( # For backward compatibility. mailing_list=mlist, member=email, email=email, ))) # This message should appear to come from the <list>-owner so as # to avoid any useless bounce processing. msg = UserNotification(mlist.owner_address, mlist.owner_address, subject, text, mlist.preferred_language) msg.send(mlist) return request_id
def create(ctx, language, owners, notify, quiet, create_domain, fqdn_listname): language_code = (language if language is not None else system_preferences.preferred_language.code) # Make sure that the selected language code is known. if language_code not in getUtility(ILanguageManager).codes: ctx.fail(_('Invalid language code: $language_code')) # Check to see if the domain exists or not. listname, at, domain = fqdn_listname.partition('@') domain_manager = getUtility(IDomainManager) if domain_manager.get(domain) is None and create_domain: domain_manager.add(domain) # Validate the owner email addresses. The problem with doing this check in # create_list() is that you wouldn't be able to distinguish between an # InvalidEmailAddressError for the list name or the owners. I suppose we # could subclass that exception though. if len(owners) > 0: validator = getUtility(IEmailValidator) invalid_owners = [ owner for owner in owners if not validator.is_valid(owner) ] if invalid_owners: invalid = COMMASPACE.join(sorted(invalid_owners)) # noqa: F841 ctx.fail(_('Illegal owner addresses: $invalid')) try: mlist = create_list(fqdn_listname, owners) except InvalidEmailAddressError: ctx.fail(_('Illegal list name: $fqdn_listname')) except ListAlreadyExistsError: ctx.fail(_('List already exists: $fqdn_listname')) except BadDomainSpecificationError as domain: # noqa: F841 ctx.fail(_('Undefined domain: $domain')) # Find the language associated with the code, then set the mailing list's # preferred language to that. language_manager = getUtility(ILanguageManager) with transaction(): mlist.preferred_language = language_manager[language_code] # Do the notification. if not quiet: print(_('Created mailing list: $mlist.fqdn_listname')) if notify: template = getUtility(ITemplateLoader).get( 'domain:admin:notice:new-list', mlist) text = wrap( expand( template, mlist, dict( # For backward compatibility. requestaddr=mlist.request_address, siteowner=mlist.no_reply_address, ))) # Set the I18N language to the list's preferred language so the header # will match the template language. Stashing and restoring the old # translation context is just (healthy? :) paranoia. with _.using(mlist.preferred_language.code): msg = UserNotification(owners, mlist.no_reply_address, _('Your new mailing list: $fqdn_listname'), text, mlist.preferred_language) msg.send(mlist)
def list_url(mlist): """See `IArchiver`.""" # XXX What about private MHonArc archives? return expand(config.archiver.mhonarc.base_url, dict(listname=mlist.fqdn_listname, hostname=mlist.domain.url_host, fqdn_listname=mlist.fqdn_listname, ))
def list_url(self, mlist): """See `IArchiver`.""" # XXX What about private MHonArc archives? return expand(self.base_url, dict(listname=mlist.fqdn_listname, hostname=mlist.domain.url_host, fqdn_listname=mlist.fqdn_listname, ))
def list_url(self, mlist): """See `IArchiver`.""" # XXX What about private MHonArc archives? return expand(self.base_url, mlist, dict( # For backward compatibility. hostname=mlist.domain.mail_host, fqdn_listname=mlist.fqdn_listname, ))
def decorate(mlist, uri, extradict=None): """Expand the decoration template from its URI.""" if uri is None: return "" # Get the decorator template. loader = getUtility(ITemplateLoader) template_uri = expand(uri, dict(listname=mlist.fqdn_listname, language=mlist.preferred_language.code)) template = loader.get(template_uri) return decorate_template(mlist, template, extradict)
def send_probe(member, msg): """Send a VERP probe to the member. :param member: The member to send the probe to. From this object, both the user and the mailing list can be determined. :type member: IMember :param msg: The bouncing message that caused the probe to be sent. :type msg: :return: The token representing this probe in the pendings database. :rtype: string """ mlist = getUtility(IListManager).get_by_list_id( member.mailing_list.list_id) template = getUtility(ITemplateLoader).get( 'list:user:notice:probe', mlist, language=member.preferred_language.code, # For backward compatibility. code=member.preferred_language.code, ) text = wrap(expand(template, mlist, dict( sender_email=member.subscriber.email, # For backward compatibility. address=member.address.email, email=member.address.email, owneraddr=mlist.owner_address, ))) message_id = msg['message-id'] if isinstance(message_id, bytes): message_id = message_id.decode('ascii') pendable = _ProbePendable( # We can only pend unicodes. member_id=member.member_id.hex, message_id=message_id, ) token = getUtility(IPendings).add(pendable) mailbox, domain_parts = split_email(mlist.bounces_address) probe_sender = Template(config.mta.verp_probe_format).safe_substitute( bounces=mailbox, token=token, domain=DOT.join(domain_parts), ) # Calculate the Subject header, in the member's preferred language. with _.using(member.preferred_language.code): subject = _('$mlist.display_name mailing list probe message') # Craft the probe message. This will be a multipart where the first part # is the probe text and the second part is the message that caused this # probe to be sent. probe = UserNotification(member.address.email, probe_sender, subject, lang=member.preferred_language) probe.set_type('multipart/mixed') notice = MIMEText(text, _charset=mlist.preferred_language.charset) probe.attach(notice) probe.attach(MIMEMessage(msg)) # Probes should not have the Precedence: bulk header. probe.send(mlist, envsender=probe_sender, verp=False, probe_token=token, add_precedence=False) return token
def initialize(): """Initialize the global switchboards for input/output.""" for conf in config.runner_configs: name = conf.name.split('.')[-1] assert name not in config.switchboards, ( 'Duplicate runner name: {0}'.format(name)) substitutions = config.paths substitutions['name'] = name path = expand(conf.path, substitutions) config.switchboards[name] = Switchboard(name, path)
def decorate(mlist, uri, extradict=None): """Expand the decoration template from its URI.""" if uri is None: return '' # Get the decorator template. loader = getUtility(ITemplateLoader) template_uri = expand(uri, dict( listname=mlist.fqdn_listname, language=mlist.preferred_language.code, )) template = loader.get(template_uri) return decorate_template(mlist, template, extradict)
def handle_ConfigurationUpdatedEvent(event): """Initialize the global switchboards for input/output.""" if not isinstance(event, ConfigurationUpdatedEvent): return config = event.config for conf in config.runner_configs: name = conf.name.split('.')[-1] assert name not in config.switchboards, ( 'Duplicate runner name: {0}'.format(name)) substitutions = config.paths substitutions['name'] = name path = expand(conf.path, substitutions) config.switchboards[name] = Switchboard(name, path)
def archive_message(self, mlist, msg): """See `IArchiver`.""" substitutions = config.__dict__.copy() substitutions["listname"] = mlist.fqdn_listname command = expand(self.command, substitutions) proc = Popen(command, stdin=PIPE, stdout=PIPE, stderr=PIPE, universal_newlines=True, shell=True) stdout, stderr = proc.communicate(msg.as_string()) if proc.returncode != 0: log.error("%s: mhonarc subprocess had non-zero exit code: %s" % (msg["message-id"], proc.returncode)) log.info(stdout) log.error(stderr) # Can we get more information, such as the url to the message just # archived, out of MHonArc? return None
def archive_message(mlist, msg): """See `IArchiver`.""" substitutions = config.__dict__.copy() substitutions['listname'] = mlist.fqdn_listname command = expand(config.archiver.mhonarc.command, substitutions) proc = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) stdout, stderr = proc.communicate(msg.as_string()) if proc.returncode != 0: log.error('%s: mhonarc subprocess had non-zero exit code: %s' % (msg['message-id'], proc.returncode)) log.info(stdout) log.error(stderr)
def send_welcome_message(mlist, member, language, text=''): """Send a welcome message to a subscriber. Prepending to the standard welcome message template is the mailing list's welcome message, if there is one. :param mlist: The mailing list. :type mlist: IMailingList :param member: The member to send the welcome message to. :param address: IMember :param language: The language of the response. :type language: ILanguage """ welcome_message = _get_message(mlist.welcome_message_uri, mlist, language) options_url = member.options_url # Try to find a non-empty display name. We first look at the directly # subscribed record, which will either be the address or the user. That's # handled automatically by going through member.subscriber. If that # doesn't give us something useful, try whatever user is linked to the # subscriber. if member.subscriber.display_name: display_name = member.subscriber.display_name # If an unlinked address is subscribed tehre will be no .user. elif member.user is not None and member.user.display_name: display_name = member.user.display_name else: display_name = '' # Get the text from the template. text = expand( welcome_message, dict( fqdn_listname=mlist.fqdn_listname, list_name=mlist.display_name, listinfo_uri=mlist.script_url('listinfo'), list_requests=mlist.request_address, user_name=display_name, user_address=member.address.email, user_options_uri=options_url, )) digmode = ( '' # noqa if member.delivery_mode is DeliveryMode.regular else _(' (Digest mode)')) msg = UserNotification( formataddr( (display_name, member.address.email)), mlist.request_address, _('Welcome to the "$mlist.display_name" mailing list${digmode}'), text, language) msg['X-No-Archive'] = 'yes' msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def run_migrations_offline(): """Run migrations in 'offline' mode. This configures the context with just a URL and not an Engine, though an Engine is acceptable here as well. By skipping the Engine creation we don't even need a DBAPI to be available. Calls to context.execute() here emit the given string to the script output. """ url = expand(config.database.url, config.paths) context.configure(url=url, target_metadata=Model.metadata) with context.begin_transaction(): context.run_migrations()
def check(self, mlist, msg, msgdata): """See `IRule`.""" if not as_boolean(config.mailman.hold_digest): return False # Convert the header value to a str because it may be an # email.header.Header instance. subject = str(msg.get('subject', '')).strip() if DIGRE.search(subject): msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( _('Message has a digest subject')) return True # Get the masthead, but without emails. mastheadtxt = getUtility(ITemplateLoader).get( 'list:member:digest:masthead', mlist) mastheadtxt = wrap( expand( mastheadtxt, mlist, dict( display_name=mlist.display_name, listname='', list_id=mlist.list_id, request_email='', owner_email='', ))) msgtext = '' for part in msg.walk(): if part.get_content_maintype() == 'text': cset = part.get_content_charset('utf-8') msgtext += part.get_payload(decode=True).decode( cset, errors='replace') matches = 0 lines = mastheadtxt.splitlines() for line in lines: line = line.strip() if not line: continue if msgtext.find(line) >= 0: matches += 1 if matches >= int(config.mailman.masthead_threshold): msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( _('Message quotes digest boilerplate')) return True return False
def _get_message(uri_template, mlist, language): if not uri_template: return '' try: uri = expand(uri_template, dict( listname=mlist.fqdn_listname, language=language.code, )) message = getUtility(ITemplateLoader).get(uri) except URLError: log.exception('Message URI not found ({0}): {1}'.format( mlist.fqdn_listname, uri_template)) return '' else: return wrap(message)
def handle_ConfigurationUpdatedEvent(event): """Initialize the global switchboards for input/output.""" if not isinstance(event, ConfigurationUpdatedEvent): return config = event.config for conf in config.runner_configs: name = conf.name.split(".")[-1] assert name not in config.switchboards, "Duplicate runner name: {0}".format(name) # Path is empty for non-queue runners. Check for path and create # switchboard instances only for queue runners. if conf.path: substitutions = config.paths substitutions["name"] = name path = expand(conf.path, substitutions) config.switchboards[name] = Switchboard(name, path)
def run_migrations_online(): """Run migrations in 'online' mode. In this scenario we need to create an Engine and associate a connection with the context. """ url = expand(config.database.url, config.paths) engine = create_engine(url) connection = engine.connect() with closing(connection): context.configure( connection=connection, target_metadata=Model.metadata) with context.begin_transaction(): context.run_migrations()
def set(self, store, name, context, uri, username=None, password=''): """See `ITemplateManager`.""" # Just record the fact that we have a template set. Make sure that if # there is an existing template with the same context and name, we # override any of its settings (and evict the cache). template = store.query(Template).filter( Template.name == name, Template.context == context).one_or_none() if template is None: template = Template(name, context, uri, username, password) store.add(template) else: template.reset(uri, username, password) # Now, evict the cache for the previous template. cache_mgr = getUtility(ICacheManager) actual_uri = expand(uri, None) cache_mgr.evict(actual_uri)
def archive_message(self, mlist, msg): """See `IArchiver`.""" substitutions = config.__dict__.copy() substitutions['listname'] = mlist.fqdn_listname command = expand(self.command, substitutions) proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, shell=True) stdout, stderr = proc.communicate(msg.as_string()) if proc.returncode != 0: log.error('%s: mhonarc subprocess had non-zero exit code: %s' % (msg['message-id'], proc.returncode)) log.info(stdout) log.error(stderr)
def handle_ConfigurationUpdatedEvent(event): """Initialize the global switchboards for input/output.""" if not isinstance(event, ConfigurationUpdatedEvent): return config = event.config for conf in config.runner_configs: name = conf.name.split('.')[-1] assert name not in config.switchboards, ( 'Duplicate runner name: {0}'.format(name)) # Path is empty for non-queue runners. Check for path and create # switchboard instances only for queue runners. if conf.path: substitutions = config.paths substitutions['name'] = name path = expand(conf.path, substitutions) config.switchboards[name] = Switchboard(name, path)
def send_welcome_message(mlist, member, language, text=''): """Send a welcome message to a subscriber. Prepending to the standard welcome message template is the mailing list's welcome message, if there is one. :param mlist: The mailing list. :type mlist: IMailingList :param member: The member to send the welcome message to. :param address: IMember :param language: The language of the response. :type language: ILanguage """ welcome_message = _get_message(mlist.welcome_message_uri, mlist, language) options_url = member.options_url # Try to find a non-empty display name. We first look at the directly # subscribed record, which will either be the address or the user. That's # handled automatically by going through member.subscriber. If that # doesn't give us something useful, try whatever user is linked to the # subscriber. if member.subscriber.display_name: display_name = member.subscriber.display_name # If an unlinked address is subscribed tehre will be no .user. elif member.user is not None and member.user.display_name: display_name = member.user.display_name else: display_name = '' # Get the text from the template. text = expand(welcome_message, dict( fqdn_listname=mlist.fqdn_listname, list_name=mlist.display_name, listinfo_uri=mlist.script_url('listinfo'), list_requests=mlist.request_address, user_name=display_name, user_address=member.address.email, user_options_uri=options_url, )) digmode = ('' # noqa if member.delivery_mode is DeliveryMode.regular else _(' (Digest mode)')) msg = UserNotification( formataddr((display_name, member.address.email)), mlist.request_address, _('Welcome to the "$mlist.display_name" mailing list${digmode}'), text, language) msg['X-No-Archive'] = 'yes' msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def send_admin_subscription_notice(mlist, address, display_name): """Send the list administrators a subscription notice. :param mlist: The mailing list. :type mlist: IMailingList :param address: The address being subscribed. :type address: string :param display_name: The name of the subscriber. :type display_name: string """ with _.using(mlist.preferred_language.code): subject = _('$mlist.display_name subscription notification') text = expand( getUtility(ITemplateLoader).get('list:admin:notice:subscribe', mlist), mlist, dict(member=formataddr((display_name, address)), )) msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators) msg.send(mlist)
def _get_message(uri_template, mlist, language): if not uri_template: return '' try: uri = expand( uri_template, dict( listname=mlist.fqdn_listname, language=language.code, )) message = getUtility(ITemplateLoader).get(uri) except URLError: log.exception('Message URI not found ({0}): {1}'.format( mlist.fqdn_listname, uri_template)) return '' else: return wrap(message)
def archive_message(self, mlist, msg): """See `IArchiver`.""" substitutions = config.__dict__.copy() substitutions['listname'] = mlist.fqdn_listname command = expand(self.command, mlist, substitutions) proc = Popen( command, stdin=PIPE, stdout=PIPE, stderr=PIPE, universal_newlines=True, shell=True) stdout, stderr = proc.communicate(msg.as_string()) if proc.returncode != 0: log.error('%s: mhonarc subprocess had non-zero exit code: %s' % (msg['message-id'], proc.returncode)) log.info(stdout) log.error(stderr) # Can we get more information, such as the url to the message just # archived, out of MHonArc? return None
def make(template_file, mlist=None, language=None, wrap=True, _trace=False, **kw): """Locate and 'make' a template file. The template file is located as with `find()`, and the resulting text is optionally wrapped and interpolated with the keyword argument dictionary. :param template_file: The name of the template file to search for. :type template_file: string :param mlist: Optional mailing list used as the context for searching for the template file. The list's preferred language will influence the search, as will the list's data directory. :type mlist: `IMailingList` :param language: Optional language code, which influences the search. :type language: string :param wrap: When True, wrap the text. :type wrap: bool :param _trace: Passed through to ``find()``, this enables printing of debugging information during template search. :type _trace: bool :param **kw: Keyword arguments for template interpolation. :return: The interpolated text. :rtype: string :raises TemplateNotFoundError: when the template could not be found. """ path, fp = find(template_file, mlist, language, _trace) try: # XXX Removing the trailing newline is a hack carried over from # Mailman 2. The (stripped) template text is then passed through the # translation catalog. This ensures that the translated text is # unicode, and also allows for volunteers to translate the templates # into the language catalogs. template = _(fp.read()[:-1]) finally: fp.close() assert isinstance(template, str), 'Translated template is not a string' text = expand(template, kw) if wrap: return wrap_text(text) return text
def test_substitutions(self): test_text = ('UNIT TESTING %(real_name)s mailing list\n' '%(real_name)s@%(host_name)s\n' '%(web_page_url)slistinfo%(cgiext)s/%(_internal_name)s') expected_text = ('UNIT TESTING $display_name mailing list\n' '$fqdn_listname\n' '$listinfo_uri') for oldvar, newvar in self._conf_mapping.items(): self._pckdict[str(oldvar)] = str(test_text) import_config_pck(self._mlist, self._pckdict) newattr = getattr(self._mlist, newvar) template_uri = expand(newattr, dict( listname=self._mlist.fqdn_listname, language=self._mlist.preferred_language.code, )) loader = getUtility(ITemplateLoader) text = loader.get(template_uri) self.assertEqual(text, expected_text, 'Old variables were not converted for %s' % newvar)
def send_admin_disable_notice(mlist, address, display_name): """Send the list administrators a membership disabled by-bounce notice. :param mlist: The mailing list :type mlist: IMailingList :param address: The address of the member :type address: string :param display_name: The name of the subscriber :type display_name: string """ member = formataddr((display_name, address)) data = {'member': member} with _.using(mlist.preferred_language.code): subject = _('$member\'s subscription disabled on $mlist.display_name') text = expand( getUtility(ITemplateLoader).get('list:admin:notice:disable', mlist), mlist, data) msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators) msg.send(mlist)
def send_welcome_message(mlist, member, language, text=''): """Send a welcome message to a subscriber. Prepending to the standard welcome message template is the mailing list's welcome message, if there is one. :param mlist: The mailing list. :type mlist: IMailingList :param member: The member to send the welcome message to. :param address: IMember :param language: The language of the response. :type language: ILanguage """ welcome_message = wrap( getUtility(ITemplateLoader).get('list:user:notice:welcome', mlist, language=language.code)) display_name = member.display_name # Get the text from the template. text = expand( welcome_message, mlist, dict( user_name=display_name, user_email=member.address.email, # For backward compatibility. user_address=member.address.email, fqdn_listname=mlist.fqdn_listname, list_name=mlist.display_name, list_requests=mlist.request_address, )) digmode = ( '' # noqa: F841 if member.delivery_mode is DeliveryMode.regular else _(' (Digest mode)')) msg = UserNotification( formataddr( (display_name, member.address.email)), mlist.request_address, _('Welcome to the "$mlist.display_name" mailing list${digmode}'), text, language) msg['X-No-Archive'] = 'yes' msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def send_admin_removal_notice(mlist, address, display_name): """Send the list administrators a membership removed due to bounce notice. :param mlist: The mailing list. :type mlist: IMailingList :param address: The address of the member :type address: string :param display_name: The name of the subscriber :type display_name: string """ member = formataddr((display_name, address)) data = {'member': member, 'mlist': mlist.display_name} with _.using(mlist.preferred_language.code): subject = _('$member unsubscribed from ${mlist.display_name} ' 'mailing list due to bounces') text = expand( getUtility(ITemplateLoader).get('list:admin:notice:removal', mlist), mlist, data) msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators) msg.send(mlist)
def decorate_template(mlist, template, extradict=None): """Expand the decoration template.""" # Create a dictionary which includes the default set of interpolation # variables allowed in headers and footers. These will be augmented by # any key/value pairs in the extradict. substitutions = dict( fqdn_listname = mlist.fqdn_listname, list_name = mlist.list_name, host_name = mlist.mail_host, display_name = mlist.display_name, listinfo_uri = mlist.script_url('listinfo'), list_requests = mlist.request_address, description = mlist.description, info = mlist.info, ) if extradict is not None: substitutions.update(extradict) text = expand(template, substitutions) # Turn any \r\n line endings into just \n return re.sub(r' *\r?\n', r'\n', text)
def delete_member(mlist, email, admin_notif=None, userack=None): """Delete a member right now. :param mlist: The mailing list to remove the member from. :type mlist: `IMailingList` :param email: The email address to unsubscribe. :type email: string :param admin_notif: Whether the list administrator should be notified that this member was deleted. :type admin_notif: bool, or None to let the mailing list's `admin_notify_mchange` attribute decide. :raises NotAMemberError: if the address is not a member of the mailing list. """ if userack is None: userack = mlist.send_goodbye_message if admin_notif is None: admin_notif = mlist.admin_notify_mchanges # Delete a member, for which we know the approval has been made. member = mlist.members.get_member(email) if member is None: raise NotAMemberError(mlist, email) language = member.preferred_language member.unsubscribe() # And send an acknowledgement to the user... if userack: send_goodbye_message(mlist, email, language) # ...and to the administrator. if admin_notif: user = getUtility(IUserManager).get_user(email) display_name = user.display_name subject = _('$mlist.display_name unsubscription notification') text = expand( getUtility(ITemplateLoader).get('list:admin:notice:unsubscribe', mlist), mlist, dict(member=formataddr((display_name, email)), )) msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators) msg.send(mlist)
def decorate_template(mlist, template, extradict=None): """Expand the decoration template.""" # Create a dictionary which includes the default set of interpolation # variables allowed in headers and footers. These will be augmented by # any key/value pairs in the extradict. substitutions = { key: getattr(mlist, key) for key in ('fqdn_listname', 'list_name', 'mail_host', 'display_name', 'request_address', 'description', 'info', ) } if extradict is not None: substitutions.update(extradict) text = expand(template, mlist, substitutions) # Turn any \r\n line endings into just \n return re.sub(r' *\r?\n', r'\n', text)
def process(self, mlist, msg, msgdata): """See `IHandler`.""" # Extract the sender's address and find them in the user database sender = msgdata.get('original_sender', msg.sender) member = mlist.members.get_member(sender) if member is None or not member.acknowledge_posts: # Either the sender is not a member, in which case we can't know # whether they want an acknowlegment or not, or they are a member # who definitely does not want an acknowlegment. return # Okay, they are a member that wants an acknowledgment of their post. # Give them their original subject. BAW: do we want to use the # decoded header? original_subject = msgdata.get('origsubj', msg.get('subject', _('(no subject)'))) # Get the user's preferred language. language_manager = getUtility(ILanguageManager) language = (language_manager[msgdata['lang']] if 'lang' in msgdata else member.preferred_language) # Now get the acknowledgement template. display_name = mlist.display_name # noqa: F841 template = getUtility(ITemplateLoader).get('list:user:notice:post', mlist, language=language.code) text = expand( template, mlist, dict( subject=oneline(original_subject, in_unicode=True), # For backward compatibility. list_name=mlist.list_name, )) # Craft the outgoing message, with all headers and attributes # necessary for general delivery. Then enqueue it to the outgoing # queue. subject = _('$display_name post acknowledgment') usermsg = UserNotification(sender, mlist.bounces_address, subject, text, language) usermsg.send(mlist)
def test_substitutions(self): test_text = ('UNIT TESTING %(real_name)s mailing list\n' '%(real_name)s@%(host_name)s\n' '%(web_page_url)slistinfo%(cgiext)s/%(_internal_name)s') expected_text = ('UNIT TESTING $display_name mailing list\n' '$fqdn_listname\n' '$listinfo_uri') for oldvar, newvar in self._conf_mapping.items(): self._pckdict[str(oldvar)] = str(test_text) import_config_pck(self._mlist, self._pckdict) newattr = getattr(self._mlist, newvar) template_uri = expand( newattr, dict( listname=self._mlist.fqdn_listname, language=self._mlist.preferred_language.code, )) loader = getUtility(ITemplateLoader) text = loader.get(template_uri) self.assertEqual( text, expected_text, 'Old variables were not converted for %s' % newvar)
def __init__(self, name, slice=None): """Create a runner. :param slice: The slice number for this runner. This is passed directly to the underlying `ISwitchboard` object. This is ignored for runners that don't manage a queue. :type slice: int or None """ # Grab the configuration section. self.name = name section = getattr(config, "runner." + name) substitutions = config.paths substitutions["name"] = name self.queue_directory = expand(section.path, substitutions) numslices = int(section.instances) self.switchboard = Switchboard(name, self.queue_directory, slice, numslices, True) self.sleep_time = as_timedelta(section.sleep_time) # sleep_time is a timedelta; turn it into a float for time.sleep(). self.sleep_float = 86400 * self.sleep_time.days + self.sleep_time.seconds + self.sleep_time.microseconds / 1.0e6 self.max_restarts = int(section.max_restarts) self.start = as_boolean(section.start) self._stop = False
def decorate_template(mlist, template, extradict=None): """Expand the decoration template.""" # Create a dictionary which includes the default set of interpolation # variables allowed in headers and footers. These will be augmented by # any key/value pairs in the extradict. substitutions = { key: getattr(mlist, key) for key in ('fqdn_listname', 'list_name', 'mail_host', 'display_name', 'request_address', 'description', 'info', ) } # This must eventually go away. substitutions['listinfo_uri'] = mlist.script_url('listinfo') if extradict is not None: substitutions.update(extradict) text = expand(template, substitutions) # Turn any \r\n line endings into just \n return re.sub(r' *\r?\n', r'\n', text)
def deliver(mlist, msg, msgdata): """Deliver a message to the outgoing mail server.""" # If there are no recipients, there's nothing to do. recipients = msgdata.get('recipients') if not recipients: # Could be None, could be an empty sequence. return # Which delivery agent should we use? Several situations can cause us to # use individual delivery. If not specified, use bulk delivery. See the # to-outgoing handler for when the 'verp' key is set in the metadata. if msgdata.get('verp', False): agent = Deliver() elif mlist.personalize != Personalization.none: agent = Deliver() else: agent = BulkDelivery(int(config.mta.max_recipients)) log.debug('Using agent: %s', agent) # Keep track of the original recipients and the original sender for # logging purposes. original_recipients = msgdata['recipients'] original_sender = msgdata.get('original-sender', msg.sender) # Let the agent attempt to deliver to the recipients. Record all failures # for re-delivery later. t0 = time.time() refused = agent.deliver(mlist, msg, msgdata) t1 = time.time() # Log this posting. size = getattr(msg, 'original_size', msgdata.get('original_size')) if size is None: size = len(msg.as_string()) substitutions = dict( msgid = msg.get('message-id', 'n/a'), listname = mlist.fqdn_listname, sender = original_sender, recip = len(original_recipients), size = size, time = t1 - t0, refused = len(refused), smtpcode = 'n/a', smtpmsg = 'n/a', ) template = config.logging.smtp.every if template.lower() != 'no': log.info('%s', expand(template, substitutions)) if refused: template = config.logging.smtp.refused if template.lower() != 'no': log.info('%s', expand(template, substitutions)) else: # Log the successful post, but if it was not destined to the mailing # list (e.g. to the owner or admin), print the actual recipients # instead of just the number. if not msgdata.get('tolist', False): recips = msg.get_all('to', []) recips.extend(msg.get_all('cc', [])) substitutions['recips'] = COMMA.join(recips) template = config.logging.smtp.success if template.lower() != 'no': log.info('%s', expand(template, substitutions)) # Process any failed deliveries. temporary_failures = [] permanent_failures = [] for recipient, (code, smtp_message) in refused.items(): # RFC 5321, $4.5.3.1.10 says: # # RFC 821 [1] incorrectly listed the error where an SMTP server # exhausts its implementation limit on the number of RCPT commands # ("too many recipients") as having reply code 552. The correct # reply code for this condition is 452. Clients SHOULD treat a 552 # code in this case as a temporary, rather than permanent, failure # so the logic below works. # if code >= 500 and code != 552: # A permanent failure permanent_failures.append(recipient) else: # Deal with persistent transient failures by queuing them up for # future delivery. TBD: this could generate lots of log entries! temporary_failures.append(recipient) template = config.logging.smtp.failure if template.lower() != 'no': substitutions.update( recip = recipient, smtpcode = code, smtpmsg = smtp_message, ) log.info('%s', expand(template, substitutions)) # Return the results if temporary_failures or permanent_failures: raise SomeRecipientsFailed(temporary_failures, permanent_failures)
def confirm_address(self, cookie): """See `IMailingList`.""" local_part = expand(config.mta.verp_confirm_format, dict( address = '{0}-confirm'.format(self.list_name), cookie = cookie)) return '{0}@{1}'.format(local_part, self.mail_host)
def setUp(cls): # Set up the basic configuration stuff. Turn off path creation until # we've pushed the testing config. config.create_paths = False initialize.initialize_1(INHIBIT_CONFIG_FILE) assert cls.var_dir is None, 'Layer already set up' # Calculate a temporary VAR_DIR directory so that run-time artifacts # of the tests won't tread on the installation's data. This also # makes it easier to clean up after the tests are done, and insures # isolation of test suite runs. cls.var_dir = tempfile.mkdtemp() # We need a test configuration both for the foreground process and any # child processes that get spawned. lazr.config would allow us to do # it all in a string that gets pushed, and we'll do that for the # foreground, but because we may be spawning processes (such as # runners) we'll need a file that we can specify to the with the -C # option. Craft the full test configuration string here, push it, and # also write it out to a temp file for -C. # # Create a dummy postfix.cfg file so that the test suite doesn't try # to run the actual postmap command, which may not exist anyway. postfix_cfg = os.path.join(cls.var_dir, 'postfix.cfg') with open(postfix_cfg, 'w') as fp: print(dedent(""" [postfix] postmap_command: true """), file=fp) test_config = dedent(""" [mailman] layout: testing [paths.testing] var_dir: {0} [devmode] testing: yes [mta] configuration: {1} """.format(cls.var_dir, postfix_cfg)) # Read the testing config and push it. more = resource_bytes('mailman.testing', 'testing.cfg') test_config += more.decode('utf-8') config.create_paths = True config.push('test config', test_config) # Initialize everything else. initialize.initialize_2(testing=True) initialize.initialize_3() # When stderr debugging is enabled, subprocess root loggers should # also be more verbose. if cls.stderr: test_config += dedent(""" [logging.root] level: debug """) # Enable log message propagation and reset the log paths so that the # doctests can check the output. for logger_config in config.logger_configs: sub_name = logger_config.name.split('.')[-1] if sub_name == 'root': continue logger_name = 'mailman.' + sub_name log = logging.getLogger(logger_name) log.propagate = cls.stderr # Reopen the file to a new path that tests can get at. Instead of # using the configuration file path though, use a path that's # specific to the logger so that tests can find expected output # more easily. path = os.path.join(config.LOG_DIR, sub_name) get_handler(sub_name).reopen(path) log.setLevel(logging.DEBUG) # If stderr debugging is enabled, make sure subprocesses are also # more verbose. if cls.stderr: test_config += expand(dedent(""" [logging.$name] propagate: yes level: debug """), dict(name=sub_name, path=path)) # The root logger will already have a handler, but it's not the right # handler. Remove that and set our own. if cls.stderr: console = logging.StreamHandler(sys.stderr) formatter = logging.Formatter(config.logging.root.format, config.logging.root.datefmt) console.setFormatter(formatter) root = logging.getLogger() del root.handlers[:] root.addHandler(console) # Write the configuration file for subprocesses and set up the config # object to pass that properly on the -C option. config_file = os.path.join(cls.var_dir, 'test.cfg') with open(config_file, 'w') as fp: fp.write(test_config) print(file=fp) config.filename = config_file
def process(self, mlist, msg, msgdata): """See `IHandler`.""" # There are several cases where the replybot is short-circuited: # * the original message has an "X-Ack: No" header # * the message has a Precedence header with values bulk, junk, or # list, and there's no explicit "X-Ack: yes" header # * the message metadata has a true 'noack' key ack = msg.get('x-ack', '').lower() if ack == 'no' or msgdata.get('noack'): return precedence = msg.get('precedence', '').lower() if ack != 'yes' and precedence in ('bulk', 'junk', 'list'): return # Check to see if the list is even configured to autorespond to this # email message. Note: the incoming message processors should set the # destination key in the message data. if msgdata.get('to_owner'): if mlist.autorespond_owner is ResponseAction.none: return response_type = Response.owner response_text = mlist.autoresponse_owner_text elif msgdata.get('to_request'): if mlist.autorespond_requests is ResponseAction.none: return response_type = Response.command response_text = mlist.autoresponse_request_text elif msgdata.get('to_list'): if mlist.autorespond_postings is ResponseAction.none: return response_type = Response.postings response_text = mlist.autoresponse_postings_text else: # There are no automatic responses for any other destination. return # Now see if we're in the grace period for this sender. grace_period # = 0 means always automatically respond, as does an "X-Ack: yes" # header (useful for debugging). response_set = IAutoResponseSet(mlist) user_manager = getUtility(IUserManager) address = user_manager.get_address(msg.sender) if address is None: address = user_manager.create_address(msg.sender) grace_period = mlist.autoresponse_grace_period if grace_period > ALWAYS_REPLY and ack != 'yes': last = response_set.last_response(address, response_type) if last is not None and last.date_sent + grace_period > today(): return # Okay, we know we're going to respond to this sender, craft the # message, send it, and update the database. display_name = mlist.display_name subject = _( 'Auto-response for your message to the "$display_name" ' 'mailing list') # Do string interpolation into the autoresponse text d = dict(list_name = mlist.list_name, display_name = display_name, listurl = mlist.script_url('listinfo'), requestemail = mlist.request_address, owneremail = mlist.owner_address, ) # Interpolation and Wrap the response text. text = wrap(expand(response_text, d)) outmsg = UserNotification(msg.sender, mlist.bounces_address, subject, text, mlist.preferred_language) outmsg['X-Mailer'] = _('The Mailman Replybot') # prevent recursions and mail loops! outmsg['X-Ack'] = 'No' outmsg.send(mlist) response_set.response_sent(address, response_type)
def setUp(cls): # Set up the basic configuration stuff. Turn off path creation until # we've pushed the testing config. config.create_paths = False initialize.initialize_1(INHIBIT_CONFIG_FILE) assert cls.var_dir is None, "Layer already set up" # Calculate a temporary VAR_DIR directory so that run-time artifacts # of the tests won't tread on the installation's data. This also # makes it easier to clean up after the tests are done, and insures # isolation of test suite runs. cls.var_dir = tempfile.mkdtemp() # We need a test configuration both for the foreground process and any # child processes that get spawned. lazr.config would allow us to do # it all in a string that gets pushed, and we'll do that for the # foreground, but because we may be spawning processes (such as # runners) we'll need a file that we can specify to the with the -C # option. Craft the full test configuration string here, push it, and # also write it out to a temp file for -C. test_config = dedent( """ [mailman] layout: testing [passwords] password_scheme: cleartext [paths.testing] var_dir: %s [devmode] testing: yes """ % cls.var_dir ) # Read the testing config and push it. test_config += resource_string("mailman.testing", "testing.cfg") config.create_paths = True config.push("test config", test_config) # Initialize everything else. initialize.initialize_2() initialize.initialize_3() # When stderr debugging is enabled, subprocess root loggers should # also be more verbose. if cls.stderr: test_config += dedent( """ [logging.root] propagate: yes level: debug """ ) # Enable log message propagation and reset the log paths so that the # doctests can check the output. for logger_config in config.logger_configs: sub_name = logger_config.name.split(".")[-1] if sub_name == "root": continue logger_name = "mailman." + sub_name log = logging.getLogger(logger_name) log.propagate = True # Reopen the file to a new path that tests can get at. Instead of # using the configuration file path though, use a path that's # specific to the logger so that tests can find expected output # more easily. path = os.path.join(config.LOG_DIR, sub_name) get_handler(sub_name).reopen(path) log.setLevel(logging.DEBUG) # If stderr debugging is enabled, make sure subprocesses are also # more verbose. if cls.stderr: test_config += expand( dedent( """ [logging.$name] propagate: yes level: debug """ ), dict(name=sub_name, path=path), ) # zope.testing sets up logging before we get to our own initialization # function. This messes with the root logger, so explicitly set it to # go to stderr. if cls.stderr: console = logging.StreamHandler(sys.stderr) formatter = logging.Formatter(config.logging.root.format, config.logging.root.datefmt) console.setFormatter(formatter) logging.getLogger().addHandler(console) # Write the configuration file for subprocesses and set up the config # object to pass that properly on the -C option. config_file = os.path.join(cls.var_dir, "test.cfg") with open(config_file, "w") as fp: fp.write(test_config) print(file=fp) config.filename = config_file
__all__ = [ 'run_migrations_offline', 'run_migrations_online', ] from alembic import context from contextlib import closing from mailman.core.initialize import initialize_1 from mailman.config import config from mailman.database.model import Model from mailman.utilities.string import expand from sqlalchemy import create_engine try: url = expand(config.database.url, config.paths) except AttributeError: # Initialize config object for external alembic calls initialize_1() url = expand(config.database.url, config.paths) def run_migrations_offline(): """Run migrations in 'offline' mode. This configures the context with just a URL and not an Engine, though an Engine is acceptable here as well. By skipping the Engine creation we don't even need a DBAPI to be available. Calls to context.execute() here emit the given string to the script output.