1
# -*- test-case-name: twisted.mail.test.test_pop3 -*-
3
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
4
# See LICENSE for details.
7
"""Post-office Protocol version 3
9
@author: U{Glyph Lefkowitz<mailto:glyph@twistedmatrix.com>}
10
@author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>}
12
API Stability: Unstable
21
from zope.interface import implements, Interface
23
from twisted.mail import smtp
24
from twisted.protocols import basic
25
from twisted.protocols import policies
26
from twisted.internet import task
27
from twisted.internet import defer
28
from twisted.internet import interfaces
29
from twisted.python import log
31
from twisted import cred
32
import twisted.cred.error
33
import twisted.cred.credentials
38
class APOPCredentials:
39
implements(cred.credentials.IUsernamePassword)
41
def __init__(self, magic, username, digest):
43
self.username = username
46
def checkPassword(self, password):
47
seed = self.magic + password
48
myDigest = md5.new(seed).hexdigest()
49
return myDigest == self.digest
52
class _HeadersPlusNLines:
53
def __init__(self, f, n):
61
def read(self, bytes):
64
data = self.f.read(bytes)
68
df, sz = data.find('\r\n\r\n'), 4
70
df, sz = data.find('\n\n'), 2
79
if self.linecount > 0:
80
dsplit = (self.buf+data).split('\n')
82
for ln in dsplit[:-1]:
83
if self.linecount > self.n:
94
class _POP3MessageDeleted(Exception):
96
Internal control-flow exception. Indicates the file of a deleted message
101
class POP3Error(Exception):
106
class _IteratorBuffer(object):
109
def __init__(self, write, iterable, memoryBufferSize=None):
111
Create a _IteratorBuffer.
113
@param write: A one-argument callable which will be invoked with a list
114
of strings which have been buffered.
116
@param iterable: The source of input strings as any iterable.
118
@param memoryBufferSize: The upper limit on buffered string length,
119
beyond which the buffer will be flushed to the writer.
123
self.iterator = iter(iterable)
124
if memoryBufferSize is None:
125
memoryBufferSize = 2 ** 16
126
self.memoryBufferSize = memoryBufferSize
135
v = self.iterator.next()
136
except StopIteration:
138
self.write(self.lines)
139
# Drop some references, in case they're edges in a cycle.
140
del self.iterator, self.lines, self.write
145
self.bufSize += len(v)
146
if self.bufSize > self.memoryBufferSize:
147
self.write(self.lines)
153
def iterateLineGenerator(proto, gen):
155
Hook the given protocol instance up to the given iterator with an
156
_IteratorBuffer and schedule the result to be exhausted via the protocol.
160
@rtype: L{twisted.internet.defer.Deferred}
162
coll = _IteratorBuffer(proto.transport.writeSequence, gen)
163
return proto.schedule(coll)
167
def successResponse(response):
169
Format the given object as a positive response.
171
response = str(response)
172
return '+OK %s\r\n' % (response,)
176
def formatStatResponse(msgs):
178
Format the list of message sizes appropriately for a STAT response.
180
Yields None until it finishes computing a result, then yields a str
181
instance that is suitable for use as a response to the STAT command.
182
Intended to be used with a L{twisted.internet.task.Cooperator}.
190
yield successResponse('%d %d' % (i, bytes))
194
def formatListLines(msgs):
196
Format a list of message sizes appropriately for the lines of a LIST
199
Yields str instances formatted appropriately for use as lines in the
200
response to the LIST command. Does not include the trailing '.'.
205
yield '%d %d\r\n' % (i, size)
209
def formatListResponse(msgs):
211
Format a list of message sizes appropriately for a complete LIST response.
213
Yields str instances formatted appropriately for use as a LIST command
216
yield successResponse(len(msgs))
217
for ele in formatListLines(msgs):
223
def formatUIDListLines(msgs, getUidl):
225
Format the list of message sizes appropriately for the lines of a UIDL
228
Yields str instances formatted appropriately for use as lines in the
229
response to the UIDL command. Does not include the trailing '.'.
231
for i, m in enumerate(msgs):
234
yield '%d %s\r\n' % (i + 1, uid)
238
def formatUIDListResponse(msgs, getUidl):
240
Format a list of message sizes appropriately for a complete UIDL response.
242
Yields str instances formatted appropriately for use as a UIDL command
245
yield successResponse('')
246
for ele in formatUIDListLines(msgs, getUidl):
252
class POP3(basic.LineOnlyReceiver, policies.TimeoutMixin):
254
POP3 server protocol implementation.
256
@ivar portal: A reference to the L{twisted.cred.portal.Portal} instance we
257
will authenticate through.
259
@ivar factory: A L{twisted.mail.pop3.IServerFactory} which will be used to
260
determine some extended behavior of the server.
262
@ivar timeOut: An integer which defines the minimum amount of time which
263
may elapse without receiving any traffic after which the client will be
266
@ivar schedule: A one-argument callable which should behave like
267
L{twisted.internet.task.coiterate}.
269
implements(interfaces.IProducer)
275
AUTH_CMDS = ['CAPA', 'USER', 'PASS', 'APOP', 'AUTH', 'RPOP', 'QUIT']
280
# The mailbox we're serving
283
# Set this pretty low -- POP3 clients are expected to log in, download
284
# everything, and log out.
287
# Current protocol state
293
# Cooperate and suchlike.
294
schedule = staticmethod(task.coiterate)
296
# Message index of the highest retrieved message.
299
def connectionMade(self):
300
if self.magic is None:
301
self.magic = self.generateMagic()
302
self.successResponse(self.magic)
303
self.setTimeout(self.timeOut)
304
if getattr(self.factory, 'noisy', True):
305
log.msg("New connection from " + str(self.transport.getPeer()))
308
def connectionLost(self, reason):
309
if self._onLogout is not None:
311
self._onLogout = None
312
self.setTimeout(None)
315
def generateMagic(self):
316
return smtp.messageid()
319
def successResponse(self, message=''):
320
self.transport.write(successResponse(message))
322
def failResponse(self, message=''):
323
self.sendLine('-ERR ' + str(message))
325
# def sendLine(self, line):
326
# print 'S:', repr(line)
327
# basic.LineOnlyReceiver.sendLine(self, line)
329
def lineReceived(self, line):
330
# print 'C:', repr(line)
332
getattr(self, 'state_' + self.state)(line)
334
def _unblock(self, _):
335
commands = self.blocked
337
while commands and self.blocked is None:
338
cmd, args = commands.pop(0)
339
self.processCommand(cmd, *args)
340
if self.blocked is not None:
341
self.blocked.extend(commands)
343
def state_COMMAND(self, line):
345
return self.processCommand(*line.split(' '))
346
except (ValueError, AttributeError, POP3Error, TypeError), e:
348
self.failResponse('bad protocol or server: %s: %s' % (e.__class__.__name__, e))
350
def processCommand(self, command, *args):
351
if self.blocked is not None:
352
self.blocked.append((command, args))
355
command = string.upper(command)
356
authCmd = command in self.AUTH_CMDS
357
if not self.mbox and not authCmd:
358
raise POP3Error("not authenticated yet: cannot do " + command)
359
f = getattr(self, 'do_' + command, None)
362
raise POP3Error("Unknown protocol command: " + command)
365
def listCapabilities(self):
376
if IServerFactory.providedBy(self.factory):
377
# Oh my god. We can't just loop over a list of these because
378
# each has spectacularly different return value semantics!
380
v = self.factory.cap_IMPLEMENTATION()
381
except NotImplementedError:
386
baseCaps.append("IMPLEMENTATION " + str(v))
389
v = self.factory.cap_EXPIRE()
390
except NotImplementedError:
397
if self.factory.perUserExpiration():
399
v = str(self.mbox.messageExpiration)
403
baseCaps.append("EXPIRE " + v)
406
v = self.factory.cap_LOGIN_DELAY()
407
except NotImplementedError:
412
if self.factory.perUserLoginDelay():
414
v = str(self.mbox.loginDelay)
418
baseCaps.append("LOGIN-DELAY " + v)
421
v = self.factory.challengers
422
except AttributeError:
427
baseCaps.append("SASL " + ' '.join(v.keys()))
431
self.successResponse("I can do the following:")
432
for cap in self.listCapabilities():
436
def do_AUTH(self, args=None):
437
if not getattr(self.factory, 'challengers', None):
438
self.failResponse("AUTH extension unsupported")
442
self.successResponse("Supported authentication methods:")
443
for a in self.factory.challengers:
444
self.sendLine(a.upper())
448
auth = self.factory.challengers.get(args.strip().upper())
449
if not self.portal or not auth:
450
self.failResponse("Unsupported SASL selected")
454
chal = self._auth.getChallenge()
456
self.sendLine('+ ' + base64.encodestring(chal).rstrip('\n'))
459
def state_AUTH(self, line):
460
self.state = "COMMAND"
462
parts = base64.decodestring(line).split(None, 1)
463
except binascii.Error:
464
self.failResponse("Invalid BASE64 encoding")
467
self.failResponse("Invalid AUTH response")
469
self._auth.username = parts[0]
470
self._auth.response = parts[1]
471
d = self.portal.login(self._auth, None, IMailbox)
472
d.addCallback(self._cbMailbox, parts[0])
473
d.addErrback(self._ebMailbox)
474
d.addErrback(self._ebUnexpected)
476
def do_APOP(self, user, digest):
477
d = defer.maybeDeferred(self.authenticateUserAPOP, user, digest)
478
d.addCallbacks(self._cbMailbox, self._ebMailbox, callbackArgs=(user,)
479
).addErrback(self._ebUnexpected)
481
def _cbMailbox(self, (interface, avatar, logout), user):
482
if interface is not IMailbox:
483
self.failResponse('Authentication failed')
484
log.err("_cbMailbox() called with an interface other than IMailbox")
488
self._onLogout = logout
489
self.successResponse('Authentication succeeded')
490
if getattr(self.factory, 'noisy', True):
491
log.msg("Authenticated login for " + user)
493
def _ebMailbox(self, failure):
494
failure = failure.trap(cred.error.LoginDenied, cred.error.LoginFailed)
495
if issubclass(failure, cred.error.LoginDenied):
496
self.failResponse("Access denied: " + str(failure))
497
elif issubclass(failure, cred.error.LoginFailed):
498
self.failResponse('Authentication failed')
499
if getattr(self.factory, 'noisy', True):
500
log.msg("Denied login attempt from " + str(self.transport.getPeer()))
502
def _ebUnexpected(self, failure):
503
self.failResponse('Server error: ' + failure.getErrorMessage())
506
def do_USER(self, user):
508
self.successResponse('USER accepted, send PASS')
510
def do_PASS(self, password):
511
if self._userIs is None:
512
self.failResponse("USER required before PASS")
516
d = defer.maybeDeferred(self.authenticateUserPASS, user, password)
517
d.addCallbacks(self._cbMailbox, self._ebMailbox, callbackArgs=(user,)
518
).addErrback(self._ebUnexpected)
521
def _longOperation(self, d):
522
# Turn off timeouts and block further processing until the Deferred
523
# fires, then reverse those changes.
524
timeOut = self.timeOut
525
self.setTimeout(None)
527
d.addCallback(self._unblock)
528
d.addCallback(lambda ign: self.setTimeout(timeOut))
532
def _coiterate(self, gen):
533
return self.schedule(_IteratorBuffer(self.transport.writeSequence, gen))
537
d = defer.maybeDeferred(self.mbox.listMessages)
538
def cbMessages(msgs):
539
return self._coiterate(formatStatResponse(msgs))
541
self.failResponse(err.getErrorMessage())
542
log.msg("Unexpected do_STAT failure:")
544
return self._longOperation(d.addCallbacks(cbMessages, ebMessages))
547
def do_LIST(self, i=None):
549
d = defer.maybeDeferred(self.mbox.listMessages)
550
def cbMessages(msgs):
551
return self._coiterate(formatListResponse(msgs))
553
self.failResponse(err.getErrorMessage())
554
log.msg("Unexpected do_LIST failure:")
556
return self._longOperation(d.addCallbacks(cbMessages, ebMessages))
563
self.failResponse("Invalid message-number: %r" % (i,))
565
d = defer.maybeDeferred(self.mbox.listMessages, i - 1)
567
self.successResponse('%d %d' % (i, msg))
569
errcls = err.check(ValueError, IndexError)
570
if errcls is not None:
571
if errcls is IndexError:
572
# IndexError was supported for a while, but really
573
# shouldn't be. One error condition, one exception
576
"twisted.mail.pop3.IMailbox.listMessages may not "
577
"raise IndexError for out-of-bounds message numbers: "
578
"raise ValueError instead.",
579
PendingDeprecationWarning)
580
self.failResponse("Invalid message-number: %r" % (i,))
582
self.failResponse(err.getErrorMessage())
583
log.msg("Unexpected do_LIST failure:")
585
return self._longOperation(d.addCallbacks(cbMessage, ebMessage))
588
def do_UIDL(self, i=None):
590
d = defer.maybeDeferred(self.mbox.listMessages)
591
def cbMessages(msgs):
592
return self._coiterate(formatUIDListResponse(msgs, self.mbox.getUidl))
594
self.failResponse(err.getErrorMessage())
595
log.msg("Unexpected do_UIDL failure:")
597
return self._longOperation(d.addCallbacks(cbMessages, ebMessages))
604
self.failResponse("Bad message number argument")
607
msg = self.mbox.getUidl(i - 1)
609
# XXX TODO See above comment regarding IndexError.
611
"twisted.mail.pop3.IMailbox.getUidl may not "
612
"raise IndexError for out-of-bounds message numbers: "
613
"raise ValueError instead.",
614
PendingDeprecationWarning)
615
self.failResponse("Bad message number argument")
617
self.failResponse("Bad message number argument")
619
self.successResponse(str(msg))
622
def _getMessageFile(self, i):
624
Retrieve the size and contents of a given message, as a two-tuple.
626
@param i: The number of the message to operate on. This is a base-ten
627
string representation starting at 1.
629
@return: A Deferred which fires with a two-tuple of an integer and a
637
self.failResponse("Bad message number argument")
638
return defer.succeed(None)
640
sizeDeferred = defer.maybeDeferred(self.mbox.listMessages, msg)
641
def cbMessageSize(size):
643
return defer.fail(_POP3MessageDeleted())
644
fileDeferred = defer.maybeDeferred(self.mbox.getMessage, msg)
645
fileDeferred.addCallback(lambda fObj: (size, fObj))
648
def ebMessageSomething(err):
649
errcls = err.check(_POP3MessageDeleted, ValueError, IndexError)
650
if errcls is _POP3MessageDeleted:
651
self.failResponse("message deleted")
652
elif errcls in (ValueError, IndexError):
653
if errcls is IndexError:
654
# XXX TODO See above comment regarding IndexError.
656
"twisted.mail.pop3.IMailbox.listMessages may not "
657
"raise IndexError for out-of-bounds message numbers: "
658
"raise ValueError instead.",
659
PendingDeprecationWarning)
660
self.failResponse("Bad message number argument")
662
log.msg("Unexpected _getMessageFile failure:")
666
sizeDeferred.addCallback(cbMessageSize)
667
sizeDeferred.addErrback(ebMessageSomething)
671
def _sendMessageContent(self, i, fpWrapper, successResponse):
672
d = self._getMessageFile(i)
673
def cbMessageFile(info):
675
# Some error occurred - a failure response has been sent
676
# already, just give up.
679
self._highest = max(self._highest, int(i))
682
self.successResponse(successResponse(resp))
683
s = basic.FileSender()
684
d = s.beginFileTransfer(fp, self.transport, self.transformChunk)
686
def cbFileTransfer(lastsent):
693
def ebFileTransfer(err):
694
self.transport.loseConnection()
695
log.msg("Unexpected error in _sendMessageContent:")
698
d.addCallback(cbFileTransfer)
699
d.addErrback(ebFileTransfer)
701
return self._longOperation(d.addCallback(cbMessageFile))
704
def do_TOP(self, i, size):
710
self.failResponse("Bad line count argument")
712
return self._sendMessageContent(
714
lambda fp: _HeadersPlusNLines(fp, size),
715
lambda size: "Top of message follows")
718
def do_RETR(self, i):
719
return self._sendMessageContent(
722
lambda size: "%d" % (size,))
725
def transformChunk(self, chunk):
726
return chunk.replace('\n', '\r\n').replace('\r\n.', '\r\n..')
729
def finishedFileTransfer(self, lastsent):
737
def do_DELE(self, i):
739
self.mbox.deleteMessage(i)
740
self.successResponse()
744
"""Perform no operation. Return a success code"""
745
self.successResponse()
749
"""Unset all deleted message flags"""
751
self.mbox.undeleteMessages()
757
self.successResponse()
762
Return the index of the highest message yet downloaded.
764
self.successResponse(self._highest)
767
def do_RPOP(self, user):
768
self.failResponse('permission denied, sucker')
774
self.successResponse()
775
self.transport.loseConnection()
778
def authenticateUserAPOP(self, user, digest):
779
"""Perform authentication of an APOP login.
782
@param user: The name of the user attempting to log in.
785
@param digest: The response string with which the user replied.
788
@return: A deferred whose callback is invoked if the login is
789
successful, and whose errback will be invoked otherwise. The
790
callback will be passed a 3-tuple consisting of IMailbox,
791
an object implementing IMailbox, and a zero-argument callable
792
to be invoked when this session is terminated.
794
if self.portal is not None:
795
return self.portal.login(
796
APOPCredentials(self.magic, user, digest),
800
raise cred.error.UnauthorizedLogin()
802
def authenticateUserPASS(self, user, password):
803
"""Perform authentication of a username/password login.
806
@param user: The name of the user attempting to log in.
808
@type password: C{str}
809
@param password: The password to attempt to authenticate with.
812
@return: A deferred whose callback is invoked if the login is
813
successful, and whose errback will be invoked otherwise. The
814
callback will be passed a 3-tuple consisting of IMailbox,
815
an object implementing IMailbox, and a zero-argument callable
816
to be invoked when this session is terminated.
818
if self.portal is not None:
819
return self.portal.login(
820
cred.credentials.UsernamePassword(user, password),
824
raise cred.error.UnauthorizedLogin()
827
class IServerFactory(Interface):
828
"""Interface for querying additional parameters of this POP3 server.
830
Any cap_* method may raise NotImplementedError if the particular
831
capability is not supported. If cap_EXPIRE() does not raise
832
NotImplementedError, perUserExpiration() must be implemented, otherwise
833
they are optional. If cap_LOGIN_DELAY() is implemented,
834
perUserLoginDelay() must be implemented, otherwise they are optional.
836
@ivar challengers: A dictionary mapping challenger names to classes
837
implementing C{IUsernameHashedPassword}.
840
def cap_IMPLEMENTATION():
841
"""Return a string describing this POP3 server implementation."""
844
"""Return the minimum number of days messages are retained."""
846
def perUserExpiration():
847
"""Indicate whether message expiration is per-user.
849
@return: True if it is, false otherwise.
852
def cap_LOGIN_DELAY():
853
"""Return the minimum number of seconds between client logins."""
855
def perUserLoginDelay():
856
"""Indicate whether the login delay period is per-user.
858
@return: True if it is, false otherwise.
861
class IMailbox(Interface):
863
@type loginDelay: C{int}
864
@ivar loginDelay: The number of seconds between allowed logins for the
865
user associated with this mailbox. None
867
@type messageExpiration: C{int}
868
@ivar messageExpiration: The number of days messages in this mailbox will
869
remain on the server before being deleted.
872
def listMessages(index=None):
873
"""Retrieve the size of one or more messages.
875
@type index: C{int} or C{None}
876
@param index: The number of the message for which to retrieve the
877
size (starting at 0), or None to retrieve the size of all messages.
879
@rtype: C{int} or any iterable of C{int} or a L{Deferred} which fires
882
@return: The number of octets in the specified message, or an iterable
883
of integers representing the number of octets in all the messages. Any
884
value which would have referred to a deleted message should be set to
887
@raise ValueError: if C{index} is greater than the index of any message
891
def getMessage(index):
892
"""Retrieve a file-like object for a particular message.
895
@param index: The number of the message to retrieve
897
@rtype: A file-like object
898
@return: A file containing the message data with lines delimited by
903
"""Get a unique identifier for a particular message.
906
@param index: The number of the message for which to retrieve a UIDL
909
@return: A string of printable characters uniquely identifying for all
910
time the specified message.
912
@raise ValueError: if C{index} is greater than the index of any message
916
def deleteMessage(index):
917
"""Delete a particular message.
919
This must not change the number of messages in this mailbox. Further
920
requests for the size of deleted messages should return 0. Further
921
requests for the message itself may raise an exception.
924
@param index: The number of the message to delete.
927
def undeleteMessages():
929
Undelete any messages which have been marked for deletion since the
930
most recent L{sync} call.
932
Any message which can be undeleted should be returned to its
933
original position in the message sequence and retain its original
938
"""Perform checkpointing.
940
This method will be called to indicate the mailbox should attempt to
941
clean up any remaining deleted messages.
949
def listMessages(self, i=None):
951
def getMessage(self, i):
953
def getUidl(self, i):
955
def deleteMessage(self, i):
957
def undeleteMessages(self):
963
NONE, SHORT, FIRST_LONG, LONG = range(4)
968
NEXT[FIRST_LONG] = LONG
971
class POP3Client(basic.LineOnlyReceiver):
976
welcomeRe = re.compile('<(.*)>')
980
warnings.warn("twisted.mail.pop3.POP3Client is deprecated, "
981
"please use twisted.mail.pop3.AdvancedPOP3Client "
982
"instead.", DeprecationWarning,
985
def sendShort(self, command, params=None):
986
if params is not None:
987
self.sendLine('%s %s' % (command, params))
989
self.sendLine(command)
990
self.command = command
993
def sendLong(self, command, params):
995
self.sendLine('%s %s' % (command, params))
997
self.sendLine(command)
998
self.command = command
999
self.mode = FIRST_LONG
1001
def handle_default(self, line):
1002
if line[:-4] == '-ERR':
1005
def handle_WELCOME(self, line):
1006
code, data = line.split(' ', 1)
1008
self.transport.loseConnection()
1010
m = self.welcomeRe.match(line)
1012
self.welcomeCode = m.group(1)
1014
def _dispatch(self, command, default, *args):
1016
method = getattr(self, 'handle_'+command, default)
1017
if method is not None:
1022
def lineReceived(self, line):
1023
if self.mode == SHORT or self.mode == FIRST_LONG:
1024
self.mode = NEXT[self.mode]
1025
self._dispatch(self.command, self.handle_default, line)
1026
elif self.mode == LONG:
1028
self.mode = NEXT[self.mode]
1029
self._dispatch(self.command+'_end', None)
1033
self._dispatch(self.command+"_continue", None, line)
1035
def apopAuthenticate(self, user, password, magic):
1036
digest = md5.new(magic + password).hexdigest()
1037
self.apop(user, digest)
1039
def apop(self, user, digest):
1040
self.sendLong('APOP', ' '.join((user, digest)))
1042
self.sendLong('RETR', i)
1044
self.sendShort('DELE', i)
1045
def list(self, i=''):
1046
self.sendLong('LIST', i)
1047
def uidl(self, i=''):
1048
self.sendLong('UIDL', i)
1049
def user(self, name):
1050
self.sendShort('USER', name)
1051
def pass_(self, pass_):
1052
self.sendShort('PASS', pass_)
1054
self.sendShort('QUIT')
1056
from twisted.mail.pop3client import POP3Client as AdvancedPOP3Client
1057
from twisted.mail.pop3client import POP3ClientError
1058
from twisted.mail.pop3client import InsecureAuthenticationDisallowed
1059
from twisted.mail.pop3client import ServerErrorResponse
1060
from twisted.mail.pop3client import LineTooLong
1064
'IMailbox', 'IServerFactory',
1067
'POP3Error', 'POP3ClientError', 'InsecureAuthenticationDisallowed',
1068
'ServerErrorResponse', 'LineTooLong',
1071
'POP3', 'POP3Client', 'AdvancedPOP3Client',
1074
'APOPCredentials', 'Mailbox']