def test_email_metrics(self): config = _createConfig() context = _createMailServerContext(config) message = SMTPMessage(context) message.lines = [ "From: [email protected]", "To: [email protected]", "Subject: testing", "", "get transport obfs4", ] message.message = message.getIncomingMessage() responder = message.responder tr = proto_helpers.StringTransportWithDisconnection() tr.protocol = responder responder.makeConnection(tr) email_metrix = metrics.EmailMetrics() key1 = "email.obfs4.gmail.success.none" email_metrix.recordValidEmailRequest(responder) self.assertTrue(email_metrix.hotMetrics[key1] == 1) key2 = "email.obfs4.gmail.fail.none" email_metrix.recordInvalidEmailRequest(responder) self.assertTrue(email_metrix.hotMetrics[key2] == 1)
def setUp(self): self.config = _createConfig() self.context = _createMailServerContext(self.config) self.message = SMTPMessage(self.context)
class SMTPAutoresponderTests(unittest.TestCase): """Unittests for :class:`bridgedb.distributors.email.autoresponder.SMTPAutoresponder`.""" timeout = 10 def setUp(self): self.config = _createConfig() self.context = _createMailServerContext(self.config) self.message = SMTPMessage(self.context) def _getIncomingLines(self, clientAddress="*****@*****.**"): """Generate the lines of an incoming email from **clientAddress**.""" lines = [ "From: %s" % clientAddress, "To: bridges@localhost", "Subject: testing", "", "get bridges", ] self.message.lines = lines def _setUpResponder(self): """Set up the incoming message of our autoresponder. This is necessary because normally our SMTP server acts as a line protocol, waiting for an EOM which sets off a chain of deferreds resulting in the autoresponder sending out the response. This should be called after :meth:`_getIncomingLines` so that we can hook into the SMTP protocol without actually triggering all the deferreds. """ self.message.message = self.message.getIncomingMessage() self.responder = self.message.responder # The following are needed to provide client disconnection methods for # the call to ``twisted.mail.smtp.SMTPClient.sendError`` in # ``bridgedb.distributors.email.autoresponder.SMTPAutoresponder.sendError``: #protocol = proto_helpers.AccumulatingProtocol() #transport = proto_helpers.StringTransportWithDisconnection() self.tr = proto_helpers.StringTransportWithDisconnection() # Set the transport's protocol, because # StringTransportWithDisconnection is a bit janky: self.tr.protocol = self.responder self.responder.makeConnection(self.tr) def test_SMTPAutoresponder_getMailFrom_notbridgedb_at_yikezors_dot_net( self): """SMTPAutoresponder.getMailFrom() for an incoming email sent to any email address other than the one we're listening for should return our configured address, not the one in the incoming email. """ self._getIncomingLines() self.message.lines[1] = 'To: [email protected]' self._setUpResponder() recipient = str(self.responder.getMailFrom()) self.assertEqual(recipient, self.context.fromAddr) def test_SMTPAutoresponder_getMailFrom_givemebridges_at_seriously(self): """SMTPAutoresponder.getMailFrom() for an incoming email sent to any email address other than the one we're listening for should return our configured address, not the one in the incoming email. """ self._getIncomingLines() self.message.lines[1] = 'To: [email protected]' self._setUpResponder() recipient = str(self.responder.getMailFrom()) self.assertEqual(recipient, self.context.fromAddr) def test_SMTPAutoresponder_getMailFrom_bad_address(self): """SMTPAutoresponder.getMailFrom() for an incoming email sent to a malformed email address should log an smtp.AddressError and then return our configured email address. """ self._getIncomingLines() self.message.lines[1] = 'To: ><@><<<>>.foo' self._setUpResponder() recipient = str(self.responder.getMailFrom()) self.assertEqual(recipient, self.context.fromAddr) def test_SMTPAutoresponder_getMailFrom_plus_address(self): """SMTPAutoresponder.getMailFrom() for an incoming email sent with a valid plus address should respond. """ self._getIncomingLines() ours = Address(self.context.fromAddr) plus = '@'.join([ ours.local.decode('utf-8') + '+zh_cn', ours.domain.decode('utf-8') ]) self.message.lines[1] = 'To: {0}'.format(plus) self._setUpResponder() recipient = str(self.responder.getMailFrom()) self.assertEqual(recipient, plus) def test_SMTPAutoresponder_getMailFrom_getbridges_at_localhost(self): """SMTPAutoresponder.getMailFrom() for an incoming email sent with 'getbridges+zh_cn@localhost' should be responded to from the default address. """ self._getIncomingLines() ours = Address(self.context.fromAddr) plus = '@'.join([ 'get' + ours.local.decode('utf-8') + '+zh_cn', ours.domain.decode('utf-8') ]) self.message.lines[1] = 'To: {0}'.format(plus) self._setUpResponder() recipient = str(self.responder.getMailFrom()) self.assertEqual(recipient, self.context.fromAddr) def test_SMTPAutoresponder_getMailTo_UnsupportedDomain(self): """getMailTo() should catch emails from UnsupportedDomains.""" emailFrom = '*****@*****.**' self._getIncomingLines(emailFrom) self._setUpResponder() clients = self.responder.getMailTo() self.assertIsInstance( clients, list, ("Returned value of SMTPAutoresponder.getMailTo() isn't a list! " "Type: %s" % type(clients))) self.assertTrue(emailFrom not in clients) # The client was from an unsupported domain; they shouldn't be in the # clients list: self.assertEqual(len(clients), 0, "clients = %s" % repr(clients)) def test_SMTPAutoresponder_reply_noFrom(self): """A received email without a "From:" or "Sender:" header shouldn't receive a response. """ self._getIncomingLines() self.message.lines[0] = "" self._setUpResponder() ret = self.responder.reply() self.assertIsInstance(ret, defer.Deferred) def test_SMTPAutoresponder_reply_badAddress(self): """Don't respond to RFC2822 malformed source addresses.""" self._getIncomingLines("testing*.?\"@example.com") self._setUpResponder() ret = self.responder.reply() # This will call ``self.responder.reply()``: #ret = self.responder.incoming.eomReceived() self.assertIsInstance(ret, defer.Deferred) def test_SMTPAutoresponder_reply_anotherBadAddress(self): """Don't respond to RFC2822 malformed source addresses.""" self._getIncomingLines("Mallory <>>@example.com") self._setUpResponder() ret = self.responder.reply() self.assertIsInstance(ret, defer.Deferred) def test_SMTPAutoresponder_reply_invalidDomain(self): """Don't respond to RFC2822 malformed source addresses.""" self._getIncomingLines("testing@exa#mple.com") self._setUpResponder() ret = self.responder.reply() self.assertIsInstance(ret, defer.Deferred) def test_SMTPAutoresponder_reply_anotherInvalidDomain(self): """Don't respond to RFC2822 malformed source addresses.""" self._getIncomingLines("testing@exam+ple.com") self._setUpResponder() ret = self.responder.reply() self.assertIsInstance(ret, defer.Deferred) def test_SMTPAutoresponder_reply_DKIM_badDKIMheader(self): """An email with an 'X-DKIM-Authentication-Result:' header appended after the body should not receive a response. """ self._getIncomingLines("*****@*****.**") self.message.lines.append("X-DKIM-Authentication-Result: ") self._setUpResponder() ret = self.responder.reply() self.assertIsInstance(ret, defer.Deferred) def test_SMTPAutoresponder_reply_goodDKIMheader(self): """An email with a good DKIM header should be responded to.""" self._getIncomingLines("*****@*****.**") self.message.lines.insert(3, "X-DKIM-Authentication-Result: pass") self._setUpResponder() ret = self.responder.reply() self.assertIsInstance(ret, defer.Deferred) def test_SMTPAutoresponder_reply_transport_invalid(self): """An invalid request for 'transport obfs3' should get help text.""" #self.skip = True #raise unittest.SkipTest("We need to fake the reactor for this one") def cb(success): pass self._getIncomingLines("*****@*****.**") self.message.lines[4] = "transport obfs3" self._setUpResponder() ret = self.responder.reply() self.assertIsInstance(ret, defer.Deferred) #self.assertSubstring("COMMANDs", ret) print(self.tr.value()) return ret def test_SMTPAutoresponder_reply_transport_valid(self): """An valid request for 'get transport obfs3' should get obfs3.""" #self.skip = True #raise unittest.SkipTest("We need to fake the reactor for this one") self._getIncomingLines("*****@*****.**") self.message.lines[4] = "transport obfs3" self._setUpResponder() ret = self.responder.reply() self.assertIsInstance(ret, defer.Deferred) #self.assertSubstring("obfs3", ret) print(self.tr.value()) return ret def test_SMTPAutoresponder_sentMail(self): """``SMTPAutoresponder.sendMail()`` should handle successes from an :api:`twisted.mail.smtp.SMTPSenderFactory`. """ success = (1, [( '*****@*****.**', 250, 'OK', )]) self._getIncomingLines() self._setUpResponder() self.responder.sentMail(success) def test_SMTPAutoresponder_sendError_fail(self): """``SMTPAutoresponder.sendError()`` should handle failures.""" fail = Failure(ValueError('This failure was sent on purpose.')) self._getIncomingLines() self._setUpResponder() self.responder.sendError(fail) def test_SMTPAutoresponder_sendError_exception(self): """``SMTPAutoresponder.sendError()`` should handle exceptions.""" error = ValueError('This error was sent on purpose.') self._getIncomingLines() self._setUpResponder() self.responder.sendError(error) def test_SMTPAutoresponder_runChecks_RCPTTO_From_mismatched_domain(self): """runChecks() should catch emails where the SMTP 'MAIL FROM:' command reported being from an email address at one supported domain and the email's 'From:' header reported another domain. """ smtpFrom = '*****@*****.**' emailFrom = Address('*****@*****.**') self._getIncomingLines(str(emailFrom)) self._setUpResponder() self.responder.incoming.canonicalFromSMTP = smtpFrom self.assertFalse(self.responder.runChecks(emailFrom)) def test_SMTPAutoresponder_runChecks_RCPTTO_From_mismatched_username(self): """runChecks() should catch emails where the SMTP 'MAIL FROM:' command reported being from an email address and the email's 'From:' header reported another email address, even if the only the username part is mismatched. """ smtpFrom = '*****@*****.**' emailFrom = Address('*****@*****.**') self._getIncomingLines(str(emailFrom)) self._setUpResponder() self.responder.incoming.canonicalFromSMTP = smtpFrom self.assertFalse(self.responder.runChecks(emailFrom)) def test_SMTPAutoresponder_runChecks_DKIM_dunno(self): """runChecks() should catch emails with bad DKIM headers (``"X-DKIM-Authentication-Results: dunno"``) for canonical domains which we're configured to check DKIM verification results for. """ emailFrom = Address('*****@*****.**') header = "X-DKIM-Authentication-Results: dunno" self._getIncomingLines(str(emailFrom)) self.message.lines.insert(3, header) self._setUpResponder() self.assertFalse(self.responder.runChecks(emailFrom)) def test_SMTPAutoresponder_runChecks_DKIM_bad(self): """runChecks() should catch emails with bad DKIM headers (``"X-DKIM-Authentication-Results: dunno"``) for canonical domains which we're configured to check DKIM verification results for. """ emailFrom = Address('*****@*****.**') header = "X-DKIM-Authentication-Results: wowie zowie there's a sig here" self._getIncomingLines(str(emailFrom)) self.message.lines.insert(3, header) self._setUpResponder() self.assertFalse(self.responder.runChecks(emailFrom)) def test_SMTPAutoresponder_runChecks_blacklisted(self): """runChecks() on an blacklisted email address should return False.""" emailFrom = Address('*****@*****.**') self._getIncomingLines(str(emailFrom)) self._setUpResponder() self.assertFalse(self.responder.runChecks(emailFrom))