Exemple #1
0
def handle_unsubscription(mlist, id, action, comment=None):
    requestdb = IListRequests(mlist)
    key, data = requestdb.get_request(id)
    address = data['address']
    if action is Action.defer:
        # Nothing to do.
        return
    elif action is Action.discard:
        # Nothing to do except delete the request from the database.
        pass
    elif action is Action.reject:
        key, data = requestdb.get_request(id)
        _refuse(mlist, _('Unsubscription request'), address,
                comment or _('[No reason given]'))
    elif action is Action.accept:
        key, data = requestdb.get_request(id)
        try:
            delete_member(mlist, address)
        except NotAMemberError:
            # User has already been unsubscribed.
            pass
        slog.info('%s: deleted %s', mlist.fqdn_listname, address)
    else:
        raise AssertionError('Unexpected action: {0}'.format(action))
    # Delete the request from the database.
    requestdb.delete_request(id)
Exemple #2
0
 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.
     requests_db = IListRequests(self._mlist)
     key, data = requests_db.get_request(request_id)
     self.assertEqual(key, '<alpha>')
     handle_message(self._mlist, request_id, Action.hold)
     key, data = requests_db.get_request(request_id)
     self.assertEqual(key, '<alpha>')
Exemple #3
0
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))
Exemple #4
0
 def test_get_request_with_type(self):
     # get_request() takes an optional request type.
     request_id = hold_message(self._mlist, self._msg)
     requests_db = IListRequests(self._mlist)
     # Submit a request with a non-matching type.  This should return None
     # as if there were no matches.
     response = requests_db.get_request(
         request_id, RequestType.subscription)
     self.assertEqual(response, None)
     # Submit the same request with a matching type.
     key, data = 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 = requests_db.get_request(request_id)
     self.assertEqual(key, '<alpha>')
Exemple #5
0
 def on_post(self, request, response):
     try:
         validator = Validator(action=enum_validator(Action))
         arguments = validator(request)
     except ValueError as error:
         bad_request(response, str(error))
         return
     requests = IListRequests(self._mlist)
     try:
         request_id = int(self._request_id)
     except ValueError:
         bad_request(response)
         return
     results = requests.get_request(request_id)
     if results is None:
         not_found(response)
         return
     key, data = results
     try:
         request_type = RequestType[data['_request_type']]
     except ValueError:
         bad_request(response)
         return
     if request_type is RequestType.subscription:
         handle_subscription(self._mlist, request_id, **arguments)
     elif request_type is RequestType.unsubscription:
         handle_unsubscription(self._mlist, request_id, **arguments)
     else:
         bad_request(response)
         return
     no_content(response)
Exemple #6
0
 def _make_resource(self, request_id):
     requests = IListRequests(self._mlist)
     results = requests.get_request(request_id)
     if results is None:
         return None
     key, data = results
     resource = dict(key=key, request_id=request_id)
     # Flatten the IRequest payload into the JSON representation.
     if data is not None:
         resource.update(data)
     # Check for a matching request type, and insert the type name into the
     # resource.
     try:
         request_type = RequestType[resource.pop('_request_type', None)]
     except KeyError:
         request_type = None
     if request_type is not RequestType.held_message:
         return None
     resource['type'] = RequestType.held_message.name
     # This key isn't what you think it is.  Usually, it's the Pendable
     # record's row id, which isn't helpful at all.  If it's not there,
     # that's fine too.
     resource.pop('id', None)
     # Add a self_link.
     resource['self_link'] = self.api.path_to(
         'lists/{}/held/{}'.format(self._mlist.list_id, request_id))
     return resource
