OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.mail.test.test_pop3client -*- | |
2 # Copyright (c) 2001-2004 Divmod Inc. | |
3 # See LICENSE for details. | |
4 | |
5 from zope.interface import directlyProvides | |
6 | |
7 from twisted.mail.pop3 import AdvancedPOP3Client as POP3Client | |
8 from twisted.mail.pop3 import InsecureAuthenticationDisallowed | |
9 from twisted.mail.pop3 import ServerErrorResponse | |
10 from twisted.protocols import loopback | |
11 from twisted.internet import reactor, defer, error, protocol, interfaces | |
12 from twisted.python import log | |
13 | |
14 from twisted.trial import unittest | |
15 from twisted.test.proto_helpers import StringTransport | |
16 from twisted.protocols import basic | |
17 | |
18 from twisted.mail.test import pop3testserver | |
19 | |
20 try: | |
21 from twisted.test.ssl_helpers import ClientTLSContext, ServerTLSContext | |
22 except ImportError: | |
23 ClientTLSContext = ServerTLSContext = None | |
24 | |
25 | |
26 class StringTransportWithConnectionLosing(StringTransport): | |
27 def loseConnection(self): | |
28 self.protocol.connectionLost(error.ConnectionDone()) | |
29 | |
30 | |
31 capCache = {"TOP": None, "LOGIN-DELAY": "180", "UIDL": None, \ | |
32 "STLS": None, "USER": None, "SASL": "LOGIN"} | |
33 def setUp(greet=True): | |
34 p = POP3Client() | |
35 | |
36 # Skip the CAPA login will issue if it doesn't already have a | |
37 # capability cache | |
38 p._capCache = capCache | |
39 | |
40 t = StringTransportWithConnectionLosing() | |
41 t.protocol = p | |
42 p.makeConnection(t) | |
43 | |
44 if greet: | |
45 p.dataReceived('+OK Hello!\r\n') | |
46 | |
47 return p, t | |
48 | |
49 def strip(f): | |
50 return lambda result, f=f: f() | |
51 | |
52 class POP3ClientLoginTestCase(unittest.TestCase): | |
53 def testNegativeGreeting(self): | |
54 p, t = setUp(greet=False) | |
55 p.allowInsecureLogin = True | |
56 d = p.login("username", "password") | |
57 p.dataReceived('-ERR Offline for maintenance\r\n') | |
58 return self.assertFailure( | |
59 d, ServerErrorResponse).addCallback( | |
60 lambda exc: self.assertEquals(exc.args[0], "Offline for maintenance"
)) | |
61 | |
62 | |
63 def testOkUser(self): | |
64 p, t = setUp() | |
65 d = p.user("username") | |
66 self.assertEquals(t.value(), "USER username\r\n") | |
67 p.dataReceived("+OK send password\r\n") | |
68 return d.addCallback(self.assertEqual, "send password") | |
69 | |
70 def testBadUser(self): | |
71 p, t = setUp() | |
72 d = p.user("username") | |
73 self.assertEquals(t.value(), "USER username\r\n") | |
74 p.dataReceived("-ERR account suspended\r\n") | |
75 return self.assertFailure( | |
76 d, ServerErrorResponse).addCallback( | |
77 lambda exc: self.assertEquals(exc.args[0], "account suspended")) | |
78 | |
79 def testOkPass(self): | |
80 p, t = setUp() | |
81 d = p.password("password") | |
82 self.assertEquals(t.value(), "PASS password\r\n") | |
83 p.dataReceived("+OK you're in!\r\n") | |
84 return d.addCallback(self.assertEqual, "you're in!") | |
85 | |
86 def testBadPass(self): | |
87 p, t = setUp() | |
88 d = p.password("password") | |
89 self.assertEquals(t.value(), "PASS password\r\n") | |
90 p.dataReceived("-ERR go away\r\n") | |
91 return self.assertFailure( | |
92 d, ServerErrorResponse).addCallback( | |
93 lambda exc: self.assertEquals(exc.args[0], "go away")) | |
94 | |
95 def testOkLogin(self): | |
96 p, t = setUp() | |
97 p.allowInsecureLogin = True | |
98 d = p.login("username", "password") | |
99 self.assertEquals(t.value(), "USER username\r\n") | |
100 p.dataReceived("+OK go ahead\r\n") | |
101 self.assertEquals(t.value(), "USER username\r\nPASS password\r\n") | |
102 p.dataReceived("+OK password accepted\r\n") | |
103 return d.addCallback(self.assertEqual, "password accepted") | |
104 | |
105 def testBadPasswordLogin(self): | |
106 p, t = setUp() | |
107 p.allowInsecureLogin = True | |
108 d = p.login("username", "password") | |
109 self.assertEquals(t.value(), "USER username\r\n") | |
110 p.dataReceived("+OK waiting on you\r\n") | |
111 self.assertEquals(t.value(), "USER username\r\nPASS password\r\n") | |
112 p.dataReceived("-ERR bogus login\r\n") | |
113 return self.assertFailure( | |
114 d, ServerErrorResponse).addCallback( | |
115 lambda exc: self.assertEquals(exc.args[0], "bogus login")) | |
116 | |
117 def testBadUsernameLogin(self): | |
118 p, t = setUp() | |
119 p.allowInsecureLogin = True | |
120 d = p.login("username", "password") | |
121 self.assertEquals(t.value(), "USER username\r\n") | |
122 p.dataReceived("-ERR bogus login\r\n") | |
123 return self.assertFailure( | |
124 d, ServerErrorResponse).addCallback( | |
125 lambda exc: self.assertEquals(exc.args[0], "bogus login")) | |
126 | |
127 def testServerGreeting(self): | |
128 p, t = setUp(greet=False) | |
129 p.dataReceived("+OK lalala this has no challenge\r\n") | |
130 self.assertEquals(p.serverChallenge, None) | |
131 | |
132 def testServerGreetingWithChallenge(self): | |
133 p, t = setUp(greet=False) | |
134 p.dataReceived("+OK <here is the challenge>\r\n") | |
135 self.assertEquals(p.serverChallenge, "<here is the challenge>") | |
136 | |
137 def testAPOP(self): | |
138 p, t = setUp(greet=False) | |
139 p.dataReceived("+OK <challenge string goes here>\r\n") | |
140 d = p.login("username", "password") | |
141 self.assertEquals(t.value(), "APOP username f34f1e464d0d7927607753129cab
e39a\r\n") | |
142 p.dataReceived("+OK Welcome!\r\n") | |
143 return d.addCallback(self.assertEqual, "Welcome!") | |
144 | |
145 def testInsecureLoginRaisesException(self): | |
146 p, t = setUp(greet=False) | |
147 p.dataReceived("+OK Howdy\r\n") | |
148 d = p.login("username", "password") | |
149 self.failIf(t.value()) | |
150 return self.assertFailure( | |
151 d, InsecureAuthenticationDisallowed) | |
152 | |
153 | |
154 def testSSLTransportConsideredSecure(self): | |
155 """ | |
156 If a server doesn't offer APOP but the transport is secured using | |
157 SSL or TLS, a plaintext login should be allowed, not rejected with | |
158 an InsecureAuthenticationDisallowed exception. | |
159 """ | |
160 p, t = setUp(greet=False) | |
161 directlyProvides(t, interfaces.ISSLTransport) | |
162 p.dataReceived("+OK Howdy\r\n") | |
163 d = p.login("username", "password") | |
164 self.assertEquals(t.value(), "USER username\r\n") | |
165 t.clear() | |
166 p.dataReceived("+OK\r\n") | |
167 self.assertEquals(t.value(), "PASS password\r\n") | |
168 p.dataReceived("+OK\r\n") | |
169 return d | |
170 | |
171 | |
172 | |
173 class ListConsumer: | |
174 def __init__(self): | |
175 self.data = {} | |
176 | |
177 def consume(self, (item, value)): | |
178 self.data.setdefault(item, []).append(value) | |
179 | |
180 class MessageConsumer: | |
181 def __init__(self): | |
182 self.data = [] | |
183 | |
184 def consume(self, line): | |
185 self.data.append(line) | |
186 | |
187 class POP3ClientListTestCase(unittest.TestCase): | |
188 def testListSize(self): | |
189 p, t = setUp() | |
190 d = p.listSize() | |
191 self.assertEquals(t.value(), "LIST\r\n") | |
192 p.dataReceived("+OK Here it comes\r\n") | |
193 p.dataReceived("1 3\r\n2 2\r\n3 1\r\n.\r\n") | |
194 return d.addCallback(self.assertEqual, [3, 2, 1]) | |
195 | |
196 def testListSizeWithConsumer(self): | |
197 p, t = setUp() | |
198 c = ListConsumer() | |
199 f = c.consume | |
200 d = p.listSize(f) | |
201 self.assertEquals(t.value(), "LIST\r\n") | |
202 p.dataReceived("+OK Here it comes\r\n") | |
203 p.dataReceived("1 3\r\n2 2\r\n3 1\r\n") | |
204 self.assertEquals(c.data, {0: [3], 1: [2], 2: [1]}) | |
205 p.dataReceived("5 3\r\n6 2\r\n7 1\r\n") | |
206 self.assertEquals(c.data, {0: [3], 1: [2], 2: [1], 4: [3], 5: [2], 6: [1
]}) | |
207 p.dataReceived(".\r\n") | |
208 return d.addCallback(self.assertIdentical, f) | |
209 | |
210 def testFailedListSize(self): | |
211 p, t = setUp() | |
212 d = p.listSize() | |
213 self.assertEquals(t.value(), "LIST\r\n") | |
214 p.dataReceived("-ERR Fatal doom server exploded\r\n") | |
215 return self.assertFailure( | |
216 d, ServerErrorResponse).addCallback( | |
217 lambda exc: self.assertEquals(exc.args[0], "Fatal doom server explod
ed")) | |
218 | |
219 def testListUID(self): | |
220 p, t = setUp() | |
221 d = p.listUID() | |
222 self.assertEquals(t.value(), "UIDL\r\n") | |
223 p.dataReceived("+OK Here it comes\r\n") | |
224 p.dataReceived("1 abc\r\n2 def\r\n3 ghi\r\n.\r\n") | |
225 return d.addCallback(self.assertEqual, ["abc", "def", "ghi"]) | |
226 | |
227 def testListUIDWithConsumer(self): | |
228 p, t = setUp() | |
229 c = ListConsumer() | |
230 f = c.consume | |
231 d = p.listUID(f) | |
232 self.assertEquals(t.value(), "UIDL\r\n") | |
233 p.dataReceived("+OK Here it comes\r\n") | |
234 p.dataReceived("1 xyz\r\n2 abc\r\n5 mno\r\n") | |
235 self.assertEquals(c.data, {0: ["xyz"], 1: ["abc"], 4: ["mno"]}) | |
236 p.dataReceived(".\r\n") | |
237 return d.addCallback(self.assertIdentical, f) | |
238 | |
239 def testFailedListUID(self): | |
240 p, t = setUp() | |
241 d = p.listUID() | |
242 self.assertEquals(t.value(), "UIDL\r\n") | |
243 p.dataReceived("-ERR Fatal doom server exploded\r\n") | |
244 return self.assertFailure( | |
245 d, ServerErrorResponse).addCallback( | |
246 lambda exc: self.assertEquals(exc.args[0], "Fatal doom server explod
ed")) | |
247 | |
248 class POP3ClientMessageTestCase(unittest.TestCase): | |
249 def testRetrieve(self): | |
250 p, t = setUp() | |
251 d = p.retrieve(7) | |
252 self.assertEquals(t.value(), "RETR 8\r\n") | |
253 p.dataReceived("+OK Message incoming\r\n") | |
254 p.dataReceived("La la la here is message text\r\n") | |
255 p.dataReceived("..Further message text tra la la\r\n") | |
256 p.dataReceived(".\r\n") | |
257 return d.addCallback( | |
258 self.assertEqual, | |
259 ["La la la here is message text", | |
260 ".Further message text tra la la"]) | |
261 | |
262 def testRetrieveWithConsumer(self): | |
263 p, t = setUp() | |
264 c = MessageConsumer() | |
265 f = c.consume | |
266 d = p.retrieve(7, f) | |
267 self.assertEquals(t.value(), "RETR 8\r\n") | |
268 p.dataReceived("+OK Message incoming\r\n") | |
269 p.dataReceived("La la la here is message text\r\n") | |
270 p.dataReceived("..Further message text\r\n.\r\n") | |
271 return d.addCallback(self._cbTestRetrieveWithConsumer, f, c) | |
272 | |
273 def _cbTestRetrieveWithConsumer(self, result, f, c): | |
274 self.assertIdentical(result, f) | |
275 self.assertEquals(c.data, ["La la la here is message text", | |
276 ".Further message text"]) | |
277 | |
278 def testPartialRetrieve(self): | |
279 p, t = setUp() | |
280 d = p.retrieve(7, lines=2) | |
281 self.assertEquals(t.value(), "TOP 8 2\r\n") | |
282 p.dataReceived("+OK 2 lines on the way\r\n") | |
283 p.dataReceived("Line the first! Woop\r\n") | |
284 p.dataReceived("Line the last! Bye\r\n") | |
285 p.dataReceived(".\r\n") | |
286 return d.addCallback( | |
287 self.assertEqual, | |
288 ["Line the first! Woop", | |
289 "Line the last! Bye"]) | |
290 | |
291 def testPartialRetrieveWithConsumer(self): | |
292 p, t = setUp() | |
293 c = MessageConsumer() | |
294 f = c.consume | |
295 d = p.retrieve(7, f, lines=2) | |
296 self.assertEquals(t.value(), "TOP 8 2\r\n") | |
297 p.dataReceived("+OK 2 lines on the way\r\n") | |
298 p.dataReceived("Line the first! Woop\r\n") | |
299 p.dataReceived("Line the last! Bye\r\n") | |
300 p.dataReceived(".\r\n") | |
301 return d.addCallback(self._cbTestPartialRetrieveWithConsumer, f, c) | |
302 | |
303 def _cbTestPartialRetrieveWithConsumer(self, result, f, c): | |
304 self.assertIdentical(result, f) | |
305 self.assertEquals(c.data, ["Line the first! Woop", | |
306 "Line the last! Bye"]) | |
307 | |
308 def testFailedRetrieve(self): | |
309 p, t = setUp() | |
310 d = p.retrieve(0) | |
311 self.assertEquals(t.value(), "RETR 1\r\n") | |
312 p.dataReceived("-ERR Fatal doom server exploded\r\n") | |
313 return self.assertFailure( | |
314 d, ServerErrorResponse).addCallback( | |
315 lambda exc: self.assertEquals(exc.args[0], "Fatal doom server explod
ed")) | |
316 | |
317 | |
318 def test_concurrentRetrieves(self): | |
319 """ | |
320 Issue three retrieve calls immediately without waiting for any to | |
321 succeed and make sure they all do succeed eventually. | |
322 """ | |
323 p, t = setUp() | |
324 messages = [ | |
325 p.retrieve(i).addCallback( | |
326 self.assertEquals, | |
327 ["First line of %d." % (i + 1,), | |
328 "Second line of %d." % (i + 1,)]) | |
329 for i | |
330 in range(3)] | |
331 | |
332 for i in range(1, 4): | |
333 self.assertEquals(t.value(), "RETR %d\r\n" % (i,)) | |
334 t.clear() | |
335 p.dataReceived("+OK 2 lines on the way\r\n") | |
336 p.dataReceived("First line of %d.\r\n" % (i,)) | |
337 p.dataReceived("Second line of %d.\r\n" % (i,)) | |
338 self.assertEquals(t.value(), "") | |
339 p.dataReceived(".\r\n") | |
340 | |
341 return defer.DeferredList(messages, fireOnOneErrback=True) | |
342 | |
343 | |
344 | |
345 class POP3ClientMiscTestCase(unittest.TestCase): | |
346 def testCapability(self): | |
347 p, t = setUp() | |
348 d = p.capabilities(useCache=0) | |
349 self.assertEquals(t.value(), "CAPA\r\n") | |
350 p.dataReceived("+OK Capabilities on the way\r\n") | |
351 p.dataReceived("X\r\nY\r\nZ\r\nA 1 2 3\r\nB 1 2\r\nC 1\r\n.\r\n") | |
352 return d.addCallback( | |
353 self.assertEqual, | |
354 {"X": None, "Y": None, "Z": None, | |
355 "A": ["1", "2", "3"], | |
356 "B": ["1", "2"], | |
357 "C": ["1"]}) | |
358 | |
359 def testCapabilityError(self): | |
360 p, t = setUp() | |
361 d = p.capabilities(useCache=0) | |
362 self.assertEquals(t.value(), "CAPA\r\n") | |
363 p.dataReceived("-ERR This server is lame!\r\n") | |
364 return d.addCallback(self.assertEquals, {}) | |
365 | |
366 def testStat(self): | |
367 p, t = setUp() | |
368 d = p.stat() | |
369 self.assertEquals(t.value(), "STAT\r\n") | |
370 p.dataReceived("+OK 1 1212\r\n") | |
371 return d.addCallback(self.assertEqual, (1, 1212)) | |
372 | |
373 def testStatError(self): | |
374 p, t = setUp() | |
375 d = p.stat() | |
376 self.assertEquals(t.value(), "STAT\r\n") | |
377 p.dataReceived("-ERR This server is lame!\r\n") | |
378 return self.assertFailure( | |
379 d, ServerErrorResponse).addCallback( | |
380 lambda exc: self.assertEquals(exc.args[0], "This server is lame!")) | |
381 | |
382 def testNoop(self): | |
383 p, t = setUp() | |
384 d = p.noop() | |
385 self.assertEquals(t.value(), "NOOP\r\n") | |
386 p.dataReceived("+OK No-op to you too!\r\n") | |
387 return d.addCallback(self.assertEqual, "No-op to you too!") | |
388 | |
389 def testNoopError(self): | |
390 p, t = setUp() | |
391 d = p.noop() | |
392 self.assertEquals(t.value(), "NOOP\r\n") | |
393 p.dataReceived("-ERR This server is lame!\r\n") | |
394 return self.assertFailure( | |
395 d, ServerErrorResponse).addCallback( | |
396 lambda exc: self.assertEquals(exc.args[0], "This server is lame!")) | |
397 | |
398 def testRset(self): | |
399 p, t = setUp() | |
400 d = p.reset() | |
401 self.assertEquals(t.value(), "RSET\r\n") | |
402 p.dataReceived("+OK Reset state\r\n") | |
403 return d.addCallback(self.assertEqual, "Reset state") | |
404 | |
405 def testRsetError(self): | |
406 p, t = setUp() | |
407 d = p.reset() | |
408 self.assertEquals(t.value(), "RSET\r\n") | |
409 p.dataReceived("-ERR This server is lame!\r\n") | |
410 return self.assertFailure( | |
411 d, ServerErrorResponse).addCallback( | |
412 lambda exc: self.assertEquals(exc.args[0], "This server is lame!")) | |
413 | |
414 def testDelete(self): | |
415 p, t = setUp() | |
416 d = p.delete(3) | |
417 self.assertEquals(t.value(), "DELE 4\r\n") | |
418 p.dataReceived("+OK Hasta la vista\r\n") | |
419 return d.addCallback(self.assertEqual, "Hasta la vista") | |
420 | |
421 def testDeleteError(self): | |
422 p, t = setUp() | |
423 d = p.delete(3) | |
424 self.assertEquals(t.value(), "DELE 4\r\n") | |
425 p.dataReceived("-ERR Winner is not you.\r\n") | |
426 return self.assertFailure( | |
427 d, ServerErrorResponse).addCallback( | |
428 lambda exc: self.assertEquals(exc.args[0], "Winner is not you.")) | |
429 | |
430 | |
431 class SimpleClient(POP3Client): | |
432 def __init__(self, deferred, contextFactory = None): | |
433 self.deferred = deferred | |
434 self.allowInsecureLogin = True | |
435 | |
436 def serverGreeting(self, challenge): | |
437 self.deferred.callback(None) | |
438 | |
439 class POP3HelperMixin: | |
440 serverCTX = None | |
441 clientCTX = None | |
442 | |
443 def setUp(self): | |
444 d = defer.Deferred() | |
445 self.server = pop3testserver.POP3TestServer(contextFactory=self.serverCT
X) | |
446 self.client = SimpleClient(d, contextFactory=self.clientCTX) | |
447 self.client.timeout = 30 | |
448 self.connected = d | |
449 | |
450 def tearDown(self): | |
451 del self.server | |
452 del self.client | |
453 del self.connected | |
454 | |
455 def _cbStopClient(self, ignore): | |
456 self.client.transport.loseConnection() | |
457 | |
458 def _ebGeneral(self, failure): | |
459 self.client.transport.loseConnection() | |
460 self.server.transport.loseConnection() | |
461 return failure | |
462 | |
463 def loopback(self): | |
464 return loopback.loopbackTCP(self.server, self.client, noisy=False) | |
465 | |
466 | |
467 class TLSServerFactory(protocol.ServerFactory): | |
468 class protocol(basic.LineReceiver): | |
469 context = None | |
470 output = [] | |
471 def connectionMade(self): | |
472 self.factory.input = [] | |
473 self.output = self.output[:] | |
474 map(self.sendLine, self.output.pop(0)) | |
475 def lineReceived(self, line): | |
476 self.factory.input.append(line) | |
477 map(self.sendLine, self.output.pop(0)) | |
478 if line == 'STLS': | |
479 self.transport.startTLS(self.context) | |
480 | |
481 | |
482 class POP3TLSTestCase(unittest.TestCase): | |
483 def testStartTLS(self): | |
484 sf = TLSServerFactory() | |
485 sf.protocol.output = [ | |
486 ['+OK'], # Server greeting | |
487 ['+OK', 'STLS', '.'], # CAPA response | |
488 ['+OK'], # STLS response | |
489 ['+OK', '.'], # Second CAPA response | |
490 ['+OK'] # QUIT response | |
491 ] | |
492 sf.protocol.context = ServerTLSContext() | |
493 port = reactor.listenTCP(0, sf, interface='127.0.0.1') | |
494 H = port.getHost().host | |
495 P = port.getHost().port | |
496 | |
497 cp = SimpleClient(defer.Deferred(), ClientTLSContext()) | |
498 cf = protocol.ClientFactory() | |
499 cf.protocol = lambda: cp | |
500 | |
501 conn = reactor.connectTCP(H, P, cf) | |
502 | |
503 def cbConnected(ignored): | |
504 log.msg("Connected to server; starting TLS") | |
505 return cp.startTLS() | |
506 | |
507 def cbStartedTLS(ignored): | |
508 log.msg("Started TLS; disconnecting") | |
509 return cp.quit() | |
510 | |
511 def cbDisconnected(ign): | |
512 log.msg("Disconnected; asserting correct input received") | |
513 self.assertEquals( | |
514 sf.input, | |
515 ['CAPA', 'STLS', 'CAPA', 'QUIT']) | |
516 | |
517 def cleanup(result): | |
518 log.msg("Asserted correct input; disconnecting client and shutting d
own server") | |
519 conn.disconnect() | |
520 | |
521 def cbShutdown(ignored): | |
522 log.msg("Shut down server") | |
523 return result | |
524 | |
525 return defer.maybeDeferred(port.stopListening).addCallback(cbShutdow
n) | |
526 | |
527 cp.deferred.addCallback(cbConnected) | |
528 cp.deferred.addCallback(cbStartedTLS) | |
529 cp.deferred.addCallback(cbDisconnected) | |
530 cp.deferred.addBoth(cleanup) | |
531 | |
532 return cp.deferred | |
533 | |
534 | |
535 class POP3TimeoutTestCase(POP3HelperMixin, unittest.TestCase): | |
536 def testTimeout(self): | |
537 def login(): | |
538 d = self.client.login('test', 'twisted') | |
539 d.addCallback(loggedIn) | |
540 d.addErrback(timedOut) | |
541 return d | |
542 | |
543 def loggedIn(result): | |
544 self.fail("Successfully logged in!? Impossible!") | |
545 | |
546 | |
547 def timedOut(failure): | |
548 failure.trap(error.TimeoutError) | |
549 self._cbStopClient(None) | |
550 | |
551 def quit(): | |
552 return self.client.quit() | |
553 | |
554 self.client.timeout = 0.01 | |
555 | |
556 # Tell the server to not return a response to client. This | |
557 # will trigger a timeout. | |
558 pop3testserver.TIMEOUT_RESPONSE = True | |
559 | |
560 methods = [login, quit] | |
561 map(self.connected.addCallback, map(strip, methods)) | |
562 self.connected.addCallback(self._cbStopClient) | |
563 self.connected.addErrback(self._ebGeneral) | |
564 return self.loopback() | |
565 | |
566 | |
567 if ClientTLSContext is None: | |
568 for case in (POP3TLSTestCase,): | |
569 case.skip = "OpenSSL not present" | |
570 elif interfaces.IReactorSSL(reactor, None) is None: | |
571 for case in (POP3TLSTestCase,): | |
572 case.skip = "Reactor doesn't support SSL" | |
573 | |
OLD | NEW |