def test_accepted_message_gets_posted(self): # A message that is accepted by the moderator should get posted to the # mailing list. LP: #827697 msgdata = dict(listname='*****@*****.**', recipients=['*****@*****.**']) request_id = hold_message(self._mlist, self._msg, msgdata) handle_message(self._mlist, request_id, Action.accept) self._in.run() self._pipeline.run() self._out.run() messages = list(SMTPLayer.smtpd.messages) self.assertEqual(len(messages), 1) message = messages[0] # We don't need to test the entire posted message, just the bits that # prove it got sent out. self.assertTrue('x-mailman-version' in message) self.assertTrue('x-peer' in message) # The X-Mailman-Approved-At header has local timezone information in # it, so test that separately. self.assertEqual(message['x-mailman-approved-at'][:-5], 'Mon, 01 Aug 2005 07:49:23 ') del message['x-mailman-approved-at'] # The Message-ID matches the original. self.assertEqual(message['message-id'], '<alpha>') # Anne sent the message and the mailing list received it. self.assertEqual(message['from'], '*****@*****.**') self.assertEqual(message['to'], '*****@*****.**') # The Subject header has the list's prefix. self.assertEqual(message['subject'], '[Test] hold me') # The list's -bounce address is the actual sender, and Bart is the # only actual recipient. These headers are added by the testing # framework and don't show up in production. They match the RFC 5321 # envelope. self.assertEqual(message['x-mailfrom'], '*****@*****.**') self.assertEqual(message['x-rcptto'], '*****@*****.**')
def test_request_order(self): # Requests must be sorted in creation order. # # This test only "works" for PostgreSQL, in the sense that if you # remove the fix in ../requests.py, it will still pass in SQLite. # Apparently SQLite auto-sorts results by ID but PostgreSQL autosorts # by insertion time. It's still worth keeping the test to prevent # regressions. # # We modify the auto-incremented ids by listening to SQLAlchemy's # flush event, and hacking all the _Request object id's to the next # value in a descending counter. request_ids = [] counter = count(200, -1) def id_hacker(session, flush_context, instances): # noqa: E306 for instance in session.new: if isinstance(instance, _Request): instance.id = next(counter) with before_flush(id_hacker): for index in range(10): msg = mfs(self._msg.as_string()) msg.replace_header('Message-ID', '<alpha{}>'.format(index)) request_ids.append(hold_message(self._mlist, msg)) config.db.store.flush() # Make sure that our ID are not already sorted. self.assertNotEqual(request_ids, sorted(request_ids)) # Get requests and check their order. requests = self._requests_db.of_type(RequestType.held_message) self.assertEqual([r.id for r in requests], sorted(request_ids))
def test_request_order(self): # Requests must be sorted in creation order. # # This test only "works" for PostgreSQL, in the sense that if you # remove the fix in ../requests.py, it will still pass in SQLite. # Apparently SQLite auto-sorts results by ID but PostgreSQL autosorts # by insertion time. It's still worth keeping the test to prevent # regressions. # # We modify the auto-incremented ids by listening to SQLAlchemy's # flush event, and hacking all the _Request object id's to the next # value in a descending counter. request_ids = [] counter = count(200, -1) def id_hacker(session, flush_context, instances): # noqa for instance in session.new: if isinstance(instance, _Request): instance.id = next(counter) with before_flush(id_hacker): for index in range(10): msg = mfs(self._msg.as_string()) msg.replace_header('Message-ID', '<alpha{}>'.format(index)) request_ids.append(hold_message(self._mlist, msg)) config.db.store.flush() # Make sure that our ID are not already sorted. self.assertNotEqual(request_ids, sorted(request_ids)) # Get requests and check their order. requests = self._requests_db.of_type(RequestType.held_message) self.assertEqual([r.id for r in requests], sorted(request_ids))
def test_preserving_disposition(self): # Preserving a message keeps it in the store. request_id = hold_message(self._mlist, self._msg) handle_message(self._mlist, request_id, Action.discard, preserve=True) message_store = getUtility(IMessageStore) preserved_message = message_store.get_message_by_id('<alpha>') self.assertEqual(preserved_message['message-id'], '<alpha>')
def test_handled_message_stays_in_store(self): # The message is still available in the store, even when it's been # disposed of. request_id = hold_message(self._mlist, self._msg) handle_message(self._mlist, request_id, Action.discard) self.assertEqual(self._request_db.count, 0) message = getUtility(IMessageStore).get_message_by_id('<alpha>') self.assertEqual(message['subject'], 'hold me')
def test_bad_held_message_action(self): # POSTing to a held message with a bad action. held_id = hold_message(self._mlist, self._msg) url = "http://*****:*****@example.com/held/{0}" with self.assertRaises(HTTPError) as cm: call_api(url.format(held_id), {"action": "bogus"}) self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.msg, "Cannot convert parameters: action")
def test_bad_held_message_action(self): # POSTing to a held message with a bad action. held_id = hold_message(self._mlist, self._msg) url = 'http://*****:*****@example.com/held/{}' with self.assertRaises(HTTPError) as cm: call_api(url.format(held_id), {'action': 'bogus'}) self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.msg, 'Cannot convert parameters: action')
def test_view_malformed_held_message(self): # Opening a bad (i.e. bad structure) email and holding it. email_path = resource_filename('mailman.rest.tests.data', 'bad_email.eml') with open(email_path, 'rb') as fp: msg = message_from_binary_file(fp) msg.sender = '*****@*****.**' with transaction(): hold_message(self._mlist, msg) # Now trying to access held messages from REST API should not give # 500 server error if one of the messages can't be parsed properly. json, response = call_api( 'http://*****:*****@example.com/held') self.assertEqual(response.status_code, 200) self.assertEqual(len(json['entries']), 1) self.assertEqual(json['entries'][0]['msg'], 'This message is defective')
def test_survive_a_deleted_message(self): # When the message that should be deleted is not found in the store, # no error is raised. request_id = hold_message(self._mlist, self._msg) message_store = getUtility(IMessageStore) message_store.delete_message('<alpha>') handle_message(self._mlist, request_id, Action.discard) self.assertEqual(self._request_db.count, 0)
def test_lp_1031391(self): # LP: #1031391 msgdata['received_time'] gets added by the LMTP server. # The value is a datetime. If this message gets held, it will break # pending requests since they require string keys and values. received_time = now() msgdata = dict(received_time=received_time) request_id = hold_message(self._mlist, self._msg, msgdata) key, data = self._request_db.get_request(request_id) self.assertEqual(data['received_time'], received_time)
def test_list_held_messages(self): # We can view all the held requests. with transaction(): held_id = hold_message(self._mlist, self._msg) json, response = call_api( 'http://*****:*****@example.com/held') self.assertEqual(response.status_code, 200) self.assertEqual(json['total_size'], 1) self.assertEqual(json['entries'][0]['request_id'], held_id)
def test_list_held_messages(self): # We can view all the held requests. with transaction(): held_id = hold_message(self._mlist, self._msg) content, response = call_api( 'http://*****:*****@example.com/held') self.assertEqual(response.status, 200) self.assertEqual(content['total_size'], 1) self.assertEqual(content['entries'][0]['request_id'], held_id)
def test_cant_get_other_lists_holds(self): # Issue #161: It was possible to moderate a held message for another # list via the REST API. with transaction(): held_id = hold_message(self._mlist, self._msg) create_list("*****@*****.**") with self.assertRaises(HTTPError) as cm: call_api("http://localhost:9001/3.0/lists/bee.example.com" "/held/{}".format(held_id)) self.assertEqual(cm.exception.code, 404)
def test_cant_get_other_lists_holds(self): # Issue #161: It was possible to moderate a held message for another # list via the REST API. with transaction(): held_id = hold_message(self._mlist, self._msg) create_list('*****@*****.**') with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/lists/bee.example.com' '/held/{}'.format(held_id)) self.assertEqual(cm.exception.code, 404)
def test_bad_action(self): # POSTing to a held message with a bad action. held_id = hold_message(self._mlist, self._msg) url = "http://*****:*****@example.com/held/{0}" try: call_api(url.format(held_id), {"action": "bogus"}) except HTTPError as exc: self.assertEqual(exc.code, 400) self.assertEqual(exc.msg, "Cannot convert parameters: action") else: raise AssertionError("Expected HTTPError")
def test_hold_action_alias_for_defer(self): # In handle_message(), the 'hold' action is the same as 'defer' for # purposes of this API. request_id = hold_message(self._mlist, self._msg) handle_message(self._mlist, request_id, Action.defer) # The message is still in the pending requests. key, data = self._request_db.get_request(request_id) self.assertEqual(key, '<alpha>') handle_message(self._mlist, request_id, Action.hold) key, data = self._request_db.get_request(request_id) self.assertEqual(key, '<alpha>')
def test_discard(self): # Discarding a message removes it from the moderation queue. with transaction(): held_id = hold_message(self._mlist, self._msg) url = "http://*****:*****@example.com/held/{}".format(held_id) content, response = call_api(url, dict(action="discard")) self.assertEqual(response.status, 204) # Now it's gone. with self.assertRaises(HTTPError) as cm: call_api(url, dict(action="discard")) self.assertEqual(cm.exception.code, 404)
def test_bad_held_message_action(self): # POSTing to a held message with a bad action. held_id = hold_message(self._mlist, self._msg) url = 'http://*****:*****@example.com/held/{}' with self.assertRaises(HTTPError) as cm: call_api(url.format(held_id), {'action': 'bogus'}) self.assertEqual(cm.exception.code, 400) self.assertEqual( cm.exception.msg, 'Invalid Parameter "action": Accepted Values are:' ' hold, reject, discard, accept, defer.')
def test_forward(self): # We can forward the message to an email address. request_id = hold_message(self._mlist, self._msg) handle_message(self._mlist, request_id, Action.discard, forward=['*****@*****.**']) # The forwarded message lives in the virgin queue. items = get_queue_messages('virgin', expected_count=1) self.assertEqual(str(items[0].msg['subject']), 'Forward of moderated message') self.assertEqual(items[0].msgdata['recipients'], ['*****@*****.**'])
def test_discard(self): # Discarding a message removes it from the moderation queue. with transaction(): held_id = hold_message(self._mlist, self._msg) url = 'http://*****:*****@example.com/held/{}'.format( held_id) json, response = call_api(url, dict(action='discard')) self.assertEqual(response.status_code, 204) # Now it's gone. with self.assertRaises(HTTPError) as cm: call_api(url, dict(action='discard')) self.assertEqual(cm.exception.code, 404)
def setUp(self): self._mlist = create_list('*****@*****.**') self._mlist2 = create_list('*****@*****.**') self._mlist.subscription_policy = SubscriptionPolicy.moderate self._mlist.unsubscription_policy = SubscriptionPolicy.moderate msg = mfs("""\ To: [email protected] From: [email protected] Subject: message 1 """) # Hold this message. hold_message(self._mlist, msg, {}, 'Non-member post') # And a second one too. msg2 = mfs("""\ To: [email protected] From: [email protected] Subject: message 2 """) hold_message(self._mlist, msg2, {}, 'Some other reason') usermanager = getUtility(IUserManager) submanager = ISubscriptionManager(self._mlist) # Generate held subscription. usera = usermanager.make_user('*****@*****.**') usera.addresses[0].verified_on = usera.addresses[0].registered_on usera.preferred_address = usera.addresses[0] submanager.register(usera) # Generate a held unsubscription. userb = usermanager.make_user('*****@*****.**') userb.addresses[0].verified_on = userb.addresses[0].registered_on userb.preferred_address = userb.addresses[0] submanager.register(userb, pre_verified=True, pre_confirmed=True, pre_approved=True) submanager.unregister(userb) self._command = CliRunner()
def test_get_request_with_type(self): # get_request() takes an optional request type. request_id = hold_message(self._mlist, self._msg) # Submit a request with a non-matching type. This should return None # as if there were no matches. response = self._requests_db.get_request(request_id, RequestType.subscription) self.assertEqual(response, None) # Submit the same request with a matching type. key, data = self._requests_db.get_request(request_id, RequestType.held_message) self.assertEqual(key, '<alpha>') # It should also succeed with no optional request type given. key, data = self._requests_db.get_request(request_id) self.assertEqual(key, '<alpha>')
def test_get_request_with_type(self): # get_request() takes an optional request type. request_id = hold_message(self._mlist, self._msg) # Submit a request with a non-matching type. This should return None # as if there were no matches. response = self._requests_db.get_request( request_id, RequestType.subscription) self.assertEqual(response, None) # Submit the same request with a matching type. key, data = self._requests_db.get_request( request_id, RequestType.held_message) self.assertEqual(key, '<alpha>') # It should also succeed with no optional request type given. key, data = self._requests_db.get_request(request_id) self.assertEqual(key, '<alpha>')
def test_subscription_request_as_held_message(self): # Provide the request id of a subscription request using the held # message API returns a not-found even though the request id is # in the database. held_id = hold_message(self._mlist, self._msg) subscribe_id = hold_subscription( self._mlist, "*****@*****.**", "Bart Person", "xyz", DeliveryMode.regular, "en" ) config.db.store.commit() url = "http://*****:*****@example.com/held/{0}" with self.assertRaises(HTTPError) as cm: call_api(url.format(subscribe_id)) self.assertEqual(cm.exception.code, 404) # But using the held_id returns a valid response. response, content = call_api(url.format(held_id)) self.assertEqual(response["message_id"], "<alpha>")
def test_handle_message_with_comment(self): self._msg = mfs("""\ From: [email protected] To: [email protected] Subject: Hello Message-ID: <alpha> Something else. """) with transaction(): held_id = hold_message(self._mlist, self._msg) json, response = call_api( 'http://*****:*****@example.com' '/held/{}'.format(held_id), dict(action='reject', comment='Because I want to.')) self.assertEqual(response.status_code, 204)
def test_preserve_and_forward(self): # We can both preserve and forward the message. request_id = hold_message(self._mlist, self._msg) handle_message(self._mlist, request_id, Action.discard, preserve=True, forward=['*****@*****.**']) # The message is preserved in the store. message_store = getUtility(IMessageStore) preserved_message = message_store.get_message_by_id('<alpha>') self.assertEqual(preserved_message['message-id'], '<alpha>') # And the forwarded message lives in the virgin queue. messages = get_queue_messages('virgin') self.assertEqual(len(messages), 1) self.assertEqual(str(messages[0].msg['subject']), 'Forward of moderated message') self.assertEqual(messages[0].msgdata['recipients'], ['*****@*****.**'])
def test_subscription_request_as_held_message(self): # Provide the request id of a subscription request using the held # message API returns a not-found even though the request id is # in the database. held_id = hold_message(self._mlist, self._msg) subscribe_id = hold_subscription(self._mlist, '*****@*****.**', 'Bart Person', 'xyz', DeliveryMode.regular, 'en') config.db.store.commit() url = 'http://*****:*****@example.com/held/{0}' with self.assertRaises(HTTPError) as cm: call_api(url.format(subscribe_id)) self.assertEqual(cm.exception.code, 404) # But using the held_id returns a valid response. response, content = call_api(url.format(held_id)) self.assertEqual(response['message_id'], '<alpha>')
def test_subject_encoding_error(self): # GL#383: messages with badly encoded Subject headers crash the REST # server. self._msg = mfs("""\ From: [email protected] To: [email protected] Subject: =?GB2312?B?saa9o7fmtNPEpbVaQ2h1o6zDt7uoz+PX1L/guq7AtKGj?= Message-ID: <alpha> Something else. """) with transaction(): held_id = hold_message(self._mlist, self._msg) json, response = call_api( 'http://*****:*****@example.com/held') self.assertEqual(response.status_code, 200) self.assertEqual(json['total_size'], 1) self.assertEqual(json['entries'][0]['request_id'], held_id)
def test_requests_are_deleted_when_mailing_list_is_deleted(self): # When a mailing list is deleted, its requests database is deleted # too, e.g. all its message hold requests (but not the messages # themselves). msg = specialized_message_from_string("""\ From: [email protected] To: [email protected] Subject: Hold me Message-ID: <argon> """) request_id = hold_message(self._ant, msg) getUtility(IListManager).delete(self._ant) # This is a hack. ListRequests don't access self._mailinglist in # their get_request() method. requestsdb = IListRequests(self._bee) request = requestsdb.get_request(request_id) self.assertEqual(request, None) saved_message = getUtility(IMessageStore).get_message_by_id('<argon>') self.assertEqual(saved_message.as_string(), msg.as_string())
def test_held_message_count(self): # Initially, the count should be zero. url = 'http://*****:*****@example.com/held/count' json, resp = call_api(url) self.assertEqual(resp.status_code, 200) self.assertEqual(json['count'], 0) # Now, verify that we get the number when a held message is added. with transaction(): hold_message(self._mlist, self._msg) json, resp = call_api(url) self.assertEqual(resp.status_code, 200) self.assertEqual(json['count'], 1) # Hold some more to see if we get the right numbers. with transaction(): hold_message(self._mlist, self._msg) hold_message(self._mlist, self._msg) json, resp = call_api(url) self.assertEqual(resp.status_code, 200) self.assertEqual(json['count'], 3)
def test_non_preserving_disposition(self): # By default, disposed messages are not preserved. request_id = hold_message(self._mlist, self._msg) handle_message(self._mlist, request_id, Action.discard) message_store = getUtility(IMessageStore) self.assertIsNone(message_store.get_message_by_id('<alpha>'))
def test_only_return_this_lists_requests(self): # Issue #161: get_requests() returns requests that are not specific to # the mailing list in question. request_id = hold_message(self._mlist, self._msg) bee = create_list('*****@*****.**') self.assertIsNone(IListRequests(bee).get_request(request_id))
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 `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))