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 bounce_message(mlist, msg, error=None): """Bounce the message back to the original author. :param mlist: The mailing list that the message was posted to. :type mlist: `IMailingList` :param msg: The original message. :type msg: `email.message.Message` :param error: Optional exception causing the bounce. The exception instance must have a `.message` attribute. :type error: Exception """ # Bounce a message back to the sender, with an error message if provided # in the exception argument. .sender might be None or the empty string. if not msg.sender: # We can't bounce the message if we don't know who it's supposed to go # to. return subject = msg.get('subject', _('(no subject)')) subject = oneline(subject, mlist.preferred_language.charset) if error is None: notice = _('[No bounce details are available]') else: notice = _(error.message) # Currently we always craft bounces as MIME messages. bmsg = UserNotification(msg.sender, mlist.owner_address, subject, lang=mlist.preferred_language) # BAW: Be sure you set the type before trying to attach, or you'll get # a MultipartConversionError. bmsg.set_type('multipart/mixed') txt = MIMEText(notice, _charset=mlist.preferred_language.charset) bmsg.attach(txt) bmsg.attach(MIMEMessage(msg)) bmsg.send(mlist)
def register(self, mlist, email, display_name=None, delivery_mode=None): """See `IUserRegistrar`.""" if delivery_mode is None: delivery_mode = DeliveryMode.regular # First, do validation on the email address. If the address is # invalid, it will raise an exception, otherwise it just returns. getUtility(IEmailValidator).validate(email) # Create a pendable for the registration. pendable = PendableRegistration( type=PendableRegistration.PEND_KEY, email=email, display_name=display_name, delivery_mode=delivery_mode.name ) pendable["list_name"] = mlist.fqdn_listname token = getUtility(IPendings).add(pendable) # There are three ways for a user to confirm their subscription. They # can reply to the original message and let the VERP'd return address # encode the token, they can reply to the robot and keep the token in # the Subject header, or they can click on the URL in the body of the # message and confirm through the web. subject = "confirm " + token confirm_address = mlist.confirm_address(token) # For i18n interpolation. confirm_url = mlist.domain.confirm_url(token) email_address = email domain_name = mlist.domain.mail_host contact_address = mlist.domain.contact_address # Send a verification email to the address. template = getUtility(ITemplateLoader).get( "mailman:///{0}/{1}/confirm.txt".format(mlist.fqdn_listname, mlist.preferred_language.code) ) text = _(template) msg = UserNotification(email, confirm_address, subject, text) msg.send(mlist) return token
def hold_subscription(mlist, address, display_name, password, mode, language): data = dict(when=now().isoformat(), address=address, display_name=display_name, password=password, delivery_mode=mode.name, language=language) # Now hold this request. We'll use the address as the key. requestsdb = IListRequests(mlist) request_id = requestsdb.hold_request( RequestType.subscription, address, data) vlog.info('%s: held subscription request from %s', mlist.fqdn_listname, address) # Possibly notify the administrator in default list language if mlist.admin_immed_notify: subject = _( 'New subscription request to $mlist.display_name from $address') text = make('subauth.txt', mailing_list=mlist, username=address, listname=mlist.fqdn_listname, admindb_url=mlist.script_url('admindb'), ) # 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, tomoderators=True) return request_id
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)) text = make('subauth.txt', mailing_list=self.mlist, username=username, listname=self.mlist.fqdn_listname, ) # 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, tomoderators=True) # The workflow must stop running here. raise StopIteration
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 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_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 main(): opts, args, parser = parseargs() initialize(opts.config) for name in config.list_manager.names: # The list must be locked in order to open the requests database mlist = MailList.MailList(name) try: count = IListRequests(mlist).count # While we're at it, let's evict yesterday's autoresponse data midnight_today = midnight() evictions = [] for sender in mlist.hold_and_cmd_autoresponses.keys(): date, respcount = mlist.hold_and_cmd_autoresponses[sender] if midnight(date) < midnight_today: evictions.append(sender) if evictions: for sender in evictions: del mlist.hold_and_cmd_autoresponses[sender] # This is the only place we've changed the list's database mlist.Save() if count: # Set the default language the the list's preferred language. _.default = mlist.preferred_language realname = mlist.real_name discarded = auto_discard(mlist) if discarded: count = count - discarded text = _('Notice: $discarded old request(s) ' 'automatically expired.\n\n') else: text = '' if count: text += Utils.maketext( 'checkdbs.txt', {'count' : count, 'mail_host': mlist.mail_host, 'adminDB' : mlist.GetScriptURL('admindb', absolute=1), 'real_name': realname, }, mlist=mlist) text += '\n' + pending_requests(mlist) subject = _('$count $realname moderator ' 'request(s) waiting') else: subject = _('$realname moderator request check result') msg = UserNotification(mlist.GetOwnerEmail(), mlist.GetBouncesEmail(), subject, text, mlist.preferred_language) msg.send(mlist, **{'tomoderators': True}) finally: mlist.Unlock()
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_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_goodbye_message(mlist, address, language): """Send a goodbye message to a subscriber. Prepending to the standard goodbye message template is the mailing list's goodbye 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: string """ goodbye_message = _get_message(mlist.goodbye_message_uri, mlist, language) msg = UserNotification( address, mlist.bounces_address, _('You have been unsubscribed from the $mlist.display_name ' 'mailing list'), goodbye_message, language) msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
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) charset = language_manager[language.code].charset # Now get the acknowledgement template. display_name = mlist.display_name text = make('postack.txt', mailing_list=mlist, language=language.code, wrap=False, subject=oneline(original_subject, charset), list_name=mlist.list_name, display_name=display_name, listinfo_url=mlist.script_url('listinfo'), optionsurl=member.options_url, ) # 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)
class TestMessage(unittest.TestCase): """Test the message API.""" layer = ConfigLayer def setUp(self): self._mlist = create_list('*****@*****.**') self._msg = UserNotification( '*****@*****.**', '*****@*****.**', 'Something you need to know', 'I needed to tell you this.') def test_one_precedence_header(self): # Ensure that when the original message already has a Precedence: # header, UserNotification.send(..., add_precedence=True, ...) does # not add a second header. self.assertEqual(self._msg['precedence'], None) self._msg['Precedence'] = 'omg wtf bbq' self._msg.send(self._mlist) items = get_queue_messages('virgin', expected_count=1) self.assertEqual(items[0].msg.get_all('precedence'), ['omg wtf bbq'])
def handle_ConfirmationNeededEvent(event): if not isinstance(event, ConfirmationNeededEvent): return # There are three ways for a user to confirm their subscription. They # can reply to the original message and let the VERP'd return address # encode the token, they can reply to the robot and keep the token in # the Subject header, or they can click on the URL in the body of the # message and confirm through the web. subject = 'confirm ' + event.token confirm_address = event.mlist.confirm_address(event.token) # For i18n interpolation. confirm_url = event.mlist.domain.confirm_url(event.token) email_address = event.email domain_name = event.mlist.domain.mail_host contact_address = event.mlist.owner_address # Send a verification email to the address. template = getUtility(ITemplateLoader).get( 'mailman:///{0}/{1}/confirm.txt'.format( event.mlist.fqdn_listname, event.mlist.preferred_language.code)) text = _(template) msg = UserNotification(email_address, confirm_address, subject, text) msg.send(event.mlist, add_precedence=False)
def hold_unsubscription(mlist, address): data = dict(address=address) requestsdb = IListRequests(mlist) request_id = requestsdb.hold_request( RequestType.unsubscription, address, data) vlog.info('%s: held unsubscription request from %s', mlist.fqdn_listname, address) # Possibly notify the administrator of the hold if mlist.admin_immed_notify: subject = _( 'New unsubscription request from $mlist.display_name by $address') text = make('unsubauth.txt', mailing_list=mlist, address=address, listname=mlist.fqdn_listname, admindb_url=mlist.script_url('admindb'), ) # 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, tomoderators=True) return request_id
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 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 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 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 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. template = getUtility(ITemplateLoader).get( 'list:user:notice:no-more-today', mlist, language=language.code) text = wrap( expand( template, mlist, dict( language=language.code, count=todays_count, sender_email=sender, # For backward compatibility. sender=sender, 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 _dispose(self, mlist, msg, msgdata): message_id = msg.get('message-id', 'n/a') # The policy here is similar to the Replybot policy. If a message has # "Precedence: bulk|junk|list" and no "X-Ack: yes" header, we discard # the command message. precedence = msg.get('precedence', '').lower() ack = msg.get('x-ack', '').lower() if ack != 'yes' and precedence in ('bulk', 'junk', 'list'): log.info('%s Precedence: %s message discarded by: %s', message_id, precedence, mlist.request_address) return False # Do replybot for commands. replybot = config.handlers['replybot'] replybot.process(mlist, msg, msgdata) if mlist.autorespond_requests == 1: # Respond and discard. log.info('%s -request message replied and discard', message_id) return False # Now craft the response and process the command lines. charset = msg.get_param('charset') if charset is None: charset = 'us-ascii' results = Results(charset) # Include just a few key pieces of information from the original: the # sender, date, and message id. print(_('- Original message details:'), file=results) subject = msg.get('subject', 'n/a') date = msg.get('date', 'n/a') from_ = msg.get('from', 'n/a') print(_(' From: $from_'), file=results) print(_(' Subject: $subject'), file=results) print(_(' Date: $date'), file=results) print(_(' Message-ID: $message_id'), file=results) print(_('\n- Results:'), file=results) finder = CommandFinder(msg, msgdata, results) for parts in finder: command = None # Try to find a command on this line. There may be a Re: prefix # (possibly internationalized) so try with the first and second # words on the line. if len(parts) > 0: command_name = parts.pop(0) command = config.commands.get(command_name) if command is None and len(parts) > 0: command_name = parts.pop(0) command = config.commands.get(command_name) if command is None: print(_('No such command: $command_name'), file=results) else: status = command.process(mlist, msg, msgdata, parts, results) assert status in ContinueProcessing, ('Invalid status: %s' % status) if status == ContinueProcessing.no: break # All done. Strip blank lines and send the response. lines = [line.strip() for line in finder.command_lines if line] if len(lines) > 0: print(_('\n- Unprocessed:'), file=results) for line in lines: print(line, file=results) lines = [line.strip() for line in finder.ignored_lines if line] if len(lines) > 0: print(_('\n- Ignored:'), file=results) for line in lines: print(line, file=results) print(_('\n- Done.'), file=results) # Send a reply, but do not attach the original message. This is a # compromise because the original message is often helpful in tracking # down problems, but it's also a vector for backscatter spam. language = getUtility(ILanguageManager)[msgdata['lang']] reply = UserNotification(msg.sender, mlist.bounces_address, _('The results of your email commands'), lang=language) cte = msg.get('content-transfer-encoding') if cte is not None: reply['Content-Transfer-Encoding'] = cte # Find a charset for the response body. Try the original message's # charset first, then ascii, then latin-1 and finally falling back to # utf-8. reply_body = str(results) for charset in (results.charset, 'us-ascii', 'latin-1'): try: reply_body.encode(charset) break except UnicodeError: pass else: charset = 'utf-8' reply.set_payload(reply_body, charset=charset) reply.send(mlist)
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 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: F841 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: 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( 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 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, requestemail=mlist.request_address, owneremail=mlist.owner_address, ) # Interpolation and Wrap the response text. text = wrap(expand(response_text, mlist, 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 _dispose(self, mlist, msg, msgdata): message_id = msg.get('message-id', 'n/a') # The policy here is similar to the Replybot policy. If a message has # "Precedence: bulk|junk|list" and no "X-Ack: yes" header, we discard # the command message. precedence = msg.get('precedence', '').lower() ack = msg.get('x-ack', '').lower() if ack <> 'yes' and precedence in ('bulk', 'junk', 'list'): log.info('%s Precedence: %s message discarded by: %s', message_id, precedence, mlist.request_address) return False # Do replybot for commands. replybot = config.handlers['replybot'] replybot.process(mlist, msg, msgdata) if mlist.autorespond_requests == 1: # Respond and discard. log.info('%s -request message replied and discard', message_id) return False # Now craft the response and process the command lines. charset = msg.get_param('charset') if charset is None: charset = 'us-ascii' results = Results(charset) # Include just a few key pieces of information from the original: the # sender, date, and message id. print(_('- Original message details:'), file=results) subject = msg.get('subject', 'n/a') date = msg.get('date', 'n/a') from_ = msg.get('from', 'n/a') print(_(' From: $from_'), file=results) print(_(' Subject: $subject'), file=results) print(_(' Date: $date'), file=results) print(_(' Message-ID: $message_id'), file=results) print(_('\n- Results:'), file=results) finder = CommandFinder(msg, msgdata, results) for parts in finder: command = None # Try to find a command on this line. There may be a Re: prefix # (possibly internationalized) so try with the first and second # words on the line. if len(parts) > 0: command_name = parts.pop(0) command = config.commands.get(command_name) if command is None and len(parts) > 0: command_name = parts.pop(0) command = config.commands.get(command_name) if command is None: print(_('No such command: $command_name'), file=results) else: status = command.process( mlist, msg, msgdata, parts, results) assert status in ContinueProcessing, ( 'Invalid status: %s' % status) if status == ContinueProcessing.no: break # All done. Strip blank lines and send the response. lines = filter(None, (line.strip() for line in finder.command_lines)) if len(lines) > 0: print(_('\n- Unprocessed:'), file=results) for line in lines: print(line, file=results) lines = filter(None, (line.strip() for line in finder.ignored_lines)) if len(lines) > 0: print(_('\n- Ignored:'), file=results) for line in lines: print(line, file=results) print(_('\n- Done.'), file=results) # Send a reply, but do not attach the original message. This is a # compromise because the original message is often helpful in tracking # down problems, but it's also a vector for backscatter spam. language = getUtility(ILanguageManager)[msgdata['lang']] reply = UserNotification(msg.sender, mlist.bounces_address, _('The results of your email commands'), lang=language) cte = msg.get('content-transfer-encoding') if cte is not None: reply['Content-Transfer-Encoding'] = cte # Find a charset for the response body. Try the original message's # charset first, then ascii, then latin-1 and finally falling back to # utf-8. reply_body = unicode(results) for charset in (results.charset, 'us-ascii', 'latin-1'): try: reply_body.encode(charset) break except UnicodeError: pass else: charset = 'utf-8' reply.set_payload(reply_body, charset=charset) reply.send(mlist)
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 _dispose(self, mlist, msg, msgdata): message_id = msg.get("message-id", "n/a") # The policy here is similar to the Replybot policy. If a message has # "Precedence: bulk|junk|list" and no "X-Ack: yes" header, we discard # the command message. precedence = msg.get("precedence", "").lower() ack = msg.get("x-ack", "").lower() if ack != "yes" and precedence in ("bulk", "junk", "list"): log.info("%s Precedence: %s message discarded by: %s", message_id, precedence, mlist.request_address) return False # Do replybot for commands. replybot = config.handlers["replybot"] replybot.process(mlist, msg, msgdata) if mlist.autorespond_requests == 1: # Respond and discard. log.info("%s -request message replied and discard", message_id) return False # Now craft the response and process the command lines. charset = msg.get_param("charset") if charset is None: charset = "us-ascii" results = Results(charset) # Include just a few key pieces of information from the original: the # sender, date, and message id. print(_("- Original message details:"), file=results) subject = msg.get("subject", "n/a") date = msg.get("date", "n/a") from_ = msg.get("from", "n/a") print(_(" From: $from_"), file=results) print(_(" Subject: $subject"), file=results) print(_(" Date: $date"), file=results) print(_(" Message-ID: $message_id"), file=results) print(_("\n- Results:"), file=results) finder = CommandFinder(msg, msgdata, results) for parts in finder: command = None # Try to find a command on this line. There may be a Re: prefix # (possibly internationalized) so try with the first and second # words on the line. if len(parts) > 0: command_name = parts.pop(0) command = config.commands.get(command_name) if command is None and len(parts) > 0: command_name = parts.pop(0) command = config.commands.get(command_name) if command is None: print(_("No such command: $command_name"), file=results) else: status = command.process(mlist, msg, msgdata, parts, results) assert status in ContinueProcessing, "Invalid status: %s" % status if status == ContinueProcessing.no: break # All done. Strip blank lines and send the response. lines = [line.strip() for line in finder.command_lines if line] if len(lines) > 0: print(_("\n- Unprocessed:"), file=results) for line in lines: print(line, file=results) lines = [line.strip() for line in finder.ignored_lines if line] if len(lines) > 0: print(_("\n- Ignored:"), file=results) for line in lines: print(line, file=results) print(_("\n- Done."), file=results) # Send a reply, but do not attach the original message. This is a # compromise because the original message is often helpful in tracking # down problems, but it's also a vector for backscatter spam. language = getUtility(ILanguageManager)[msgdata["lang"]] reply = UserNotification( msg.sender, mlist.bounces_address, _("The results of your email commands"), lang=language ) cte = msg.get("content-transfer-encoding") if cte is not None: reply["Content-Transfer-Encoding"] = cte # Find a charset for the response body. Try the original message's # charset first, then ascii, then latin-1 and finally falling back to # utf-8. reply_body = str(results) for charset in (results.charset, "us-ascii", "latin-1"): try: reply_body.encode(charset) break except UnicodeError: pass else: charset = "utf-8" reply.set_payload(reply_body, charset=charset) reply.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) reasons = format_reasons(msgdata.get('moderation_reasons', ['n/a'])) # Hold the message by adding it to the list's request database. request_id = hold_message(mlist, msg, msgdata, SEMISPACE.join(reasons)) # 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) # Log the held message. Log messages are not translated, so recast # the reasons in the English. with _.using('en'): reasons = format_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))