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

« back to all changes in this revision

Viewing changes to twisted/protocols/pop3.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2004-06-21 22:01:11 UTC
  • mto: (2.2.3 sid)
  • mto: This revision was merged to the branch mainline in revision 3.
  • Revision ID: james.westby@ubuntu.com-20040621220111-vkf909euqnyrp3nr
Tags: upstream-1.3.0
ImportĀ upstreamĀ versionĀ 1.3.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
 
 
1
# -*- test-case-name: twisted.test.test_pop3 -*-
 
2
#
2
3
# Twisted, the Framework of Your Internet
3
4
# Copyright (C) 2001 Matthew W. Lefkowitz
4
5
#
16
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
18
 
18
19
"""Post-office Protocol version 3
 
20
 
 
21
@author U{Glyph Lefkowitz<mailto:glyph@twistedmatrix.com>}
 
22
@author U{Jp Calderone<mailto:exarkun@twistedmatrix.com>}
 
23
 
 
24
API Stability: Unstable
19
25
"""
20
26
 
 
27
import time
 
28
import string
 
29
import operator
 
30
import base64
 
31
import binascii
 
32
import md5
21
33
 
 
34
from twisted.protocols import smtp
22
35
from twisted.protocols import basic
23
 
import os, time, string, operator, stat, md5, binascii
24
 
import protocol
 
36
from twisted.protocols import policies
 
37
from twisted.internet import protocol
 
38
from twisted.internet import defer
 
39
from twisted.internet import interfaces
 
40
from twisted.python import components
 
41
from twisted.python import log
 
42
 
 
43
from twisted import cred
 
44
import twisted.cred.error
 
45
import twisted.cred.credentials
 
46
 
 
47
##
 
48
## Authentication
 
49
##
 
50
class APOPCredentials:
 
51
    __implements__ = (cred.credentials.IUsernamePassword,)
 
52
 
 
53
    def __init__(self, magic, username, digest):
 
54
        self.magic = magic
 
55
        self.username = username
 
56
        self.digest = digest
 
57
 
 
58
    def checkPassword(self, password):
 
59
        seed = self.magic + password
 
60
        my_digest = md5.new(seed).hexdigest()
 
61
        if my_digest == self.digest:
 
62
            return True
 
63
        return False
 
64
##
 
65
 
 
66
class _HeadersPlusNLines:
 
67
    def __init__(self, f, n):
 
68
        self.f = f
 
69
        self.n = n
 
70
        self.linecount = 0
 
71
        self.headers = 1
 
72
        self.done = 0
 
73
        self.buf = ''
 
74
 
 
75
    def read(self, bytes):
 
76
        if self.done:
 
77
            return ''
 
78
        data = self.f.read(bytes)
 
79
        if not data:
 
80
            return data
 
81
        if self.headers:
 
82
            df, sz = data.find('\r\n\r\n'), 4
 
83
            if df == -1:
 
84
                df, sz = data.find('\n\n'), 2
 
85
            if df!=-1:
 
86
                df += sz
 
87
                val = data[:df]
 
88
                data = data[df:]
 
89
                self.linecount = 1
 
90
                self.headers = 0
 
91
        else:
 
92
            val = ''
 
93
        if self.linecount > 0:
 
94
            dsplit = (self.buf+data).split('\n')
 
95
            self.buf = dsplit[-1]
 
96
            for ln in dsplit[:-1]:
 
97
                if self.linecount > self.n:
 
98
                    self.done = 1
 
99
                    return val
 
100
                val += (ln + '\n')
 
101
                self.linecount += 1
 
102
            return val
 
103
        else:
 
104
            return data
 
105
 
25
106
 
26
107
class POP3Error(Exception):
27
108
    pass
28
109
 
29
 
class POP3(basic.LineReceiver):
 
110
class POP3(basic.LineOnlyReceiver, policies.TimeoutMixin):
 
111
 
 
112
    __implements__ = (interfaces.IProducer,)
30
113
 
31
114
    magic = None
 
