~divmod-dev/divmod.org/imap-server-440

« back to all changes in this revision

Viewing changes to Quotient/xquotient/imapout.py

  • Committer: glyph
  • Date: 2006-01-31 19:39:28 UTC
  • Revision ID: svn-v4:866e43f7-fbfc-0310-8f2a-ec88d1da2979:branches/imap-server-440:4391
factored out of mailservers branch

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
 
 
2
from OpenSSL import SSL
 
3
 
 
4
from zope.interface import implements
 
5
 
 
6
from twisted.mail import imap4
 
7
 
 
8
from twisted.internet.protocol import Factory
 
9
from twisted.internet import defer
 
10
 
 
11
from epsilon.extime import Time
 
12
 
 
13
from axiom import item, attributes
 
14
 
 
15
from xquotient import exmess
 
16
 
 
17
from twisted.protocols import policies
 
18
 
 
19
from twisted.cred import portal, checkers
 
20
 
 
21
from vertex import sslverify
 
22
 
 
23
from twisted.internet import reactor
 
24
 
 
25
from twisted.application import service
 
26
 
 
27
 
 
28
class MailConfigurationError(Exception):
 
29
    pass
 
30
 
 
31
class IMAP4PartInfo:
 
32
    implements(imap4.IMessagePart)
 
33
 
 
34
    def __init__(self, part):
 
35
        self.part = part
 
36
 
 
37
    def getHeaders(self, negate, *names):
 
38
        result = {}
 
39
        hdrs = self.part.getAllHeaders()
 
40
        for hdr in hdrs:
 
41
            if (hdr.name in names) ^ negate:
 
42
                result[hdr.name] = hdr.value
 
43
        return result
 
44
 
 
45
    def getBodyFile(self):
 
46
        """Retrieve a file object containing only the body of this message.
 
47
        """
 
48
        f = self.part.source.open()
 
49
        f.seek(self.part.bodyOffset)
 
50
        return f
 
51
 
 
52
    def getSize(self):
 
53
        """Retrieve the total size, in octets, of this message.
 
54
 
 
55
        @rtype: C{int}
 
56
        """
 
57
        return self.part.source.getsize()
 
58
 
 
59
    def isMultipart(self):
 
60
        from xquotient.mimestorage import Part
 
61
        return bool(self.part.store.query(Part, Part.parent == self.part).count())
 
62
 
 
63
    def getSubPart(self, part):
 
64
        return IMAP4PartInfo(self.part.getSubPart(part))
 
65
 
 
66
 
 
67
class IMAP4MessageInfo(item.Item):
 
68
    implements(imap4.IMessage)
 
69
 
 
70
    message = attributes.reference(allowNone=False)
 
71
 
 
72
    imapUID = attributes.integer(allowNone=False)
 
73
    imapMailbox = attributes.reference(allowNone=False)
 
74
    imapSequenceNumber = attributes.integer(allowNone=False)
 
75
 
 
76
    _rootPart = attributes.inmemory()
 
77
 
 
78
    def activate(self):
 
79
        self._rootPart = None
 
80
 
 
81
    def _getRootPart(self):
 
82
        if self._rootPart is None:
 
83
            self._rootPart = IMAP4PartInfo(self.message.impl)
 
84
        return self._rootPart
 
85
 
 
86
    rootPart = property(_getRootPart)
 
87
 
 
88
    # Just messages
 
89
 
 
90
    def getUID(self):
 
91
        """Retrieve the unique identifier associated with this message.
 
92
        """
 
93
 
 
94
    def getFlags(self):
 
95
        """Retrieve the flags associated with this message.
 
96
 
 
97
        @rtype: C{iterable}
 
98
        @return: The flags, represented as strings.
 
99
        """
 
100
        return ['']
 
101
 
 
102
    def getInternalDate(self):
 
103
        return self.message.received.asRFC2822()
 
104
 
 
105
    # All parts
 
106
 
 
107
    def getHeaders(self, negate, *names):
 
108
        return self.rootPart.getHeaders(negate, *names)
 
109
 
 
110
    def getBodyFile(self):
 