Exemple #7
0
 def moderate(self, request):
     try:
         validator = Validator(action=enum_validator(Action))
         arguments = validator(request)
     except ValueError as error:
         return http.bad_request([], str(error))
     requests = IListRequests(self._mlist)
     try:
         request_id = int(self._request_id)
     except ValueError:
         return http.bad_request()
     results = requests.get_request(request_id)
     if results is None:
         return http.not_found()
     key, data = results
     try:
         request_type = RequestType(data['_request_type'])
     except ValueError:
         return http.bad_request()
     if request_type is RequestType.subscription:
         handle_subscription(self._mlist, request_id, **arguments)
     elif request_type is RequestType.unsubscription:
         handle_unsubscription(self._mlist, request_id, **arguments)
     else:
         return http.bad_request()
     return no_content()
Exemple #8
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)
     requests_db = IListRequests(self._mlist)
     key, data = requests_db.get_request(request_id)
     self.assertEqual(data['received_time'], received_time)
Exemple #9
0
def handle_subscription(mlist, id, action, comment=None):
    requestdb = IListRequests(mlist)
    if action is Action.defer:
        # Nothing to do.
        return
    elif action is Action.discard:
        # Nothing to do except delete the request from the database.
        pass
    elif action is Action.reject:
        key, data = requestdb.get_request(id)
        _refuse(mlist, _('Subscription request'),
                data['address'],
                comment or _('[No reason given]'),
                lang=getUtility(ILanguageManager)[data['language']])
    elif action is Action.accept:
        key, data = requestdb.get_request(id)
        enum_value = data['delivery_mode'].split('.')[-1]
        delivery_mode = DeliveryMode(enum_value)
        address = data['address']
        display_name = data['display_name']
        language = getUtility(ILanguageManager)[data['language']]
        password = data['password']
        try:
            add_member(mlist, address, display_name, password,
                       delivery_mode, language)
        except AlreadySubscribedError:
            # The address got subscribed in some other way after the original
            # request was made and accepted.
            pass
        else:
            if mlist.send_welcome_message:
                send_welcome_message(mlist, address, language, delivery_mode)
            if mlist.admin_notify_mchanges:
                send_admin_subscription_notice(
                    mlist, address, display_name, language)
        slog.info('%s: new %s, %s %s', mlist.fqdn_listname,
                  delivery_mode, formataddr((display_name, address)),
                  'via admin approval')
    else:
        raise AssertionError('Unexpected action: {0}'.format(action))
    # Delete the request from the database.
    requestdb.delete_request(id)
Exemple #10
0
def auto_discard(mlist):
    # Discard old held messages
    discard_count = 0
    expire = config.days(mlist.max_days_to_hold)
    requestsdb = IListRequests(mlist)
    heldmsgs = list(requestsdb.of_type(RequestType.held_message))
    if expire and heldmsgs:
        for request in heldmsgs:
            key, data = requestsdb.get_request(request.id)
            if now - data['date'] > expire:
                handle_request(mlist, request.id, config.DISCARD)
                discard_count += 1
        mlist.Save()
    return discard_count
Exemple #11
0
def handle_subscription(mlist, id, action, comment=None):
    requestdb = IListRequests(mlist)
    if action is Action.defer:
        # Nothing to do.
        return
    elif action is Action.discard:
        # Nothing to do except delete the request from the database.
        pass
    elif action is Action.reject:
        key, data = requestdb.get_request(id)
        send_rejection(
            mlist, _('Subscription request'),
            data['email'],
            comment or _('[No reason given]'),
            lang=getUtility(ILanguageManager)[data['language']])
    elif action is Action.accept:
        key, data = requestdb.get_request(id)
        delivery_mode = DeliveryMode[data['delivery_mode']]
        email = data['email']
        display_name = data['display_name']
        language = getUtility(ILanguageManager)[data['language']]
        try:
            add_member(
                mlist,
                RequestRecord(email, display_name, delivery_mode, language))
        except AlreadySubscribedError:
            # The address got subscribed in some other way after the original
            # request was made and accepted.
            pass
        slog.info('%s: new %s, %s %s', mlist.fqdn_listname,
                  delivery_mode, formataddr((display_name, email)),
                  'via admin approval')
    else:
        raise AssertionError('Unexpected action: {0}'.format(action))
    # Delete the request from the database.
    requestdb.delete_request(id)