115
    _userIs = None
 
116
    _onLogout = None
 
117
    highest = 0
 
118
 
 
119
    AUTH_CMDS = ['CAPA', 'USER', 'PASS', 'APOP', 'AUTH', 'RPOP', 'QUIT']
 
120
 
 
121
    # A reference to the newcred Portal instance we will authenticate
 
122
    # through.
 
123
    portal = None
 
124
 
 
125
    # Who created us
 
126
    factory = None
 
127
 
 
128
    # The mailbox we're serving
 
129
    mbox = None
 
130
 
 
131
    # Set this pretty low -- POP3 clients are expected to log in, download
 
132
    # everything, and log out.
 
133
    timeOut = 300
 
134
 
 
135
    # Current protocol state
 
136
    state = "COMMAND"
 
137
 
 
138
    # PIPELINE
 
139
    blocked = None
32
140
 
33
141
    def connectionMade(self):
34
142
        if self.magic is None:
35
 
            self.magic = '<%s>' % time.time()
36
 
        self.mbox = None
 
143
            self.magic = self.generateMagic()
37
144
        self.successResponse(self.magic)
 
145
        self.setTimeout(self.timeOut)
 
146
        log.msg("New connection from " + str(self.transport.getPeer()))
 
147
 
 
148
    def connectionLost(self, reason):
 
149
        if self._onLogout is not None:
 
150
            self._onLogout()
 
151
            self._onLogout = None
 
152
        self.setTimeout(None)
 
153
 
 
154
    def generateMagic(self):
 
155
        return smtp.messageid()
38
156
 
39
157
    def successResponse(self, message=''):
40
 
        self.transport.write('+OK %s\r\n' % message)
 
158
        self.sendLine('+OK ' + str(message))
41
159
 
42
160
    def failResponse(self, message=''):
43
 
        self.transport.write('-ERR %s\r\n' % message)
 
161
        self.sendLine('-ERR ' + str(message))
 
162
 
 
163
#    def sendLine(self, line):
 
164
#        print 'S:', repr(line)
 
165
#        basic.LineOnlyReceiver.sendLine(self, line)
44
166
 
45
167
    def lineReceived(self, line):
 
168
#        print 'C:', repr(line)
 
169
        self.resetTimeout()
 
170
        getattr(self, 'state_' + self.state)(line)
 
171
 
 
172
    def _unblock(self, _):
 
173
        commands = self.blocked
 
174
        self.blocked = None
 
175
        while commands and self.blocked is None:
 
176
            cmd, args = commands.pop(0)
 
177
            self.processCommand(cmd, *args)
 
178
        if self.blocked is not None:
 
179
            self.blocked.extend(commands)
 
180
 
 
181
    def state_COMMAND(self, line):
46
182
        try:
47
 
            return apply(self.processCommand, tuple(string.split(line)))
48
 
        except (ValueError, AttributeError, POP3Error), e:
 
183
            return self.processCommand(*line.split())
 
184
        except (ValueError, AttributeError, POP3Error, TypeError), e:
 
185
            log.err()
49
186
            self.failResponse('bad protocol or server: %s: %s' % (e.__class__.__name__, e))
50
187
 
51
188
    def processCommand(self, command, *args):
 
189
        if self.blocked is not None:
 
190
            self.blocked.append((command, args))
 
191
            return
 
192
 
52
193
        command = string.upper(command)
53
 
        if self.mbox is None and command != 'APOP':
54
 
            raise POP3Error("not authenticated yet: cannot do %s" % command)
55
 
        return apply(getattr(self, 'do_'+command), args)
 
194
        authCmd = command in self.AUTH_CMDS
 
195
        if not self.mbox and not authCmd:
 
196
            raise POP3Error("not authenticated yet: cannot do " + command)
 
197
        f = getattr(self, 'do_' + command, None)
 
198
        if f:
 
199
            return f(*args)
 
200
        raise POP3Error("Unknown protocol command: " + command)
 