111
        return self.rootPart.getBodyFile()
 
112
 
 
113
    def getSize(self):
 
114
        return self.rootPart.getSize()
 
115
 
 
116
    def isMultipart(self):
 
117
        return self.rootPart.isMultipart()
 
118
 
 
119
    def getSubPart(self, part):
 
120
        return self.rootPart.getSubPart(part)
 
121
 
 
122
 
 
123
class IMAP4MailboxImpl:
 
124
    def __init__(self, mboxitem):
 
125
        self.mbox = mboxitem
 
126
 
 
127
    def getHierarchicalDelimiter(self):
 
128
        return '/'
 
129
 
 
130
 
 
131
    def getUIDValidity(self):
 
132
        return int(self.mbox.uidValidity.asPOSIXTimestamp())
 
133
 
 
134
 
 
135
    def getUIDNext(self):
 
136
        """Return the likely UID for the next message added to this mailbox.
 
137
 
 
138
        @rtype: C{int}
 
139
        """
 
140
        return self.mbox.uidCounter + 1
 
141
 
 
142
    def getUID(self, message):
 
143
        """Return the UID of a message in the mailbox
 
144
 
 
145
        @type message: C{int}
 
146
        @param message: The message sequence number
 
147
 
 
148
        @rtype: C{int}
 
149
        @return: The UID of the message.
 
150
        """
 
151
        return self.mbox.store.findUnique(
 
152
            IMAP4MessageInfo,
 
153
            attributes.AND(IMAP4MessageInfo.imapMailbox == self.mbox,
 
154
                           IMAP4MessageInfo.imapSequenceNumber == message)).imapUID
 
155
 
 
156
    def getMessageCount(self):
 
157
        """Return the number of messages in this mailbox.
 
158
 
 
159
        @rtype: C{int}
 
160
        """
 
161
        return self.mbox.store.query(IMAP4MessageInfo,
 
162
                                     IMAP4MessageInfo.imapMailbox == self.mbox)
 
163
 
 
164
    def getRecentCount(self):
 
165
        """Return the number of messages with the 'Recent' flag.
 
166
 
 
167
        @rtype: C{int}
 
168
        """
 
169
        # what's 'recent'?
 
170
        return 0
 
171
 
 
172
    def getUnseenCount(self):
 
173
        """Return the number of messages with the 'Unseen' flag.
 
174
 
 
175
        @rtype: C{int}
 
176
        """
 
177
        return self.mbox.store.query(
 
178
            IMAP4MessageInfo,
 
179
            attributes.AND(IMAP4MessageInfo.imapMailbox == self.mbox,
 
180
                           IMAP4MessageInfo.message == exmess.Message.storeID,
 
181
                           exmess.Message.read == False))
 
182
 
 
183
    def isWriteable(self):
 
184
        return True
 
185
 
 
186
    def destroy(self):
 
187
        """Called before this mailbox is deleted, permanently.
 
188
 
 
189
        If necessary, all resources held by this mailbox should be cleaned
 
190
        up here.  This function _must_ set the \\Noselect flag on this
 
191
        mailbox.
 
192
        """
 
193
 
 
194
    def requestStatus(self, names):
 
195
        return imap4.statusRequestHelper(self, names)
 
196
 
 
197
    def addListener(self, listener):
 
198
        pass
 
199
 
 
200
    def removeListener(self, listener):
 
201
        pass
 
202
 
 
203
    def addMessage(self, message, flags = (), date = None):
 
204
        """Add the given message to this mailbox.
 
205
 
 
206
        @type message: A file-like object
 
207
        @param message: The RFC822 formatted message
 
208
 
 
209
        @type flags: Any iterable of C{str}
 
210
        @param flags: The flags to associate with this message
 
211
 
 
212
        @type date: C{str}
 
213
        @param date: If specified, the date to associate with this
 
214
        message.
 
215
 
 
216
        @rtype: C{Deferred}
 
217
        @return: A deferred whose callback is invoked with the message
 
218
        id if the message is added successfully and whose errback is
 
219
        invoked otherwise.
 
220
 
 
221
        @raise ReadOnlyMailbox: Raised if this Mailbox is not open for
 
222
        read-write.
 
223
        """
 
