root / trunk / twisted / mail / pop3.py

Revision 25457, 32.8 kB (checked in by exarkun, 8 months ago)

Merge hashlib-2763-3

Author: wsanchez, exarkun
Reviewer: exarkun, mwhudson
Fixes: #2763

Replace uses of md5 and sha modules in Twisted with use of a new twisted.python.hashlib
module which transparently uses the new hashlib standard library module if it is available
or falls back to md5 and sha if not.

Line 
1 # -*- test-case-name: twisted.mail.test.test_pop3 -*-
2 #
3 # Copyright (c) 2001-2008 Twisted Matrix Laboratories.
4 # See LICENSE for details.
5
6
7 """
8 Post-office Protocol version 3
9
10 @author: Glyph Lefkowitz
11 @author: Jp Calderone
12 """
13
14 import string
15 import base64
16 import binascii
17 import warnings
18
19 from zope.interface import implements, Interface
20
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
29
30 from twisted import cred
31 import twisted.cred.error
32 import twisted.cred.credentials
33
34 ##
35 ## Authentication
36 ##
37 class APOPCredentials:
38     implements(cred.credentials.IUsernamePassword)
39
40     def __init__(self, magic, username, digest):
41         self.magic = magic
42         self.username = username
43         self.digest = digest
44
45     def checkPassword(self, password):
46         seed = self.magic + password
47         myDigest = md5(seed).hexdigest()
48         return myDigest == self.digest
49
50
51 class _HeadersPlusNLines:
52     def __init__(self, f, n):
53         self.f = f
54         self.n = n
55         self.linecount = 0
56         self.headers = 1
57         self.done = 0
58         self.buf = ''
59
60     def read(self, bytes):
61         if self.done:
62             return ''
63         data = self.f.read(bytes)
64         if not data:
65             return data
66         if self.headers:
67             df, sz = data.find('\r\n\r\n'), 4
68             if df == -1:
69                 df, sz = data.find('\n\n'), 2
70             if df != -1:
71                 df += sz
72                 val = data[:df]
73                 data = data[df:]
74                 self.linecount = 1
75                 self.headers = 0
76         else:
77             val = ''
78         if self.linecount > 0:
79             dsplit = (self.buf+data).split('\n')
80             self.buf = dsplit[-1]
81             for ln in dsplit[:-1]:
82                 if self.linecount > self.n:
83                     self.done = 1
84                     return val
85                 val += (ln + '\n')
86                 self.linecount += 1
87             return val
88         else:
89             return data
90
91
92
93 class _POP3MessageDeleted(Exception):
94     """
95     Internal control-flow exception.  Indicates the file of a deleted message
96     was requested.
97     """
98
99
100 class POP3Error(Exception):
101     pass
102
103
104
105 class _IteratorBuffer(object):
106     bufSize = 0
107
108     def __init__(self, write, iterable, memoryBufferSize=None):
109         """
110         Create a _IteratorBuffer.
111
112         @param write: A one-argument callable which will be invoked with a list
113         of strings which have been buffered.
114
115         @param iterable: The source of input strings as any iterable.
116
117         @param memoryBufferSize: The upper limit on buffered string length,
118         beyond which the buffer will be flushed to the writer.
119         """
120         self.lines = []
121         self.write = write
122         self.iterator = iter(iterable)
123         if memoryBufferSize is None:
124             memoryBufferSize = 2 ** 16
125         self.memoryBufferSize = memoryBufferSize
126
127
128     def __iter__(self):
129         return self
130
131
132     def next(self):
133         try:
134             v = self.iterator.next()
135         except StopIteration:
136             if self.lines:
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
140             raise
141         else:
142             if v is not None:
143                 self.lines.append(v)
144                 self.bufSize += len(v)
145                 if self.bufSize > self.memoryBufferSize:
146                     self.write(self.lines)
147                     self.lines = []
148                     self.bufSize = 0
149
150
151
152 def iterateLineGenerator(proto, gen):
153     """
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.
156
157     @type proto: L{POP3}
158     @type gen: iterator
159     @rtype: L{twisted.internet.defer.Deferred}
160     """
161     coll = _IteratorBuffer(proto.transport.writeSequence, gen)
162     return proto.schedule(coll)
163
164
165
166 def successResponse(response):
167     """
168     Format the given object as a positive response.
169     """
170     response = str(response)
171     return '+OK %s\r\n' % (response,)
172
173
174
175 def formatStatResponse(msgs):
176     """
177     Format the list of message sizes appropriately for a STAT response.
178
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}.
182     """
183     i = 0
184     bytes = 0
185     for size in msgs:
186         i += 1
187         bytes += size
188         yield None
189     yield successResponse('%d %d' % (i, bytes))
190
191
192
193 def formatListLines(msgs):
194     """
195     Format a list of message sizes appropriately for the lines of a LIST
196     response.
197
198     Yields str instances formatted appropriately for use as lines in the
199     response to the LIST command.  Does not include the trailing '.'.
200     """
201     i = 0
202     for size in msgs:
203         i += 1
204         yield '%d %d\r\n' % (i, size)
205
206
207
208 def formatListResponse(msgs):
209     """
210     Format a list of message sizes appropriately for a complete LIST response.
211
212     Yields str instances formatted appropriately for use as a LIST command
213     response.
214     """
215     yield successResponse(len(msgs))
216     for ele in formatListLines(msgs):
217         yield ele
218     yield '.\r\n'
219
220
221
222 def formatUIDListLines(msgs, getUidl):
223     """
224     Format the list of message sizes appropriately for the lines of a UIDL
225     response.
226
227     Yields str instances formatted appropriately for use as lines in the
228     response to the UIDL command.  Does not include the trailing '.'.
229     """
230     for i, m in enumerate(msgs):
231         if m is not None:
232             uid = getUidl(i)
233             yield '%d %s\r\n' % (i + 1, uid)
234
235
236
237 def formatUIDListResponse(msgs, getUidl):
238     """
239     Format a list of message sizes appropriately for a complete UIDL response.
240
241     Yields str instances formatted appropriately for use as a UIDL command
242     response.
243     """
244     yield successResponse('')
245     for ele in formatUIDListLines(msgs, getUidl):
246         yield ele
247     yield '.\r\n'
248
249
250
251 class POP3(basic.LineOnlyReceiver, policies.TimeoutMixin):
252     """
253     POP3 server protocol implementation.
254
255     @ivar portal: A reference to the L{twisted.cred.portal.Portal} instance we
256     will authenticate through.
257
258     @ivar factory: A L{twisted.mail.pop3.IServerFactory} which will be used to
259     determine some extended behavior of the server.
260
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
263     disconnected.
264
265     @ivar schedule: A one-argument callable which should behave like
266     L{twisted.internet.task.coiterate}.
267     """
268     implements(interfaces.IProducer)
269
270     magic = None
271     _userIs = None
272     _onLogout = None
273
274     AUTH_CMDS = ['CAPA', 'USER', 'PASS', 'APOP', 'AUTH', 'RPOP', 'QUIT']
275
276     portal = None
277     factory = None
278
279     # The mailbox we're serving
280     mbox = None
281
282     # Set this pretty low -- POP3 clients are expected to log in, download
283     # everything, and log out.
284     timeOut = 300
285
286     # Current protocol state
287     state = "COMMAND"
288
289     # PIPELINE
290     blocked = None
291
292     # Cooperate and suchlike.
293     schedule = staticmethod(task.coiterate)
294
295     # Message index of the highest retrieved message.
296     _highest = 0
297
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()))
305
306
307     def connectionLost(self, reason):
308         if self._onLogout is not None:
309             self._onLogout()
310             self._onLogout = None
311         self.setTimeout(None)
312
313
314     def generateMagic(self):
315         return smtp.messageid()
316
317
318     def successResponse(self, message=''):
319         self.transport.write(successResponse(message))
320
321     def failResponse(self, message=''):
322         self.sendLine('-ERR ' + str(message))
323
324 #    def sendLine(self, line):
325 #        print 'S:', repr(line)
326 #        basic.LineOnlyReceiver.sendLine(self, line)
327
328     def lineReceived(self, line):
329 #        print 'C:', repr(line)
330         self.resetTimeout()
331         getattr(self, 'state_' + self.state)(line)
332
333     def _unblock(self, _):
334         commands = self.blocked
335         self.blocked = None
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)
341
342     def state_COMMAND(self, line):
343         try:
344             return self.processCommand(*line.split(' '))
345         except (ValueError, AttributeError, POP3Error, TypeError), e:
346             log.err()
347             self.failResponse('bad protocol or server: %s: %s' % (e.__class__.__name__, e))
348
349     def processCommand(self, command, *args):
350         if self.blocked is not None:
351             self.blocked.append((command, args))
352             return
353
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)
359         if f:
360             return f(*args)
361         raise POP3Error("Unknown protocol command: " + command)
362
363
364     def listCapabilities(self):
365         baseCaps = [
366             "TOP",
367             "USER",
368             "UIDL",
369             "PIPELINE",
370             "CELERITY",
371             "AUSPEX",
372             "POTENCE",
373         ]
374
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!
378             try:
379                 v = self.factory.cap_IMPLEMENTATION()
380             except NotImplementedError:
381                 pass
382             except:
383                 log.err()
384             else:
385                 baseCaps.append("IMPLEMENTATION " + str(v))
386
387             try:
388                 v = self.factory.cap_EXPIRE()
389             except NotImplementedError:
390                 pass
391             except:
392                 log.err()
393             else:
394                 if v is None:
395                     v = "NEVER"
396                 if self.factory.perUserExpiration():
397                     if self.mbox:
398                         v = str(self.mbox.messageExpiration)
399                     else:
400                         v = str(v) + " USER"
401                 v = str(v)
402                 baseCaps.append("EXPIRE " + v)
403
404             try:
405                 v = self.factory.cap_LOGIN_DELAY()
406             except NotImplementedError:
407                 pass
408             except:
409                 log.err()
410             else:
411                 if self.factory.perUserLoginDelay():
412                     if self.mbox:
413                         v = str(self.mbox.loginDelay)
414                     else:
415                         v = str(v) + " USER"
416                 v = str(v)
417                 baseCaps.append("LOGIN-DELAY " + v)
418
419             try:
420                 v = self.factory.challengers
421             except AttributeError:
422                 pass
423             except:
424                 log.err()
425             else:
426                 baseCaps.append("SASL " + ' '.join(v.keys()))
427         return baseCaps
428
429     def do_CAPA(self):
430         self.successResponse("I can do the following:")
431         for cap in self.listCapabilities():
432             self.sendLine(cap)
433         self.sendLine(".")
434
435     def do_AUTH(self, args=None):
436         if not getattr(self.factory, 'challengers', None):
437             self.failResponse("AUTH extension unsupported")
438             return
439
440         if args is None:
441             self.successResponse("Supported authentication methods:")
442             for a in self.factory.challengers:
443                 self.sendLine(a.upper())
444             self.sendLine(".")
445             return
446
447         auth = self.factory.challengers.get(args.strip().upper())
448         if not self.portal or not auth:
449             self.failResponse("Unsupported SASL selected")
450             return
451
452         self._auth = auth()
453         chal = self._auth.getChallenge()
454
455         self.sendLine('+ ' + base64.encodestring(chal).rstrip('\n'))
456         self.state = 'AUTH'
457
458     def state_AUTH(self, line):
459         self.state = "COMMAND"
460         try:
461             parts = base64.decodestring(line).split(None, 1)
462         except binascii.Error:
463             self.failResponse("Invalid BASE64 encoding")
464         else:
465             if len(parts) != 2:
466                 self.failResponse("Invalid AUTH response")
467                 return
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)
474
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)
479
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")
484             return
485
486         self.mbox = avatar
487         self._onLogout = logout
488         self.successResponse('Authentication succeeded')
489         if getattr(self.factory, 'noisy', True):
490             log.msg("Authenticated login for " + user)
491
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()))
500
501     def _ebUnexpected(self, failure):
502         self.failResponse('Server error: ' + failure.getErrorMessage())
503         log.err(failure)
504
505     def do_USER(self, user):
506         self._userIs = user
507         self.successResponse('USER accepted, send PASS')
508
509     def do_PASS(self, password):
510         if self._userIs is None:
511             self.failResponse("USER required before PASS")
512             return
513         user = self._userIs
514         self._userIs = None
515         d = defer.maybeDeferred(self.authenticateUserPASS, user, password)
516         d.addCallbacks(self._cbMailbox, self._ebMailbox, callbackArgs=(user,)
517         ).addErrback(self._ebUnexpected)
518
519
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)
525         self.blocked = []
526         d.addCallback(self._unblock)
527         d.addCallback(lambda ign: self.setTimeout(timeOut))
528         return d
529
530
531     def _coiterate(self, gen):
532         return self.schedule(_IteratorBuffer(self.transport.writeSequence, gen))
533
534
535     def do_STAT(self):
536         d = defer.maybeDeferred(self.mbox.listMessages)
537         def cbMessages(msgs):
538             return self._coiterate(formatStatResponse(msgs))
539         def ebMessages(err):
540             self.failResponse(err.getErrorMessage())
541             log.msg("Unexpected do_STAT failure:")
542             log.err(err)
543         return self._longOperation(d.addCallbacks(cbMessages, ebMessages))
544
545
546     def do_LIST(self, i=None):
547         if i is None:
548             d = defer.maybeDeferred(self.mbox.listMessages)
549             def cbMessages(msgs):
550                 return self._coiterate(formatListResponse(msgs))
551             def ebMessages(err):
552                 self.failResponse(err.getErrorMessage())
553                 log.msg("Unexpected do_LIST failure:")
554                 log.err(err)
555             return self._longOperation(d.addCallbacks(cbMessages, ebMessages))
556         else:
557             try:
558                 i = int(i)
559                 if i < 1:
560                     raise ValueError()
561             except ValueError:
562                 self.failResponse("Invalid message-number: %r" % (i,))
563             else:
564                 d = defer.maybeDeferred(self.mbox.listMessages, i - 1)
565                 def cbMessage(msg):
566                     self.successResponse('%d %d' % (i, msg))
567                 def ebMessage(err):
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
573                             # type.
574                             warnings.warn(
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,))
580                     else:
581                         self.failResponse(err.getErrorMessage())
582                         log.msg("Unexpected do_LIST failure:")
583                         log.err(err)
584                 return self._longOperation(d.addCallbacks(cbMessage, ebMessage))
585
586
587     def do_UIDL(self, i=None):
588         if i is None:
589             d = defer.maybeDeferred(self.mbox.listMessages)
590             def cbMessages(msgs):
591                 return self._coiterate(formatUIDListResponse(msgs, self.mbox.getUidl))
592             def ebMessages(err):
593                 self.failResponse(err.getErrorMessage())
594                 log.msg("Unexpected do_UIDL failure:")
595                 log.err(err)
596             return self._longOperation(d.addCallbacks(cbMessages, ebMessages))
597         else:
598             try:
599                 i = int(i)
600                 if i < 1:
601                     raise ValueError()
602             except ValueError:
603                 self.failResponse("Bad message number argument")
604             else:
605                 try:
606                     msg = self.mbox.getUidl(i - 1)
607                 except IndexError:
608                     # XXX TODO See above comment regarding IndexError.
609                     warnings.warn(
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")
615                 except ValueError:
616                     self.failResponse("Bad message number argument")
617                 else:
618                     self.successResponse(str(msg))
619
620
621     def _getMessageFile(self, i):
622         """
623         Retrieve the size and contents of a given message, as a two-tuple.
624
625         @param i: The number of the message to operate on.  This is a base-ten
626         string representation starting at 1.
627
628         @return: A Deferred which fires with a two-tuple of an integer and a
629         file-like object.
630         """
631         try:
632             msg = int(i) - 1
633             if msg < 0:
634                 raise ValueError()
635         except ValueError:
636             self.failResponse("Bad message number argument")
637             return defer.succeed(None)
638
639         sizeDeferred = defer.maybeDeferred(self.mbox.listMessages, msg)
640         def cbMessageSize(size):
641             if not size:
642                 return defer.fail(_POP3MessageDeleted())
643             fileDeferred = defer.maybeDeferred(self.mbox.getMessage, msg)
644             fileDeferred.addCallback(lambda fObj: (size, fObj))
645             return fileDeferred
646
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.
654                     warnings.warn(
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")
660             else:
661                 log.msg("Unexpected _getMessageFile failure:")
662                 log.err(err)
663             return None
664
665         sizeDeferred.addCallback(cbMessageSize)
666         sizeDeferred.addErrback(ebMessageSomething)
667         return sizeDeferred
668
669
670     def _sendMessageContent(self, i, fpWrapper, successResponse):
671         d = self._getMessageFile(i)
672         def cbMessageFile(info):
673             if info is None:
674                 # Some error occurred - a failure response has been sent
675                 # already, just give up.
676                 return
677
678             self._highest = max(self._highest, int(i))
679             resp, fp = info
680             fp = fpWrapper(fp)
681             self.successResponse(successResponse(resp))
682             s = basic.FileSender()
683             d = s.beginFileTransfer(fp, self.transport, self.transformChunk)
684
685             def cbFileTransfer(lastsent):
686                 if lastsent != '\n':
687                     line = '\r\n.'
688                 else:
689                     line = '.'
690                 self.sendLine(line)
691
692             def ebFileTransfer(err):
693                 self.transport.loseConnection()
694                 log.msg("Unexpected error in _sendMessageContent:")
695                 log.err(err)
696
697             d.addCallback(cbFileTransfer)
698             d.addErrback(ebFileTransfer)
699             return d
700         return self._longOperation(d.addCallback(cbMessageFile))
701
702
703     def do_TOP(self, i, size):
704         try:
705             size = int(size)
706             if size < 0:
707                 raise ValueError
708         except ValueError:
709             self.failResponse("Bad line count argument")
710         else:
711             return self._sendMessageContent(
712                 i,
713                 lambda fp: _HeadersPlusNLines(fp, size),
714                 lambda size: "Top of message follows")
715
716
717     def do_RETR(self, i):
718         return self._sendMessageContent(
719             i,
720             lambda fp: fp,
721             lambda size: "%d" % (size,))
722
723
724     def transformChunk(self, chunk):
725         return chunk.replace('\n', '\r\n').replace('\r\n.', '\r\n..')
726
727
728     def finishedFileTransfer(self, lastsent):
729         if lastsent != '\n':
730             line = '\r\n.'
731         else:
732             line = '.'
733         self.sendLine(line)
734
735
736     def do_DELE(self, i):
737         i = int(i)-1
738         self.mbox.deleteMessage(i)
739         self.successResponse()
740
741
742     def do_NOOP(self):
743         """Perform no operation.  Return a success code"""
744         self.successResponse()
745
746
747     def do_RSET(self):
748         """Unset all deleted message flags"""
749         try:
750             self.mbox.undeleteMessages()
751         except:
752             log.err()
753             self.failResponse()
754         else:
755             self._highest = 0
756             self.successResponse()
757
758
759     def do_LAST(self):
760         """
761         Return the index of the highest message yet downloaded.
762         """
763         self.successResponse(self._highest)
764
765
766     def do_RPOP(self, user):
767         self.failResponse('permission denied, sucker')
768
769
770     def do_QUIT(self):
771         if self.mbox:
772             self.mbox.sync()
773         self.successResponse()
774         self.transport.loseConnection()
775
776
777     def authenticateUserAPOP(self, user, digest):
778         """Perform authentication of an APOP login.
779
780         @type user: C{str}
781         @param user: The name of the user attempting to log in.
782
783         @type digest: C{str}
784         @param digest: The response string with which the user replied.
785
786         @rtype: C{Deferred}
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.
792         """
793         if self.portal is not None:
794             return self.portal.login(
795                 APOPCredentials(self.magic, user, digest),
796                 None,
797                 IMailbox
798             )
799         raise cred.error.UnauthorizedLogin()
800
801     def authenticateUserPASS(self, user, password):
802         """Perform authentication of a username/password login.
803
804         @type user: C{str}
805         @param user: The name of the user attempting to log in.
806
807         @type password: C{str}
808         @param password: The password to attempt to authenticate with.
809
810         @rtype: C{Deferred}
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.
816         """
817         if self.portal is not None:
818             return self.portal.login(
819                 cred.credentials.UsernamePassword(user, password),
820                 None,
821                 IMailbox
822             )
823         raise cred.error.UnauthorizedLogin()
824
825
826 class IServerFactory(Interface):
827     """Interface for querying additional parameters of this POP3 server.
828
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.
834
835     @ivar challengers: A dictionary mapping challenger names to classes
836     implementing C{IUsernameHashedPassword}.
837     """
838
839     def cap_IMPLEMENTATION():
840         """Return a string describing this POP3 server implementation."""
841
842     def cap_EXPIRE():
843         """Return the minimum number of days messages are retained."""
844
845     def perUserExpiration():
846         """Indicate whether message expiration is per-user.
847
848         @return: True if it is, false otherwise.
849         """
850
851     def cap_LOGIN_DELAY():
852         """Return the minimum number of seconds between client logins."""
853
854     def perUserLoginDelay():
855         """Indicate whether the login delay period is per-user.
856
857         @return: True if it is, false otherwise.
858         """
859
860 class IMailbox(Interface):
861     """
862     @type loginDelay: C{int}
863     @ivar loginDelay: The number of seconds between allowed logins for the
864     user associated with this mailbox.  None
865
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.
869     """
870
871     def listMessages(index=None):
872         """Retrieve the size of one or more messages.
873
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.
877
878         @rtype: C{int} or any iterable of C{int} or a L{Deferred} which fires
879         with one of these.
880
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.
884
885         @raise ValueError: if C{index} is greater than the index of any message
886         in the mailbox.
887         """
888
889     def getMessage(index):
890         """Retrieve a file-like object for a particular message.
891
892         @type index: C{int}
893         @param index: The number of the message to retrieve
894
895         @rtype: A file-like object
896         @return: A file containing the message data with lines delimited by
897         C{\\n}.
898         """
899
900     def getUidl(index):
901         """Get a unique identifier for a particular message.
902
903         @type index: C{int}
904         @param index: The number of the message for which to retrieve a UIDL
905
906         @rtype: C{str}
907         @return: A string of printable characters uniquely identifying for all
908         time the specified message.
909
910         @raise ValueError: if C{index} is greater than the index of any message
911         in the mailbox.
912         """
913
914     def deleteMessage(index):
915         """Delete a particular message.
916
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.
920
921         @type index: C{int}
922         @param index: The number of the message to delete.
923         """
924
925     def undeleteMessages():
926         """
927         Undelete any messages which have been marked for deletion since the
928         most recent L{sync} call.
929
930         Any message which can be undeleted should be returned to its
931         original position in the message sequence and retain its original
932         UID.
933         """
934
935     def sync():
936         """Perform checkpointing.
937
938         This method will be called to indicate the mailbox should attempt to
939         clean up any remaining deleted messages.
940         """
941
942
943
944 class Mailbox:
945     implements(IMailbox)
946
947     def listMessages(self, i=None):
948         return []
949     def getMessage(self, i):
950         raise ValueError
951     def getUidl(self, i):
952         raise ValueError
953     def deleteMessage(self, i):
954         raise ValueError
955     def undeleteMessages(self):
956         pass
957     def sync(self):
958         pass
959
960
961 NONE, SHORT, FIRST_LONG, LONG = range(4)
962
963 NEXT = {}
964 NEXT[NONE] = NONE
965 NEXT[SHORT] = NONE
966 NEXT[FIRST_LONG] = LONG
967 NEXT[LONG] = NONE
968
969 class POP3Client(basic.LineOnlyReceiver):
970
971     mode = SHORT
972     command = 'WELCOME'
973     import re
974     welcomeRe = re.compile('<(.*)>')
975
976     def __init__(self):
977         import warnings
978         warnings.warn("twisted.mail.pop3.POP3Client is deprecated, "
979                       "please use twisted.mail.pop3.AdvancedPOP3Client "
980                       "instead.", DeprecationWarning,
981                       stacklevel=3)
982
983     def sendShort(self, command, params=None):
984         if params is not None:
985             self.sendLine('%s %s' % (command, params))
986         else:
987             self.sendLine(command)
988         self.command = command
989         self.mode = SHORT
990
991     def sendLong(self, command, params):
992         if params:
993             self.sendLine('%s %s' % (command, params))
994         else:
995             self.sendLine(command)
996         self.command = command
997         self.mode = FIRST_LONG
998
999     def handle_default(self, line):
1000         if line[:-4] == '-ERR':
1001             self.mode = NONE
1002
1003     def handle_WELCOME(self, line):
1004         code, data = line.split(' ', 1)
1005         if code != '+OK':
1006             self.transport.loseConnection()
1007         else:
1008             m = self.welcomeRe.match(line)
1009             if m:
1010                 self.welcomeCode = m.group(1)
1011
1012     def _dispatch(self, command, default, *args):
1013         try:
1014             method = getattr(self, 'handle_'+command, default)
1015             if method is not None:
1016                 method(*args)
1017         except:
1018             log.err()
1019
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:
1025             if line == '.':
1026                 self.mode = NEXT[self.mode]
1027                 self._dispatch(self.command+'_end', None)
1028                 return
1029             if line[:1] == '.':
1030                 line = line[1:]
1031             self._dispatch(self.command+"_continue", None, line)
1032
1033     def apopAuthenticate(self, user, password, magic):
1034         digest = md5(magic + password).hexdigest()
1035         self.apop(user, digest)
1036
1037     def apop(self, user, digest):
1038         self.sendLong('APOP', ' '.join((user, digest)))
1039     def retr(self, i):
1040         self.sendLong('RETR', i)
1041     def dele(self, 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_)
1051     def quit(self):
1052         self.sendShort('QUIT')
1053
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
1059
1060 __all__ = [
1061     # Interfaces
1062     'IMailbox', 'IServerFactory',
1063
1064     # Exceptions
1065     'POP3Error', 'POP3ClientError', 'InsecureAuthenticationDisallowed',
1066     'ServerErrorResponse', 'LineTooLong',
1067
1068     # Protocol classes
1069     'POP3', 'POP3Client', 'AdvancedPOP3Client',
1070
1071     # Misc
1072     'APOPCredentials', 'Mailbox']
Note: See TracBrowser for help on using the browser.