201
 
 
202
 
 
203
    def listCapabilities(self):
 
204
        baseCaps = [
 
205
            "TOP",
 
206
            "USER",
 
207
            "UIDL",
 
208
            "PIPELINE",
 
209
            "CELERITY",
 
210
            "AUSPEX",
 
211
            "POTENCE",
 
212
        ]
 
213
 
 
214
        if components.implements(self.factory, IServerFactory):
 
215
            # Oh my god.  We can't just loop over a list of these because
 
216
            # each has spectacularly different return value semantics!
 
217
            try:
 
218
                v = self.factory.cap_IMPLEMENTATION()
 
219
            except NotImplementedError:
 
220
                pass
 
221
            except:
 
222
                log.err()
 
223
            else:
 
224
                baseCaps.append("IMPLEMENTATION " + str(v))
 
225
 
 
226
            try:
 
227
                v = self.factory.cap_EXPIRE()
 
228
            except NotImplementedError:
 
229
                pass
 
230
            except:
 
231
                log.err()
 
232
            else:
 
233
                if v is None:
 
234
                    v = "NEVER"
 
235
                if self.factory.perUserExpiration():
 
236
                    if self.mbox:
 
237
                        v = str(self.mbox.messageExpiration)
 
238
                    else:
 
239
                        v = str(v) + " USER"
 
240
                v = str(v)
 
241
                baseCaps.append("EXPIRE " + v)
 
242
 
 
243
            try:
 
244
                v = self.factory.cap_LOGIN_DELAY()
 
245
            except NotImplementedError:
 
246
                pass
 
247
            except:
 
248
                log.err()
 
249
            else:
 
250
                if self.factory.perUserLoginDelay():
 
251
                    if self.mbox:
 
252
                        v = str(self.mbox.loginDelay)
 
253
                    else:
 
254
                        v = str(v) + " USER"
 
255
                v = str(v)
 
256
                baseCaps.append("LOGIN-DELAY " + v)
 
257
 
 
258
            try:
 
259
                v = self.factory.challengers
 
260
            except AttributeError:
 
261
                pass
 
262
            except:
 
263
                log.err()
 
264
            else:
 
265
                baseCaps.append("SASL " + ' '.join(v.keys()))
 
266
        return baseCaps
 
267
 
 
268
    def do_CAPA(self):
 
269
        self.successResponse("I can do the following:")
 
270
        for cap in self.listCapabilities():
 
271
            self.sendLine(cap)
 
272
        self.sendLine(".")
 
273
 
 
274
    def do_AUTH(self, args=None):
 
275
        if not getattr(self.factory, 'challengers', None):
 
276
            self.failResponse("AUTH extension unsupported")
 
277
            return
 
278
 
 
279
        if args is None:
 
280
            self.successResponse("Supported authentication methods:")
 
281
            for a in self.factory.challengers:
 
282
                self.sendLine(a.upper())
 
283
            self.sendLine(".")
 
284
            return
 
285
 
 
286
        auth = self.factory.challengers.get(args.strip().upper())
 
287
        if not self.portal or not auth:
 
288
            self.failResponse("Unsupported SASL selected")
 
289
            return
 
290
 
 
291
        self._auth = auth()
 
292
        chal = self._auth.getChallenge()
 
293
 
 
294
        self.sendLine('+ ' + base64.encodestring(chal).rstrip('\n'))
 
295
        self.state = 'AUTH'
 
296
 
 
297
    def state_AUTH(self, line):
 
298
        self.state = "COMMAND"
 
299
        try:
 
300
            parts = base64.decodestring(line).split(None, 1)
 
301
        except binascii.Error:
 
302
            self.failResponse("Invalid BASE64 encoding")
 
303
        else:
 
304
            if len(parts) != 2:
 
305
                self.failResponse("Invalid AUTH response")
 
306
                return
 
307
            self._auth.username = parts[0]
 
308
            self._auth.response = parts[1]
 
309
            d = self.portal.login(self._auth, None, IMailbox)
 