224
        return defer.fail(RuntimeError("adding messages not supported"))
 
225
 
 
226
    def expunge(self):
 
227
        """Remove all messages flagged \\Deleted.
 
228
 
 
229
        @rtype: C{list} or C{Deferred}
 
230
        @return: The list of message sequence numbers which were deleted,
 
231
        or a C{Deferred} whose callback will be invoked with such a list.
 
232
 
 
233
        @raise ReadOnlyMailbox: Raised if this Mailbox is not open for
 
234
        read-write.
 
235
        """
 
236
 
 
237
    def fetch(self, messages, uid):
 
238
        if uid:
 
239
            determinant = IMAP4MessageInfo.imapUID
 
240
        else:
 
241
            determinant = IMAP4MessageInfo.imapSequenceNumber
 
242
 
 
243
        fetcher = lambda objid: self.mbox.store.findUnique(
 
244
                IMAP4MessageInfo,
 
245
                attributes.AND(determinant == objid,
 
246
                               IMAP4MessageInfo.imapMailbox == self.mbox))
 
247
 
 
248
        for ojd in messages:
 
249
            m = fetcher(ojd)
 
250
            yield m.imapSequenceNumber, m
 
251
 
 
252
    def store(self, messages, flags, mode, uid):
 
253
        """Set the flags of one or more messages.
 
254
 
 
255
        @type messages: A MessageSet object with the list of messages requested
 
256
        @param messages: The identifiers of the messages to set the flags of.
 
257
 
 
258
        @type flags: sequence of C{str}
 
259
        @param flags: The flags to set, unset, or add.
 
260
 
 
261
        @type mode: -1, 0, or 1
 
262
        @param mode: If mode is -1, these flags should be removed from the
 
263
        specified messages.  If mode is 1, these flags should be added to
 
264
        the specified messages.  If mode is 0, all existing flags should be
 
265
        cleared and these flags should be added.
 
266
 
 
267
        @type uid: C{bool}
 
268
        @param uid: If true, the IDs specified in the query are UIDs;
 
269
        otherwise they are message sequence IDs.
 
270
 
 
271
        @rtype: C{dict} or C{Deferred}
 
272
        @return: A C{dict} mapping message sequence numbers to sequences of C{str}
 
273
        representing the flags set on the message after this operation has
 
274
        been performed, or a C{Deferred} whose callback will be invoked with
 
275
        such a C{dict}.
 
276
 
 
277
        @raise ReadOnlyMailbox: Raised if this mailbox is not open for
 
278
        read-write.
 
279
        """
 
280
 
 
281
    def getFlags(self):
 
282
        return ['\\NoSelect',
 
283
                '\\Seen',
 
284
                '\\Answered',
 
285
                '\\Forwarded',
 
286
                '\\Redirected']
 
287
 
 
288
 
 
289
class IMAP4MailboxItem(item.Item):
 
290
    typeName = 'quotient_imap4_mailbox'
 
291
 
 
292
    uidValidity = attributes.timestamp()
 
293
    uidCounter = attributes.integer(default=0)
 
294
 
 
295
    implements(imap4.IMailbox)
 
296
 
 
297
    def __init__(self):
 
298
        self.uidValidity = Time()
 
299
 
 
300
 
 
301
 
 
302
class IMAP4Up(item.Item):
 
303
    typeName = 'quotient_imap4_user_powerup'
 
304
 
 
305
    implements(imap4.IAccount)
 
306
 
 
307
 
 
308
    def addMailbox(self, name, mbox = None):
 
309
        raise imap4.MailboxException("Adding mailboxes not yet implemented.")
 
310
 
 
311
 
 
312
    def create(self, pathspec):
 
313
        raise imap4.MailboxException("Adding mailboxes not yet implemented.")
 
314
 
 
315
    def select(self, name, rw=True):
 
316
        pass
 
317
 
 
318
    def delete(self, name):
 
