2
from OpenSSL import SSL
4
from zope.interface import implements
6
from twisted.mail import imap4
8
from twisted.internet.protocol import Factory
9
from twisted.internet import defer
11
from epsilon.extime import Time
13
from axiom import item, attributes
15
from xquotient import exmess
17
from twisted.protocols import policies
19
from twisted.cred import portal, checkers
21
from vertex import sslverify
23
from twisted.internet import reactor
25
from twisted.application import service
28
class MailConfigurationError(Exception):
32
implements(imap4.IMessagePart)
34
def __init__(self, part):
37
def getHeaders(self, negate, *names):
39
hdrs = self.part.getAllHeaders()
41
if (hdr.name in names) ^ negate:
42
result[hdr.name] = hdr.value
45
def getBodyFile(self):
46
"""Retrieve a file object containing only the body of this message.
48
f = self.part.source.open()
49
f.seek(self.part.bodyOffset)
53
"""Retrieve the total size, in octets, of this message.
57
return self.part.source.getsize()
59
def isMultipart(self):
60
from xquotient.mimestorage import Part
61
return bool(self.part.store.query(Part, Part.parent == self.part).count())
63
def getSubPart(self, part):
64
return IMAP4PartInfo(self.part.getSubPart(part))
67
class IMAP4MessageInfo(item.Item):
68
implements(imap4.IMessage)
70
message = attributes.reference(allowNone=False)
72
imapUID = attributes.integer(allowNone=False)
73
imapMailbox = attributes.reference(allowNone=False)
74
imapSequenceNumber = attributes.integer(allowNone=False)
76
_rootPart = attributes.inmemory()
81
def _getRootPart(self):
82
if self._rootPart is None:
83
self._rootPart = IMAP4PartInfo(self.message.impl)
86
rootPart = property(_getRootPart)
91
"""Retrieve the unique identifier associated with this message.
95
"""Retrieve the flags associated with this message.
98
@return: The flags, represented as strings.
102
def getInternalDate(self):
103
return self.message.received.asRFC2822()
107
def getHeaders(self, negate, *names):
108
return self.rootPart.getHeaders(negate, *names)
110
def getBodyFile(self):
111
return self.rootPart.getBodyFile()
114
return self.rootPart.getSize()
116
def isMultipart(self):
117
return self.rootPart.isMultipart()
119
def getSubPart(self, part):
120
return self.rootPart.getSubPart(part)
123
class IMAP4MailboxImpl:
124
def __init__(self, mboxitem):
127
def getHierarchicalDelimiter(self):
131
def getUIDValidity(self):
132
return int(self.mbox.uidValidity.asPOSIXTimestamp())
135
def getUIDNext(self):
136
"""Return the likely UID for the next message added to this mailbox.
140
return self.mbox.uidCounter + 1
142
def getUID(self, message):
143
"""Return the UID of a message in the mailbox
145
@type message: C{int}
146
@param message: The message sequence number
149
@return: The UID of the message.
151
return self.mbox.store.findUnique(
153
attributes.AND(IMAP4MessageInfo.imapMailbox == self.mbox,
154
IMAP4MessageInfo.imapSequenceNumber == message)).imapUID
156
def getMessageCount(self):
157
"""Return the number of messages in this mailbox.
161
return self.mbox.store.query(IMAP4MessageInfo,
162
IMAP4MessageInfo.imapMailbox == self.mbox)
164
def getRecentCount(self):
165
"""Return the number of messages with the 'Recent' flag.
172
def getUnseenCount(self):
173
"""Return the number of messages with the 'Unseen' flag.
177
return self.mbox.store.query(
179
attributes.AND(IMAP4MessageInfo.imapMailbox == self.mbox,
180
IMAP4MessageInfo.message == exmess.Message.storeID,
181
exmess.Message.read == False))
183
def isWriteable(self):
187
"""Called before this mailbox is deleted, permanently.
189
If necessary, all resources held by this mailbox should be cleaned
190
up here. This function _must_ set the \\Noselect flag on this
194
def requestStatus(self, names):
195
return imap4.statusRequestHelper(self, names)
197
def addListener(self, listener):
200
def removeListener(self, listener):
203
def addMessage(self, message, flags = (), date = None):
204
"""Add the given message to this mailbox.
206
@type message: A file-like object
207
@param message: The RFC822 formatted message
209
@type flags: Any iterable of C{str}
210
@param flags: The flags to associate with this message
213
@param date: If specified, the date to associate with this
217
@return: A deferred whose callback is invoked with the message
218
id if the message is added successfully and whose errback is
221
@raise ReadOnlyMailbox: Raised if this Mailbox is not open for
224
return defer.fail(RuntimeError("adding messages not supported"))
227
"""Remove all messages flagged \\Deleted.
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.
233
@raise ReadOnlyMailbox: Raised if this Mailbox is not open for
237
def fetch(self, messages, uid):
239
determinant = IMAP4MessageInfo.imapUID
241
determinant = IMAP4MessageInfo.imapSequenceNumber
243
fetcher = lambda objid: self.mbox.store.findUnique(
245
attributes.AND(determinant == objid,
246
IMAP4MessageInfo.imapMailbox == self.mbox))
250
yield m.imapSequenceNumber, m
252
def store(self, messages, flags, mode, uid):
253
"""Set the flags of one or more messages.
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.
258
@type flags: sequence of C{str}
259
@param flags: The flags to set, unset, or add.
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.
268
@param uid: If true, the IDs specified in the query are UIDs;
269
otherwise they are message sequence IDs.
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
277
@raise ReadOnlyMailbox: Raised if this mailbox is not open for
282
return ['\\NoSelect',
289
class IMAP4MailboxItem(item.Item):
290
typeName = 'quotient_imap4_mailbox'
292
uidValidity = attributes.timestamp()
293
uidCounter = attributes.integer(default=0)
295
implements(imap4.IMailbox)
298
self.uidValidity = Time()
302
class IMAP4Up(item.Item):
303
typeName = 'quotient_imap4_user_powerup'
305
implements(imap4.IAccount)
308
def addMailbox(self, name, mbox = None):
309
raise imap4.MailboxException("Adding mailboxes not yet implemented.")
312
def create(self, pathspec):
313
raise imap4.MailboxException("Adding mailboxes not yet implemented.")
315
def select(self, name, rw=True):
318
def delete(self, name):
319
"""Delete the mailbox with the specified name.
322
@param name: The mailbox to delete.
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
329
@raise MailboxException: Raised if this mailbox cannot be deleted.
330
This may also be raised asynchronously, if a C{Deferred} is returned.
333
def rename(self, oldname, newname):
336
@type oldname: C{str}
337
@param oldname: The current name of the mailbox to rename.
339
@type newname: C{str}
340
@param newname: The new name to associate with the mailbox.
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
347
@raise MailboxException: Raised if this mailbox cannot be
348
renamed. This may also be raised asynchronously, if a C{Deferred}
352
def isSubscribed(self, name):
353
"""Check the subscription status of a mailbox
356
@param name: The name of the mailbox to check
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.
364
def subscribe(self, name):
365
"""Subscribe to a mailbox
368
@param name: The name of the mailbox to subscribe to
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.
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.
380
def unsubscribe(self, name):
381
"""Unsubscribe from a mailbox
384
@param name: The name of the mailbox to unsubscribe from
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.
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.
396
def listMailboxes(self, ref, wildcard):
397
"""List all the mailboxes that meet a certain criteria
400
@param ref: The context in which to apply the wildcard
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.
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.
415
class IMAP4ServerFactory(Factory):
416
def __init__(self, portal):
419
def buildProtocol(self, addr):
420
s = imap4.IMAP4Server()
421
s.portal = self.portal
426
class IMAP4Listener(item.Item, service.Service):
428
typeName = "quotient_imap4_listener"
431
# These are for the Service stuff
432
parent = attributes.inmemory()
433
running = attributes.inmemory()
435
# A cred portal, a Twisted TCP factory and as many as two
437
portal = attributes.inmemory()
438
factory = attributes.inmemory()
439
port = attributes.inmemory()
440
securePort = attributes.inmemory()
442
installedOn = attributes.reference(
443
"A reference to the store or avatar which we have powered up.")
445
portNumber = attributes.integer(
446
"The TCP port to bind to serve SMTP.",
449
securePortNumber = attributes.integer(
450
"The TCP port to bind to serve SMTP/SSL.",
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.",
458
# When enabled, toss all traffic into logfiles.
466
self.securePort = None
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
473
def privilegedStartService(self):
474
realm = portal.IRealm(self.installedOn, None)
476
raise MailConfigurationError(
478
"you need to install a userbase before using this service.")
480
chk = checkers.ICredentialsChecker(self.installedOn, None)
482
raise MailConfigurationError(
484
"you need to install a userbase before using this service.")
486
self.portal = portal.Portal(realm, [chk])
487
self.factory = IMAP4ServerFactory(self.portal)
490
self.factory = policies.TrafficLoggingFactory(self.factory, 'imap4')
492
if self.portNumber is not None:
493
self.port = reactor.listenTCP(self.portNumber, self.factory)
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,
500
requireCertificate=False,
501
method=SSL.SSLv23_METHOD)
502
self.securePort = reactor.listenSSL(self.securePortNumber, self.factory, certOpts)
504
def stopService(self):
506
if self.port is not None:
507
L.append(defer.maybeDeferred(self.port.stopListening))
509
if self.securePort is not None:
510
L.append(defer.maybeDeferred(self.securePort.stopListening))
511
self.securePort = None
512
return defer.DeferredList(L)