PageRenderTime 673ms CodeModel.GetById 160ms app.highlight 212ms RepoModel.GetById 156ms app.codeStats 0ms

/Lib/test/test_smtplib.py

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