| 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 |