class TestRequests(unittest.TestCase): layer = ConfigLayer def setUp(self): self._mlist = create_list('*****@*****.**') self._requests_db = IListRequests(self._mlist) self._msg = mfs("""\ From: [email protected] To: [email protected] Subject: Something Message-ID: <alpha> Something else. """) 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_hold_with_bogus_type(self): # Calling hold_request() with a bogus request type is an error. with self.assertRaises(TypeError) as cm: self._requests_db.hold_request(5, 'foo') self.assertEqual(cm.exception.args[0], 5) def test_delete_missing_request(self): # Trying to delete a missing request is an error. with self.assertRaises(KeyError) as cm: self._requests_db.delete_request(801) self.assertEqual(cm.exception.args[0], 801) 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 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 hold_unsubscription(mlist, email): data = dict(email=email) requestsdb = IListRequests(mlist) request_id = requestsdb.hold_request(RequestType.unsubscription, email, data) vlog.info('%s: held unsubscription request from %s', mlist.fqdn_listname, email) # Possibly notify the administrator of the hold if mlist.admin_immed_notify: subject = _( 'New unsubscription request from $mlist.display_name by $email') template = getUtility(ITemplateLoader).get( 'list:admin:action:unsubscribe', mlist) text = wrap( expand( template, mlist, dict( # For backward compatibility. mailing_list=mlist, member=email, email=email, ))) # This message should appear to come from the <list>-owner so as # to avoid any useless bounce processing. msg = UserNotification(mlist.owner_address, mlist.owner_address, subject, text, mlist.preferred_language) msg.send(mlist) return request_id
def 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 test_request_is_not_held_message(self): requests = IListRequests(self._mlist) with transaction(): request_id = requests.hold_request(RequestType.subscription, 'foo') with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/lists/ant.example.com' '/held/{}'.format(request_id)) self.assertEqual(cm.exception.code, 404)
def test_request_is_not_held_message(self): requests = IListRequests(self._mlist) with transaction(): request_id = requests.hold_request(RequestType.subscription, 'foo') with self.assertRaises(HTTPError) as cm: call_api('http://localhost:9001/3.0/lists/ant.example.com' '/held/{}'.format(request_id)) self.assertEqual(cm.exception.code, 404)
class TestRequests(unittest.TestCase): layer = ConfigLayer def setUp(self): self._mlist = create_list('*****@*****.**') self._requests_db = IListRequests(self._mlist) self._msg = mfs("""\ From: [email protected] To: [email protected] Subject: Something Message-ID: <alpha> Something else. """) 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_hold_with_bogus_type(self): # Calling hold_request() with a bogus request type is an error. with self.assertRaises(TypeError) as cm: self._requests_db.hold_request(5, 'foo') self.assertEqual(cm.exception.args[0], 5) def test_delete_missing_request(self): # Trying to delete a missing request is an error. with self.assertRaises(KeyError) as cm: self._requests_db.delete_request(801) self.assertEqual(cm.exception.args[0], 801)
def hold_message(mlist, msg, msgdata=None, reason=None): """Hold a message for moderator approval. The message is added to the mailing list's request database. :param mlist: The mailing list to hold the message on. :param msg: The message to hold. :param msgdata: Optional message metadata to hold. If not given, a new metadata dictionary is created and held with the message. :param reason: Optional string reason why the message is being held. If not given, the empty string is used. :return: An id used to handle the held message later. """ if msgdata is None: msgdata = {} else: # Make a copy of msgdata so that subsequent changes won't corrupt the # request database. TBD: remove the `filebase' key since this will # not be relevant when the message is resurrected. msgdata = msgdata.copy() if reason is None: reason = '' # Add the message to the message store. It is required to have a # Message-ID header. message_id = msg.get('message-id') if message_id is None: msg['Message-ID'] = message_id = make_msgid() elif isinstance(message_id, bytes): message_id = message_id.decode('ascii') getUtility(IMessageStore).add(msg) # Prepare the message metadata with some extra information needed only by # the moderation interface. msgdata['_mod_message_id'] = message_id msgdata['_mod_listid'] = mlist.list_id msgdata['_mod_sender'] = msg.sender # The subject can sometimes be a Header instance. Stringify it. msgdata['_mod_subject'] = str(msg.get('subject', _('(no subject)'))) msgdata['_mod_reason'] = reason msgdata['_mod_hold_date'] = now().isoformat() # Now hold this request. We'll use the message_id as the key. requestsdb = IListRequests(mlist) request_id = requestsdb.hold_request(RequestType.held_message, message_id, msgdata) return request_id
def hold_message(mlist, msg, msgdata=None, reason=None): """Hold a message for moderator approval. The message is added to the mailing list's request database. :param mlist: The mailing list to hold the message on. :param msg: The message to hold. :param msgdata: Optional message metadata to hold. If not given, a new metadata dictionary is created and held with the message. :param reason: Optional string reason why the message is being held. If not given, the empty string is used. :return: An id used to handle the held message later. """ if msgdata is None: msgdata = {} else: # Make a copy of msgdata so that subsequent changes won't corrupt the # request database. TBD: remove the `filebase' key since this will # not be relevant when the message is resurrected. msgdata = msgdata.copy() if reason is None: reason = '' # Add the message to the message store. It is required to have a # Message-ID header. message_id = msg.get('message-id') if message_id is None: msg['Message-ID'] = message_id = unicode(make_msgid()) assert isinstance(message_id, unicode), ( 'Message-ID is not a unicode: %s' % message_id) getUtility(IMessageStore).add(msg) # Prepare the message metadata with some extra information needed only by # the moderation interface. msgdata['_mod_message_id'] = message_id msgdata['_mod_fqdn_listname'] = mlist.fqdn_listname msgdata['_mod_sender'] = msg.sender msgdata['_mod_subject'] = msg.get('subject', _('(no subject)')) msgdata['_mod_reason'] = reason msgdata['_mod_hold_date'] = now().isoformat() # Now hold this request. We'll use the message_id as the key. requestsdb = IListRequests(mlist) request_id = requestsdb.hold_request( RequestType.held_message, message_id, msgdata) return request_id
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 hold_unsubscription(mlist, email): data = dict(email=email) requestsdb = IListRequests(mlist) request_id = requestsdb.hold_request( RequestType.unsubscription, email, data) vlog.info('%s: held unsubscription request from %s', mlist.fqdn_listname, email) # Possibly notify the administrator of the hold if mlist.admin_immed_notify: subject = _( 'New unsubscription request from $mlist.display_name by $email') text = make('unsubauth.txt', mailing_list=mlist, email=email, 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
class TestRequests(unittest.TestCase): layer = ConfigLayer def setUp(self): self._mlist = create_list('*****@*****.**') self._requests_db = IListRequests(self._mlist) self._msg = mfs("""\ From: [email protected] To: [email protected] Subject: Something Message-ID: <alpha> Something else. """) 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_hold_with_bogus_type(self): # Calling hold_request() with a bogus request type is an error. with self.assertRaises(TypeError) as cm: self._requests_db.hold_request(5, 'foo') self.assertEqual(cm.exception.args[0], 5) def test_delete_missing_request(self): # Trying to delete a missing request is an error. with self.assertRaises(KeyError) as cm: self._requests_db.delete_request(801) self.assertEqual(cm.exception.args[0], 801) 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 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))
class TestRequests(unittest.TestCase): layer = ConfigLayer def setUp(self): self._mlist = create_list('*****@*****.**') self._requests_db = IListRequests(self._mlist) self._msg = mfs("""\ From: [email protected] To: [email protected] Subject: Something Message-ID: <alpha> Something else. """) 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_hold_with_bogus_type(self): # Calling hold_request() with a bogus request type is an error. with self.assertRaises(TypeError) as cm: self._requests_db.hold_request(5, 'foo') self.assertEqual(cm.exception.args[0], 5) def test_delete_missing_request(self): # Trying to delete a missing request is an error. with self.assertRaises(KeyError) as cm: self._requests_db.delete_request(801) self.assertEqual(cm.exception.args[0], 801) 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 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))