Exemple #12
0
 def moderate(self, request):
     try:
         validator = Validator(action=enum_validator(Action))
         arguments = validator(request)
     except ValueError as error:
         return http.bad_request([], str(error))
     requests = IListRequests(self._mlist)
     try:
         request_id = int(self._request_id)
     except ValueError:
         return http.bad_request()
     results = requests.get_request(request_id, RequestType.held_message)
     if results is None:
         return http.not_found()
     handle_message(self._mlist, request_id, **arguments)
     return no_content()
Exemple #13
0
    def test_hold_chain_no_reasons_given(self):
        msg = mfs("""\
From: [email protected]
To: [email protected]
Subject: A message
Message-ID: <ant>
MIME-Version: 1.0

A message body.
""")
        process_chain(self._mlist, msg, {}, start_chain='hold')
        # No reason was given, so a default is used.
        requests = IListRequests(self._mlist)
        self.assertEqual(requests.count_of(RequestType.held_message), 1)
        request = requests.of_type(RequestType.held_message)[0]
        key, data = requests.get_request(request.id)
        self.assertEqual(data.get('_mod_reason'), 'n/a')
Exemple #14
0
    def test_hold_chain(self):
        msg = mfs("""\
From: [email protected]
To: [email protected]
Subject: A message
Message-ID: <ant>
MIME-Version: 1.0

A message body.
""")
        msgdata = dict(moderation_reasons=[
            'TEST-REASON-1',
            'TEST-REASON-2',
            ('TEST-{}-REASON-{}', 'FORMAT', 3),
            ])
        logfile = LogFileMark('mailman.vette')
        process_chain(self._mlist, msg, msgdata, start_chain='hold')
        messages = get_queue_messages('virgin', expected_count=2)
        payloads = {}
        for item in messages:
            if item.msg['to'] == '*****@*****.**':
                part = item.msg.get_payload(0)
                payloads['owner'] = part.get_payload().splitlines()
            elif item.msg['To'] == '*****@*****.**':
                payloads['sender'] = item.msg.get_payload().splitlines()
            else:
                self.fail('Unexpected message: %s' % item.msg)
        self.assertIn('    TEST-REASON-1', payloads['owner'])
        self.assertIn('    TEST-REASON-2', payloads['owner'])
        self.assertIn('    TEST-FORMAT-REASON-3', payloads['owner'])
        self.assertIn('    TEST-REASON-1', payloads['sender'])
        self.assertIn('    TEST-REASON-2', payloads['sender'])
        self.assertIn('    TEST-FORMAT-REASON-3', payloads['sender'])
        logged = logfile.read()
        self.assertIn('TEST-REASON-1', logged)
        self.assertIn('TEST-REASON-2', logged)
        self.assertIn('TEST-FORMAT-REASON-3', logged)
        # Check the reason passed to hold_message().
        requests = IListRequests(self._mlist)
        self.assertEqual(requests.count_of(RequestType.held_message), 1)
        request = requests.of_type(RequestType.held_message)[0]
        key, data = requests.get_request(request.id)
        self.assertEqual(
            data.get('_mod_reason'),
            'TEST-REASON-1; TEST-REASON-2; TEST-FORMAT-REASON-3')
