Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(3)

Side by Side Diff: third_party/twisted_8_1/twisted/mail/pop3client.py

Issue 12261012: Remove third_party/twisted_8_1 (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Created 7 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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']
OLDNEW
« no previous file with comments | « third_party/twisted_8_1/twisted/mail/pop3.py ('k') | third_party/twisted_8_1/twisted/mail/protocols.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698