310
            d.addCallback(self._cbMailbox, parts[0])
 
311
            d.addErrback(self._ebMailbox)
 
312
            d.addErrback(self._ebUnexpected)
56
313
 
57
314
    def do_APOP(self, user, digest):
58
 
        self.mbox = self.authenticateUserAPOP(user, digest)
59
 
        self.successResponse()
 
315
        d = defer.maybeDeferred(self.authenticateUserAPOP, user, digest)
 
316
        d.addCallbacks(self._cbMailbox, self._ebMailbox, callbackArgs=(user,)
 
317
        ).addErrback(self._ebUnexpected)
 
318
 
 
319
    def _cbMailbox(self, (interface, avatar, logout), user):
 
320
        if interface is not IMailbox:
 
321
            self.failResponse('Authentication failed')
 
322
            log.err("_cbMailbox() called with an interface other than IMailbox")
 
323
            return
 
324
 
 
325
        self.mbox = avatar
 
326
        self._onLogout = logout
 
327
        self.successResponse('Authentication succeeded')
 
328
        log.msg("Authenticated login for " + user)
 
329
 
 
330
    def _ebMailbox(self, failure):
 
331
        failure = failure.trap(cred.error.LoginDenied, cred.error.LoginFailed)
 
332
        if issubclass(failure, cred.error.LoginDenied):
 
333
            self.failResponse("Access denied: " + str(failure))
 
334
        elif issubclass(failure, cred.error.LoginFailed):
 
335
            self.failResponse('Authentication failed')
 
336
        log.msg("Denied login attempt from " + str(self.transport.getPeer()))
 
337
 
 
338
    def _ebUnexpected(self, failure):
 
339
        self.failResponse('Server error: ' + failure.getErrorMessage())
 
340
        log.err(failure)
 
341
 
 
342
    def do_USER(self, user):
 
343
        self._userIs = user
 
344
        self.successResponse('USER accepted, send PASS')
 
345
 
 
346
    def do_PASS(self, password):
 
347
        if self._userIs is None:
 
348
            self.failResponse("USER required before PASS")
 
349
            return
 
350
        user = self._userIs
 
351
        self._userIs = None
 
352
        d = defer.maybeDeferred(self.authenticateUserPASS, user, password)
 
353
        d.addCallbacks(self._cbMailbox, self._ebMailbox, callbackArgs=(user,)
 
354
        ).addErrback(self._ebUnexpected)
 
355
 
 
356
    def do_STAT(self):
 
357
        i = 0
 
358
        sum = 0
 
359
        msg = self.mbox.listMessages()
 
360
        for e in msg:
 
361
            i += 1
 
362
            sum += e
 
363
        self.successResponse('%d %d' % (i, sum))
60
364
 
61
365
    def do_LIST(self, i=None):
62
 
        messages = self.mbox.listMessages()
63
 
        total = reduce(operator.add, messages, 0)
64
 
        self.successResponse(len(messages))
65
 
        i = 1
66
 
        for message in messages:
67
 
            if message:
68
 
                self.transport.write('%d %d\r\n' % (i, message))
69
 
            i = i+1
70
 
        self.transport.write('.\r\n')
 
366
        if i is None:
 
367
            messages = self.mbox.listMessages()
 
368
            lines = []
 
369
            for msg in messages:
 
370
                lines.append('%d %d%s' % (len(lines) + 1, msg, self.delimiter))
 
371
            self.successResponse(len(lines))
 
372
            self.transport.writeSequence(lines)
 
373
            self.sendLine('.')
 
374
        else:
 
375
            msg = self.mbox.listMessages(int(i) - 1)
 
376
            self.successResponse(str(msg))
71
377
 
72
378
    def do_UIDL(self, i=None):
73
 
        messages = self.mbox.listMessages()
74
 
        self.successResponse()
75
 
        for i in range(len(messages)):
76
 
            if messages[i]:
77
 
                self.transport.write('%d %s\r\n' % (i+1, self.mbox.getUidl(i)))
