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 """ | |
6 POP3 client protocol implementation | |
7 | |
8 Don't use this module directly. Use twisted.mail.pop3 instead. | |
9 | |
10 @author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>} | |
11 """ | |
12 | |
13 import re, md5 | |
14 | |
15 from twisted.python import log | |
16 from twisted.internet import defer | |
17 from twisted.protocols import basic | |
18 from twisted.protocols import policies | |
19 from twisted.internet import error | |
20 from twisted.internet import interfaces | |
21 | |
22 OK = '+OK' | |
23 ERR = '-ERR' | |
24 | |
25 class POP3ClientError(Exception): | |
26 """Base class for all exceptions raised by POP3Client. | |
27 """ | |
28 | |
29 class InsecureAuthenticationDisallowed(POP3ClientError): | |
30 """Secure authentication was required but no mechanism could be found. | |
31 """ | |
32 | |
33 class TLSError(POP3ClientError): | |
34 """ | |
35 Secure authentication was required but either the transport does | |
36 not support TLS or no TLS context factory was supplied. | |
37 """ | |
38 | |
39 class TLSNotSupportedError(POP3ClientError): | |
40 """ | |
41 Secure authentication was required but the server does not support | |
42 TLS. | |
43 """ | |
44 | |
45 class ServerErrorResponse(POP3ClientError): | |
46 """The server returned an error response to a request. | |
47 """ | |
48 def __init__(self, reason, consumer=None): | |
49 POP3ClientError.__init__(self, reason) | |
50 self.consumer = consumer | |
51 | |
52 class LineTooLong(POP3ClientError): | |
53 """The server sent an extremely long line. | |
54 """ | |
55 | |
56 class _ListSetter: | |
57 # Internal helper. POP3 responses sometimes occur in the | |
58 # form of a list of lines containing two pieces of data, | |
59 # a message index and a value of some sort. When a message | |
60 # is deleted, it is omitted from these responses. The | |
61 # setitem method of this class is meant to be called with | |
62 # these two values. In the cases where indexes are skipped, | |
63 # it takes care of padding out the missing values with None. | |
64 def __init__(self, L): | |
65 self.L = L | |
66 def setitem(self, (item, value)): | |
67 diff = item - len(self.L) + 1 | |
68 if diff > 0: | |
69 self.L.extend([None] * diff) | |
70 self.L[item] = value | |
71 | |
72 | |
73 def _statXform(line): | |
74 # Parse a STAT response | |
75 numMsgs, totalSize = line.split(None, 1) | |
76 return int(numMsgs), int(totalSize) | |
77 | |
78 | |
79 def _listXform(line): | |
80 # Parse a LIST response | |
81 index, size = line.split(None, 1) | |
82 return int(index) - 1, int(size) | |
83 | |
84 | |
85 def _uidXform(line): | |
86 # Parse a UIDL response | |
87 index, uid = line.split(None, 1) | |
88 return int(index) - 1, uid | |
89 | |
90 def _codeStatusSplit(line): | |
91 # Parse an +OK or -ERR response | |
92 parts = line.split(' ', 1) | |
93 if len(parts) == 1: | |
94 return parts[0], '' | |
95 return parts | |
96 | |
97 def _dotUnquoter(line): | |
98 """ | |
99 C{'.'} characters which begin a line of a message are doubled to avoid | |
100 confusing with the terminating C{'.\\r\\n'} sequence. This function | |
101 unquotes them. | |
102 """ | |
103 if line.startswith('..'): | |
104 return line[1:] | |
105 return line | |
106 | |
107 class POP3Client(basic.LineOnlyReceiver, policies.TimeoutMixin): | |
108 """POP3 client protocol implementation class | |
109 | |
110 Instances of this class provide a convenient, efficient API for | |
111 retrieving and deleting messages from a POP3 server. | |
112 | |
113 @type startedTLS: C{bool} | |
114 @ivar startedTLS: Whether TLS has been negotiated successfully. | |
115 | |
116 | |
117 @type allowInsecureLogin: C{bool} | |
118 @ivar allowInsecureLogin: Indicate whether login() should be | |
119 allowed if the server offers no authentication challenge and if | |
120 our transport does not offer any protection via encryption. | |
121 | |
122 @type serverChallenge: C{str} or C{None} | |
123 @ivar serverChallenge: Challenge received from the server | |
124 | |
125 @type timeout: C{int} | |
126 @ivar timeout: Number of seconds to wait before timing out a | |
127 connection. If the number is <= 0, no timeout checking will be | |
128 performed. | |
129 """ | |
130 | |
131 startedTLS = False | |
132 allowInsecureLogin = False | |
133 timeout = 0 | |
134 serverChallenge = None | |
135 | |
136 # Capabilities are not allowed to change during the session | |
137 # (except when TLS is negotiated), so cache the first response and | |
138 # use that for all later lookups | |
139 _capCache = None | |
140 | |
141 # Regular expression to search for in the challenge string in the server | |
142 # greeting line. | |
143 _challengeMagicRe = re.compile('(<[^>]+>)') | |
144 | |
145 # List of pending calls. | |
146 # We are a pipelining API but don't actually | |
147 # support pipelining on the network yet. | |
148 _blockedQueue = None | |
149 | |
150 # The Deferred to which the very next result will go. | |
151 _waiting = None | |
152 | |
153 # Whether we dropped the connection because of a timeout | |
154 _timedOut = False | |
155 | |
156 # If the server sends an initial -ERR, this is the message it sent | |
157 # with it. | |
158 _greetingError = None | |
159 | |
160 def _blocked(self, f, *a): | |
161 # Internal helper. If commands are being blocked, append | |
162 # the given command and arguments to a list and return a Deferred | |
163 # that will be chained with the return value of the function | |
164 # when it eventually runs. Otherwise, set up for commands to be | |
165 | |
166 # blocked and return None. | |
167 if self._blockedQueue is not None: | |
168 d = defer.Deferred() | |
169 self._blockedQueue.append((d, f, a)) | |
170 return d | |
171 self._blockedQueue = [] | |
172 return None | |
173 | |
174 def _unblock(self): | |
175 # Internal helper. Indicate that a function has completed. | |
176 # If there are blocked commands, run the next one. If there | |
177 # are not, set up for the next command to not be blocked. | |
178 if self._blockedQueue == []: | |
179 self._blockedQueue = None | |
180 elif self._blockedQueue is not None: | |
181 _blockedQueue = self._blockedQueue | |
182 self._blockedQueue = None | |
183 | |
184 d, f, a = _blockedQueue.pop(0) | |
185 d2 = f(*a) | |
186 d2.chainDeferred(d) | |
187 # f is a function which uses _blocked (otherwise it wouldn't | |
188 # have gotten into the blocked queue), which means it will have | |
189 # re-set _blockedQueue to an empty list, so we can put the rest | |
190 # of the blocked queue back into it now. | |
191 self._blockedQueue.extend(_blockedQueue) | |
192 | |
193 | |
194 def sendShort(self, cmd, args): | |
195 # Internal helper. Send a command to which a short response | |
196 # is expected. Return a Deferred that fires when the response | |
197 # is received. Block all further commands from being sent until | |
198 # the response is received. Transition the state to SHORT. | |
199 d = self._blocked(self.sendShort, cmd, args) | |
200 if d is not None: | |
201 return d | |
202 | |
203 if args: | |
204 self.sendLine(cmd + ' ' + args) | |
205 else: | |
206 self.sendLine(cmd) | |
207 self.state = 'SHORT' | |
208 self._waiting = defer.Deferred() | |
209 return self._waiting | |
210 | |
211 def sendLong(self, cmd, args, consumer, xform): | |
212 # Internal helper. Send a command to which a multiline | |
213 # response is expected. Return a Deferred that fires when | |
214 # the entire response is received. Block all further commands | |
215 # from being sent until the entire response is received. | |
216 # Transition the state to LONG_INITIAL. | |
217 d = self._blocked(self.sendLong, cmd, args, consumer, xform) | |
218 if d is not None: | |
219 return d | |
220 | |
221 if args: | |
222 self.sendLine(cmd + ' ' + args) | |
223 else: | |
224 self.sendLine(cmd) | |
225 self.state = 'LONG_INITIAL' | |
226 self._xform = xform | |
227 self._consumer = consumer | |
228 self._waiting = defer.Deferred() | |
229 return self._waiting | |
230 | |
231 # Twisted protocol callback | |
232 def connectionMade(self): | |
233 if self.timeout > 0: | |
234 self.setTimeout(self.timeout) | |
235 | |
236 self.state = 'WELCOME' | |
237 self._blockedQueue = [] | |
238 | |
239 def timeoutConnection(self): | |
240 self._timedOut = True | |
241 self.transport.loseConnection() | |
242 | |
243 def connectionLost(self, reason): | |
244 if self.timeout > 0: | |
245 self.setTimeout(None) | |
246 | |
247 if self._timedOut: | |
248 reason = error.TimeoutError() | |
249 elif self._greetingError: | |
250 reason = ServerErrorResponse(self._greetingError) | |
251 | |
252 d = [] | |
253 if self._waiting is not None: | |
254 d.append(self._waiting) | |
255 self._waiting = None | |
256 if self._blockedQueue is not None: | |
257 d.extend([deferred for (deferred, f, a) in self._blockedQueue]) | |
258 self._blockedQueue = None | |
259 for w in d: | |
260 w.errback(reason) | |
261 | |
262 def lineReceived(self, line): | |
263 if self.timeout > 0: | |
264 self.resetTimeout() | |
265 | |
266 state = self.state | |
267 self.state = None | |
268 state = getattr(self, 'state_' + state)(line) or state | |
269 if self.state is None: | |
270 self.state = state | |
271 | |
272 def lineLengthExceeded(self, buffer): | |
273 # XXX - We need to be smarter about this | |
274 if self._waiting is not None: | |
275 waiting, self._waiting = self._waiting, None | |
276 waiting.errback(LineTooLong()) | |
277 self.transport.loseConnection() | |
278 | |
279 # POP3 Client state logic - don't touch this. | |
280 def state_WELCOME(self, line): | |
281 # WELCOME is the first state. The server sends one line of text | |
282 # greeting us, possibly with an APOP challenge. Transition the | |
283 # state to WAITING. | |
284 code, status = _codeStatusSplit(line) | |
285 if code != OK: | |
286 self._greetingError = status | |
287 self.transport.loseConnection() | |
288 else: | |
289 m = self._challengeMagicRe.search(status) | |
290 | |
291 if m is not None: | |
292 self.serverChallenge = m.group(1) | |
293 | |
294 self.serverGreeting(status) | |
295 | |
296 self._unblock() | |
297 return 'WAITING' | |
298 | |
299 def state_WAITING(self, line): | |
300 # The server isn't supposed to send us anything in this state. | |
301 log.msg("Illegal line from server: " + repr(line)) | |
302 | |
303 def state_SHORT(self, line): | |
304 # This is the state we are in when waiting for a single | |
305 # line response. Parse it and fire the appropriate callback | |
306 # or errback. Transition the state back to WAITING. | |
307 deferred, self._waiting = self._waiting, None | |
308 self._unblock() | |
309 code, status = _codeStatusSplit(line) | |
310 if code == OK: | |
311 deferred.callback(status) | |
312 else: | |
313 deferred.errback(ServerErrorResponse(status)) | |
314 return 'WAITING' | |
315 | |
316 def state_LONG_INITIAL(self, line): | |
317 # This is the state we are in when waiting for the first | |
318 # line of a long response. Parse it and transition the | |
319 # state to LONG if it is an okay response; if it is an | |
320 # error response, fire an errback, clean up the things | |
321 # waiting for a long response, and transition the state | |
322 # to WAITING. | |
323 code, status = _codeStatusSplit(line) | |
324 if code == OK: | |
325 return 'LONG' | |
326 consumer = self._consumer | |
327 deferred = self._waiting | |
328 self._consumer = self._waiting = self._xform = None | |
329 self._unblock() | |
330 deferred.errback(ServerErrorResponse(status, consumer)) | |
331 return 'WAITING' | |
332 | |
333 def state_LONG(self, line): | |
334 # This is the state for each line of a long response. | |
335 # If it is the last line, finish things, fire the | |
336 # Deferred, and transition the state to WAITING. | |
337 # Otherwise, pass the line to the consumer. | |
338 if line == '.': | |
339 consumer = self._consumer | |
340 deferred = self._waiting | |
341 self._consumer = self._waiting = self._xform = None | |
342 self._unblock() | |
343 deferred.callback(consumer) | |
344 return 'WAITING' | |
345 else: | |
346 if self._xform is not None: | |
347 self._consumer(self._xform(line)) | |
348 else: | |
349 self._consumer(line) | |
350 return 'LONG' | |
351 | |
352 | |
353 # Callbacks - override these | |
354 def serverGreeting(self, greeting): | |
355 """Called when the server has sent us a greeting. | |
356 | |
357 @type greeting: C{str} or C{None} | |
358 @param greeting: The status message sent with the server | |
359 greeting. For servers implementing APOP authentication, this | |
360 will be a challenge string. . | |
361 """ | |
362 | |
363 | |
364 # External API - call these (most of 'em anyway) | |
365 def startTLS(self, contextFactory=None): | |
366 """ | |
367 Initiates a 'STLS' request and negotiates the TLS / SSL | |
368 Handshake. | |
369 | |
370 @type contextFactory: C{ssl.ClientContextFactory} @param | |
371 contextFactory: The context factory with which to negotiate | |
372 TLS. If C{None}, try to create a new one. | |
373 | |
374 @return: A Deferred which fires when the transport has been | |
375 secured according to the given contextFactory, or which fails | |
376 if the transport cannot be secured. | |
377 """ | |
378 tls = interfaces.ITLSTransport(self.transport, None) | |
379 if tls is None: | |
380 return defer.fail(TLSError( | |
381 "POP3Client transport does not implement " | |
382 "interfaces.ITLSTransport")) | |
383 | |
384 if contextFactory is None: | |
385 contextFactory = self._getContextFactory() | |
386 | |
387 if contextFactory is None: | |
388 return defer.fail(TLSError( | |
389 "POP3Client requires a TLS context to " | |
390 "initiate the STLS handshake")) | |
391 | |
392 d = self.capabilities() | |
393 d.addCallback(self._startTLS, contextFactory, tls) | |
394 return d | |
395 | |
396 | |
397 def _startTLS(self, caps, contextFactory, tls): | |
398 assert not self.startedTLS, "Client and Server are currently communicati
ng via TLS" | |
399 | |
400 if 'STLS' not in caps: | |
401 return defer.fail(TLSNotSupportedError( | |
402 "Server does not support secure communication " | |
403 "via TLS / SSL")) | |
404 | |
405 d = self.sendShort('STLS', None) | |
406 d.addCallback(self._startedTLS, contextFactory, tls) | |
407 d.addCallback(lambda _: self.capabilities()) | |
408 return d | |
409 | |
410 | |
411 def _startedTLS(self, result, context, tls): | |
412 self.transport = tls | |
413 self.transport.startTLS(context) | |
414 self._capCache = None | |
415 self.startedTLS = True | |
416 return result | |
417 | |
418 | |
419 def _getContextFactory(self): | |
420 try: | |
421 from twisted.internet import ssl | |
422 except ImportError: | |
423 return None | |
424 else: | |
425 context = ssl.ClientContextFactory() | |
426 context.method = ssl.SSL.TLSv1_METHOD | |
427 return context | |
428 | |
429 | |
430 def login(self, username, password): | |
431 """Log into the server. | |
432 | |
433 If APOP is available it will be used. Otherwise, if TLS is | |
434 available an 'STLS' session will be started and plaintext | |
435 login will proceed. Otherwise, if the instance attribute | |
436 allowInsecureLogin is set to True, insecure plaintext login | |
437 will proceed. Otherwise, InsecureAuthenticationDisallowed | |
438 will be raised (asynchronously). | |
439 | |
440 @param username: The username with which to log in. | |
441 @param password: The password with which to log in. | |
442 | |
443 @rtype: C{Deferred} | |
444 @return: A deferred which fires when login has | |
445 completed. | |
446 """ | |
447 d = self.capabilities() | |
448 d.addCallback(self._login, username, password) | |
449 return d | |
450 | |
451 | |
452 def _login(self, caps, username, password): | |
453 if self.serverChallenge is not None: | |
454 return self._apop(username, password, self.serverChallenge) | |
455 | |
456 tryTLS = 'STLS' in caps | |
457 | |
458 #If our transport supports switching to TLS, we might want to try to swi
tch to TLS. | |
459 tlsableTransport = interfaces.ITLSTransport(self.transport, None) is not
None | |
460 | |
461 # If our transport is not already using TLS, we might want to try to swi
tch to TLS. | |
462 nontlsTransport = interfaces.ISSLTransport(self.transport, None) is None | |
463 | |
464 if not self.startedTLS and tryTLS and tlsableTransport and nontlsTranspo
rt: | |
465 d = self.startTLS() | |
466 | |
467 d.addCallback(self._loginTLS, username, password) | |
468 return d | |
469 | |
470 elif self.startedTLS or not nontlsTransport or self.allowInsecureLogin: | |
471 return self._plaintext(username, password) | |
472 else: | |
473 return defer.fail(InsecureAuthenticationDisallowed()) | |
474 | |
475 | |
476 def _loginTLS(self, res, username, password): | |
477 return self._plaintext(username, password) | |
478 | |
479 def _plaintext(self, username, password): | |
480 # Internal helper. Send a username/password pair, returning a Deferred | |
481 # that fires when both have succeeded or fails when the server rejects | |
482 # either. | |
483 return self.user(username).addCallback(lambda r: self.password(password)
) | |
484 | |
485 def _apop(self, username, password, challenge): | |
486 # Internal helper. Computes and sends an APOP response. Returns | |
487 # a Deferred that fires when the server responds to the response. | |
488 digest = md5.new(challenge + password).hexdigest() | |
489 return self.apop(username, digest) | |
490 | |
491 def apop(self, username, digest): | |
492 """Perform APOP login. | |
493 | |
494 This should be used in special circumstances only, when it is | |
495 known that the server supports APOP authentication, and APOP | |
496 authentication is absolutely required. For the common case, | |
497 use L{login} instead. | |
498 | |
499 @param username: The username with which to log in. | |
500 @param digest: The challenge response to authenticate with. | |
501 """ | |
502 return self.sendShort('APOP', username + ' ' + digest) | |
503 | |
504 def user(self, username): | |
505 """Send the user command. | |
506 | |
507 This performs the first half of plaintext login. Unless this | |
508 is absolutely required, use the L{login} method instead. | |
509 | |
510 @param username: The username with which to log in. | |
511 """ | |
512 return self.sendShort('USER', username) | |
513 | |
514 def password(self, password): | |
515 """Send the password command. | |
516 | |
517 This performs the second half of plaintext login. Unless this | |
518 is absolutely required, use the L{login} method instead. | |
519 | |
520 @param password: The plaintext password with which to authenticate. | |
521 """ | |
522 return self.sendShort('PASS', password) | |
523 | |
524 def delete(self, index): | |
525 """Delete a message from the server. | |
526 | |
527 @type index: C{int} | |
528 @param index: The index of the message to delete. | |
529 This is 0-based. | |
530 | |
531 @rtype: C{Deferred} | |
532 @return: A deferred which fires when the delete command | |
533 is successful, or fails if the server returns an error. | |
534 """ | |
535 return self.sendShort('DELE', str(index + 1)) | |
536 | |
537 def _consumeOrSetItem(self, cmd, args, consumer, xform): | |
538 # Internal helper. Send a long command. If no consumer is | |
539 # provided, create a consumer that puts results into a list | |
540 # and return a Deferred that fires with that list when it | |
541 # is complete. | |
542 if consumer is None: | |
543 L = [] | |
544 consumer = _ListSetter(L).setitem | |
545 return self.sendLong(cmd, args, consumer, xform).addCallback(lambda
r: L) | |
546 return self.sendLong(cmd, args, consumer, xform) | |
547 | |
548 def _consumeOrAppend(self, cmd, args, consumer, xform): | |
549 # Internal helper. Send a long command. If no consumer is | |
550 # provided, create a consumer that appends results to a list | |
551 # and return a Deferred that fires with that list when it is | |
552 # complete. | |
553 if consumer is None: | |
554 L = [] | |
555 consumer = L.append | |
556 return self.sendLong(cmd, args, consumer, xform).addCallback(lambda
r: L) | |
557 return self.sendLong(cmd, args, consumer, xform) | |
558 | |
559 def capabilities(self, useCache=True): | |
560 """Retrieve the capabilities supported by this server. | |
561 | |
562 Not all servers support this command. If the server does not | |
563 support this, it is treated as though it returned a successful | |
564 response listing no capabilities. At some future time, this may be | |
565 changed to instead seek out information about a server's | |
566 capabilities in some other fashion (only if it proves useful to do | |
567 so, and only if there are servers still in use which do not support | |
568 CAPA but which do support POP3 extensions that are useful). | |
569 | |
570 @type useCache: C{bool} | |
571 @param useCache: If set, and if capabilities have been | |
572 retrieved previously, just return the previously retrieved | |
573 results. | |
574 | |
575 @return: A Deferred which fires with a C{dict} mapping C{str} | |
576 to C{None} or C{list}s of C{str}. For example:: | |
577 | |
578 C: CAPA | |
579 S: +OK Capability list follows | |
580 S: TOP | |
581 S: USER | |
582 S: SASL CRAM-MD5 KERBEROS_V4 | |
583 S: RESP-CODES | |
584 S: LOGIN-DELAY 900 | |
585 S: PIPELINING | |
586 S: EXPIRE 60 | |
587 S: UIDL | |
588 S: IMPLEMENTATION Shlemazle-Plotz-v302 | |
589 S: . | |
590 | |
591 will be lead to a result of:: | |
592 | |
593 | {'TOP': None, | |
594 | 'USER': None, | |
595 | 'SASL': ['CRAM-MD5', 'KERBEROS_V4'], | |
596 | 'RESP-CODES': None, | |
597 | 'LOGIN-DELAY': ['900'], | |
598 | 'PIPELINING': None, | |
599 | 'EXPIRE': ['60'], | |
600 | 'UIDL': None, | |
601 | 'IMPLEMENTATION': ['Shlemazle-Plotz-v302']} | |
602 """ | |
603 if useCache and self._capCache is not None: | |
604 return defer.succeed(self._capCache) | |
605 | |
606 cache = {} | |
607 def consume(line): | |
608 tmp = line.split() | |
609 if len(tmp) == 1: | |
610 cache[tmp[0]] = None | |
611 elif len(tmp) > 1: | |
612 cache[tmp[0]] = tmp[1:] | |
613 | |
614 def capaNotSupported(err): | |
615 err.trap(ServerErrorResponse) | |
616 return None | |
617 | |
618 def gotCapabilities(result): | |
619 self._capCache = cache | |
620 return cache | |
621 | |
622 d = self._consumeOrAppend('CAPA', None, consume, None) | |
623 d.addErrback(capaNotSupported).addCallback(gotCapabilities) | |
624 return d | |
625 | |
626 | |
627 def noop(self): | |
628 """Do nothing, with the help of the server. | |
629 | |
630 No operation is performed. The returned Deferred fires when | |
631 the server responds. | |
632 """ | |
633 return self.sendShort("NOOP", None) | |
634 | |
635 | |
636 def reset(self): | |
637 """Remove the deleted flag from any messages which have it. | |
638 | |
639 The returned Deferred fires when the server responds. | |
640 """ | |
641 return self.sendShort("RSET", None) | |
642 | |
643 | |
644 def retrieve(self, index, consumer=None, lines=None): | |
645 """Retrieve a message from the server. | |
646 | |
647 If L{consumer} is not None, it will be called with | |
648 each line of the message as it is received. Otherwise, | |
649 the returned Deferred will be fired with a list of all | |
650 the lines when the message has been completely received. | |
651 """ | |
652 idx = str(index + 1) | |
653 if lines is None: | |
654 return self._consumeOrAppend('RETR', idx, consumer, _dotUnquoter) | |
655 | |
656 return self._consumeOrAppend('TOP', '%s %d' % (idx, lines), consumer, _d
otUnquoter) | |
657 | |
658 | |
659 def stat(self): | |
660 """Get information about the size of this mailbox. | |
661 | |
662 The returned Deferred will be fired with a tuple containing | |
663 the number or messages in the mailbox and the size (in bytes) | |
664 of the mailbox. | |
665 """ | |
666 return self.sendShort('STAT', None).addCallback(_statXform) | |
667 | |
668 | |
669 def listSize(self, consumer=None): | |
670 """Retrieve a list of the size of all messages on the server. | |
671 | |
672 If L{consumer} is not None, it will be called with two-tuples | |
673 of message index number and message size as they are received. | |
674 Otherwise, a Deferred which will fire with a list of B{only} | |
675 message sizes will be returned. For messages which have been | |
676 deleted, None will be used in place of the message size. | |
677 """ | |
678 return self._consumeOrSetItem('LIST', None, consumer, _listXform) | |
679 | |
680 | |
681 def listUID(self, consumer=None): | |
682 """Retrieve a list of the UIDs of all messages on the server. | |
683 | |
684 If L{consumer} is not None, it will be called with two-tuples | |
685 of message index number and message UID as they are received. | |
686 Otherwise, a Deferred which will fire with of list of B{only} | |
687 message UIDs will be returned. For messages which have been | |
688 deleted, None will be used in place of the message UID. | |
689 """ | |
690 return self._consumeOrSetItem('UIDL', None, consumer, _uidXform) | |
691 | |
692 | |
693 def quit(self): | |
694 """Disconnect from the server. | |
695 """ | |
696 return self.sendShort('QUIT', None) | |
697 | |
698 __all__ = [ | |
699 # Exceptions | |
700 'InsecureAuthenticationDisallowed', 'LineTooLong', 'POP3ClientError', | |
701 'ServerErrorResponse', 'TLSError', 'TLSNotSupportedError', | |
702 | |
703 # Protocol classes | |
704 'POP3Client'] | |
OLD | NEW |