Exemple #15
0
 def details(self, request):
     requests = IListRequests(self._mlist)
     try:
         request_id = int(self._request_id)
     except ValueError:
         return http.bad_request()
     results = requests.get_request(request_id, RequestType.held_message)
     if results is None:
         return http.not_found()
     key, data = results
     msg = getUtility(IMessageStore).get_message_by_id(key)
     resource = dict(
         key=key,
         data=data,
         msg=msg.as_string(),
         id=request_id,
         )
     return http.ok([], etag(resource))
 def on_post(self, request, response):
     try:
         validator = Validator(action=enum_validator(Action))
         arguments = validator(request)
     except ValueError as error:
         bad_request(response, str(error))
         return
     requests = IListRequests(self._mlist)
     try:
         request_id = int(self._request_id)
     except ValueError:
         bad_request(response)
         return
     results = requests.get_request(request_id, RequestType.held_message)
     if results is None:
         not_found(response)
     else:
         handle_message(self._mlist, request_id, **arguments)
         no_content(response)
 def on_post(self, request, response):
     try:
         validator = Validator(action=enum_validator(Action))
         arguments = validator(request)
     except ValueError as error:
         bad_request(response, str(error))
         return
     requests = IListRequests(self._mlist)
     try:
         request_id = int(self._request_id)
     except ValueError:
         not_found(response)
         return
     results = requests.get_request(request_id, RequestType.held_message)
     if results is None:
         not_found(response)
     else:
         handle_message(self._mlist, request_id, **arguments)
         no_content(response)
Exemple #18
0
    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())
Exemple #19
0
 def _make_resource(self, request_id, expected_request_types):
     requests = IListRequests(self._mlist)
     results = requests.get_request(request_id)
     if results is None:
         return None
     key, data = results
     resource = dict(key=key, request_id=request_id)
     # Flatten the IRequest payload into the JSON representation.
     resource.update(data)
     # Check for a matching request type, and insert the type name into the
     # resource.
     request_type = RequestType(resource.pop('_request_type'))
     if request_type not in expected_request_types:
         return None
     resource['type'] = request_type.name
     # This key isn't what you think it is.  Usually, it's the Pendable
     # record's row id, which isn't helpful at all.  If it's not there,
     # that's fine too.
     resource.pop('id', None)
     return resource
    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())
Exemple #21
0
 def _make_resource(self, request_id):
     requests = IListRequests(self._mlist)
     results = requests.get_request(request_id)
     if results is None:
         return None
     key, data = results
     resource = dict(key=key, request_id=request_id)
     # Flatten the IRequest payload into the JSON representation.
     resource.update(data)
     # Check for a matching request type, and insert the type name into the
     # resource.
     request_type = RequestType[resource.pop('_request_type')]
     if request_type is not RequestType.held_message:
         return None
     resource['type'] = RequestType.held_message.name
     # This key isn't what you think it is.  Usually, it's the Pendable
     # record's row id, which isn't helpful at all.  If it's not there,
     # that's fine too.
     resource.pop('id', None)
     return resource