319
        """Delete the mailbox with the specified name.
 
320
 
 
321
        @type name: C{str}
 
322
        @param name: The mailbox to delete.
 
323
 
 
324
        @rtype: C{Deferred} or C{bool}
 
325
        @return: A true value if the mailbox is successfully deleted, or a
 
326
        C{Deferred} whose callback will be invoked when the deletion
 
327
        completes.
 
328
 
 
329
        @raise MailboxException: Raised if this mailbox cannot be deleted.
 
330
        This may also be raised asynchronously, if a C{Deferred} is returned.
 
331
        """
 
332
 
 
333
    def rename(self, oldname, newname):
 
334
        """Rename a mailbox
 
335
 
 
336
        @type oldname: C{str}
 
337
        @param oldname: The current name of the mailbox to rename.
 
338
 
 
339
        @type newname: C{str}
 
340
        @param newname: The new name to associate with the mailbox.
 
341
 
 
342
        @rtype: C{Deferred} or C{bool}
 
343
        @return: A true value if the mailbox is successfully renamed, or a
 
344
        C{Deferred} whose callback will be invoked when the rename operation
 
345
        is completed.
 
346
 
 
347
        @raise MailboxException: Raised if this mailbox cannot be
 
348
        renamed.  This may also be raised asynchronously, if a C{Deferred}
 
349
        is returned.
 
350
        """
 
351
 
 
352
    def isSubscribed(self, name):
 
353
        """Check the subscription status of a mailbox
 
354
 
 
355
        @type name: C{str}
 
356
        @param name: The name of the mailbox to check
 
357
 
 
358
        @rtype: C{Deferred} or C{bool}
 
359
        @return: A true value if the given mailbox is currently subscribed
 
360
        to, a false value otherwise.  A C{Deferred} may also be returned
 
361
        whose callback will be invoked with one of these values.
 
362
        """
 
363
 
 
364
    def subscribe(self, name):
 
365
        """Subscribe to a mailbox
 
366
 
 
367
        @type name: C{str}
 
368
        @param name: The name of the mailbox to subscribe to
 
369
 
 
370
        @rtype: C{Deferred} or C{bool}
 
371
        @return: A true value if the mailbox is subscribed to successfully,
 
372
        or a Deferred whose callback will be invoked with this value when
 
373
        the subscription is successful.
 
374
 
 
375
        @raise MailboxException: Raised if this mailbox cannot be
 
376
        subscribed to.  This may also be raised asynchronously, if a
 
377
        C{Deferred} is returned.
 
378
        """
 
379
 
 
380
    def unsubscribe(self, name):
 
381
        """Unsubscribe from a mailbox
 
382
 
 
383
        @type name: C{str}
 
384
        @param name: The name of the mailbox to unsubscribe from
 
385
 
 
386
        @rtype: C{Deferred} or C{bool}
 
387
        @return: A true value if the mailbox is unsubscribed from successfully,
 
388
        or a Deferred whose callback will be invoked with this value when
 
389
        the unsubscription is successful.
 
390
 
 
391
        @raise MailboxException: Raised if this mailbox cannot be
 
392
        unsubscribed from.  This may also be raised asynchronously, if a
 
393
        C{Deferred} is returned.
 
394
        """
 
395
 
 
396
    def listMailboxes(self, ref, wildcard):
 
397
        """List all the mailboxes that meet a certain criteria
 
398
 
 
399
        @type ref: C{str}
 
400
        @param ref: The context in which to apply the wildcard
 
401
 
 
402
        @type wildcard: C{str}
 
403
        @param wildcard: An expression against which to match mailbox names.
 
404
        '*' matches any number of characters in a mailbox name, and '%'
 
405
        matches similarly, but will not match across hierarchical boundaries.
 
406
 
 
407
        @rtype: C{list} of C{tuple}
 
408
        @return: A list of C{(mailboxName, mailboxObject)} which meet the
 
409
        given criteria.  C{mailboxObject} should implement either
 
410
        C{IMailboxInfo} or C{IMailbox}.  A Deferred may also be returned. 
 
411
        """
 