78
 
        self.transport.write('.\r\n')
 
379
        if i is None:
 
380
            messages = self.mbox.listMessages()
 
381
            self.successResponse()
 
382
            i = 0
 
383
            lines = []
 
384
            for msg in messages:
 
385
                if msg:
 
386
                    uid = self.mbox.getUidl(i)
 
387
                    lines.append('%d %s%s' % (i + 1, uid, self.delimiter))
 
388
                i += 1
 
389
            self.transport.writeSequence(lines)
 
390
            self.sendLine('.')
 
391
        else:
 
392
            msg = self.mbox.getUidl(int(i) - 1)
 
393
            self.successResponse(str(msg))
79
394
 
80
395
    def getMessageFile(self, i):
81
 
        i = int(i)-1
82
 
        list = self.mbox.listMessages()
 
396
        i = int(i) - 1
83
397
        try:
84
 
            resp = list[i]
85
 
        except IndexError:
 
398
            resp = self.mbox.listMessages(i)
 
399
        except (IndexError, ValueError), e:
86
400
            self.failResponse('index out of range')
87
401
            return None, None
88
402
        if not resp:
91
405
        return resp, self.mbox.getMessage(i)
92
406
 
93
407
    def do_TOP(self, i, size):
 
408
        self.highest = max(self.highest, i)
94
409
        resp, fp = self.getMessageFile(i)
95
410
        if not fp:
96
411
            return
97
 
        size = max(int(size), resp)
98
 
        self.successResponse(size)
99
 
        while size:
100
 
            line = fp.readline()
101
 
            if not line:
102
 
                break
103
 
            if line[-1] == '\n':
104
 
                line = line[:-1]
105
 
            if line[:1] == '.':
106
 
                line = '.'+line
107
 
            self.transport.write(line[:size]+'\r\n')
108
 
            size = size-len(line[:size])
109
 
 
 
412
        size = int(size)
 
413
        fp = _HeadersPlusNLines(fp, size)
 
414
        self.successResponse("Top of message follows")
 
415
        s = basic.FileSender()
 
416
        self.blocked = []
 
417
        s.beginFileTransfer(fp, self.transport, self.transformChunk
 
418
            ).addCallback(self.finishedFileTransfer
 
419
            ).addCallback(self._unblock
 
420
            ).addErrback(log.err
 
421
            )
110
422
 
111
423
    def do_RETR(self, i):
 
424
        self.highest = max(self.highest, i)
112
425
        resp, fp = self.getMessageFile(i)
113
426
        if not fp:
114
427
            return
115
428
        self.successResponse(resp)
116
 
        while 1:
117
 
            line = fp.readline()
118
 
            if not line:
119
 
                break
120
 
            if line[-1] == '\n':
121
 
                line = line[:-1]
122
 
            if line[:1] == '.':
123
 
                line = '.'+line
124
 
            self.transport.write(line+'\r\n')
125
 
        self.transport.write('.\r\n')
 
429
        s = basic.FileSender()
 
430
        self.blocked = []
 
431
        s.beginFileTransfer(fp, self.transport, self.transformChunk
 
432
            ).addCallback(self.finishedFileTransfer
 
433
            ).addCallback(self._unblock
 
434
            ).addErrback(log.err
 
435
            )
 
436
 
 
437
    def transformChunk(self, chunk):
 
438
        return chunk.replace('\n', '\r\n').replace('\r\n.', '\r\n..')
 
439
 
 
440
    def finishedFileTransfer(self, lastsent):
 
441
        if lastsent != '\n':
 
442
            line = '\r\n.'
 
443
        else:
 
444
            line = '.'
 
445
        self.sendLine(line)
126
446
 
127
447
    def do_DELE(self, i):
128
448
        i = int(i)-1
129
449
        self.mbox.deleteMessage(i)
130
450
        self.successResponse()
131
451
 
 
452
    def do_NOOP(self):
 
453
        """Perform no operation.  Return a success code"""
 
