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 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_attempt_delivery_permanentrelayerror_nullsender(self): task = self.mox.CreateMockAnything() self.relay.attempt(self.bounce, 0).AndRaise(PermanentRelayError('permanent', Reply('550', 'permanent error'))) self.celery.task(IgnoreArg()).AndReturn(task) self.mox.ReplayAll() def return_bounce(envelope, reply): self.fail('Tried to generate a bounce to a NULL sender.') queue = CeleryQueue(self.celery, self.relay, bounce_factory=return_bounce) queue.attempt_delivery(self.bounce, 0)
def test_enqueue_relayerror(self): err = PermanentRelayError('msg failure', Reply('550', 'Not Ok')) self.relay._attempt(self.env, 0).AndRaise(err) self.mox.ReplayAll() q = ProxyQueue(self.relay) ret = q.enqueue(self.env) self.assertEqual(1, len(ret)) self.assertEqual(2, len(ret[0])) self.assertEqual(self.env, ret[0][0]) self.assertEqual(err, ret[0][1])
def test_enqueue_wait_permanentfail(self): self.store.write(self.env, IsA(float)).AndReturn('1234') self.relay._attempt(self.env, 0).AndRaise(PermanentRelayError('permanent', Reply('550', 'permanent'))) self.store.remove('1234') self.mox.ReplayAll() def no_bounce(envelope, reply): return None queue = Queue(self.store, self.relay, bounce_factory=no_bounce, relay_pool=5) queue.enqueue(self.env) queue.relay_pool.join()
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_attempt_delivery_permanentrelayerror(self): task = self.mox.CreateMockAnything() subtask = self.mox.CreateMockAnything() result = self.mox.CreateMockAnything() result.id = '12345' self.relay.attempt(self.env, 0).AndRaise(PermanentRelayError('permanent', Reply('550', 'permanent error'))) self.celery.task(IgnoreArg()).AndReturn(task) task.s(self.bounce, 0).AndReturn(subtask) subtask.apply_async().AndReturn(result) self.mox.ReplayAll() def return_bounce(envelope, reply): self.assertEqual(self.env, envelope) return self.bounce queue = CeleryQueue(self.celery, self.relay, bounce_factory=return_bounce) queue.attempt_delivery(self.env, 0)
def test_default_replies(self): perm = PermanentRelayError('test msg') transient = TransientRelayError('test msg') self.assertEqual('550 5.0.0 test msg', str(perm.reply)) self.assertEqual('450 4.0.0 test msg', str(transient.reply))
def apply(self, envelope): identifier = envelope.headers.get( settings.TRANSACTIONAL['X_MESSAGE_ID_HEADER']) if not identifier or not envelope.client.get('auth'): # FIXME: log exact error raise PermanentRelayError('Internal application error') # Process mail content munchers_kwargs = { 'mail_identifier': identifier, 'app_url': get_app_url(domain=envelope.sending_domain, organization=envelope.organization), } msg_links = {} mail_kwargs = {} track_open = envelope.headers.get( settings.TRANSACTIONAL.get('X_MAIL_TRACK_OPEN_HEADER', None)) track_open_done = False if track_open: munchers_kwargs.update({'track_open': True}) mail_kwargs.update({'track_open': True}) track_clicks = envelope.headers.get( settings.TRANSACTIONAL.get('X_MAIL_TRACK_CLICKS_HEADER', None)) if track_clicks: mail_kwargs.update({'track_clicks': True}) munchers_kwargs.update({'track_clicks': True}) add_unsubscribe = envelope.headers.get( settings.TRANSACTIONAL.get('X_MAIL_UNSUBSCRIBE_HEADER', None)) if add_unsubscribe: munchers_kwargs.update({ 'unsubscribe_url': Mail.unsubscribe_url(identifier, envelope.user, envelope.sending_domain) }) # Retrieve full html to extract links html = '' message = email.message_from_bytes(b'\n'.join(envelope.flatten())) for part in message.walk(): if part.get_content_type() == 'text/html': html += part.get_payload() msg_links = get_msg_links(html) mail_kwargs.update({'msg_links': msg_links}) munchers_kwargs.update({'links_map': msg_links}) # Walk throught every parts to apply munchers on it for part in message.walk(): if part.get_content_type() == 'text/html': html = part.get_payload() if track_open and not track_open_done: html = add_tracking_image(html, **munchers_kwargs) track_open_done = True if track_clicks: html = rewrite_html_links(html, **munchers_kwargs) part.set_payload(html) content = part.get_payload() if add_unsubscribe: content = set_unsubscribe_url(content, **munchers_kwargs) part.set_payload(content) envelope.parse_msg(message) batch = envelope.headers.get( settings.TRANSACTIONAL.get('X_MAIL_BATCH_HEADER', None)) if batch: batch, created = MailBatch.objects.get_or_create( name=batch, author=envelope.user, defaults={'msg_links': msg_links}) if not created: batch.msg_links = msg_links batch.save() category = envelope.headers.get( settings.TRANSACTIONAL.get('X_MAIL_BATCH_CATEGORY_HEADER', None)) if category: category, _ = Category.objects.get_or_create( author=envelope.user, name=category) batch.category = category batch.save() raw_mail, _ = RawMail.objects.get_or_create(content=envelope.message) mail = Mail.objects.create( author=envelope.user, batch=batch, identifier=envelope.headers.get( settings.TRANSACTIONAL['X_MESSAGE_ID_HEADER']), headers={ k: v.encode('utf-8', 'surrogateescape').decode('utf-8') for k, v in envelope.headers.raw_items() }, message=raw_mail, sender=envelope.sender, recipient=envelope.recipients[0], **mail_kwargs) MailStatus.objects.create(mail=mail, destination_domain=extract_domain( envelope.recipients[0]))