def test_enqueue_wait_partial_relay_expired(self): env = Envelope( '*****@*****.**', ['*****@*****.**', '*****@*****.**', '*****@*****.**']) bounce_mock = self.mox.CreateMockAnything() bounce_mock(IsA(Envelope), IsA(Reply)).AndReturn(None) bounce_mock(IsA(Envelope), IsA(Reply)).AndReturn(None) self.store.write(env, IsA(float)).AndReturn('1234') self.relay._attempt(env, 0).AndReturn({ '*****@*****.**': TransientRelayError('transient', Reply('450', 'transient 1')), '*****@*****.**': TransientRelayError('transient', Reply('450', 'transient 1')), '*****@*****.**': TransientRelayError('transient', Reply('450', 'transient 2')) }) self.store.increment_attempts('1234') self.store.remove('1234') self.mox.ReplayAll() queue = Queue(self.store, self.relay, bounce_factory=bounce_mock, relay_pool=5) queue.enqueue(env) queue.relay_pool.join()
def test_str(self): if PY3: r = Reply('250', '2.1.0 Ok \U0001f44d') self.assertEqual('250 2.1.0 Ok \U0001f44d', str(r)) else: r = Reply('250', u'2.1.0 Ok \U0001f44d') self.assertEqual('250 2.1.0 Ok \xf0\x9f\x91\x8d', str(r))
def apply(self, envelope): sender_email = parseaddr(envelope.headers.get('From'))[1] try: validate_email(sender_email) except ValidationError: sender_email = None if not sender_email: error = QueueError() error.reply = Reply( code='554', message=( '5.7.1 <{}>: Relay access denied: ' 'Sending domain invalid').format(envelope.recipients[0])) raise error organization_domain = SendingDomain.objects.filter( organization=envelope.organization) domain = organization_domain.get_from_email_addr( sender_email, must_raise=False) if not domain: error = QueueError() error.reply = Reply( code='554', message=( '5.7.1 <{}>: Relay access denied: Sending ' 'domain for <{}> is not properly configured').format( envelope.recipients[0], sender_email)) raise error envelope.sending_domain = domain
def raise_error(self, status, stdout, stderr): """This method may be over-ridden by sub-classes if you need to control how the relay error is generated. By default, the error raised is a :class:`~slimta.relay.TransientRelayError` unless the process output begins with a ``5.X.X`` enhanced status code. This behavior attempts to mimic the postfix pipe_ daemon. This method is only called if the subprocess returns a non-zero exit status. :param status: The non-zero exit status of the subprocess. :param stdout: The subprocess's standard output, as received by :py:meth:`~subprocess.Popen.communicate`. :type stdout: string :param stderr: The subprocess's standard error output, as received by :py:meth:`~subprocess.Popen.communicate`. :type stderr: string :raises: :class:`~slimta.relay.TransientRelayError`, :class:`~slimta.relay.PermanentRelayError` """ error_msg = stdout.rstrip() or stderr.rstrip() or 'Delivery failed' if isinstance(error_msg, bytes): error_msg = error_msg.decode('utf-8') if self._permanent_error_pattern.match(error_msg): reply = Reply('550', error_msg) raise PermanentRelayError(error_msg, reply) else: reply = Reply('450', error_msg) raise TransientRelayError(error_msg, reply)
def test_enqueue_wait_partial_relay(self): env = Envelope( '*****@*****.**', ['*****@*****.**', '*****@*****.**', '*****@*****.**']) self.store.write(env, IsA(float)).AndReturn('1234') self.relay._attempt(env, 0).AndReturn({ '*****@*****.**': None, '*****@*****.**': TransientRelayError('transient', Reply('450', 'transient')), '*****@*****.**': PermanentRelayError('permanent', Reply('550', 'permanent')) }) self.store.increment_attempts('1234') self.store.set_timestamp('1234', IsA(float)) self.store.set_recipients_delivered('1234', set([0, 2])) self.mox.ReplayAll() def backoff(envelope, attempts): return 0 def no_bounce(envelope, reply): return None queue = Queue(self.store, self.relay, backoff=backoff, bounce_factory=no_bounce, relay_pool=5) queue.enqueue(env) queue.relay_pool.join()
def test_copy(self): r1 = Reply('250', '2.1.0 Ok') r2 = Reply(command='RCPT') r2.copy(r1) assert_equal('250', r2.code) assert_equal('2.1.0', r2.enhanced_status_code) assert_equal('2.1.0 Ok', r2.message) assert_equal('RCPT', r2.command)
def raise_error(self, status, stdout, stderr): error_msg = stdout.rstrip() or stderr.rstrip() or 'LDA delivery failed' if status == self.EX_TEMPFAIL: reply = Reply('450', error_msg) raise TransientRelayError(error_msg, reply) else: reply = Reply('550', error_msg) raise PermanentRelayError(error_msg, reply)
def test_eq(self): r1 = Reply('250', '2.1.0 Ok') r2 = Reply('250', '2.1.0 Ok') r3 = Reply('251', '2.1.0 Ok') r4 = Reply('250', '2.1.1 Ok') self.assertEqual(r1, r2) self.assertNotEqual(r1, r3) self.assertNotEqual(r1, r4) self.assertNotEqual(r3, r4)
def _enqueue_envelope(self, env): results = self.handoff(env) if isinstance(results[0][1], QueueError): reply = Reply('550', '5.6.0 Error queuing message') raise _build_http_response(reply) elif isinstance(results[0][1], RelayError): relay_reply = results[0][1].reply raise _build_http_response(relay_reply) reply = Reply('250', '2.6.0 Message accepted for delivery') raise _build_http_response(reply)
def test_simple_handshake(self): mock = self.mox.CreateMockAnything() mock.__call__(IsA(SmtpSession)).AndReturn(mock) mock.handle_banner(IsA(Reply), ('127.0.0.1', 0)) mock.handle_helo(IsA(Reply), 'there') self.mox.ReplayAll() h = SmtpSession(('127.0.0.1', 0), mock, None) h.BANNER_(Reply('220')) h.HELO(Reply('250'), 'there') self.assertEqual('there', h.ehlo_as) self.assertFalse(h.extended_smtp)
def raise_error(self, status, stdout, stderr): error_msg = 'Delivery failed' if stdout.startswith('maildrop: '): error_msg = stdout[10:].rstrip() elif stderr.startswith('maildrop: '): error_msg = stderr[10:].rstrip() if status == self.EX_TEMPFAIL: reply = Reply('450', error_msg) raise TransientRelayError(error_msg, reply) else: reply = Reply('550', error_msg) raise PermanentRelayError(error_msg, reply)
def test_mail_rcpt_data_rset(self): mock = self.mox.CreateMockAnything() mock.__call__(IsA(SmtpSession)).AndReturn(mock) mock.handle_mail(IsA(Reply), '*****@*****.**', {}) mock.handle_rcpt(IsA(Reply), '*****@*****.**', {}) mock.handle_data(IsA(Reply)) self.mox.ReplayAll() h = SmtpSession(None, mock, None) h.MAIL(Reply('250'), '*****@*****.**', {}) h.RCPT(Reply('250'), '*****@*****.**', {}) self.assertEqual('*****@*****.**', h.envelope.sender) self.assertEqual(['*****@*****.**'], h.envelope.recipients) h.DATA(Reply('550')) h.RSET(Reply('250')) self.assertFalse(h.envelope)
def test_deliver_conversion(self): result = AsyncResult() env = Envelope('*****@*****.**', ['*****@*****.**']) env.parse(b'From: [email protected]\r\n\r\ntest test \x81\r\n') self.sock.sendall(b'EHLO there\r\n') self.sock.recv(IsA(int)).AndReturn(b'250-Hello\r\n250 PIPELINING\r\n') self.sock.sendall( b'MAIL FROM:<*****@*****.**>\r\nRCPT TO:<*****@*****.**>\r\nDATA\r\n' ) self.sock.recv( IsA(int)).AndReturn(b'250 Ok\r\n250 Ok\r\n354 Go ahead\r\n') if pycompat.PY3: self.sock.sendall( b'From: [email protected]\r\nContent-Transfer-Encoding: base64\r\n\r\ndGVzdCB0ZXN0IIEK\r\n.\r\n' ) else: self.sock.sendall( b'From: [email protected]\r\nContent-Transfer-Encoding: base64\r\n\r\ndGVzdCB0ZXN0IIENCg==\r\n.\r\n' ) self.sock.recv(IsA(int)).AndReturn(b'250 Ok\r\n') self.mox.ReplayAll() client = SmtpRelayClient(('addr', 0), self.queue, socket_creator=self._socket_creator, ehlo_as='there', binary_encoder=encode_base64) client._connect() client._ehlo() client._deliver(result, env) self.assertEqual({'*****@*****.**': Reply('250', 'Ok')}, result.get_nowait())
def test_on_validate_rcpt(self): espf = EnforceSpf() espf.set_enforcement('fail', match_code='550') class TestSession(object): address = ('1.2.3.4', 56789) envelope = Envelope('*****@*****.**') ehlo_as = 'testehlo' class TestValidators(object): def __init__(self): self.session = TestSession() @espf.check def validate_rcpt(self, reply, recipient): pass spf.check2(i='1.2.3.4', s='*****@*****.**', h='testehlo').AndReturn(('fail', 'the reason')) self.mox.ReplayAll() validators = TestValidators() reply = Reply('250', '2.0.0 Ok') validators.validate_rcpt(reply, 'asdf') self.assertEqual('550', reply.code) self.assertEqual('5.7.1 Access denied', reply.message)
def test_deliver(self): result = AsyncResult() env = Envelope('*****@*****.**', ['*****@*****.**']) env.parse(b'From: [email protected]\r\n\r\ntest test \x81\r\n') self.sock.sendall(b'EHLO there\r\n') self.sock.recv(IsA(int)).AndReturn(b'250-Hello\r\n250 8BITMIME\r\n') self.sock.sendall(b'MAIL FROM:<*****@*****.**>\r\n') self.sock.recv(IsA(int)).AndReturn(b'250 Ok\r\n') self.sock.sendall(b'RCPT TO:<*****@*****.**>\r\n') self.sock.recv(IsA(int)).AndReturn(b'250 Ok\r\n') self.sock.sendall(b'DATA\r\n') self.sock.recv(IsA(int)).AndReturn(b'354 Go ahead\r\n') self.sock.sendall( b'From: [email protected]\r\n\r\ntest test \x81\r\n.\r\n') self.sock.recv(IsA(int)).AndReturn(b'250 Ok\r\n') self.mox.ReplayAll() client = SmtpRelayClient(('addr', 0), self.queue, socket_creator=self._socket_creator, ehlo_as='there') client._connect() client._ehlo() client._deliver(result, env) self.assertEqual({'*****@*****.**': Reply('250', 'Ok')}, result.get_nowait())
def _connect(self): try: with Timeout(self.connect_timeout): self.socket = self._socket_creator(self.address) except socket_error as (err, msg): reply = Reply('451', '4.3.0 Connection failed') raise SmtpRelayError.factory(reply)
def _run(self): result, envelope = self.poll() if not result: return try: self._connect() self._handshake() while result: if self._check_server_timeout(): self.queue.appendleft((result, envelope)) break if self._send_envelope(result, envelope): result.set(True) if self.idle_timeout is None: break result, envelope = self.poll() except SmtpRelayError as e: result.set_exception(e) except SmtpError as e: if not result.ready(): reply = Reply('421', '4.3.0 {0!s}'.format(e)) relay_error = SmtpRelayError.factory(reply) result.set_exception(relay_error) except Timeout: if not result.ready(): relay_error = SmtpRelayError.factory(timed_out) result.set_exception(relay_error) except Exception as e: if not result.ready(): result.set_exception(e) raise finally: self._disconnect()
def test_not_populated(self): r = Reply(command='SOMECOMMAND') assert_equal(None, r.code) assert_equal(None, r.message) assert_equal(None, r.enhanced_status_code) assert_false(r) assert_equal('SOMECOMMAND', r.command)
def test_message_set_clear_esc(self): r = Reply('250', '2.3.4 Ok') assert_equal('2.3.4 Ok', r.message) assert_equal('2.3.4', r.enhanced_status_code) r.message = None assert_equal(None, r.message) assert_equal('2.0.0', r.enhanced_status_code)
def test_check_dnsrbl(self): class TestSession(object): address = ('1.2.3.4', 56789) class TestValidators(object): def __init__(self): self.session = TestSession() @check_dnsbl('test.example.com') def validate_mail(self, reply, sender): assert False DNSResolver.query('4.3.2.1.test.example.com', 'A').AndRaise(DNSError(ARES_ENOTFOUND)) DNSResolver.query('4.3.2.1.test.example.com', 'A').AndReturn(FakeAsyncResult()) self.mox.ReplayAll() validators = TestValidators() reply = Reply('250', '2.0.0 Ok') self.assertRaises(AssertionError, validators.validate_mail, reply, 'asdf') self.assertEqual('250', reply.code) self.assertEqual('2.0.0 Ok', reply.message) validators.validate_mail(reply, 'asdf') self.assertEqual('550', reply.code) self.assertEqual('5.7.1 Access denied', reply.message)
def test_esc_set_false(self): r = Reply('250', 'Ok') assert_equal('2.0.0 Ok', r.message) r.enhanced_status_code = None assert_equal('2.0.0 Ok', r.message) r.enhanced_status_code = False assert_equal('Ok', r.message)
def _handle_encoding(self, envelope): if '8BITMIME' not in self.client.extensions: try: envelope.encode_7bit(self.binary_encoder) except UnicodeError: reply = Reply('554', '5.6.3 Conversion not allowed') raise SmtpRelayError.factory(reply)
def test_is_error(self): replies = [Reply(str(i) + '50', 'Test') for i in range(1, 6)] assert_false(replies[0].is_error()) assert_false(replies[1].is_error()) assert_false(replies[2].is_error()) assert_true(replies[3].is_error()) assert_true(replies[4].is_error())
def test_have_data_errors(self): h = SmtpSession(None, None, None) reply = Reply('250') h.HAVE_DATA(reply, None, MessageTooBig()) self.assertEqual('552', reply.code) with self.assertRaises(ValueError): h.HAVE_DATA(reply, None, ValueError())
def test_no_policy_match(self): espf = EnforceSpf() espf.set_enforcement('fail', match_code='550') class TestSession(object): address = ('1.2.3.4', 56789) envelope = None ehlo_as = 'testehlo' class TestValidators(object): def __init__(self): self.session = TestSession() @espf.check def validate_mail(self, reply, sender): pass spf.check2(i='1.2.3.4', s='*****@*****.**', h='testehlo').AndReturn(('none', 'the reason')) self.mox.ReplayAll() validators = TestValidators() reply = Reply('250', '2.0.0 Ok') validators.validate_mail(reply, '*****@*****.**') self.assertEqual('250', reply.code) self.assertEqual('2.0.0 Ok', reply.message)
def test_run(self): result = AsyncResult() env = Envelope('*****@*****.**', ['*****@*****.**']) env.parse(b'From: [email protected]\r\n\r\ntest test\r\n') queue = BlockingDeque() queue.append((result, env)) self.sock.recv(IsA(int)).AndReturn(b'220 Welcome\r\n') self.sock.sendall(b'EHLO there\r\n') self.sock.recv(IsA(int)).AndReturn(b'250-Hello\r\n250 PIPELINING\r\n') self.sock.sendall( b'MAIL FROM:<*****@*****.**>\r\nRCPT TO:<*****@*****.**>\r\nDATA\r\n' ) self.sock.recv( IsA(int)).AndReturn(b'250 Ok\r\n250 Ok\r\n354 Go ahead\r\n') self.sock.sendall( b'From: [email protected]\r\n\r\ntest test\r\n.\r\n') self.sock.recv(IsA(int)).AndReturn(b'250 Ok\r\n') self.sock.sendall(b'QUIT\r\n') self.sock.recv(IsA(int)).AndReturn(b'221 Goodbye\r\n') self.sock.close() self.mox.ReplayAll() client = SmtpRelayClient(('addr', 0), queue, socket_creator=self._socket_creator, ehlo_as='there') client._run() self.assertEqual({'*****@*****.**': Reply('250', 'Ok')}, result.get_nowait())
def HAVE_DATA(self, reply, data, err): if isinstance(err, MessageTooBig): reply.code = '552' reply.message = '5.3.4 Message exceeded size limit' return elif err: raise err self._call_validator('have_data', reply, data) if reply.code != '250': return if self._ptr_lookup is not None: self.reverse_address = self._ptr_lookup.finish() self.envelope.client['ip'] = self.address[0] self.envelope.client['host'] = self.reverse_address self.envelope.client['name'] = self.ehlo_as self.envelope.client['protocol'] = self.protocol self.envelope.client['auth'] = self.auth self.envelope.parse(data) results = self.handoff(self.envelope) if isinstance(results[0][1], QueueError): default_reply = Reply('451', '4.3.0 Error queuing message') queue_reply = getattr(results[0][1], 'reply', default_reply) reply.copy(queue_reply) elif isinstance(results[0][1], RelayError): relay_reply = results[0][1].reply reply.copy(relay_reply) else: reply.message = '2.6.0 Message accepted for delivery' self._call_validator('queued', reply, results) self.envelope = None
def test_not_populated(self): r = Reply(command=b'SOMECOMMAND') self.assertEqual(None, r.code) self.assertEqual(None, r.message) self.assertEqual(None, r.enhanced_status_code) self.assertFalse(r) self.assertEqual(b'SOMECOMMAND', r.command)
def __init__(self, msg, reply=None): super(RelayError, self).__init__(msg) if reply: self.reply = reply else: reply_msg = self._default_esc + ' ' + msg self.reply = Reply(self._default_code, reply_msg)
def test_deliver_multircpt(self): result = self.mox.CreateMockAnything() env = Envelope( '*****@*****.**', ['*****@*****.**', '*****@*****.**', '*****@*****.**']) env.parse(b'From: [email protected]\r\n\r\ntest test\r\n') self.sock.sendall(b'LHLO there\r\n') self.sock.recv(IsA(int)).AndReturn(b'250-Hello\r\n250 PIPELINING\r\n') self.sock.sendall( b'MAIL FROM:<*****@*****.**>\r\nRCPT TO:<*****@*****.**>\r\nRCPT TO:<*****@*****.**>\r\nRCPT TO:<*****@*****.**>\r\nDATA\r\n' ) self.sock.recv(IsA(int)).AndReturn( b'250 Ok\r\n250 Ok\r\n550 Nope\r\n250 Ok\r\n354 Go ahead\r\n') self.sock.sendall( b'From: [email protected]\r\n\r\ntest test\r\n.\r\n') self.sock.recv(IsA(int)).AndReturn(b'250 Ok\r\n450 Yikes\r\n') result.set({ '*****@*****.**': Reply('250', 'Ok'), '*****@*****.**': IsA(PermanentRelayError), '*****@*****.**': IsA(TransientRelayError) }) self.sock.sendall(b'RSET\r\n') self.sock.recv(IsA(int)).AndReturn(b'250 Ok\r\n') self.mox.ReplayAll() client = LmtpRelayClient('addr', self.queue, socket_creator=self._socket_creator, ehlo_as='there') client._connect() client._ehlo() client._deliver(result, env)