1
# -*- test-case-name: twisted.mail.test.test_pop3 -*-
3
# Copyright (c) 2001-2008 Twisted Matrix Laboratories.
4
# See LICENSE for details.
8
Post-office Protocol version 3
10
@author: Glyph Lefkowitz
19
from zope.interface import implements, Interface
21
from twisted.mail import smtp
22
from twisted.protocols import basic
23
from twisted.protocols import policies
24
from twisted.internet import task
25
from twisted.internet import defer
26
from twisted.internet import interfaces
27
from twisted.python import log
28
from twisted.python.hashlib import md5
30
from twisted import cred
31
import twisted.cred.error
32
import twisted.cred.credentials
37
class APOPCredentials:
38
implements(cred.credentials.IUsernamePassword)
40
def __init__(self, magic, username, digest):
42
self.username = username
45
def checkPassword(self, password):
46
seed = self.magic + password
47
myDigest = md5(seed).hexdigest()
48
return myDigest == self.digest
51
class _HeadersPlusNLines:
52
def __init__(self, f, n):
60
def read(self, bytes):
63
data = self.f.read(bytes)
67
df, sz = data.find('\r\n\r\n'), 4
69
df, sz = data.find('\n\n'), 2
78
if self.linecount > 0:
79
dsplit = (self.buf+data).split('\n')
81
for ln in dsplit[:-1]:
82
if self.linecount > self.n:
93
class _POP3MessageDeleted(Exception):
95
Internal control-flow exception. Indicates the file of a deleted message
100
class POP3Error(Exception):
105
class _IteratorBuffer(object):
108
def __init__(self, write, iterable, memoryBufferSize=None):
110
Create a _IteratorBuffer.
112
@param write: A one-argument callable which will be invoked with a list
113
of strings which have been buffered.
115
@param iterable: The source of input strings as any iterable.
117
@param memoryBufferSize: The upper limit on buffered string length,
118
beyond which the buffer will be flushed to the writer.
122
self.iterator = iter(iterable)
123
if memoryBufferSize is None:
124
memoryBufferSize = 2 ** 16
125
self.memoryBufferSize = memoryBufferSize
134
v = self.iterator.next()
135
except StopIteration:
137
self.write(self.lines)
138
# Drop some references, in case they're edges in a cycle.
139
del self.iterator, self.lines, self.write
144
self.bufSize += len(v)
145
if self.bufSize > self.memoryBufferSize:
146
self.write(self.lines)
152
def iterateLineGenerator(proto, gen):
154
Hook the given protocol instance up to the given iterator with an
155
_IteratorBuffer and schedule the result to be exhausted via the protocol.
159
@rtype: L{twisted.internet.defer.Deferred}
161
coll = _IteratorBuffer(proto.transport.writeSequence, gen)
162
return proto.schedule(coll)
166
def successResponse(response):
168
Format the given object as a positive response.
170
response = str(response)
171
return '+OK %s\r\n' % (response,)
175
def formatStatResponse(msgs):
177
Format the list of message sizes appropriately for a STAT response.
179
Yields None until it finishes computing a result, then yields a str
180
instance that is suitable for use as a response to the STAT command.
181
Intended to be used with a L{twisted.internet.task.Cooperator}.
189
yield successResponse('%d %d' % (i, bytes))
193
def formatListLines(msgs):
195
Format a list of message sizes appropriately for the lines of a LIST
198
Yields str instances formatted appropriately for use as lines in the
199
response to the LIST command. Does not include the trailing '.'.
204
yield '%d %d\r\n' % (i, size)
208
def formatListResponse(msgs):
210
Format a list of message sizes appropriately for a complete LIST response.
212
Yields str instances formatted appropriately for use as a LIST command
215
yield successResponse(len(msgs))
216
for ele in formatListLines(msgs):
222
def formatUIDListLines(msgs, getUidl):
224
Format the list of message sizes appropriately for the lines of a UIDL
227
Yields str instances formatted appropriately for use as lines in the
228
response to the UIDL command. Does not include the trailing '.'.
230
for i, m in enumerate(msgs):
233
yield '%d %s\r\n' % (i + 1, uid)
237
def formatUIDListResponse(msgs, getUidl):
239
Format a list of message sizes appropriately for a complete UIDL response.
241
Yields str instances formatted appropriately for use as a UIDL command
244
yield successResponse('')
245
for ele in formatUIDListLines(msgs, getUidl):
251
class POP3(basic.LineOnlyReceiver, policies.TimeoutMixin):
253
POP3 server protocol implementation.
255
@ivar portal: A reference to the L{twisted.cred.portal.Portal} instance we
256
will authenticate through.
258
@ivar factory: A L{twisted.mail.pop3.IServerFactory} which will be used to
259
determine some extended behavior of the server.
261
@ivar timeOut: An integer which defines the minimum amount of time which
262
may elapse without receiving any traffic after which the client will be
265
@ivar schedule: A one-argument callable which should behave like
266
L{twisted.internet.task.coiterate}.
268
implements(interfaces.IProducer)
274
AUTH_CMDS = ['CAPA', 'USER', 'PASS', 'APOP', 'AUTH', 'RPOP', 'QUIT']
279
# The mailbox we're serving
282
# Set this pretty low -- POP3 clients are expected to log in, download
283
# everything, and log out.
286
# Current protocol state
292
# Cooperate and suchlike.
293
schedule = staticmethod(task.coiterate)
295
# Message index of the highest retrieved message.
298
def connectionMade(self):
299
if self.magic is None:
300
self.magic = self.generateMagic()
301
self.successResponse(self.magic)
302
self.setTimeout(self.timeOut)
303
if getattr(self.factory, 'noisy', True):
304
log.msg("New connection from " + str(self.transport.getPeer()))
307
def connectionLost(self, reason):
308
if self._onLogout is not None:
310
self._onLogout = None
311
self.setTimeout(None)
314
def generateMagic(self):
315
return smtp.messageid()
318
def successResponse(self, message=''):
319
self.transport.write(successResponse(message))
321
def failResponse(self, message=''):
322
self.sendLine('-ERR ' + str(message))
324
# def sendLine(self, line):
325
# print 'S:', repr(line)
326
# basic.LineOnlyReceiver.sendLine(self, line)
328
def lineReceived(self, line):
329
# print 'C:', repr(line)
331
getattr(self, 'state_' + self.state)(line)
333
def _unblock(self, _):
334
commands = self.blocked
336
while commands and self.blocked is None:
337
cmd, args = commands.pop(0)
338
self.processCommand(cmd, *args)
339
if self.blocked is not None:
340
self.blocked.extend(commands)
342
def state_COMMAND(self, line):
344
return self.processCommand(*line.split(' '))
345
except (ValueError, AttributeError, POP3Error, TypeError), e:
347
self.failResponse('bad protocol or server: %s: %s' % (e.__class__.__name__, e))
349
def processCommand(self, command, *args):
350
if self.blocked is not None:
351
self.blocked.append((command, args))
354
command = string.upper(command)
355
authCmd = command in self.AUTH_CMDS
356
if not self.mbox and not authCmd:
357
raise POP3Error("not authenticated yet: cannot do " + command)
358
f = getattr(self, 'do_' + command, None)
361
raise POP3Error("Unknown protocol command: " + command)
364
def listCapabilities(self):
375
if IServerFactory.providedBy(self.factory):
376
# Oh my god. We can't just loop over a list of these because
377
# each has spectacularly different return value semantics!
379
v = self.factory.cap_IMPLEMENTATION()
380
except NotImplementedError:
385
baseCaps.append("IMPLEMENTATION " + str(v))
388
v = self.factory.cap_EXPIRE()
389
except NotImplementedError:
396
if self.factory.perUserExpiration():
398
v = str(self.mbox.messageExpiration)
402
baseCaps.append("EXPIRE " + v)
405
v = self.factory.cap_LOGIN_DELAY()
406
except NotImplementedError:
411
if self.factory.perUserLoginDelay():
413
v = str(self.mbox.loginDelay)
417
baseCaps.append("LOGIN-DELAY " + v)
420
v = self.factory.challengers
421
except AttributeError:
426
baseCaps.append("SASL " + ' '.join(v.keys()))
430
self.successResponse("I can do the following:")
431
for cap in self.listCapabilities():
435
def do_AUTH(self, args=None):
436
if not getattr(self.factory, 'challengers', None):
437
self.failResponse("AUTH extension unsupported")
441
self.successResponse("Supported authentication methods:")
442
for a in self.factory.challengers:
443
self.sendLine(a.upper())
447
auth = self.factory.challengers.get(args.strip().upper())
448
if not self.portal or not auth:
449
self.failResponse("Unsupported SASL selected")
453
chal = self._auth.getChallenge()
455
self.sendLine('+ ' + base64.encodestring(chal).rstrip('\n'))
458
def state_AUTH(self, line):
459
self.state = "COMMAND"
461
parts = base64.decodestring(line).split(None, 1)
462
except binascii.Error:
463
self.failResponse("Invalid BASE64 encoding")
466
self.failResponse("Invalid AUTH response")
468
self._auth.username = parts[0]
469
self._auth.response = parts[1]
470
d = self.portal.login(self._auth, None, IMailbox)
471
d.addCallback(self._cbMailbox, parts[0])
472
d.addErrback(self._ebMailbox)
473
d.addErrback(self._ebUnexpected)
475
def do_APOP(self, user, digest):
476
d = defer.maybeDeferred(self.authenticateUserAPOP, user, digest)
477
d.addCallbacks(self._cbMailbox, self._ebMailbox, callbackArgs=(user,)
478
).addErrback(self._ebUnexpected)
480
def _cbMailbox(self, (interface, avatar, logout), user):
481
if interface is not IMailbox:
482
self.failResponse('Authentication failed')
483
log.err("_cbMailbox() called with an interface other than IMailbox")
487
self._onLogout = logout
488
self.successResponse('Authentication succeeded')
489
if getattr(self.factory, 'noisy', True):
490
log.msg("Authenticated login for " + user)
492
def _ebMailbox(self, failure):
493
failure = failure.trap(cred.error.LoginDenied, cred.error.LoginFailed)
494
if issubclass(failure, cred.error.LoginDenied):
495
self.failResponse("Access denied: " + str(failure))
496
elif issubclass(failure, cred.error.LoginFailed):
497
self.failResponse('Authentication failed')
498
if getattr(self.factory, 'noisy', True):
499
log.msg("Denied login attempt from " + str(self.transport.getPeer()))
501
def _ebUnexpected(self, failure):
502
self.failResponse('Server error: ' + failure.getErrorMessage())
505
def do_USER(self, user):
507
self.successResponse('USER accepted, send PASS')
509
def do_PASS(self, password):
510
if self._userIs is None:
511
self.failResponse("USER required before PASS")
515
d = defer.maybeDeferred(self.authenticateUserPASS, user, password)
516
d.addCallbacks(self._cbMailbox, self._ebMailbox, callbackArgs=(user,)
517
).addErrback(self._ebUnexpected)
520
def _longOperation(self, d):
521
# Turn off timeouts and block further processing until the Deferred
522
# fires, then reverse those changes.
523
timeOut = self.timeOut
524
self.setTimeout(None)
526
d.addCallback(self._unblock)
527
d.addCallback(lambda ign: self.setTimeout(timeOut))
531
def _coiterate(self, gen):
532
return self.schedule(_IteratorBuffer(self.transport.writeSequence, gen))
536
d = defer.maybeDeferred(self.mbox.listMessages)
537
def cbMessages(msgs):
538
return self._coiterate(formatStatResponse(msgs))
540
self.failResponse(err.getErrorMessage())
541
log.msg("Unexpected do_STAT failure:")
543
return self._longOperation(d.addCallbacks(cbMessages, ebMessages))
546
def do_LIST(self, i=None):
548
d = defer.maybeDeferred(self.mbox.listMessages)
549
def cbMessages(msgs):
550
return self._coiterate(formatListResponse(msgs))
552
self.failResponse(err.getErrorMessage())
553
log.msg("Unexpected do_LIST failure:")
555
return self._longOperation(d.addCallbacks(cbMessages, ebMessages))
562
self.failResponse("Invalid message-number: %r" % (i,))
564
d = defer.maybeDeferred(self.mbox.listMessages, i - 1)
566
self.successResponse('%d %d' % (i, msg))
568
errcls = err.check(ValueError, IndexError)
569
if errcls is not None:
570
if errcls is IndexError:
571
# IndexError was supported for a while, but really
572
# shouldn't be. One error condition, one exception
575
"twisted.mail.pop3.IMailbox.listMessages may not "
576
"raise IndexError for out-of-bounds message numbers: "
577
"raise ValueError instead.",
578
PendingDeprecationWarning)
579
self.failResponse("Invalid message-number: %r" % (i,))
581
self.failResponse(err.getErrorMessage())
582
log.msg("Unexpected do_LIST failure:")
584
return self._longOperation(d.addCallbacks(cbMessage, ebMessage))
587
def do_UIDL(self, i=None):
589
d = defer.maybeDeferred(self.mbox.listMessages)
590
def cbMessages(msgs):
591
return self._coiterate(formatUIDListResponse(msgs, self.mbox.getUidl))
593
self.failResponse(err.getErrorMessage())
594
log.msg("Unexpected do_UIDL failure:")
596
return self._longOperation(d.addCallbacks(cbMessages, ebMessages))
603
self.failResponse("Bad message number argument")
606
msg = self.mbox.getUidl(i - 1)
608
# XXX TODO See above comment regarding IndexError.
610
"twisted.mail.pop3.IMailbox.getUidl may not "
611
"raise IndexError for out-of-bounds message numbers: "
612
"raise ValueError instead.",
613
PendingDeprecationWarning)
614
self.failResponse("Bad message number argument")
616
self.failResponse("Bad message number argument")
618
self.successResponse(str(msg))
621
def _getMessageFile(self, i):
623
Retrieve the size and contents of a given message, as a two-tuple.
625
@param i: The number of the message to operate on. This is a base-ten
626
string representation starting at 1.
628
@return: A Deferred which fires with a two-tuple of an integer and a
636
self.failResponse("Bad message number argument")
637
return defer.succeed(None)
639
sizeDeferred = defer.maybeDeferred(self.mbox.listMessages, msg)
640
def cbMessageSize(size):
642
return defer.fail(_POP3MessageDeleted())
643
fileDeferred = defer.maybeDeferred(self.mbox.getMessage, msg)
644
fileDeferred.addCallback(lambda fObj: (size, fObj))
647
def ebMessageSomething(err):
648
errcls = err.check(_POP3MessageDeleted, ValueError, IndexError)
649
if errcls is _POP3MessageDeleted:
650
self.failResponse("message deleted")
651
elif errcls in (ValueError, IndexError):
652
if errcls is IndexError:
653
# XXX TODO See above comment regarding IndexError.
655
"twisted.mail.pop3.IMailbox.listMessages may not "
656
"raise IndexError for out-of-bounds message numbers: "
657
"raise ValueError instead.",
658
PendingDeprecationWarning)
659
self.failResponse("Bad message number argument")
661
log.msg("Unexpected _getMessageFile failure:")
665
sizeDeferred.addCallback(cbMessageSize)
666
sizeDeferred.addErrback(ebMessageSomething)
670
def _sendMessageContent(self, i, fpWrapper, successResponse):
671
d = self._getMessageFile(i)
672
def cbMessageFile(info):
674
# Some error occurred - a failure response has been sent
675
# already, just give up.
678
self._highest = max(self._highest, int(i))
681
self.successResponse(successResponse(resp))
682
s = basic.FileSender()
683
d = s.beginFileTransfer(fp, self.transport, self.transformChunk)
685
def cbFileTransfer(lastsent):
692
def ebFileTransfer(err):
693
self.transport.loseConnection()
694
log.msg("Unexpected error in _sendMessageContent:")
697
d.addCallback(cbFileTransfer)
698
d.addErrback(ebFileTransfer)
700
return self._longOperation(d.addCallback(cbMessageFile))
703
def do_TOP(self, i, size):
709
self.failResponse("Bad line count argument")
711
return self._sendMessageContent(
713
lambda fp: _HeadersPlusNLines(fp, size),
714
lambda size: "Top of message follows")
717
def do_RETR(self, i):
718
return self._sendMessageContent(
721
lambda size: "%d" % (size,))
724
def transformChunk(self, chunk):
725
return chunk.replace('\n', '\r\n').replace('\r\n.', '\r\n..')
728
def finishedFileTransfer(self, lastsent):
736
def do_DELE(self, i):
738
self.mbox.deleteMessage(i)
739
self.successResponse()
743
"""Perform no operation. Return a success code"""
744
self.successResponse()
748
"""Unset all deleted message flags"""
750
self.mbox.undeleteMessages()
756
self.successResponse()
761
Return the index of the highest message yet downloaded.
763
self.successResponse(self._highest)
766
def do_RPOP(self, user):
767
self.failResponse('permission denied, sucker')
773
self.successResponse()
774
self.transport.loseConnection()
777
def authenticateUserAPOP(self, user, digest):
778
"""Perform authentication of an APOP login.
781
@param user: The name of the user attempting to log in.
784
@param digest: The response string with which the user replied.
787
@return: A deferred whose callback is invoked if the login is
788
successful, and whose errback will be invoked otherwise. The
789
callback will be passed a 3-tuple consisting of IMailbox,
790
an object implementing IMailbox, and a zero-argument callable
791
to be invoked when this session is terminated.
793
if self.portal is not None:
794
return self.portal.login(
795
APOPCredentials(self.magic, user, digest),
799
raise cred.error.UnauthorizedLogin()
801
def authenticateUserPASS(self, user, password):
802
"""Perform authentication of a username/password login.
805
@param user: The name of the user attempting to log in.
807
@type password: C{str}
808
@param password: The password to attempt to authenticate with.
811
@return: A deferred whose callback is invoked if the login is
812
successful, and whose errback will be invoked otherwise. The
813
callback will be passed a 3-tuple consisting of IMailbox,
814
an object implementing IMailbox, and a zero-argument callable
815
to be invoked when this session is terminated.
817
if self.portal is not None:
818
return self.portal.login(
819
cred.credentials.UsernamePassword(user, password),
823
raise cred.error.UnauthorizedLogin()
826
class IServerFactory(Interface):
827
"""Interface for querying additional parameters of this POP3 server.
829
Any cap_* method may raise NotImplementedError if the particular
830
capability is not supported. If cap_EXPIRE() does not raise
831
NotImplementedError, perUserExpiration() must be implemented, otherwise
832
they are optional. If cap_LOGIN_DELAY() is implemented,
833
perUserLoginDelay() must be implemented, otherwise they are optional.
835
@ivar challengers: A dictionary mapping challenger names to classes
836
implementing C{IUsernameHashedPassword}.
839
def cap_IMPLEMENTATION():
840
"""Return a string describing this POP3 server implementation."""
843
"""Return the minimum number of days messages are retained."""
845
def perUserExpiration():
846
"""Indicate whether message expiration is per-user.
848
@return: True if it is, false otherwise.
851
def cap_LOGIN_DELAY():
852
"""Return the minimum number of seconds between client logins."""
854
def perUserLoginDelay():
855
"""Indicate whether the login delay period is per-user.
857
@return: True if it is, false otherwise.
860
class IMailbox(Interface):
862
@type loginDelay: C{int}
863
@ivar loginDelay: The number of seconds between allowed logins for the
864
user associated with this mailbox. None
866
@type messageExpiration: C{int}
867
@ivar messageExpiration: The number of days messages in this mailbox will
868
remain on the server before being deleted.
871
def listMessages(index=None):
872
"""Retrieve the size of one or more messages.
874
@type index: C{int} or C{None}
875
@param index: The number of the message for which to retrieve the
876
size (starting at 0), or None to retrieve the size of all messages.
878
@rtype: C{int} or any iterable of C{int} or a L{Deferred} which fires
881
@return: The number of octets in the specified message, or an iterable
882
of integers representing the number of octets in all the messages. Any
883
value which would have referred to a deleted message should be set to 0.
885
@raise ValueError: if C{index} is greater than the index of any message
889
def getMessage(index):
890
"""Retrieve a file-like object for a particular message.
893
@param index: The number of the message to retrieve
895
@rtype: A file-like object
896
@return: A file containing the message data with lines delimited by
901
"""Get a unique identifier for a particular message.
904
@param index: The number of the message for which to retrieve a UIDL
907
@return: A string of printable characters uniquely identifying for all
908
time the specified message.
910
@raise ValueError: if C{index} is greater than the index of any message
914
def deleteMessage(index):
915
"""Delete a particular message.
917
This must not change the number of messages in this mailbox. Further
918
requests for the size of deleted messages should return 0. Further
919
requests for the message itself may raise an exception.
922
@param index: The number of the message to delete.
925
def undeleteMessages():
927
Undelete any messages which have been marked for deletion since the
928
most recent L{sync} call.
930
Any message which can be undeleted should be returned to its
931
original position in the message sequence and retain its original
936
"""Perform checkpointing.
938
This method will be called to indicate the mailbox should attempt to
939
clean up any remaining deleted messages.
947
def listMessages(self, i=None):
949
def getMessage(self, i):
951
def getUidl(self, i):
953
def deleteMessage(self, i):
955
def undeleteMessages(self):
961
NONE, SHORT, FIRST_LONG, LONG = range(4)
966
NEXT[FIRST_LONG] = LONG
969
class POP3Client(basic.LineOnlyReceiver):
974
welcomeRe = re.compile('<(.*)>')
978
warnings.warn("twisted.mail.pop3.POP3Client is deprecated, "
979
"please use twisted.mail.pop3.AdvancedPOP3Client "
980
"instead.", DeprecationWarning,
983
def sendShort(self, command, params=None):
984
if params is not None:
985
self.sendLine('%s %s' % (command, params))
987
self.sendLine(command)
988
self.command = command
991
def sendLong(self, command, params):
993
self.sendLine('%s %s' % (command, params))
995
self.sendLine(command)
996
self.command = command
997
self.mode = FIRST_LONG
999
def handle_default(self, line):
1000
if line[:-4] == '-ERR':
1003
def handle_WELCOME(self, line):
1004
code, data = line.split(' ', 1)
1006
self.transport.loseConnection()
1008
m = self.welcomeRe.match(line)
1010
self.welcomeCode = m.group(1)
1012
def _dispatch(self, command, default, *args):
1014
method = getattr(self, 'handle_'+command, default)
1015
if method is not None:
1020
def lineReceived(self, line):
1021
if self.mode == SHORT or self.mode == FIRST_LONG:
1022
self.mode = NEXT[self.mode]
1023
self._dispatch(self.command, self.handle_default, line)
1024
elif self.mode == LONG:
1026
self.mode = NEXT[self.mode]
1027
self._dispatch(self.command+'_end', None)
1031
self._dispatch(self.command+"_continue", None, line)
1033
def apopAuthenticate(self, user, password, magic):
1034
digest = md5(magic + password).hexdigest()
1035
self.apop(user, digest)
1037
def apop(self, user, digest):
1038
self.sendLong('APOP', ' '.join((user, digest)))
1040
self.sendLong('RETR', i)
1042
self.sendShort('DELE', i)
1043
def list(self, i=''):
1044
self.sendLong('LIST', i)
1045
def uidl(self, i=''):
1046
self.sendLong('UIDL', i)
1047
def user(self, name):
1048
self.sendShort('USER', name)
1049
def pass_(self, pass_):
1050
self.sendShort('PASS', pass_)
1052
self.sendShort('QUIT')
1054
from twisted.mail.pop3client import POP3Client as AdvancedPOP3Client
1055
from twisted.mail.pop3client import POP3ClientError
1056
from twisted.mail.pop3client import InsecureAuthenticationDisallowed
1057
from twisted.mail.pop3client import ServerErrorResponse
1058
from twisted.mail.pop3client import LineTooLong
1062
'IMailbox', 'IServerFactory',
1065
'POP3Error', 'POP3ClientError', 'InsecureAuthenticationDisallowed',
1066
'ServerErrorResponse', 'LineTooLong',
1069
'POP3', 'POP3Client', 'AdvancedPOP3Client',
1072
'APOPCredentials', 'Mailbox']