454
        self.successResponse()
 
455
 
 
456
    def do_RSET(self):
 
457
        """Unset all deleted message flags"""
 
458
        try:
 
459
            self.mbox.undeleteMessages()
 
460
        except:
 
461
            log.err()
 
462
            self.failResponse()
 
463
        else:
 
464
            self.highest = 1
 
465
            self.successResponse()
 
466
 
 
467
    def do_LAST(self):
 
468
        """Respond with the highest message access thus far"""
 
469
        # omg this is such a retarded protocol
 
470
        self.successResponse(self.highest)
 
471
 
 
472
    def do_RPOP(self, user):
 
473
        self.failResponse('permission denied, sucker')
 
474
 
132
475
    def do_QUIT(self):
133
 
        self.mbox.sync()
 
476
        if self.mbox:
 
477
            self.mbox.sync()
134
478
        self.successResponse()
135
479
        self.transport.loseConnection()
136
480
 
137
481
    def authenticateUserAPOP(self, user, digest):
138
 
        return Mailbox()
139
 
 
 
482
        """Perform authentication of an APOP login.
 
483
 
 
484
        @type user: C{str}
 
485
        @param user: The name of the user attempting to log in.
 
486
 
 
487
        @type digest: C{str}
 
488
        @param digest: The response string with which the user replied.
 
489
 
 
490
        @rtype: C{Deferred}
 
491
        @return: A deferred whose callback is invoked if the login is
 
492
        successful, and whose errback will be invoked otherwise.  The
 
493
        callback will be passed a 3-tuple consisting of IMailbox,
 
494
        an object implementing IMailbox, and a zero-argument callable
 
495
        to be invoked when this session is terminated.
 
496
        """
 
497
        if self.portal is not None:
 
498
            return self.portal.login(
 
499
                APOPCredentials(self.magic, user, digest),
 
500
                None,
 
501
                IMailbox
 
502
            )
 
503
        raise cred.error.UnauthorizedLogin()
 
504
 
 
505
    def authenticateUserPASS(self, user, password):
 
506
        """Perform authentication of a username/password login.
 
507
 
 
508
        @type user: C{str}
 
509
        @param user: The name of the user attempting to log in.
 
510
 
 
511
        @type password: C{str}
 
512
        @param password: The password to attempt to authenticate with.
 
513
 
 
514
        @rtype: C{Deferred}
 
515
        @return: A deferred whose callback is invoked if the login is
 
516
        successful, and whose errback will be invoked otherwise.  The
 
517
        callback will be passed a 3-tuple consisting of IMailbox,
 
518
        an object implementing IMailbox, and a zero-argument callable
 
519
        to be invoked when this session is terminated.
 
520
        """
 
521
        if self.portal is not None:
 
522
            return self.portal.login(
 
523
                cred.credentials.UsernamePassword(user, password),
 
524
                None,
 
525
                IMailbox
 
526
            )
 
527
        raise cred.error.UnauthorizedLogin()
 
528
 
 
529
class IServerFactory(components.Interface):
 
530
    """Interface for querying additional parameters of this POP3 server.
 
531
 
 
532
    Any cap_* method may raise NotImplementedError if the particular
 
533
    capability is not supported.  If cap_EXPIRE() does not raise
 
534
    NotImplementedError, perUserExpiration() must be implemented, otherwise
 
535
    they are optional.  If cap_LOGIN_DELAY() is implemented,
 
536
    perUserLoginDelay() must be implemented, otherwise they are optional.
 
537
 
 
538
    @ivar challengers: A dictionary mapping challenger names to classes
 
539
    implementing C{IUsernameHashedPassword}.
 
540
    """
 
541
 
 
542
    def cap_IMPLEMENTATION(self):
 
543
        """Return a string describing this POP3 server implementation."""
 
544
 
 
545
    def cap_EXPIRE(self):
 
546
        """Return the minimum number of days messages are retained."""
 
547
 
 
548
    def perUserExpiration(self):
 
