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