~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

Viewing changes to twisted/mail/pop3client.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2007-01-17 14:52:35 UTC
  • mfrom: (1.1.5 upstream) (2.1.2 etch)
  • Revision ID: james.westby@ubuntu.com-20070117145235-btmig6qfmqfen0om
Tags: 2.5.0-0ubuntu1
New upstream version, compatible with python2.5.

Show diffs side-by-side

added added

removed removed

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