549
        """Indicate whether message expiration is per-user.
 
550
 
 
551
        @return: True if it is, false otherwise.
 
552
        """
 
553
 
 
554
    def cap_LOGIN_DELAY(self):
 
555
        """Return the minimum number of seconds between client logins."""
 
556
 
 
557
    def perUserLoginDelay(self):
 
558
        """Indicate whether the login delay period is per-user.
 
559
 
 
560
        @return: True if it is, false otherwise.
 
561
        """
 
562
 
 
563
class IMailbox(components.Interface):
 
564
    """
 
565
    @type loginDelay: C{int}
 
566
    @ivar loginDelay: The number of seconds between allowed logins for the
 
567
    user associated with this mailbox.  None
 
568
 
 
569
    @type messageExpiration: C{int}
 
570
    @ivar messageExpiration: The number of days messages in this mailbox will
 
571
    remain on the server before being deleted.
 
572
    """
 
573
 
 
574
    def listMessages(self, index=None):
 
575
        """Retrieve the size of one or more messages.
 
576
 
 
577
        @type index: C{int} or C{None}
 
578
        @param index: The number of the message for which to retrieve the
 
579
        size (starting at 0), or None to retrieve the size of all messages.
 
580
 
 
581
        @rtype: C{int} or any iterable of C{int}
 
582
        @return: The number of octets in the specified message, or an
 
583
        iterable of integers representing the number of octets in all the
 
584
        messages.
 
585
        """
 
586
 
 
587
    def getMessage(self, index):
 
588
        """Retrieve a file-like object for a particular message.
 
589
 
 
590
        @type index: C{int}
 
591
        @param index: The number of the message to retrieve
 
592
 
 
593
        @rtype: A file-like object
 
594
        """
 
595
 
 
596
    def getUidl(self, index):
 
597
        """Get a unique identifier for a particular message.
 
598
 
 
599
        @type index: C{int}
 
600
        @param index: The number of the message for which to retrieve a UIDL
 
601
 
 
602
        @rtype: C{str}
 
603
        @return: A string of printable characters uniquely identifying for all
 
604
        time the specified message.
 
605
        """
 
606
 
 
607
    def deleteMessage(self, index):
 
608
        """Delete a particular message.
 
609
 
 
610
        This must not change the number of messages in this mailbox.  Further
 
611
        requests for the size of deleted messages should return 0.  Further
 
612
        requests for the message itself may raise an exception.
 
613
 
 
614
        @type index: C{int}
 
615
        @param index: The number of the message to delete.
 
616
        """
 
617
 
 
618
    def undeleteMessages(self):
 
619
        """Undelete any messages possible.
 
620
 
 
621
        If a message can be deleted it, it should return it its original
 
622
        position in the message sequence and retain the same UIDL.
 
623
        """
 
624
 
 
625
    def sync(self):
 
626
        """Perform checkpointing.
 
627
 
 
628
        This method will be called to indicate the mailbox should attempt to
 
629
        clean up any remaining deleted messages.
 
630
        """
140
631
 
141
632
class Mailbox:
 
633
    __implements__ = (IMailbox,)
142
634
 
143
 
    def listMessages(self):
 
635
    def listMessages(self, i=None):
144
636
        return []
145
637
    def getMessage(self, i):
146
638
        raise ValueError
148
640
        raise ValueError
149
641
    def deleteMessage(self, i):
150
642
        raise ValueError
 
643
    def undeleteMessages(self):
 
644
        pass
151
645
    def sync(self):
152
646
        pass
153
647
 
154
 
 
155
648
NONE, SHORT, FIRST_LONG, LONG = range(4)
156
649
 
157
650
NEXT = {}
160
653
NEXT[FIRST_LONG] = LONG
161
654
NEXT[LONG] = NONE
162
655
 
163
 
class POP3Client(basic.LineReceiver):
 
656
class POP3Client(basic.LineOnlyReceiver):
164
657
 
165
658
    mode = SHORT
166
659
    command = 'WELCOME'