Exemple #22
0
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)
Exemple #23
0
def handle_message(mlist, id, action, comment=None, forward=None):
    message_store = getUtility(IMessageStore)
    requestdb = IListRequests(mlist)
    key, msgdata = requestdb.get_request(id)
    # Handle the action.
    rejection = None
    message_id = msgdata['_mod_message_id']
    sender = msgdata['_mod_sender']
    subject = msgdata['_mod_subject']
    keep = False
    if action in (Action.defer, Action.hold):
        # Nothing to do, but preserve the message for later.
        keep = True
    elif action is Action.discard:
        rejection = 'Discarded'
    elif action is Action.reject:
        rejection = 'Refused'
        member = mlist.members.get_member(sender)
        if member:
            language = member.preferred_language
        else:
            language = None
        send_rejection(
            mlist, _('Posting of your message titled "$subject"'),
            sender, comment or _('[No reason given]'), language)
    elif action is Action.accept:
        # Start by getting the message from the message store.
        msg = message_store.get_message_by_id(message_id)
        # Delete moderation-specific entries from the message metadata.
        for key in list(msgdata):
            if key.startswith('_mod_'):
                del msgdata[key]
        # Add some metadata to indicate this message has now been approved.
        msgdata['approved'] = True
        msgdata['moderator_approved'] = True
        # Calculate a new filebase for the approved message, otherwise
        # delivery errors will cause duplicates.
        if 'filebase' in msgdata:
            del msgdata['filebase']
        # Queue the file for delivery.  Trying to deliver the message directly
        # here can lead to a huge delay in web turnaround.  Log the moderation
        # and add a header.
        msg['X-Mailman-Approved-At'] = formatdate(
            time.mktime(now().timetuple()), localtime=True)
        vlog.info('held message approved, message-id: %s',
                  msg.get('message-id', 'n/a'))
        # Stick the message back in the incoming queue for further
        # processing.
        config.switchboards['pipeline'].enqueue(msg, _metadata=msgdata)
    else:
        raise AssertionError('Unexpected action: {0}'.format(action))
    # Forward the message.
    if forward:
        # Get a copy of the original message from the message store.
        msg = message_store.get_message_by_id(message_id)
        # It's possible the forwarding address list is a comma separated list
        # of display_name/address pairs.
        addresses = [addr[1] for addr in getaddresses(forward)]
        language = mlist.preferred_language
        if len(addresses) == 1:
            # If the address getting the forwarded message is a member of
            # the list, we want the headers of the outer message to be
            # encoded in their language.  Otherwise it'll be the preferred
            # language of the mailing list.  This is better than sending a
            # separate message per recipient.
            member = mlist.members.get_member(addresses[0])
            if member:
                language = member.preferred_language
        with _.using(language.code):
            fmsg = UserNotification(
                addresses, mlist.bounces_address,
                _('Forward of moderated message'),
                lang=language)
        fmsg.set_type('message/rfc822')
        fmsg.attach(msg)
        fmsg.send(mlist)
    # Delete the request if it's not being kept.
    if not keep:
        requestdb.delete_request(id)
    # Log the rejection
    if rejection:
        note = """%s: %s posting:
\tFrom: %s
\tSubject: %s"""
        if comment:
            note += '\n\tReason: ' + comment
        vlog.info(note, mlist.fqdn_listname, rejection, sender, subject)
Exemple #24
0
class TestModeration(unittest.TestCase):
    """Test moderation functionality."""

    layer = SMTPLayer

    def setUp(self):
        self._mlist = create_list('*****@*****.**')
        self._request_db = IListRequests(self._mlist)
        self._msg = specialized_message_from_string("""\
From: [email protected]
To: [email protected]
Subject: hold me
Message-ID: <alpha>

""")
        self._in = make_testable_runner(IncomingRunner, 'in')
        self._pipeline = make_testable_runner(PipelineRunner, 'pipeline')
        self._out = make_testable_runner(OutgoingRunner, 'out')

    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_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_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_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_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_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'],
                         ['*****@*****.**'])
Exemple #25
0
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))
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))
Exemple #27
0
def pending_requests(mlist):
    # Must return a byte string
    lcset = mlist.preferred_language.charset
    pending = []
    first = True
    requestsdb = IListRequests(mlist)
    for request in requestsdb.of_type(RequestType.subscription):
        if first:
            pending.append(_('Pending subscriptions:'))
            first = False
        key, data = requestsdb.get_request(request.id)
        when = data['when']
        addr = data['addr']
        fullname = data['fullname']
        passwd = data['passwd']
        digest = data['digest']
        lang = data['lang']
        if fullname:
            if isinstance(fullname, unicode):
                fullname = fullname.encode(lcset, 'replace')
            fullname = ' (%s)' % fullname
        pending.append('    %s%s %s' % (addr, fullname, time.ctime(when)))
    first = True
    for request in requestsdb.of_type(RequestType.held_message):
        if first:
            pending.append(_('\nPending posts:'))
            first = False
        key, data = requestsdb.get_request(request.id)
        when = data['when']
        sender = data['sender']
        subject = data['subject']
        reason = data['reason']
        text = data['text']
        msgdata = data['msgdata']
        subject = Utils.oneline(subject, lcset)
        date = time.ctime(when)
        reason = _(reason)
        pending.append(
            _("""\
From: $sender on $date
Subject: $subject
Cause: $reason"""))
        pending.append('')
    # Coerce all items in pending to a Unicode so we can join them
    upending = []
    charset = mlist.preferred_language.charset
    for s in pending:
        if isinstance(s, unicode):
            upending.append(s)
        else:
            upending.append(unicode(s, charset, 'replace'))
    # Make sure that the text we return from here can be encoded to a byte
    # string in the charset of the list's language.  This could fail if for
    # example, the request was pended while the list's language was French,
    # but then it was changed to English before checkdbs ran.
    text = NL.join(upending)
    charset = Charset(mlist.preferred_language.charset)
    incodec = charset.input_codec or 'ascii'
    outcodec = charset.output_codec or 'ascii'
    if isinstance(text, unicode):
        return text.encode(outcodec, 'replace')
    # Be sure this is a byte string encodeable in the list's charset
    utext = unicode(text, incodec, 'replace')
    return utext.encode(outcodec, 'replace')