412
 
 
413
 
 
414
 
 
415
class IMAP4ServerFactory(Factory):
 
416
    def __init__(self, portal):
 
417
        self.portal = portal
 
418
 
 
419
    def buildProtocol(self, addr):
 
420
        s = imap4.IMAP4Server()
 
421
        s.portal = self.portal
 
422
        s.factory = self
 
423
        return s
 
424
 
 
425
 
 
426
class IMAP4Listener(item.Item, service.Service):
 
427
 
 
428
    typeName = "quotient_imap4_listener"
 
429
    schemaVersion = 1
 
430
 
 
431
    # These are for the Service stuff
 
432
    parent = attributes.inmemory()
 
433
    running = attributes.inmemory()
 
434
 
 
435
    # A cred portal, a Twisted TCP factory and as many as two
 
436
    # IListeningPorts
 
437
    portal = attributes.inmemory()
 
438
    factory = attributes.inmemory()
 
439
    port = attributes.inmemory()
 
440
    securePort = attributes.inmemory()
 
441
 
 
442
    installedOn = attributes.reference(
 
443
        "A reference to the store or avatar which we have powered up.")
 
444
 
 
445
    portNumber = attributes.integer(
 
446
        "The TCP port to bind to serve SMTP.",
 
447
        default=6143)
 
448
 
 
449
    securePortNumber = attributes.integer(
 
450
        "The TCP port to bind to serve SMTP/SSL.",
 
451
        default=0)
 
452
 
 
453
    certificateFile = attributes.bytes(
 
454
        "The name of a file on disk containing a private "
 
455
        "key and certificate for use by the SMTP/SSL server.",
 
456
        default=None)
 
457
 
 
458
    # When enabled, toss all traffic into logfiles.
 
459
    debug = False
 
460
 
 
461
 
 
462
    def activate(self):
 
463
        self.portal = None
 
464
        self.factory = None
 
465
        self.port = None
 
466
        self.securePort = None
 
467
 
 
468
    def installOn(self, other):
 
469
        assert self.installedOn is None, "You cannot install an IMAP4Listener on more than one thing"
 
470
        other.powerUp(self, service.IService)
 
471
        self.installedOn = other
 
472
 
 
473
    def privilegedStartService(self):
 
474
        realm = portal.IRealm(self.installedOn, None)
 
475
        if realm is None:
 
476
            raise MailConfigurationError(
 
477
                "No realm: "
 
478
                "you need to install a userbase before using this service.")
 
479
 
 
480
        chk = checkers.ICredentialsChecker(self.installedOn, None)
 
481
        if chk is None:
 
482
            raise MailConfigurationError(
 
483
                "No checkers: "
 
484
                "you need to install a userbase before using this service.")
 
485
 
 
486
        self.portal = portal.Portal(realm, [chk])
 
487
        self.factory = IMAP4ServerFactory(self.portal)
 
488
 
 
489
        if self.debug:
 
490
            self.factory = policies.TrafficLoggingFactory(self.factory, 'imap4')
 
491
 
 
492
        if self.portNumber is not None:
 
493
            self.port = reactor.listenTCP(self.portNumber, self.factory)
 
494
 
 
495
        if self.securePortNumber is not None and self.certificateFile is not None:
 
496
            cert = sslverify.PrivateCertificate.loadPEM(file(self.certificateFile).read())
 
497
            certOpts = sslverify.OpenSSLCertificateOptions(
 
498
                cert.privateKey.original,
 
499
                cert.original,
 
500
                requireCertificate=False,
 
501
                method=SSL.SSLv23_METHOD)
 
502
            self.securePort = reactor.listenSSL(self.securePortNumber, self.factory, certOpts)
 
503
 
 
504
    def stopService(self):
 
505
        L = []
 
506
        if self.port is not None:
 
507
            L.append(defer.maybeDeferred(self.port.stopListening))
 
508
            self.port = None
 
509
        if self.securePort is not None:
 
510
            L.append(defer.maybeDeferred(self.securePort.stopListening))
 
511
            self.securePort = None
 
512
        return defer.DeferredList(L)