/Lib/test/test_smtplib.py

http://unladen-swallow.googlecode.com/ · Python · 484 lines · 345 code · 89 blank · 50 comment · 46 complexity · dbb922d2f43bd6ccba1a6fb528f12b88 MD5 · raw file

  1. import asyncore
  2. import email.utils
  3. import socket
  4. import threading
  5. import smtpd
  6. import smtplib
  7. import StringIO
  8. import sys
  9. import time
  10. import select
  11. from unittest import TestCase
  12. from test import test_support
  13. HOST = test_support.HOST
  14. def server(evt, buf, serv):
  15. serv.listen(5)
  16. evt.set()
  17. try:
  18. conn, addr = serv.accept()
  19. except socket.timeout:
  20. pass
  21. else:
  22. n = 500
  23. while buf and n > 0:
  24. r, w, e = select.select([], [conn], [])
  25. if w:
  26. sent = conn.send(buf)
  27. buf = buf[sent:]
  28. n -= 1
  29. conn.close()
  30. finally:
  31. serv.close()
  32. evt.set()
  33. class GeneralTests(TestCase):
  34. def setUp(self):
  35. self.evt = threading.Event()
  36. self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  37. self.sock.settimeout(15)
  38. self.port = test_support.bind_port(self.sock)
  39. servargs = (self.evt, "220 Hola mundo\n", self.sock)
  40. threading.Thread(target=server, args=servargs).start()
  41. self.evt.wait()
  42. self.evt.clear()
  43. def tearDown(self):
  44. self.evt.wait()
  45. def testBasic1(self):
  46. # connects
  47. smtp = smtplib.SMTP(HOST, self.port)
  48. smtp.close()
  49. def testBasic2(self):
  50. # connects, include port in host name
  51. smtp = smtplib.SMTP("%s:%s" % (HOST, self.port))
  52. smtp.close()
  53. def testLocalHostName(self):
  54. # check that supplied local_hostname is used
  55. smtp = smtplib.SMTP(HOST, self.port, local_hostname="testhost")
  56. self.assertEqual(smtp.local_hostname, "testhost")
  57. smtp.close()
  58. def testTimeoutDefault(self):
  59. self.assertTrue(socket.getdefaulttimeout() is None)
  60. socket.setdefaulttimeout(30)
  61. try:
  62. smtp = smtplib.SMTP(HOST, self.port)
  63. finally:
  64. socket.setdefaulttimeout(None)
  65. self.assertEqual(smtp.sock.gettimeout(), 30)
  66. smtp.close()
  67. def testTimeoutNone(self):
  68. self.assertTrue(socket.getdefaulttimeout() is None)
  69. socket.setdefaulttimeout(30)
  70. try:
  71. smtp = smtplib.SMTP(HOST, self.port, timeout=None)
  72. finally:
  73. socket.setdefaulttimeout(None)
  74. self.assertTrue(smtp.sock.gettimeout() is None)
  75. smtp.close()
  76. def testTimeoutValue(self):
  77. smtp = smtplib.SMTP(HOST, self.port, timeout=30)
  78. self.assertEqual(smtp.sock.gettimeout(), 30)
  79. smtp.close()
  80. # Test server thread using the specified SMTP server class
  81. def debugging_server(serv, serv_evt, client_evt):
  82. serv_evt.set()
  83. try:
  84. if hasattr(select, 'poll'):
  85. poll_fun = asyncore.poll2
  86. else:
  87. poll_fun = asyncore.poll
  88. n = 1000
  89. while asyncore.socket_map and n > 0:
  90. poll_fun(0.01, asyncore.socket_map)
  91. # when the client conversation is finished, it will
  92. # set client_evt, and it's then ok to kill the server
  93. if client_evt.is_set():
  94. serv.close()
  95. break
  96. n -= 1
  97. except socket.timeout:
  98. pass
  99. finally:
  100. if not client_evt.is_set():
  101. # allow some time for the client to read the result
  102. time.sleep(0.5)
  103. serv.close()
  104. asyncore.close_all()
  105. serv_evt.set()
  106. MSG_BEGIN = '---------- MESSAGE FOLLOWS ----------\n'
  107. MSG_END = '------------ END MESSAGE ------------\n'
  108. # NOTE: Some SMTP objects in the tests below are created with a non-default
  109. # local_hostname argument to the constructor, since (on some systems) the FQDN
  110. # lookup caused by the default local_hostname sometimes takes so long that the
  111. # test server times out, causing the test to fail.
  112. # Test behavior of smtpd.DebuggingServer
  113. class DebuggingServerTests(TestCase):
  114. def setUp(self):
  115. # temporarily replace sys.stdout to capture DebuggingServer output
  116. self.old_stdout = sys.stdout
  117. self.output = StringIO.StringIO()
  118. sys.stdout = self.output
  119. self.serv_evt = threading.Event()
  120. self.client_evt = threading.Event()
  121. self.port = test_support.find_unused_port()
  122. self.serv = smtpd.DebuggingServer((HOST, self.port), ('nowhere', -1))
  123. serv_args = (self.serv, self.serv_evt, self.client_evt)
  124. threading.Thread(target=debugging_server, args=serv_args).start()
  125. # wait until server thread has assigned a port number
  126. self.serv_evt.wait()
  127. self.serv_evt.clear()
  128. def tearDown(self):
  129. # indicate that the client is finished
  130. self.client_evt.set()
  131. # wait for the server thread to terminate
  132. self.serv_evt.wait()
  133. # restore sys.stdout
  134. sys.stdout = self.old_stdout
  135. def testBasic(self):
  136. # connect
  137. smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
  138. smtp.quit()
  139. def testNOOP(self):
  140. smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
  141. expected = (250, 'Ok')
  142. self.assertEqual(smtp.noop(), expected)
  143. smtp.quit()
  144. def testRSET(self):
  145. smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
  146. expected = (250, 'Ok')
  147. self.assertEqual(smtp.rset(), expected)
  148. smtp.quit()
  149. def testNotImplemented(self):
  150. # EHLO isn't implemented in DebuggingServer
  151. smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
  152. expected = (502, 'Error: command "EHLO" not implemented')
  153. self.assertEqual(smtp.ehlo(), expected)
  154. smtp.quit()
  155. def testVRFY(self):
  156. # VRFY isn't implemented in DebuggingServer
  157. smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
  158. expected = (502, 'Error: command "VRFY" not implemented')
  159. self.assertEqual(smtp.vrfy('nobody@nowhere.com'), expected)
  160. self.assertEqual(smtp.verify('nobody@nowhere.com'), expected)
  161. smtp.quit()
  162. def testSecondHELO(self):
  163. # check that a second HELO returns a message that it's a duplicate
  164. # (this behavior is specific to smtpd.SMTPChannel)
  165. smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
  166. smtp.helo()
  167. expected = (503, 'Duplicate HELO/EHLO')
  168. self.assertEqual(smtp.helo(), expected)
  169. smtp.quit()
  170. def testHELP(self):
  171. smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
  172. self.assertEqual(smtp.help(), 'Error: command "HELP" not implemented')
  173. smtp.quit()
  174. def testSend(self):
  175. # connect and send mail
  176. m = 'A test message'
  177. smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
  178. smtp.sendmail('John', 'Sally', m)
  179. # XXX(nnorwitz): this test is flaky and dies with a bad file descriptor
  180. # in asyncore. This sleep might help, but should really be fixed
  181. # properly by using an Event variable.
  182. time.sleep(0.01)
  183. smtp.quit()
  184. self.client_evt.set()
  185. self.serv_evt.wait()
  186. self.output.flush()
  187. mexpect = '%s%s\n%s' % (MSG_BEGIN, m, MSG_END)
  188. self.assertEqual(self.output.getvalue(), mexpect)
  189. class NonConnectingTests(TestCase):
  190. def testNotConnected(self):
  191. # Test various operations on an unconnected SMTP object that
  192. # should raise exceptions (at present the attempt in SMTP.send
  193. # to reference the nonexistent 'sock' attribute of the SMTP object
  194. # causes an AttributeError)
  195. smtp = smtplib.SMTP()
  196. self.assertRaises(smtplib.SMTPServerDisconnected, smtp.ehlo)
  197. self.assertRaises(smtplib.SMTPServerDisconnected,
  198. smtp.send, 'test msg')
  199. def testNonnumericPort(self):
  200. # check that non-numeric port raises socket.error
  201. self.assertRaises(socket.error, smtplib.SMTP,
  202. "localhost", "bogus")
  203. self.assertRaises(socket.error, smtplib.SMTP,
  204. "localhost:bogus")
  205. # test response of client to a non-successful HELO message
  206. class BadHELOServerTests(TestCase):
  207. def setUp(self):
  208. self.old_stdout = sys.stdout
  209. self.output = StringIO.StringIO()
  210. sys.stdout = self.output
  211. self.evt = threading.Event()
  212. self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  213. self.sock.settimeout(15)
  214. self.port = test_support.bind_port(self.sock)
  215. servargs = (self.evt, "199 no hello for you!\n", self.sock)
  216. threading.Thread(target=server, args=servargs).start()
  217. self.evt.wait()
  218. self.evt.clear()
  219. def tearDown(self):
  220. self.evt.wait()
  221. sys.stdout = self.old_stdout
  222. def testFailingHELO(self):
  223. self.assertRaises(smtplib.SMTPConnectError, smtplib.SMTP,
  224. HOST, self.port, 'localhost', 3)
  225. sim_users = {'Mr.A@somewhere.com':'John A',
  226. 'Ms.B@somewhere.com':'Sally B',
  227. 'Mrs.C@somewhereesle.com':'Ruth C',
  228. }
  229. sim_auth = ('Mr.A@somewhere.com', 'somepassword')
  230. sim_cram_md5_challenge = ('PENCeUxFREJoU0NnbmhNWitOMjNGNn'
  231. 'dAZWx3b29kLmlubm9zb2Z0LmNvbT4=')
  232. sim_auth_credentials = {
  233. 'login': 'TXIuQUBzb21ld2hlcmUuY29t',
  234. 'plain': 'AE1yLkFAc29tZXdoZXJlLmNvbQBzb21lcGFzc3dvcmQ=',
  235. 'cram-md5': ('TXIUQUBZB21LD2HLCMUUY29TIDG4OWQ0MJ'
  236. 'KWZGQ4ODNMNDA4NTGXMDRLZWMYZJDMODG1'),
  237. }
  238. sim_auth_login_password = 'C29TZXBHC3N3B3JK'
  239. sim_lists = {'list-1':['Mr.A@somewhere.com','Mrs.C@somewhereesle.com'],
  240. 'list-2':['Ms.B@somewhere.com',],
  241. }
  242. # Simulated SMTP channel & server
  243. class SimSMTPChannel(smtpd.SMTPChannel):
  244. def __init__(self, extra_features, *args, **kw):
  245. self._extrafeatures = ''.join(
  246. [ "250-{0}\r\n".format(x) for x in extra_features ])
  247. smtpd.SMTPChannel.__init__(self, *args, **kw)
  248. def smtp_EHLO(self, arg):
  249. resp = ('250-testhost\r\n'
  250. '250-EXPN\r\n'
  251. '250-SIZE 20000000\r\n'
  252. '250-STARTTLS\r\n'
  253. '250-DELIVERBY\r\n')
  254. resp = resp + self._extrafeatures + '250 HELP'
  255. self.push(resp)
  256. def smtp_VRFY(self, arg):
  257. raw_addr = email.utils.parseaddr(arg)[1]
  258. quoted_addr = smtplib.quoteaddr(arg)
  259. if raw_addr in sim_users:
  260. self.push('250 %s %s' % (sim_users[raw_addr], quoted_addr))
  261. else:
  262. self.push('550 No such user: %s' % arg)
  263. def smtp_EXPN(self, arg):
  264. list_name = email.utils.parseaddr(arg)[1].lower()
  265. if list_name in sim_lists:
  266. user_list = sim_lists[list_name]
  267. for n, user_email in enumerate(user_list):
  268. quoted_addr = smtplib.quoteaddr(user_email)
  269. if n < len(user_list) - 1:
  270. self.push('250-%s %s' % (sim_users[user_email], quoted_addr))
  271. else:
  272. self.push('250 %s %s' % (sim_users[user_email], quoted_addr))
  273. else:
  274. self.push('550 No access for you!')
  275. def smtp_AUTH(self, arg):
  276. if arg.strip().lower()=='cram-md5':
  277. self.push('334 {0}'.format(sim_cram_md5_challenge))
  278. return
  279. mech, auth = arg.split()
  280. mech = mech.lower()
  281. if mech not in sim_auth_credentials:
  282. self.push('504 auth type unimplemented')
  283. return
  284. if mech == 'plain' and auth==sim_auth_credentials['plain']:
  285. self.push('235 plain auth ok')
  286. elif mech=='login' and auth==sim_auth_credentials['login']:
  287. self.push('334 Password:')
  288. else:
  289. self.push('550 No access for you!')
  290. class SimSMTPServer(smtpd.SMTPServer):
  291. def __init__(self, *args, **kw):
  292. self._extra_features = []
  293. smtpd.SMTPServer.__init__(self, *args, **kw)
  294. def handle_accept(self):
  295. conn, addr = self.accept()
  296. self._SMTPchannel = SimSMTPChannel(self._extra_features,
  297. self, conn, addr)
  298. def process_message(self, peer, mailfrom, rcpttos, data):
  299. pass
  300. def add_feature(self, feature):
  301. self._extra_features.append(feature)
  302. # Test various SMTP & ESMTP commands/behaviors that require a simulated server
  303. # (i.e., something with more features than DebuggingServer)
  304. class SMTPSimTests(TestCase):
  305. def setUp(self):
  306. self.serv_evt = threading.Event()
  307. self.client_evt = threading.Event()
  308. self.port = test_support.find_unused_port()
  309. self.serv = SimSMTPServer((HOST, self.port), ('nowhere', -1))
  310. serv_args = (self.serv, self.serv_evt, self.client_evt)
  311. threading.Thread(target=debugging_server, args=serv_args).start()
  312. # wait until server thread has assigned a port number
  313. self.serv_evt.wait()
  314. self.serv_evt.clear()
  315. def tearDown(self):
  316. # indicate that the client is finished
  317. self.client_evt.set()
  318. # wait for the server thread to terminate
  319. self.serv_evt.wait()
  320. def testBasic(self):
  321. # smoke test
  322. smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
  323. smtp.quit()
  324. def testEHLO(self):
  325. smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
  326. # no features should be present before the EHLO
  327. self.assertEqual(smtp.esmtp_features, {})
  328. # features expected from the test server
  329. expected_features = {'expn':'',
  330. 'size': '20000000',
  331. 'starttls': '',
  332. 'deliverby': '',
  333. 'help': '',
  334. }
  335. smtp.ehlo()
  336. self.assertEqual(smtp.esmtp_features, expected_features)
  337. for k in expected_features:
  338. self.assertTrue(smtp.has_extn(k))
  339. self.assertFalse(smtp.has_extn('unsupported-feature'))
  340. smtp.quit()
  341. def testVRFY(self):
  342. smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
  343. for email, name in sim_users.items():
  344. expected_known = (250, '%s %s' % (name, smtplib.quoteaddr(email)))
  345. self.assertEqual(smtp.vrfy(email), expected_known)
  346. u = 'nobody@nowhere.com'
  347. expected_unknown = (550, 'No such user: %s' % smtplib.quoteaddr(u))
  348. self.assertEqual(smtp.vrfy(u), expected_unknown)
  349. smtp.quit()
  350. def testEXPN(self):
  351. smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
  352. for listname, members in sim_lists.items():
  353. users = []
  354. for m in members:
  355. users.append('%s %s' % (sim_users[m], smtplib.quoteaddr(m)))
  356. expected_known = (250, '\n'.join(users))
  357. self.assertEqual(smtp.expn(listname), expected_known)
  358. u = 'PSU-Members-List'
  359. expected_unknown = (550, 'No access for you!')
  360. self.assertEqual(smtp.expn(u), expected_unknown)
  361. smtp.quit()
  362. def testAUTH_PLAIN(self):
  363. self.serv.add_feature("AUTH PLAIN")
  364. smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
  365. expected_auth_ok = (235, b'plain auth ok')
  366. self.assertEqual(smtp.login(sim_auth[0], sim_auth[1]), expected_auth_ok)
  367. # SimSMTPChannel doesn't fully support LOGIN or CRAM-MD5 auth because they
  368. # require a synchronous read to obtain the credentials...so instead smtpd
  369. # sees the credential sent by smtplib's login method as an unknown command,
  370. # which results in smtplib raising an auth error. Fortunately the error
  371. # message contains the encoded credential, so we can partially check that it
  372. # was generated correctly (partially, because the 'word' is uppercased in
  373. # the error message).
  374. def testAUTH_LOGIN(self):
  375. self.serv.add_feature("AUTH LOGIN")
  376. smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
  377. try: smtp.login(sim_auth[0], sim_auth[1])
  378. except smtplib.SMTPAuthenticationError as err:
  379. if sim_auth_login_password not in str(err):
  380. raise "expected encoded password not found in error message"
  381. def testAUTH_CRAM_MD5(self):
  382. self.serv.add_feature("AUTH CRAM-MD5")
  383. smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
  384. try: smtp.login(sim_auth[0], sim_auth[1])
  385. except smtplib.SMTPAuthenticationError as err:
  386. if sim_auth_credentials['cram-md5'] not in str(err):
  387. raise "expected encoded credentials not found in error message"
  388. #TODO: add tests for correct AUTH method fallback now that the
  389. #test infrastructure can support it.
  390. def test_main(verbose=None):
  391. test_support.run_unittest(GeneralTests, DebuggingServerTests,
  392. NonConnectingTests,
  393. BadHELOServerTests, SMTPSimTests)
  394. if __name__ == '__main__':
  395. test_main()