class TestModeration(unittest.TestCase):
    """Test moderation functionality."""

    layer = SMTPLayer

    def setUp(self):
        self._mlist = create_list('*****@*****.**')
        self._request_db = IListRequests(self._mlist)
        self._msg = specialized_message_from_string("""\
From: [email protected]
To: [email protected]
Subject: hold me
Message-ID: <alpha>

""")
        self._in = make_testable_runner(IncomingRunner, 'in')
        self._pipeline = make_testable_runner(PipelineRunner, 'pipeline')
        self._out = make_testable_runner(OutgoingRunner, 'out')

    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_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_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_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_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_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'],
                         ['*****@*****.**'])
Exemple #29
0
def pending_requests(mlist):
    # Must return a byte string
    lcset = mlist.preferred_language.charset
    pending = []
    first = True
    requestsdb = IListRequests(mlist)
    for request in requestsdb.of_type(RequestType.subscription):
        if first:
            pending.append(_('Pending subscriptions:'))
            first = False
        key, data = requestsdb.get_request(request.id)
        when = data['when']
        addr = data['addr']
        fullname = data['fullname']
        passwd = data['passwd']
        digest = data['digest']
        lang = data['lang']
        if fullname:
            if isinstance(fullname, unicode):
                fullname = fullname.encode(lcset, 'replace')
            fullname = ' (%s)' % fullname
        pending.append('    %s%s %s' % (addr, fullname, time.ctime(when)))
    first = True
    for request in requestsdb.of_type(RequestType.held_message):
        if first:
            pending.append(_('\nPending posts:'))
            first = False
        key, data = requestsdb.get_request(request.id)
        when = data['when']
        sender = data['sender']
        subject = data['subject']
        reason = data['reason']
        text = data['text']
        msgdata = data['msgdata']
        subject = Utils.oneline(subject, lcset)
        date = time.ctime(when)
        reason = _(reason)
        pending.append(_("""\
From: $sender on $date
Subject: $subject
Cause: $reason"""))
        pending.append('')
    # Coerce all items in pending to a Unicode so we can join them
    upending = []
    charset = mlist.preferred_language.charset
    for s in pending:
        if isinstance(s, unicode):
            upending.append(s)
        else:
            upending.append(unicode(s, charset, 'replace'))
    # Make sure that the text we return from here can be encoded to a byte
    # string in the charset of the list's language.  This could fail if for
    # example, the request was pended while the list's language was French,
    # but then it was changed to English before checkdbs ran.
    text = NL.join(upending)
    charset = Charset(mlist.preferred_language.charset)
    incodec = charset.input_codec or 'ascii'
    outcodec = charset.output_codec or 'ascii'
    if isinstance(text, unicode):
        return text.encode(outcodec, 'replace')
    # Be sure this is a byte string encodeable in the list's charset
    utext = unicode(text, incodec, 'replace')
    return utext.encode(outcodec, 'replace')