~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

Viewing changes to twisted/mail/pop3.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2007-01-17 14:52:35 UTC
  • mfrom: (1.1.5 upstream) (2.1.2 etch)
  • Revision ID: james.westby@ubuntu.com-20070117145235-btmig6qfmqfen0om
Tags: 2.5.0-0ubuntu1
New upstream version, compatible with python2.5.

Show diffs side-by-side

added added

removed removed

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