def test_data_err_fail(self, retry, result, get_conn): """ Test that celery handles permanent SMTPDataErrors by failing and not retrying. """ # have every fourth email fail due to blacklisting: get_conn.return_value.send_messages.side_effect = cycle([ SMTPDataError(554, "Email address is blacklisted"), None, None, None ]) students = [ UserFactory() for _ in xrange(settings.BULK_EMAIL_EMAILS_PER_TASK) ] for student in students: CourseEnrollmentFactory.create(user=student, course_id=self.course.id) test_email = { 'action': 'Send email', 'send_to': 'all', 'subject': 'test subject for all', 'message': 'test message for all' } response = self.client.post(self.send_mail_url, test_email) self.assertEquals(json.loads(response.content), self.success_content) # We shouldn't retry when hitting a 5xx error self.assertFalse(retry.called) # Test that after the rejected email, the rest still successfully send ((_entry_id, _current_task_id, subtask_status), _kwargs) = result.call_args self.assertEquals(subtask_status.skipped, 0) expected_fails = int((settings.BULK_EMAIL_EMAILS_PER_TASK + 3) / 4.0) self.assertEquals(subtask_status.failed, expected_fails) self.assertEquals(subtask_status.succeeded, settings.BULK_EMAIL_EMAILS_PER_TASK - expected_fails)
def mock_smtp_send_error(mock_smtp, mocker): from smtplib import SMTPDataError mock_smtp.return_value.send_message = mocker.Mock( side_effect=SMTPDataError(402, "error")) return mock_smtp
def test_data_err_fail(self, retry, result, get_conn): """ Test that celery handles permanent SMTPDataErrors by failing and not retrying. """ get_conn.return_value.send_messages.side_effect = cycle( [SMTPDataError(554, "Email address is blacklisted"), None]) students = [UserFactory() for _ in xrange(settings.EMAILS_PER_TASK)] for student in students: CourseEnrollmentFactory.create(user=student, course_id=self.course.id) test_email = { 'action': 'Send email', 'to_option': 'all', 'subject': 'test subject for all', 'message': 'test message for all' } self.client.post(self.url, test_email) # We shouldn't retry when hitting a 5xx error self.assertFalse(retry.called) # Test that after the rejected email, the rest still successfully send ((sent, fail, optouts), _) = result.call_args self.assertEquals(optouts, 0) self.assertEquals(fail, settings.EMAILS_PER_TASK / 2) self.assertEquals(sent, settings.EMAILS_PER_TASK / 2)
def data(self, msg): self.putcmd("data") (code, repl) = self.getreply() if self.debuglevel > 0: print >> stderr, "data:", (code, repl) if code != 354: raise SMTPDataError(code, repl) else: q = quotedata(msg) if q[-2:] != CRLF: q = q + CRLF q = q + "." + CRLF # begin modified send code chunk_size = 2048 bytes_sent = 0 while bytes_sent != len(q): chunk = q[bytes_sent:bytes_sent + chunk_size] self.send(chunk) bytes_sent += len(chunk) if hasattr(self, "callback"): self.callback(bytes_sent, len(q)) # end modified send code (code, msg) = self.getreply() if self.debuglevel > 0: print>> stderr, "data:", (code, msg) return (code, msg)
def data(self, msg): """SMTP 'DATA' command -- sends message data to server. Automatically quotes lines beginning with a period per rfc821. Raises SMTPDataError if there is an unexpected reply to the DATA command; the return value from this method is the final response code received when the all data is sent. If msg is a string, lone '\\r' and '\\n' characters are converted to '\\r\\n' characters. If msg is bytes, it is transmitted as is. """ self.putcmd("data") (code, repl) = self.getreply() if self.debuglevel > 0: self._print_debug('data:', (code, repl)) if code != 354: raise SMTPDataError(code, repl) else: if isinstance(msg, str): msg = _fix_eols(msg).encode('ascii') q = _quote_periods(msg) if q[-2:] != bCRLF: q = q + bCRLF q = q + b"." + bCRLF self.send(q) (code, msg) = self.getreply() if self.debuglevel > 0: self._print_debug('data:', (code, msg)) return (code, msg)
def test_it_retries_smtp_data_error(self, mock_time): mock_msg = Mock() mock_msg.send = Mock(side_effect=[SMTPDataError(454, "hello"), None]) t = EmailThread(mock_msg) t.run() self.assertEqual(mock_msg.send.call_count, 2)
def mock_smtp_send_error(mock_smtp: MagicMock, mocker: MockerFixture) -> MagicMock: from smtplib import SMTPDataError mock_smtp.return_value.send_message = mocker.Mock( side_effect=SMTPDataError(402, "error")) return mock_smtp
def test_emailer_data_error(self, mock_sendmail): mock_sendmail.side_effect = SMTPDataError(1, 'No bueno') _, print_out = self._perform(utils.emailer, 'tester', ['*****@*****.**'], 'my subject', 'test message') mock_sendmail.assert_called_once() self.assertEqual( '{red}>>> (err) {end}Unable to send the email:'.format( red=bash.RED, end=bash.END), print_out)
def data(self, message): """Sends the SMTP 'data' command (sends message data to server) Automatically quotes lines beginning with a period per rfc821. Raises SMTPDataError if there is an unexpected reply to the DATA command. Lone '\r' and '\n' characters are converted to '\r\n' characters. Returns a (code, message) response tuple (the last one, after all data is sent.) """ code, response = yield from self.execute_command("data") if code != SMTP_START_INPUT: raise SMTPDataError(code, response) if not isinstance(message, str): message = message.decode('ascii') message = re.sub(r'(?:\r\n|\n|\r(?!\n))', "\r\n", message) message = re.sub(r'(?m)^\.', '..', message) # quote periods if message[-2:] != "\r\n": message += "\r\n" message += ".\r\n" if self.debug: logger.debug('message is: %s', message) yield from self.send_data(message) code, response = yield from self.get_response() if code != SMTP_COMPLETED: if code == SMTP_NOT_AVAILABLE: self.close() else: # reset, raise error try: yield from self.rset() except SMTPServerDisconnected: pass raise SMTPDataError(code, resp) return code, response
def test_send_email_exceptions(self) -> None: hamlet = self.example_user("hamlet") from_name = FromAddress.security_email_from_name(language="en") address = FromAddress.NOREPLY # Used to check the output mail = build_email( "zerver/emails/password_reset", to_emails=[hamlet], from_name=from_name, from_address=address, language="en", ) self.assertEqual(mail.extra_headers["From"], f"{from_name} <{FromAddress.NOREPLY}>") # We test the cases that should raise an EmailNotDeliveredException errors = { f"Unknown error sending password_reset email to {mail.to}": [0], f"Error sending password_reset email to {mail.to}": [SMTPException()], f"Error sending password_reset email to {mail.to}: {{'{address}': (550, b'User unknown')}}": [ SMTPRecipientsRefused( recipients={address: (550, b"User unknown")}) ], f"Error sending password_reset email to {mail.to} with error code 242: From field too long": [SMTPDataError(242, "From field too long.")], } for message, side_effect in errors.items(): with mock.patch.object(EmailBackend, "send_messages", side_effect=side_effect): with self.assertLogs(logger=logger) as info_log: with self.assertRaises(EmailNotDeliveredException): send_email( "zerver/emails/password_reset", to_emails=[hamlet], from_name=from_name, from_address=FromAddress.NOREPLY, language="en", ) self.assert_length(info_log.records, 2) self.assertEqual( info_log.output[0], f"INFO:{logger.name}:Sending password_reset email to {mail.to}", ) self.assertTrue(info_log.output[1].startswith( f"ERROR:zulip.send_email:{message}"))
def test_data_err_retry(self, retry, get_conn): """ Test that celery handles transient SMTPDataErrors by retrying. """ get_conn.return_value.send_messages.side_effect = SMTPDataError(455, "Throttling: Sending rate exceeded") test_email = { 'action': 'Send email', 'send_to': 'myself', 'subject': 'test subject for myself', 'message': 'test message for myself' } response = self.client.post(self.send_mail_url, test_email) self.assertEquals(json.loads(response.content), self.success_content) # Test that we retry upon hitting a 4xx error self.assertTrue(retry.called) (__, kwargs) = retry.call_args exc = kwargs['exc'] self.assertIsInstance(exc, SMTPDataError)
def test_data_err_retry(self, retry, get_conn): """ Test that celery handles transient SMTPDataErrors by retrying. """ get_conn.return_value.send_messages.side_effect = SMTPDataError( 455, "Throttling: Sending rate exceeded") test_email = { 'action': 'Send email', 'to_option': 'myself', 'subject': 'test subject for myself', 'message': 'test message for myself' } self.client.post(self.url, test_email) # Test that we retry upon hitting a 4xx error self.assertTrue(retry.called) (_, kwargs) = retry.call_args exc = kwargs['exc'] self.assertTrue(type(exc) == SMTPDataError)
def test_build_and_send_SES_incompatible_From_address(self) -> None: hamlet = self.example_user("hamlet") from_name = "Zulip" # Address by itself is > 320 bytes even without the name. Should trigger exception. overly_long_address = "a" * 320 + "@zulip.com" mail = build_email( "zerver/emails/password_reset", to_emails=[hamlet], from_name=from_name, from_address=overly_long_address, language="en", ) self.assertEqual(mail.extra_headers["From"], overly_long_address) self.assertTrue( len(sanitize_address(mail.extra_headers["From"], "utf-8")) > 320) with mock.patch.object(EmailBackend, "send_messages", side_effect=SMTPDataError( 242, "From field too long.")): with self.assertLogs(logger=logger) as info_log: with self.assertRaises(EmailNotDeliveredException): send_email( "zerver/emails/password_reset", to_emails=[hamlet], from_name=from_name, from_address=overly_long_address, language="en", ) self.assertEqual(len(info_log.records), 2) self.assertEqual( info_log.output, [ f"INFO:{logger.name}:Sending password_reset email to {mail.to}", f"ERROR:{logger.name}:Error sending password_reset email to {mail.to}", ], )
def send(self): raise SMTPDataError(500, 'Error Message')
def sendmail(self, from_addr, to_addrs, msg, mail_options=[], rcpt_options=[]): """This command performs an entire mail transaction. The arguments are: - from_addr : The address sending this mail. - to_addrs : A list of addresses to send this mail to. A bare string will be treated as a list with 1 address. - msg : The message to send. - mail_options : List of ESMTP options (such as 8bitmime) for the mail command. - rcpt_options : List of ESMTP options (such as DSN commands) for all the rcpt commands. msg may be a string containing characters in the ASCII range, or a byte string. A string is encoded to bytes using the ascii codec, and lone \\r and \\n characters are converted to \\r\\n characters. If there has been no previous EHLO or HELO command this session, this method tries ESMTP EHLO first. If the server does ESMTP, message size and each of the specified options will be passed to it. If EHLO fails, HELO will be tried and ESMTP options suppressed. This method will return normally if the mail is accepted for at least one recipient. It returns a dictionary, with one entry for each recipient that was refused. Each entry contains a tuple of the SMTP error code and the accompanying error message sent by the server. This method may raise the following exceptions: SMTPHeloError The server didn't reply properly to the helo greeting. SMTPRecipientsRefused The server rejected ALL recipients (no mail was sent). SMTPSenderRefused The server didn't accept the from_addr. SMTPDataError The server replied with an unexpected error code (other than a refusal of a recipient). SMTPNotSupportedError The mail_options parameter includes 'SMTPUTF8' but the SMTPUTF8 extension is not supported by the server. Note: the connection will be open even after an exception is raised. Example: >>> import smtplib >>> s=smtplib.SMTP("localhost") >>> tolist=["*****@*****.**","*****@*****.**","*****@*****.**","*****@*****.**"] >>> msg = '''\\ ... From: [email protected] ... Subject: testin'... ... ... This is a test ''' >>> s.sendmail("*****@*****.**",tolist,msg) { "*****@*****.**" : ( 550 ,"User unknown" ) } >>> s.quit() In the above example, the message was accepted for delivery to three of the four addresses, and one was rejected, with the error code 550. If all addresses are accepted, then the method will return an empty dictionary. """ self.ehlo_or_helo_if_needed() esmtp_opts = [] if isinstance(msg, str): msg = _fix_eols(msg).encode('ascii') if self.does_esmtp: if self.has_extn('size'): esmtp_opts.append("size=%d" % len(msg)) for option in mail_options: esmtp_opts.append(option) (code, resp) = self.mail(from_addr, esmtp_opts) if code != 250: if code == 421: self.close() else: self._rset() raise SMTPSenderRefused(code, resp, from_addr) senderrs = {} if isinstance(to_addrs, str): to_addrs = [to_addrs] for each in to_addrs: (code, resp) = self.rcpt(each, rcpt_options) if (code != 250) and (code != 251): senderrs[each] = (code, resp) if code == 421: self.close() raise SMTPRecipientsRefused(senderrs) if len(senderrs) == len(to_addrs): # the server refused all our recipients self._rset() raise SMTPRecipientsRefused(senderrs) (code, resp) = self.data(msg) if code != 250: if code == 421: self.close() else: self._rset() raise SMTPDataError(code, resp) #if we got here then somebody got our mail return senderrs
def test_smtp_blacklisted_user(self): # Test that celery handles permanent SMTPDataErrors by failing and not retrying. self._test_email_address_failures( SMTPDataError(554, "Email address is blacklisted"))
def test_retry_after_smtp_throttling_error(self): self._test_retry_after_unlimited_retry_error( SMTPDataError(455, "Throttling: Sending rate exceeded"))
def _raise_err(): """Need this because SMTPDataError takes args""" raise SMTPDataError("", "")