168
661
    welcomeRe = re.compile('<(.*)>')
169
662
 
170
663
    def sendShort(self, command, params):
171
 
        self.transport.write('%s %s\r\n' % (command, params))
 
664
        self.sendLine('%s %s' % (command, params))
172
665
        self.command = command
173
666
        self.mode = SHORT
174
667
 
175
668
    def sendLong(self, command, params):
176
 
        self.transport.write('%s %s\r\n' % (command, params))
 
669
        if params:
 
670
            self.sendLine('%s %s' % (command, params))
 
671
        else:
 
672
            self.sendLine(command)
177
673
        self.command = command
178
674
        self.mode = FIRST_LONG
179
675
 
182
678
            self.mode = NONE
183
679
 
184
680
    def handle_WELCOME(self, line):
185
 
        code, data = string.split(line)
 
681
        code, data = line.split(' ', 1)
186
682
        if code != '+OK':
187
683
            self.transport.loseConnection()
188
684
        else:
190
686
            if m:
191
687
                self.welcomeCode = m.group(1)
192
688
 
 
689
    def _dispatch(self, command, default, *args):
 
690
        try:
 
691
            method = getattr(self, 'handle_'+command, default)
 
692
            if method is not None:
 
693
                method(*args)
 
694
        except:
 
695
            log.err()
 
696
 
193
697
    def lineReceived(self, line):
194
698
        if self.mode == SHORT or self.mode == FIRST_LONG:
195
699
            self.mode = NEXT[self.mode]
196
 
            method = getattr(self, 'handle_'+self.command, self.handle_default)
197
 
            method(line)
 
700
            self._dispatch(self.command, self.handle_default, line)
198
701
        elif self.mode == LONG:
199
702
            if line == '.':
200
703
                self.mode = NEXT[self.mode]
201
 
                method = getattr(self, 'handle_'+self.command+'_end', None)
202
 
                if method is not None:
203
 
                    method()
 
704
                self._dispatch(self.command+'_end', None)
204
705
                return
205
706
            if line[:1] == '.':
206
707
                line = line[1:]
207
 
            method = getattr(self, 'handle_'+self.command+'_continue', None)
208
 
            if method is not None:
209
 
                method(line)
 
708
            self._dispatch(self.command+"_continue", None, line)
210
709
 
211
 
    def apopAuthenticate(self, user, password):
212
 
        digest = md5.new(magic+password).digest()
213
 
        digest = string.join(map(lambda x: "%02x"%ord(x), digest), '')
 
710
    def apopAuthenticate(self, user, password, magic):
 
711
        digest = md5.new(magic + password).hexdigest()
214
712
        self.apop(user, digest)
215
713
 
216
714
    def apop(self, user, digest):
217
 
        self.sendLong('APOP', user+' '+digest)
 
715
        self.sendLong('APOP', ' '.join((user, digest)))
218
716
    def retr(self, i):
219
717
        self.sendLong('RETR', i)
220
718
    def dele(self, i):
229
727
        self.sendShort('PASS', pass_)
230
728
    def quit(self):
231
729
        self.sendShort('QUIT', '')
232
 
 
233
 
 
234
 
class VirtualPOP3(POP3):
235
 
 
236
 
    domainSpecifier = '@' # Gaagh! I hate POP3. No standardized way
237
 
                          # to indicate user@host. '@' doesn't work
238
 
                          # with NS, e.g.
239
 
    def authenticateUserAPOP(self, user, digest):
240
 
        try:
241
 
            user, domain = string.split(user, self.domainSpecifier, 1)
242
 
        except ValueError:
243
 
            domain = ''
244
 
        if not self.factory.domains.has_key(domain):
245
 
             raise POP3Error("no such domain %s" % domain)
246
 
        domain = self.factory.domains[domain]
247
 
        mbox = domain.authenticateUserAPOP(user, self.magic, digest, domain)
248
 
        if mbox is None:
249
 
            raise POP3Error("bad authentication")
250
 
        return mbox