1
# -*- test-case-name: twisted.mail.test.test_pop3client -*-
2
# Copyright (c) 2001-2004 Divmod Inc.
3
# See LICENSE for details.
5
"""POP3 client protocol implementation
7
Don't use this module directly. Use twisted.mail.pop3 instead.
9
@author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>}
11
API Stability: Unstable
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
26
class POP3ClientError(Exception):
27
"""Base class for all exceptions raised by POP3Client.
30
class InsecureAuthenticationDisallowed(POP3ClientError):
31
"""Secure authentication was required but no mechanism could be found.
34
class TLSError(POP3ClientError):
36
Secure authentication was required but either the transport does
37
not support TLS or no TLS context factory was supplied.
40
class TLSNotSupportedError(POP3ClientError):
42
Secure authentication was required but the server does not support
46
class ServerErrorResponse(POP3ClientError):
47
"""The server returned an error response to a request.
49
def __init__(self, reason, consumer=None):
50
POP3ClientError.__init__(self, reason)
51
self.consumer = consumer
53
class LineTooLong(POP3ClientError):
54
"""The server sent an extremely long line.
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):
67
def setitem(self, (item, value)):
68
diff = item - len(self.L) + 1
70
self.L.extend([None] * diff)
75
# Parse a STAT response
76
numMsgs, totalSize = line.split(None, 1)
77
return int(numMsgs), int(totalSize)
81
# Parse a LIST response
82
index, size = line.split(None, 1)
83
return int(index) - 1, int(size)
87
# Parse a UIDL response
88
index, uid = line.split(None, 1)
89
return int(index) - 1, uid
91
def _codeStatusSplit(line):
92
# Parse an +OK or -ERR response
93
parts = line.split(' ', 1)
98
def _dotUnquoter(line):
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
104
if line.startswith('..'):
108
class POP3Client(basic.LineOnlyReceiver, policies.TimeoutMixin):
109
"""POP3 client protocol implementation class
111
Instances of this class provide a convenient, efficient API for
112
retrieving and deleting messages from a POP3 server.
114
@type startedTLS: C{bool}
115
@ivar startedTLS: Whether TLS has been negotiated successfully.
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.
123
@type serverChallenge: C{str} or C{None}
124
@ivar serverChallenge: Challenge received from the server
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
133
allowInsecureLogin = False
135
serverChallenge = None
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
142
# Regular expression to search for in the challenge string in the server
144
_challengeMagicRe = re.compile('(<[^>]+>)')
146
# List of pending calls.
147
# We are a pipelining API but don't actually
148
# support pipelining on the network yet.
151
# The Deferred to which the very next result will go.
154
# Whether we dropped the connection because of a timeout
157
# If the server sends an initial -ERR, this is the message it sent
159
_greetingError = None
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
167
# blocked and return None.
168
if self._blockedQueue is not None:
170
self._blockedQueue.append((d, f, a))
172
self._blockedQueue = []
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
185
d, f, a = _blockedQueue.pop(0)
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)
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)
205
self.sendLine(cmd + ' ' + args)
209
self._waiting = defer.Deferred()
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)
223
self.sendLine(cmd + ' ' + args)
226
self.state = 'LONG_INITIAL'
228
self._consumer = consumer
229
self._waiting = defer.Deferred()
232
# Twisted protocol callback
233
def connectionMade(self):
235
self.setTimeout(self.timeout)
237
self.state = 'WELCOME'
238
self._blockedQueue = []
240
def timeoutConnection(self):
241
self._timedOut = True
242
self.transport.loseConnection()
244
def connectionLost(self, reason):
246
self.setTimeout(None)
249
reason = error.TimeoutError()
250
elif self._greetingError:
251
reason = ServerErrorResponse(self._greetingError)
254
if self._waiting is not None:
255
d.append(self._waiting)
257
if self._blockedQueue is not None:
258
d.extend([deferred for (deferred, f, a) in self._blockedQueue])
259
self._blockedQueue = None
263
def lineReceived(self, line):
269
state = getattr(self, 'state_' + state)(line) or state
270
if self.state is None:
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()
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
285
code, status = _codeStatusSplit(line)
287
self._greetingError = status
288
self.transport.loseConnection()
290
m = self._challengeMagicRe.search(status)
293
self.serverChallenge = m.group(1)
295
self.serverGreeting(status)
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))
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
310
code, status = _codeStatusSplit(line)
312
deferred.callback(status)
314
deferred.errback(ServerErrorResponse(status))
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
324
code, status = _codeStatusSplit(line)
327
consumer = self._consumer
328
deferred = self._waiting
329
self._consumer = self._waiting = self._xform = None
331
deferred.errback(ServerErrorResponse(status, consumer))
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.
340
consumer = self._consumer
341
deferred = self._waiting
342
self._consumer = self._waiting = self._xform = None
344
deferred.callback(consumer)
347
if self._xform is not None:
348
self._consumer(self._xform(line))
354
# Callbacks - override these
355
def serverGreeting(self, greeting):
356
"""Called when the server has sent us a greeting.
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. .
365
# External API - call these (most of 'em anyway)
366
def startTLS(self, contextFactory=None):
368
Initiates a 'STLS' request and negotiates the TLS / SSL
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.
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.
379
tls = interfaces.ITLSTransport(self.transport, None)
381
return defer.fail(TLSError(
382
"POP3Client transport does not implement "
383
"interfaces.ITLSTransport"))
385
if contextFactory is None:
386
contextFactory = self._getContextFactory()
388
if contextFactory is None:
389
return defer.fail(TLSError(
390
"POP3Client requires a TLS context to "
391
"initiate the STLS handshake"))
393
d = self.capabilities()
394
d.addCallback(self._startTLS, contextFactory, tls)
398
def _startTLS(self, caps, contextFactory, tls):
399
assert not self.startedTLS, "Client and Server are currently communicating via TLS"
401
if 'STLS' not in caps:
402
return defer.fail(TLSNotSupportedError(
403
"Server does not support secure communication "
406
d = self.sendShort('STLS', None)
407
d.addCallback(self._startedTLS, contextFactory, tls)
408
d.addCallback(lambda _: self.capabilities())
412
def _startedTLS(self, result, context, tls):
414
self.transport.startTLS(context)
415
self._capCache = None
416
self.startedTLS = True
420
def _getContextFactory(self):
422
from twisted.internet import ssl
426
context = ssl.ClientContextFactory()
427
context.method = ssl.SSL.TLSv1_METHOD
431
def login(self, username, password):
432
"""Log into the server.
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).
441
@param username: The username with which to log in.
442
@param password: The password with which to log in.
445
@return: A deferred which fires when login has
448
d = self.capabilities()
449
d.addCallback(self._login, username, password)
453
def _login(self, caps, username, password):
454
if self.serverChallenge is not None:
455
return self._apop(username, password, self.serverChallenge)
457
tryTLS = 'STLS' in caps
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
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
465
if not self.startedTLS and tryTLS and tlsableTransport and nontlsTransport:
468
d.addCallback(self._loginTLS, username, password)
471
elif self.startedTLS or not nontlsTransport or self.allowInsecureLogin:
472
return self._plaintext(username, password)
474
return defer.fail(InsecureAuthenticationDisallowed())
477
def _loginTLS(self, res, username, password):
478
return self._plaintext(username, password)
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
484
return self.user(username).addCallback(lambda r: self.password(password))
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)
492
def apop(self, username, digest):
493
"""Perform APOP login.
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.
500
@param username: The username with which to log in.
501
@param digest: The challenge response to authenticate with.
503
return self.sendShort('APOP', username + ' ' + digest)
505
def user(self, username):
506
"""Send the user command.
508
This performs the first half of plaintext login. Unless this
509
is absolutely required, use the L{login} method instead.
511
@param username: The username with which to log in.
513
return self.sendShort('USER', username)
515
def password(self, password):
516
"""Send the password command.
518
This performs the second half of plaintext login. Unless this
519
is absolutely required, use the L{login} method instead.
521
@param password: The plaintext password with which to authenticate.
523
return self.sendShort('PASS', password)
525
def delete(self, index):
526
"""Delete a message from the server.
529
@param index: The index of the message to delete.
533
@return: A deferred which fires when the delete command
534
is successful, or fails if the server returns an error.
536
return self.sendShort('DELE', str(index + 1))
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
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)
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
557
return self.sendLong(cmd, args, consumer, xform).addCallback(lambda r: L)
558
return self.sendLong(cmd, args, consumer, xform)
560
def capabilities(self, useCache=True):
561
"""Retrieve the capabilities supported by this server.
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).
571
@type useCache: C{bool}
572
@param useCache: If set, and if capabilities have been
573
retrieved previously, just return the previously retrieved
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::
580
S: +OK Capability list follows
583
S: SASL CRAM-MD5 KERBEROS_V4
589
S: IMPLEMENTATION Shlemazle-Plotz-v302
592
will be lead to a result of::
596
| 'SASL': ['CRAM-MD5', 'KERBEROS_V4'],
597
| 'RESP-CODES': None,
598
| 'LOGIN-DELAY': ['900'],
599
| 'PIPELINING': None,
602
| 'IMPLEMENTATION': ['Shlemazle-Plotz-v302']}
604
if useCache and self._capCache is not None:
605
return defer.succeed(self._capCache)
613
cache[tmp[0]] = tmp[1:]
615
def capaNotSupported(err):
616
err.trap(ServerErrorResponse)
619
def gotCapabilities(result):
620
self._capCache = cache
623
d = self._consumeOrAppend('CAPA', None, consume, None)
624
d.addErrback(capaNotSupported).addCallback(gotCapabilities)
629
"""Do nothing, with the help of the server.
631
No operation is performed. The returned Deferred fires when
634
return self.sendShort("NOOP", None)
638
"""Remove the deleted flag from any messages which have it.
640
The returned Deferred fires when the server responds.
642
return self.sendShort("RSET", None)
645
def retrieve(self, index, consumer=None, lines=None):
646
"""Retrieve a message from the server.
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.
655
return self._consumeOrAppend('RETR', idx, consumer, _dotUnquoter)
657
return self._consumeOrAppend('TOP', '%s %d' % (idx, lines), consumer, _dotUnquoter)
661
"""Get information about the size of this mailbox.
663
The returned Deferred will be fired with a tuple containing
664
the number or messages in the mailbox and the size (in bytes)
667
return self.sendShort('STAT', None).addCallback(_statXform)
670
def listSize(self, consumer=None):
671
"""Retrieve a list of the size of all messages on the server.
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.
679
return self._consumeOrSetItem('LIST', None, consumer, _listXform)
682
def listUID(self, consumer=None):
683
"""Retrieve a list of the UIDs of all messages on the server.
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.
691
return self._consumeOrSetItem('UIDL', None, consumer, _uidXform)
695
"""Disconnect from the server.
697
return self.sendShort('QUIT', None)
701
'InsecureAuthenticationDisallowed', 'LineTooLong', 'POP3ClientError',
702
'ServerErrorResponse', 'TLSError', 'TLSNotSupportedError',