~cbehrens/nova/lp844160-build-works-with-zones

« back to all changes in this revision

Viewing changes to vendor/Twisted-10.0.0/twisted/mail/pop3.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

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-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']