def autorespond_to_sender(mlist, sender, language=None): """Should Mailman automatically respond to this sender? :param mlist: The mailing list. :type mlist: `IMailingList`. :param sender: The sender's email address. :type sender: string :param language: Optional language. :type language: `ILanguage` or None :return: True if an automatic response should be sent, otherwise False. If an automatic response is not sent, a message is sent indicating that, er no more will be sent today. :rtype: bool """ if language is None: language = mlist.preferred_language max_autoresponses_per_day = int(config.mta.max_autoresponses_per_day) if max_autoresponses_per_day == 0: # Unlimited. return True # Get an IAddress from an email address. user_manager = getUtility(IUserManager) address = user_manager.get_address(sender) if address is None: address = user_manager.create_address(sender) response_set = IAutoResponseSet(mlist) todays_count = response_set.todays_count(address, Response.hold) if todays_count < max_autoresponses_per_day: # This person has not reached their automatic response limit, so it's # okay to send a response. response_set.response_sent(address, Response.hold) return True elif todays_count == max_autoresponses_per_day: # The last one we sent was the last one we should send today. Instead # of sending an automatic response, send them the "no more today" # message. log.info('hold autoresponse limit hit: %s', sender) response_set.response_sent(address, Response.hold) # Send this notification message instead. text = make( 'nomoretoday.txt', language=language.code, sender=sender, listname=mlist.fqdn_listname, count=todays_count, owneremail=mlist.owner_address, ) with _.using(language.code): msg = UserNotification( sender, mlist.owner_address, _('Last autoresponse notification for today'), text, lang=language) msg.send(mlist) return False else: # We've sent them everything we're going to send them today. log.info('Automatic response limit discard: %s', sender) return False
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 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 if lang is None: member = mlist.members.get_member(recip) lang = (mlist.preferred_language if member is None else member.preferred_language) text = make('refuse.txt', mailing_list=mlist, language=lang.code, listname=mlist.fqdn_listname, request=request, reason=comment, 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 ascii_header(mlist, msgdata, subject, prefix, prefix_pattern, ws): if mlist.preferred_language.charset not in ASCII_CHARSETS: return None for chunk, charset in decode_header(subject.encode()): if charset not in ASCII_CHARSETS: return None subject_text = EMPTYSTRING.join(str(subject).splitlines()) rematch = re.match(RE_PATTERN, subject_text, re.I) if rematch: subject_text = subject_text[rematch.end():] recolon = 'Re: ' else: recolon = '' # At this point, the subject may become null if someone posted mail # with "Subject: [subject prefix]". if subject_text.strip() == '': with _.using(mlist.preferred_language.code): subject_text = _('(no subject)') else: subject_text = re.sub(prefix_pattern, '', subject_text) msgdata['stripped_subject'] = subject_text lines = subject_text.splitlines() first_line = [lines[0]] if recolon: first_line.insert(0, recolon) if prefix: first_line.insert(0, prefix) subject_text = EMPTYSTRING.join(first_line) return Header(subject_text, continuation_ws=ws)
def ascii_header(mlist, msgdata, subject, prefix, prefix_pattern, ws): if mlist.preferred_language.charset not in ASCII_CHARSETS: return None for chunk, charset in decode_header(subject.encode()): if charset not in ASCII_CHARSETS: return None subject_text = EMPTYSTRING.join(str(subject).splitlines()) # At this point, the subject may become null if someone posted mail # with "Subject: [subject prefix]". if subject_text.strip() == '': with _.using(mlist.preferred_language.code): subject_text = _('(no subject)') else: subject_text = re.sub(prefix_pattern, '', subject_text) msgdata['stripped_subject'] = subject_text rematch = re.match(RE_PATTERN, subject_text, re.I) if rematch: subject_text = subject_text[rematch.end():] recolon = 'Re: ' else: recolon = '' lines = subject_text.splitlines() # If the subject was only the prefix or Re:, the text could be null. first_line = [] if lines: first_line = [lines[0]] if recolon: first_line.insert(0, recolon) if prefix: first_line.insert(0, prefix) subject_text = EMPTYSTRING.join(first_line) return Header(subject_text, continuation_ws=ws)
def _refuse(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 if lang is None: member = mlist.members.get_member(recip) lang = (mlist.preferred_language if member is None else member.preferred_language) text = make('refuse.txt', mailing_list=mlist, language=lang.code, listname=mlist.fqdn_listname, request=request, reason=comment, 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 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 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) text = make( 'probe.txt', mlist, member.preferred_language.code, listname=mlist.fqdn_listname, address=member.address.email, optionsurl=member.options_url, 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 _process_one_file(self, msg, msgdata): """See `IRunner`.""" # Do some common sanity checking on the message metadata. It's got to # be destined for a particular mailing list. This switchboard is used # to shunt off badly formatted messages. We don't want to just trash # them because they may be fixable with human intervention. Just get # them out of our sight. # # Find out which mailing list this message is destined for. mlist = None missing = object() # First try to dig out the target list by id. If there's no list-id # in the metadata, fall back to the fqdn list name for backward # compatibility. list_manager = getUtility(IListManager) list_id = msgdata.get('listid', missing) fqdn_listname = None if list_id is missing: fqdn_listname = msgdata.get('listname', missing) # XXX Deprecate. if fqdn_listname is not missing: mlist = list_manager.get(fqdn_listname) else: mlist = list_manager.get_by_list_id(list_id) if mlist is None: identifier = (list_id if list_id is not None else fqdn_listname) elog.error( '%s runner "%s" shunting message for missing list: %s', msg['message-id'], self.name, identifier) config.switchboards['shunt'].enqueue(msg, msgdata) return # Now process this message. We also want to set up the language # context for this message. The context will be the preferred # language for the user if the sender is a member of the list, or it # will be the list's preferred language. However, we must take # special care to reset the defaults, otherwise subsequent messages # may be translated incorrectly. if mlist is None: language_manager = getUtility(ILanguageManager) language = language_manager[config.mailman.default_language] elif msg.sender: member = mlist.members.get_member(msg.sender) language = (member.preferred_language if member is not None else mlist.preferred_language) else: language = mlist.preferred_language with _.using(language.code): msgdata['lang'] = language.code try: keepqueued = self._dispose(mlist, msg, msgdata) except Exception as error: # Trigger the Zope event and re-raise notify(RunnerCrashEvent(self, mlist, msg, msgdata, error)) raise if keepqueued: self.switchboard.enqueue(msg, msgdata)
def autorespond_to_sender(mlist, sender, language=None): """Should Mailman automatically respond to this sender? :param mlist: The mailing list. :type mlist: `IMailingList`. :param sender: The sender's email address. :type sender: string :param language: Optional language. :type language: `ILanguage` or None :return: True if an automatic response should be sent, otherwise False. If an automatic response is not sent, a message is sent indicating that, er no more will be sent today. :rtype: bool """ if language is None: language = mlist.preferred_language max_autoresponses_per_day = int(config.mta.max_autoresponses_per_day) if max_autoresponses_per_day == 0: # Unlimited. return True # Get an IAddress from an email address. user_manager = getUtility(IUserManager) address = user_manager.get_address(sender) if address is None: address = user_manager.create_address(sender) response_set = IAutoResponseSet(mlist) todays_count = response_set.todays_count(address, Response.hold) if todays_count < max_autoresponses_per_day: # This person has not reached their automatic response limit, so it's # okay to send a response. response_set.response_sent(address, Response.hold) return True elif todays_count == max_autoresponses_per_day: # The last one we sent was the last one we should send today. Instead # of sending an automatic response, send them the "no more today" # message. log.info('hold autoresponse limit hit: %s', sender) response_set.response_sent(address, Response.hold) # Send this notification message instead. text = make('nomoretoday.txt', language=language.code, sender=sender, listname=mlist.fqdn_listname, count=todays_count, owneremail=mlist.owner_address, ) with _.using(language.code): msg = UserNotification( sender, mlist.owner_address, _('Last autoresponse notification for today'), text, lang=language) msg.send(mlist) return False else: # We've sent them everything we're going to send them today. log.info('Automatic response limit discard: %s', sender) return False
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) text = make('probe.txt', mlist, member.preferred_language.code, listname=mlist.fqdn_listname, address=member.address.email, optionsurl=member.options_url, 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 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 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_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 = make('adminsubscribeack.txt', mailing_list=mlist, listname=mlist.display_name, member=formataddr((display_name, address)), ) msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators) msg.send(mlist)
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 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 = make( 'adminsubscribeack.txt', mailing_list=mlist, listname=mlist.display_name, member=formataddr((display_name, address)), ) msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators) msg.send(mlist)
def _process_one_file(self, msg, msgdata): """See `IRunner`.""" # Do some common sanity checking on the message metadata. It's got to # be destined for a particular mailing list. This switchboard is used # to shunt off badly formatted messages. We don't want to just trash # them because they may be fixable with human intervention. Just get # them out of our sight. # # Find out which mailing list this message is destined for. missing = object() listname = msgdata.get("listname", missing) mlist = None if listname is missing else getUtility(IListManager).get(unicode(listname)) if mlist is None: elog.error( '%s runner "%s" shunting message for missing list: %s', msg["message-id"], self.name, ("n/a" if listname is missing else listname), ) config.switchboards["shunt"].enqueue(msg, msgdata) return # Now process this message. We also want to set up the language # context for this message. The context will be the preferred # language for the user if the sender is a member of the list, or it # will be the list's preferred language. However, we must take # special care to reset the defaults, otherwise subsequent messages # may be translated incorrectly. if mlist is None: language_manager = getUtility(ILanguageManager) language = language_manager[config.mailman.default_language] elif msg.sender: member = mlist.members.get_member(msg.sender) language = member.preferred_language if member is not None else mlist.preferred_language else: language = mlist.preferred_language with _.using(language.code): msgdata["lang"] = language.code keepqueued = self._dispose(mlist, msg, msgdata) if keepqueued: self.switchboard.enqueue(msg, msgdata)
def _dispose(self, mlist, msg, msgdata): """See `IRunner`.""" volume = msgdata['volume'] digest_number = msgdata['digest_number'] # Backslashes make me cry. code = mlist.preferred_language.code with Mailbox(msgdata['digest_path']) as mailbox, _.using(code): # Create the digesters. mime_digest = MIMEDigester(mlist, volume, digest_number) rfc1153_digest = RFC1153Digester(mlist, volume, digest_number) # Cruise through all the messages in the mailbox, first building # the table of contents and accumulating Subject: headers and # authors. The question really is whether it's better from a1 # performance and memory footprint to go through the mailbox once # and cache the messages in a list, or to cruise through the # mailbox twice. We'll do the latter, but it's a complete guess. count = None for count, (key, message) in enumerate(mailbox.iteritems(), 1): mime_digest.add_to_toc(message, count) rfc1153_digest.add_to_toc(message, count) assert count is not None, 'No digest messages?' # Add the table of contents. mime_digest.add_toc(count) rfc1153_digest.add_toc(count) # Cruise through the set of messages a second time, adding them to # the actual digest. for count, (key, message) in enumerate(mailbox.iteritems(), 1): mime_digest.add_message(message, count) rfc1153_digest.add_message(message, count) # Finish up the digests. mime = mime_digest.finish() rfc1153 = rfc1153_digest.finish() # Calculate the recipients lists mime_recipients = set() rfc1153_recipients = set() # When someone turns off digest delivery, they will get one last # digest to ensure that there will be no gaps in the messages they # receive. digest_members = set(mlist.digest_members.members) for member in digest_members: if member.delivery_status is not DeliveryStatus.enabled: continue # Send the digest to the case-preserved address of the digest # members. email_address = member.address.original_email if member.delivery_mode == DeliveryMode.plaintext_digests: rfc1153_recipients.add(email_address) # We currently treat summary_digests the same as mime_digests. elif member.delivery_mode in (DeliveryMode.mime_digests, DeliveryMode.summary_digests): mime_recipients.add(email_address) else: raise AssertionError( 'Digest member "{}" unexpected delivery mode: {}'.format( email_address, member.delivery_mode)) # Add also the folks who are receiving one last digest. for address, delivery_mode in mlist.last_digest_recipients: if delivery_mode == DeliveryMode.plaintext_digests: rfc1153_recipients.add(address.original_email) # We currently treat summary_digests the same as mime_digests. elif delivery_mode in (DeliveryMode.mime_digests, DeliveryMode.summary_digests): mime_recipients.add(address.original_email) else: raise AssertionError( 'OLD recipient "{}" unexpected delivery mode: {}'.format( address, delivery_mode)) # Send the digests to the virgin queue for final delivery. queue = config.switchboards['virgin'] if len(mime_recipients) > 0: queue.enqueue(mime, recipients=mime_recipients, listid=mlist.list_id, isdigest=True) if len(rfc1153_recipients) > 0: queue.enqueue(rfc1153, recipients=rfc1153_recipients, listid=mlist.list_id, isdigest=True) # Remove the digest mbox. (GL #259) os.remove(msgdata['digest_path'])
def handle_message(mlist, id, action, comment=None, preserve=False, forward=None): message_store = getUtility(IMessageStore) requestdb = IListRequests(mlist) key, msgdata = requestdb.get_request(id) # Handle the action. rejection = None message_id = msgdata['_mod_message_id'] sender = msgdata['_mod_sender'] subject = msgdata['_mod_subject'] if action in (Action.defer, Action.hold): # Nothing to do, but preserve the message for later. preserve = True elif action is Action.discard: rejection = 'Discarded' elif action is Action.reject: rejection = 'Refused' member = mlist.members.get_member(sender) if member: language = member.preferred_language else: language = None _refuse(mlist, _('Posting of your message titled "$subject"'), sender, comment or _('[No reason given]'), language) elif action is Action.accept: # Start by getting the message from the message store. msg = message_store.get_message_by_id(message_id) # Delete moderation-specific entries from the message metadata. for key in msgdata.keys(): if key.startswith('_mod_'): del msgdata[key] # Add some metadata to indicate this message has now been approved. msgdata['approved'] = True msgdata['moderator_approved'] = True # Calculate a new filebase for the approved message, otherwise # delivery errors will cause duplicates. if 'filebase' in msgdata: del msgdata['filebase'] # Queue the file for delivery. Trying to deliver the message directly # here can lead to a huge delay in web turnaround. Log the moderation # and add a header. msg['X-Mailman-Approved-At'] = formatdate( time.mktime(now().timetuple()), localtime=True) vlog.info('held message approved, message-id: %s', msg.get('message-id', 'n/a')) # Stick the message back in the incoming queue for further # processing. config.switchboards['pipeline'].enqueue(msg, _metadata=msgdata) else: raise AssertionError('Unexpected action: {0}'.format(action)) # Forward the message. if forward: # Get a copy of the original message from the message store. msg = message_store.get_message_by_id(message_id) # It's possible the forwarding address list is a comma separated list # of display_name/address pairs. addresses = [addr[1] for addr in getaddresses(forward)] language = mlist.preferred_language if len(addresses) == 1: # If the address getting the forwarded message is a member of # the list, we want the headers of the outer message to be # encoded in their language. Otherwise it'll be the preferred # language of the mailing list. This is better than sending a # separate message per recipient. member = mlist.members.get_member(addresses[0]) if member: language = member.preferred_language with _.using(language.code): fmsg = UserNotification( addresses, mlist.bounces_address, _('Forward of moderated message'), lang=language) fmsg.set_type('message/rfc822') fmsg.attach(msg) fmsg.send(mlist) # Delete the message from the message store if it is not being preserved. if not preserve: message_store.delete_message(message_id) requestdb.delete_request(id) # Log the rejection if rejection: note = """%s: %s posting: \tFrom: %s \tSubject: %s""" if comment: note += '\n\tReason: ' + comment vlog.info(note, mlist.fqdn_listname, rejection, sender, subject)
def send_probe(member, msg=None, message_id=None): """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: :param message_id: MessageID of the bouncing message. :type message_id: str :return: The token representing this probe in the pendings database. :rtype: string """ if (message_id or msg) is None: raise ValueError('Required at least one of "message_id" and "msg".') 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.address.email, # For backward compatibility. address=member.address.email, email=member.address.email, owneraddr=mlist.owner_address, ))) if message_id is None: 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, if it provied. probe = UserNotification(member.address.email, probe_sender, subject, lang=member.preferred_language) probe.set_type('multipart/mixed') notice = MIMEText(text, _charset=member.preferred_language.charset) probe.attach(notice) if msg is not None: probe.attach(MIMEMessage(msg)) # Probes should not have the Precedence: bulk header. probe.send(mlist, sender=probe_sender, verp=False, probe_token=token, add_precedence=False) # When we send a probe, we reset the score. member.bounce_score = 0 return token
def handle_message(mlist, id, action, comment=None, forward=None): message_store = getUtility(IMessageStore) requestdb = IListRequests(mlist) key, msgdata = requestdb.get_request(id) # Handle the action. rejection = None message_id = msgdata['_mod_message_id'] sender = msgdata['_mod_sender'] subject = msgdata['_mod_subject'] keep = False if action in (Action.defer, Action.hold): # Nothing to do, but preserve the message for later. keep = True elif action is Action.discard: rejection = 'Discarded' elif action is Action.reject: rejection = 'Refused' member = mlist.members.get_member(sender) if member: language = member.preferred_language else: language = None send_rejection( mlist, _('Posting of your message titled "$subject"'), sender, comment or _('[No reason given]'), language) elif action is Action.accept: # Start by getting the message from the message store. msg = message_store.get_message_by_id(message_id) # Delete moderation-specific entries from the message metadata. for key in list(msgdata): if key.startswith('_mod_'): del msgdata[key] # Add some metadata to indicate this message has now been approved. msgdata['approved'] = True msgdata['moderator_approved'] = True # Calculate a new filebase for the approved message, otherwise # delivery errors will cause duplicates. if 'filebase' in msgdata: del msgdata['filebase'] # Queue the file for delivery. Trying to deliver the message directly # here can lead to a huge delay in web turnaround. Log the moderation # and add a header. msg['X-Mailman-Approved-At'] = formatdate( time.mktime(now().timetuple()), localtime=True) vlog.info('held message approved, message-id: %s', msg.get('message-id', 'n/a')) # Stick the message back in the incoming queue for further # processing. config.switchboards['pipeline'].enqueue(msg, _metadata=msgdata) else: raise AssertionError('Unexpected action: {0}'.format(action)) # Forward the message. if forward: # Get a copy of the original message from the message store. msg = message_store.get_message_by_id(message_id) # It's possible the forwarding address list is a comma separated list # of display_name/address pairs. addresses = [addr[1] for addr in getaddresses(forward)] language = mlist.preferred_language if len(addresses) == 1: # If the address getting the forwarded message is a member of # the list, we want the headers of the outer message to be # encoded in their language. Otherwise it'll be the preferred # language of the mailing list. This is better than sending a # separate message per recipient. member = mlist.members.get_member(addresses[0]) if member: language = member.preferred_language with _.using(language.code): fmsg = UserNotification( addresses, mlist.bounces_address, _('Forward of moderated message'), lang=language) fmsg.set_type('message/rfc822') fmsg.attach(msg) fmsg.send(mlist) # Delete the request if it's not being kept. if not keep: requestdb.delete_request(id) # Log the rejection if rejection: note = """%s: %s posting: \tFrom: %s \tSubject: %s""" if comment: note += '\n\tReason: ' + comment vlog.info(note, mlist.fqdn_listname, rejection, sender, subject)
def process(self, args): """See `ICLISubCommand`.""" language_code = args.language if args.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: self.parser.error(_("Invalid language code: $language_code")) return assert len(args.listname) == 1, "Unexpected positional arguments: %s" % args.listname # Check to see if the domain exists or not. fqdn_listname = args.listname[0] listname, at, domain = fqdn_listname.partition("@") domain_manager = getUtility(IDomainManager) if domain_manager.get(domain) is None and args.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 args.owners: validator = getUtility(IEmailValidator) invalid_owners = [owner for owner in args.owners if not validator.is_valid(owner)] if invalid_owners: invalid = COMMASPACE.join(sorted(invalid_owners)) # noqa self.parser.error(_("Illegal owner addresses: $invalid")) return try: mlist = create_list(fqdn_listname, args.owners) except InvalidEmailAddressError: self.parser.error(_("Illegal list name: $fqdn_listname")) return except ListAlreadyExistsError: self.parser.error(_("List already exists: $fqdn_listname")) return except BadDomainSpecificationError as domain: self.parser.error(_("Undefined domain: $domain")) return # 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 args.quiet: print(_("Created mailing list: $mlist.fqdn_listname")) if args.notify: d = dict( listname=mlist.fqdn_listname, # noqa admin_url=mlist.script_url("admin"), # noqa listinfo_url=mlist.script_url("listinfo"), # noqa requestaddr=mlist.request_address, # noqa siteowner=mlist.no_reply_address, # noqa ) text = make("newlist.txt", mailing_list=mlist, **d) # 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( args.owners, mlist.no_reply_address, _("Your new mailing list: $fqdn_listname"), text, mlist.preferred_language, ) msg.send(mlist)
def _process(self, mlist, msg, msgdata): """See `TerminalChainBase`.""" # Start by decorating the message with a header that contains a list # of all the rules that matched. These metadata could be None or an # empty list. rule_hits = msgdata.get('rule_hits') if rule_hits: msg['X-Mailman-Rule-Hits'] = SEMISPACE.join(rule_hits) rule_misses = msgdata.get('rule_misses') if rule_misses: msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses) # Hold the message by adding it to the list's request database. request_id = hold_message(mlist, msg, msgdata, None) # Calculate a confirmation token to send to the author of the # message. pendable = HeldMessagePendable(type=HeldMessagePendable.PEND_KEY, id=request_id) token = getUtility(IPendings).add(pendable) # Get the language to send the response in. If the sender is a # member, then send it in the member's language, otherwise send it in # the mailing list's preferred language. member = mlist.members.get_member(msg.sender) language = (member.preferred_language if member else mlist.preferred_language) # A substitution dictionary for the email templates. charset = mlist.preferred_language.charset original_subject = msg.get('subject') if original_subject is None: original_subject = _('(no subject)') else: original_subject = oneline(original_subject, in_unicode=True) substitutions = dict( listname = mlist.fqdn_listname, subject = original_subject, sender = msg.sender, reasons = _compose_reasons(msgdata), ) # At this point the message is held, but now we have to craft at least # two responses. The first will go to the original author of the # message and it will contain the token allowing them to approve or # discard the message. The second one will go to the moderators of # the mailing list, if the list is so configured. # # Start by possibly sending a response to the message author. There # are several reasons why we might not go through with this. If the # message was gated from NNTP, the author may not even know about this # list, so don't spam them. If the author specifically requested that # acknowledgments not be sent, or if the message was bulk email, then # we do not send the response. It's also possible that either the # mailing list, or the author (if they are a member) have been # configured to not send such responses. if (not msgdata.get('fromusenet') and can_acknowledge(msg) and mlist.respond_to_post_requests and autorespond_to_sender(mlist, msg.sender, language)): # We can respond to the sender with a message indicating their # posting was held. subject = _( 'Your message to $mlist.fqdn_listname awaits moderator approval') send_language_code = msgdata.get('lang', language.code) text = make('postheld.txt', mailing_list=mlist, language=send_language_code, **substitutions) adminaddr = mlist.bounces_address nmsg = UserNotification( msg.sender, adminaddr, subject, text, getUtility(ILanguageManager)[send_language_code]) nmsg.send(mlist) # Now the message for the list moderators. This one should appear to # come from <list>-owner since we really don't need to do bounce # processing on it. if mlist.admin_immed_notify: # Now let's temporarily set the language context to that which the # administrators are expecting. with _.using(mlist.preferred_language.code): language = mlist.preferred_language charset = language.charset substitutions['subject'] = original_subject # We need to regenerate or re-translate a few values in the # substitution dictionary. substitutions['reasons'] = _compose_reasons(msgdata, 55) # craft the admin notification message and deliver it subject = _( '$mlist.fqdn_listname post from $msg.sender requires ' 'approval') nmsg = UserNotification(mlist.owner_address, mlist.owner_address, subject, lang=language) nmsg.set_type('multipart/mixed') text = MIMEText(make('postauth.txt', mailing_list=mlist, wrap=False, **substitutions), _charset=charset) dmsg = MIMEText(wrap(_("""\ If you reply to this message, keeping the Subject: header intact, Mailman will discard the held message. Do this if the message is spam. If you reply to this message and include an Approved: header with the list password in it, the message will be approved for posting to the list. The Approved: header can also appear in the first line of the body of the reply.""")), _charset=language.charset) dmsg['Subject'] = 'confirm ' + token dmsg['From'] = mlist.request_address dmsg['Date'] = formatdate(localtime=True) dmsg['Message-ID'] = make_msgid() nmsg.attach(text) nmsg.attach(MIMEMessage(msg)) nmsg.attach(MIMEMessage(dmsg)) nmsg.send(mlist, **dict(tomoderators=True)) # Log the held message. Log messages are not translated, so recast # the reasons in the English. with _.using('en'): reasons = _compose_reasons(msgdata) log.info('HOLD: %s post from %s held, message-id=%s: %s', mlist.fqdn_listname, msg.sender, msg.get('message-id', 'n/a'), SEMISPACE.join(reasons)) notify(HoldEvent(mlist, msg, msgdata, self))
def munged_headers(mlist, msg, msgdata): # This returns a list of tuples (header, content) where header is the # name of a header to be added to or replaced in the wrapper or message # for DMARC mitigation. It sets From: to the string # 'original From: display name' via 'list name' <list posting address> # and adds the original From: to Reply-To: or Cc: per the following. # Our goals for this process are not completely compatible, so we do # the best we can. Our goals are: # 1) as long as the list is not anonymous, the original From: address # should be obviously exposed, i.e. not just in a header that MUAs # don't display. # 2) the original From: address should not be in a comment or display # name in the new From: because it is claimed that multiple domains # in any fields in From: are indicative of spamminess. This means # it should be in Reply-To: or Cc:. # 3) the behavior of an MUA doing a 'reply' or 'reply all' should be # consistent regardless of whether or not the From: is munged. # Goal 3) implies sometimes the original From: should be in Reply-To: # and sometimes in Cc:, and even so, this goal won't be achieved in # all cases with all MUAs. In cases of conflict, the above ordering of # goals is priority order. # # Be as robust as possible here. all_froms = getaddresses(msg.get_all('from', [])) # Strip the nulls and bad emails. froms = [email for email in all_froms if '@' in email[1]] if len(froms) == 1: realname, email = original_from = froms[0] else: # No From: or multiple addresses. Just punt and take # the get_sender result. realname = '' email = msgdata['original_sender'] original_from = (realname, email) # If there was no display name in the email header, see if we have a # matching member with a display name. if len(realname) == 0: member = mlist.members.get_member(email) if member: realname = member.display_name or email else: realname = email # Remove the domain from realname if it looks like an email address. realname = re.sub(r'@([^ .]+\.)+[^ .]+$', '---', realname) # Make a display name and RFC 2047 encode it if necessary. This is # difficult and kludgy. If the realname came from From: it should be # ASCII or RFC 2047 encoded. If it came from the member record, it should # be a string. If it's from the email address, it should be an ASCII # string. In any case, ensure it's an unencoded string. realname_bits = [] for fragment, charset in decode_header(realname): if not charset: # Character set should be ASCII, but use iso-8859-1 anyway. charset = 'iso-8859-1' if not isinstance(fragment, str): realname_bits.append(str(fragment, charset, errors='replace')) else: realname_bits.append(fragment) # The member's display name is a string. realname = EMPTYSTRING.join(realname_bits) # Ensure the i18n context is the list's preferred_language. with _.using(mlist.preferred_language.code): via = _('$realname via $mlist.display_name') # Get an RFC 2047 encoded header string. display_name = str(Header(via, mlist.preferred_language.charset)) value = [('From', formataddr((display_name, mlist.posting_address)))] # We've made the munged From:. Now put the original in Reply-To: or Cc: if mlist.reply_goes_to_list is ReplyToMunging.no_munging: # Add original from to Reply-To: add_to = 'Reply-To' else: # Add original from to Cc: add_to = 'Cc' original = getaddresses(msg.get_all(add_to, [])) if original_from[1] not in [x[1] for x in original]: original.append(original_from) value.append((add_to, COMMASPACE.join(formataddr(x) for x in original))) return value
def _dispose(self, mlist, msg, msgdata): """See `IRunner`.""" volume = msgdata['volume'] digest_number = msgdata['digest_number'] # Backslashes make me cry. code = mlist.preferred_language.code with Mailbox(msgdata['digest_path']) as mailbox, _.using(code): # Create the digesters. mime_digest = MIMEDigester(mlist, volume, digest_number) rfc1153_digest = RFC1153Digester(mlist, volume, digest_number) # Cruise through all the messages in the mailbox, first building # the table of contents and accumulating Subject: headers and # authors. The question really is whether it's better from a1 # performance and memory footprint to go through the mailbox once # and cache the messages in a list, or to cruise through the # mailbox twice. We'll do the latter, but it's a complete guess. count = None for count, (key, message) in enumerate(mailbox.iteritems(), 1): mime_digest.add_to_toc(message, count) rfc1153_digest.add_to_toc(message, count) assert count is not None, 'No digest messages?' # Add the table of contents. mime_digest.add_toc(count) rfc1153_digest.add_toc(count) # Cruise through the set of messages a second time, adding them to # the actual digest. for count, (key, message) in enumerate(mailbox.iteritems(), 1): mime_digest.add_message(message, count) rfc1153_digest.add_message(message, count) # Finish up the digests. mime = mime_digest.finish() rfc1153 = rfc1153_digest.finish() # Calculate the recipients lists mime_recipients = set() rfc1153_recipients = set() # When someone turns off digest delivery, they will get one last # digest to ensure that there will be no gaps in the messages they # receive. digest_members = set(mlist.digest_members.members) for member in digest_members: if member.delivery_status is not DeliveryStatus.enabled: continue # Send the digest to the case-preserved address of the digest # members. email_address = member.address.original_email if member.delivery_mode == DeliveryMode.plaintext_digests: rfc1153_recipients.add(email_address) # We currently treat summary_digests the same as mime_digests. elif member.delivery_mode in (DeliveryMode.mime_digests, DeliveryMode.summary_digests): mime_recipients.add(email_address) else: raise AssertionError( 'Digest member "{0}" unexpected delivery mode: {1}'.format( email_address, member.delivery_mode)) # Add also the folks who are receiving one last digest. for address, delivery_mode in mlist.last_digest_recipients: if delivery_mode == DeliveryMode.plaintext_digests: rfc1153_recipients.add(address.original_email) # We currently treat summary_digests the same as mime_digests. elif delivery_mode in (DeliveryMode.mime_digests, DeliveryMode.summary_digests): mime_recipients.add(address.original_email) else: raise AssertionError( 'OLD recipient "{0}" unexpected delivery mode: {1}'.format( address, delivery_mode)) # Send the digests to the virgin queue for final delivery. queue = config.switchboards['virgin'] if len(mime_recipients) > 0: queue.enqueue(mime, recipients=mime_recipients, listid=mlist.list_id, isdigest=True) if len(rfc1153_recipients) > 0: queue.enqueue(rfc1153, recipients=rfc1153_recipients, listid=mlist.list_id, isdigest=True)
def _process(self, mlist, msg, msgdata): """See `TerminalChainBase`.""" # Start by decorating the message with a header that contains a list # of all the rules that matched. These metadata could be None or an # empty list. rule_hits = msgdata.get('rule_hits') if rule_hits: msg['X-Mailman-Rule-Hits'] = SEMISPACE.join(rule_hits) rule_misses = msgdata.get('rule_misses') if rule_misses: msg['X-Mailman-Rule-Misses'] = SEMISPACE.join(rule_misses) # Hold the message by adding it to the list's request database. request_id = hold_message(mlist, msg, msgdata, None) # Calculate a confirmation token to send to the author of the # message. pendable = HeldMessagePendable(id=request_id) token = getUtility(IPendings).add(pendable) # Get the language to send the response in. If the sender is a # member, then send it in the member's language, otherwise send it in # the mailing list's preferred language. member = mlist.members.get_member(msg.sender) language = (member.preferred_language if member else mlist.preferred_language) # A substitution dictionary for the email templates. charset = mlist.preferred_language.charset original_subject = msg.get('subject') if original_subject is None: original_subject = _('(no subject)') else: # This must be encoded to the mailing list's perferred charset, # ignoring incompatible characters, otherwise when creating the # notification messages, we could get a Unicode error. oneline_subject = oneline(original_subject, in_unicode=True) bytes_subject = oneline_subject.encode(charset, 'replace') original_subject = bytes_subject.decode(charset) substitutions = dict( subject=original_subject, sender_email=msg.sender, reasons=_compose_reasons(msgdata), # For backward compatibility. sender=msg.sender, ) # At this point the message is held, but now we have to craft at least # two responses. The first will go to the original author of the # message and it will contain the token allowing them to approve or # discard the message. The second one will go to the moderators of # the mailing list, if the list is so configured. # # Start by possibly sending a response to the message author. There # are several reasons why we might not go through with this. If the # message was gated from NNTP, the author may not even know about this # list, so don't spam them. If the author specifically requested that # acknowledgments not be sent, or if the message was bulk email, then # we do not send the response. It's also possible that either the # mailing list, or the author (if they are a member) have been # configured to not send such responses. if (not msgdata.get('fromusenet') and can_acknowledge(msg) and mlist.respond_to_post_requests and autorespond_to_sender(mlist, msg.sender, language)): # We can respond to the sender with a message indicating their # posting was held. subject = _( 'Your message to $mlist.fqdn_listname awaits moderator approval') send_language_code = msgdata.get('lang', language.code) template = getUtility(ITemplateLoader).get( 'list:user:notice:hold', mlist, language=send_language_code) text = wrap(expand(template, mlist, dict( language=send_language_code, **substitutions))) adminaddr = mlist.bounces_address nmsg = UserNotification( msg.sender, adminaddr, subject, text, getUtility(ILanguageManager)[send_language_code]) nmsg.send(mlist) # Now the message for the list moderators. This one should appear to # come from <list>-owner since we really don't need to do bounce # processing on it. if mlist.admin_immed_notify: # Now let's temporarily set the language context to that which the # administrators are expecting. with _.using(mlist.preferred_language.code): language = mlist.preferred_language charset = language.charset substitutions['subject'] = original_subject # We need to regenerate or re-translate a few values in the # substitution dictionary. substitutions['reasons'] = _compose_reasons(msgdata, 55) # craft the admin notification message and deliver it subject = _( '$mlist.fqdn_listname post from $msg.sender requires ' 'approval') nmsg = UserNotification(mlist.owner_address, mlist.owner_address, subject, lang=language) nmsg.set_type('multipart/mixed') template = getUtility(ITemplateLoader).get( 'list:admin:action:post', mlist) text = MIMEText(expand(template, mlist, substitutions), _charset=charset) dmsg = MIMEText(wrap(_("""\ If you reply to this message, keeping the Subject: header intact, Mailman will discard the held message. Do this if the message is spam. If you reply to this message and include an Approved: header with the list password in it, the message will be approved for posting to the list. The Approved: header can also appear in the first line of the body of the reply.""")), _charset=language.charset) dmsg['Subject'] = 'confirm ' + token dmsg['From'] = mlist.request_address dmsg['Date'] = formatdate(localtime=True) dmsg['Message-ID'] = make_msgid() nmsg.attach(text) nmsg.attach(MIMEMessage(msg)) nmsg.attach(MIMEMessage(dmsg)) nmsg.send(mlist, **dict(tomoderators=True)) # Log the held message. Log messages are not translated, so recast # the reasons in the English. with _.using('en'): reasons = msgdata.get('moderation_reasons', ['N/A']) log.info('HOLD: %s post from %s held, message-id=%s: %s', mlist.fqdn_listname, msg.sender, msg.get('message-id', 'n/a'), SEMISPACE.join(reasons)) notify(HoldEvent(mlist, msg, msgdata, self))
def process(self, args): """See `ICLISubCommand`.""" language_code = (args.language if args.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: self.parser.error(_('Invalid language code: $language_code')) return assert len(args.listname) == 1, ( 'Unexpected positional arguments: %s' % args.listname) # Check to see if the domain exists or not. fqdn_listname = args.listname[0] listname, at, domain = fqdn_listname.partition('@') domain_manager = getUtility(IDomainManager) if domain_manager.get(domain) is None and args.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 args.owners: validator = getUtility(IEmailValidator) invalid_owners = [owner for owner in args.owners if not validator.is_valid(owner)] if invalid_owners: invalid = COMMASPACE.join(sorted(invalid_owners)) # noqa self.parser.error(_('Illegal owner addresses: $invalid')) return try: mlist = create_list(fqdn_listname, args.owners) except InvalidEmailAddressError: self.parser.error(_('Illegal list name: $fqdn_listname')) return except ListAlreadyExistsError: self.parser.error(_('List already exists: $fqdn_listname')) return except BadDomainSpecificationError as domain: self.parser.error(_('Undefined domain: $domain')) return # 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 args.quiet: print(_('Created mailing list: $mlist.fqdn_listname')) if args.notify: d = dict( listname = mlist.fqdn_listname, # noqa admin_url = mlist.script_url('admin'), # noqa listinfo_url = mlist.script_url('listinfo'), # noqa requestaddr = mlist.request_address, # noqa siteowner = mlist.no_reply_address, # noqa ) text = make('newlist.txt', mailing_list=mlist, **d) # 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( args.owners, mlist.no_reply_address, _('Your new mailing list: $fqdn_listname'), text, mlist.preferred_language) msg.send(mlist)