On Wed, 5 Jul 2006, Travis wrote:
> Comments and questions are, of course, very welcome.
Travis,
I spent some time today reviewing server.py. My comments are inline and are
prefixed with #@#. Let me know if you have questions, I should be online again
later today.
Andi..
# Copyright (c) 2004-2006 Open Source Applications Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This module contains the pieces needed by twisted to create an IMAP server.
A good deal of this code was either taken directly or modelled after
"Twisted Network Programming Essentials" by Abe Fettig.
Copyright 2006 O'Reilly Media, Inc., 0-596-10032-9
and is used here in accordance with their guidelines on page xvii.
For documentation of methods not documented here, please see the the
specified interface documentation in twisted.mail.imap4.
"""
from application import schema
from osaf.pim.mail import MailMessage, MailMessageMixin, MIMEBase
from twisted.mail import imap4
from twisted.internet import protocol, defer, threads
from twisted.cred import portal, checkers, credentials
from twisted.cred import error as credError
from zope.interface import implements
from osaf.mail import message
import random
from cStringIO import StringIO
import email
import email.Message
import re
import traceback
import sys
import logging
log = logging.getLogger(__name__)
MAILBOXDELIMITER = "."
headersMapping = {u'From': u'_fromAddressHelper',
u'Subject': u'_subjectHelper',
u'Cc':'_ccAddressHelper',
u'To':'_toAddressHelper',
u'Date':'_dateHelper',
u'Message-Id':'_messageIdHelper',
}
flagsMapping = {u"\Seen": u'itsItem.read',
u"Deleted": None,
u"\Flagged": None,
u"\Answered": None,
u"\Recent": None
}
# We have two objects which implement imap4.IMessagePart. While
# this isn't necessarily the most elegant solution to this,
# currently it's probably the best way to do it.
class IMessagePart(object):
'''
An implementation of imap4.IMessagePart for subparts of multi part messages.
The decision to make these sub parts non-persistent was made because
of differences between Chandler's MIME message handling and the standards.
If this is fixed, this decision should be reexamined. This would require
persisting seperate subparts of MIME messages in seperate items.
Implements the imap4.IMessagePart interface.
'''
implements(imap4.IMessagePart)
def __init__(self, mimeMessage):
self.message = mimeMessage
self.data = str(self.message)
def getHeaders(self, negate, *names):
if not names: names = self.message.keys()
headers = {}
if negate:
for header in self.message.keys():
if header.upper() not in names:
headers[header.lower()] = self.message.get(header, '')
else:
for name in names:
headers[name.lower()] = self.message.get(name, '')
return headers
def getBodyFile(self):
bodyData = str(self.message.get_payload())
return StringIO(bodyData)
def getSize(self):
size = len(self.data)
return size
def getInternalDate(self):
return self.message.get('Date', '')
def isMultipart(self):
return self.message.is_multipart()
def getSubPart(self, partNo):
return IMessagePart(self.message.get_payload(partNo))
class IMessagePartAnnotation(schema.Annotation):
'''
Annotates MailMessages and implements imap4.IMessagePart. Essentially,
allows MailMessages to be used by Twisted's imap server framework.
For the moment, only being used as a superclass of ChandlerIMessage.
Subparts of multipart messages are
converted to email.Message objects and then kept in a slot of
ChandlerIMessages. They are not persisted.
Implements the imap4.IMessagePart interface.
'''
implements(imap4.IMessagePart)
schema.kindInfo(annotates=MIMEBase)
__slots__ = ["messageObject", "messageData"]
############# UNUSED FOR NOW DOWN TO NEXT COMMENT ###########
# If changes are made to Chandler's MIME support, these may be useful
#def _refListToUnicodeHeaderValue(self, refList):
# '''
# This function takes a ref list and returns a nice
# unicode string formatted as a header value.
# '''
# return ', '.join([str(name) for name in list(refList)])
#
#def _fromAddressHelper(self):
# return str(self.itsItem.fromAddress)
#def _subjectHelper(self):
# return str(self.itsItem.subject)
#
#
#
#def _ccAddressHelper(self):
# return self._refListToUnicodeHeaderValue(self.itsItem.ccAddress)
#
#def _toAddressHelper(self):
# return self._refListToUnicodeHeaderValue(self.itsItem.toAddress)
#
#def _dateHelper(self):
# return str(self.itsItem.dateSentString)
#
#def _messageIdHelper(self):
# return str(self.itsItem.messageId)
#
#def _getHeader(self, header):
# header = header.title()
# helperFunction = headersMapping.get(header, None)
#
# if helperFunction:
# value = getattr(self, helperFunction)()
# else:
# value = self.itsItem.headers.get(header, '')
# return str(value)
################## UNUSED FOR NOW UP TO NEXT COMMENT ################
def _getMessageObject(self):
'''
Implements cacheing for the message object.
'''
if not getattr(self, 'messageObject', None):
try:
self.messageObject = email.message_from_string(
str(self.itsItem.rfc2822Message.getReader().read())
)
except:
self.messageObject = email.Message.Message()
self.messageObject['Subject'] = str(self.itsItem.subject)
self.messageObject.set_payload(
"Sorry, this message could not be imported.\n\n" +
"The following exceptions were raised:\n\n" +
"\n\n".join(traceback.format_tb(sys.exc_traceback)))
self.messageObject['Date'] = str(self.itsItem.dateSentString)
return self.messageObject
else:
return self.messageObject
def _getMessageData(self):
'''
Message data can be kind of complex to generate, so we cache this too.
'''
if not getattr(self, 'messageData', None):
self.messageData = str(self._getMessageObject())
return self.messageData
else:
return self.messageData
def getHeaders(self, negate, *names):
message = self._getMessageObject()
if not names: names = message.keys()
headers = {}
if negate:
for header in message.keys():
if header.upper() not in names:
headers[header.lower()] = message.get(header, '')
else:
for name in names:
headers[name.lower()] = message.get(name, '')
return headers
def getBodyFile(self):
bodyData = str(self._getMessageObject().get_payload())
return StringIO(bodyData)
def getSize(self):
return len(self._getMessageData())
def getInternalDate(self):
return self._getMessageObject().get('Date','')
def isMultipart(self):
return self._getMessageObject().is_multipart()
def getSubPart(self, partNo):
# In most IMAP server implementations, this would return a
# IMessagePartAnnotation, but this does not work because not
# all MIME subparts in Chandler reflect the actual MIME subpart
# from the message text. Specifically, MIMEText and MIMEBinary
# do not keep track of their headers, and MailMessages keep
# track of their headers in different places...
#
# To avoid this, we created a new, non-persistent class
# that implements imap4.IMessagePart which is generated
# from the rfc2822Message attribute of chandler's MailMessage
# items.
if not self.isMultipart():
raise TypeError, "Tried to get a subpart of a non-multipart message."
return IMessagePart(self._getMessageObject().get_payload(partNo))
class IMessageAnnotation(IMessagePartAnnotation):
'''
Annotates Chandler MailMessages so that they can be treated like
messages on an IMAP server. Implements the imap4.IMessage interface.
Specifically, this involves keeping track of their UID within the IMAP
server mailbox, and handling IMAP flags.
'''
schema.kindInfo(annotates=MailMessageMixin)
uid = schema.One(
schema.Integer,
doc = 'The IMAP mailbox UID of this message. These should be integers,'
'and should be unique within the mailbox.'
)
_mailbox = schema.One(
doc='The mailbox this message is in.',
initialValue = None
)
def _get_mailbox(self):
return _mailbox #@# self._mailbox ?
def _set_mailbox(self, box):
if not self._mailbox == box:
self._mailbox = box
self.uid = box.consumeNextUID()
def _del_mailbox(self):
del _mailbox #@# self._mailbox ?
mailbox = property(_get_mailbox, _set_mailbox, _del_mailbox,
"The mailbox containing this message")
############ Methods to implement imap4.IMessage interface ############
def getUID(self):
return self.uid
def getFlags(self):
# Provides a live mapping of chandler properties to flags.
# If a property changes via chandler, the flag will always
# reflect that.
flags = []
if self.itsItem.read:
flags.append('\Seen')
return flags
def getInternalDate(self):
return str(self.itsItem.dateSentString)
#######################################################################
def _changeFlag(self, flag, value):
attr = flagsMapping.get(flag,None)
if attr:
attr_list = attr.split('.')
obj = self
for at in attr_list[:-1]:
obj = getattr(obj, at)
obj.__setattr__(attr_list[-1], value)
#@# why obj.__setattr__(...) instead of setattr(obj, ...) ?
def _setFlag(self, flag):
'''
Set flag. Currently fails silently.
'''
self._changeFlag(flag, True)
def _unsetFlag(self, flag):
'''
Unset flag. Currently fails silently.
'''
self._changeFlag(flag, False)
def setFlags(self, flags, clear=False):
'''
Set the flags specified in flags. If clear is true, unset all flags first.
'''
if clear:
for flag in flagsMapping.keys():
self._unsetFlag(flag)
for flag in flags:
self._setFlag(flag)
def unsetFlags(self, flags):
'''
Unset the flags specified in flags.
'''
for flag in flags:
self._unsetFlag(flag)
class ChandlerIMailbox(schema.Item):
'''
A Chandler item implementing the imap4.IMailbox interface.
This item serves as a mailbox for MailMessages that have been
annotated to support the imap4.IMessage interface.
'''
############################ IMPORTANT NOTE ###########################
# As per the IMAP spec, sequence numbers and UIDs start from 1 #
#######################################################################
__slots__ = ('listeners')
implements(imap4.IMailbox, imap4.IMessageCopier, imap4.ISearchableMailbox)
validityUID = schema.One(
schema.Integer,
doc = 'The unique id number of this mailbox.',
initialValue = random.randint(1000000, 9999999),
) #@# why not use a UUID ?
#@# from chandlerdb.util.c import UUID()
#@# initialValue = UUID()
#@# also, maybe this should be in __init__() so that all mailboxes
#@# don't endup with the same UID
#@# (an initial value is not required, by the way)
subscribed = schema.One(
schema.Boolean,
doc = 'True is a user is subscribed to this mailbox.',
initialValue = False
)
uidNext = schema.One(
schema.Integer,
doc = 'The next mail message uid to be assigned',
initialValue = 1,
)
#@# why not use UUIDs for uidNext ?
messages = schema.Sequence(
IMessageAnnotation,
inverse = IMessageAnnotation._mailbox,
doc = 'A list of imap messages.',
initialValue = []
)
def __init__(self, *args, **kw):
super(ChandlerIMailbox, self).__init__(*args, **kw)
########### Methods to implement imap4.IMailbox interface ############
#### These two are actually from IMailboxInfo, ####
#### a superclass of IMailbox ####
def getFlags(self):
return [r'\Seen', r'\Unseen',
r'\Flagged', r'\Answered', r'\Recent']
def getHierarchicalDelimiter(self):
return MAILBOXDELIMITER
####################################################
def getUIDValidity(self):
return self.validityUID
def getUIDNext(self):
return self.uidNext
def getUID(self, messageNum):
return IMessageAnnotation(list(self.messages)[messageNum-1]).uid
def getMessageCount(self):
return len(list(self.messages))
def getRecentCount(self):
# I'm not really sure what this flag is supposed to represent...
return 0
def getUnseenCount(self):
def messageIsUnseen(message):
'''
@type message: IMessageAnnotation
'''
if not message.read:
return True
return len(filter(messageIsUnseen, list(self.messages)))
def isWriteable(self):
return True
def destroy(self):
raise imap4.MailboxException("Permission Denied")
def requestStatus(self, names):
return imap4.statusRequestHelper(self, names)
def addListener(self, listener):
self._getListeners().append(listener)
def removeListener(self, listener):
self._getListeners().remove(listener)
def addMessage(self, msg, flags=[], date=None):
'''
This method is called when a new message is dropped into Chandler's
mailbox in an external program like Thunderbird. All it does is create
a new MailMessage object and commit it to the repository. After that,
the ChandlerMaildirectory._message_added method automatically
(through Chandler's Kind watching facilities) annotates
the MailMessage object and adds it to this mailbox.
This means that this method is not called for messages which come into
Chandler via services like the IMAP client code in osaf.mail.imap.
'''
if not flags:
# If None was passed in, make it an empty list...
flags = []
#XXX:
# Not sure I'm doing correctly within the context of Chandler.
# Should I rely on twisted's creating a new thread?
# Does the commit in a different thread have any implications?
return threads.deferToThread(self._addMessageBlocking, msg).addCallback(
self._addedMessage, flags)
#self._addMessageBlocking(msg)
#self._addedMessage(flags)
def expunge(self):
#Remove all messages flagged \Deleted
# - do nothing for now. We'll let deletions happen within chandler
# for the moment
log.info("Expunge called")
def fetch(self, messageSet, uid):
self.itsView.refresh()
if uid:
seqDict = self._uidMessageSetToSeqDict(messageSet)
else:
seqDict = self._seqMessageSetToSeqDict(messageSet)
for seq, msg in seqDict.items():
msg.unsetFlags(['\Recent'])
yield seq, msg
def store(self, messageSet, flags, mode, uid):
if uid:
seqDict = self._uidMessageSetToSeqDict(messageSet)
else:
seqDict = self._seqMessageSetToSeqDict(messageSet)
setFlags = {}
if mode == 0:
clear = True
else:
clear = False
for seq, msg in seqDict.items():
if mode == 1 or mode == 0:
msg.setFlags(flags, clear)
if mode == -1:
msg.unsetFlags(flags)
setFlags[seq] = msg.getFlags()
self.itsView.commit()
# Notify listeners...
for listener in self._getListeners():
listener.flagsChanged(setFlags)
return setFlags
#####################################################################
############# Method to implement imap4.IMailboxCopier #############
def copy(self, messageObject):
# Switch mailboxes
messageObject.mailbox = self
for listener in self._getListeners():
listener.newMessages(self.getMessageCount(), None)
####################################################################
def search(self, query, uid):
log.info("Search called")
log.info(str(query))
log.info(str(uid))
log.info( """Search not currently implemented. If we can find a client
which uses it, we may be able to fix this.""")
#@# pine uses search
return []
#### Methods to support the above methods ####
def _addMessageBlocking(self, msg):
# This method might take a little while to return,
# because of the repository commit. It is passed
# to the twisted threading mechanism so a deferred
# can be returned by addMessage (above)
# msg is a buffer contatining the actual message text
repMessage = message.messageTextToKind(self.itsView,
msg.read())
#XXX Should we be committing on every add?
# It would be nice to not commit every time we add a message,
# as this makes adding several messages at once pretty costly...
# Two options:
# * examine Twisted internals to see if there's a way
# to detect adding multiple messages
# * find out if the repository supports "commit when
# convenient," or if this happens anyway.
try:
self.itsView.commit()
except RepositoryError, e:
raise
except VersionConflictError, e1:
raise
#@# while committing in another thread IN THE SAME view, your
#@# view should really be doing nothing else. Concurrently
#@# committing in and working with a view is not supported. In other
#@# words, views are not really thread safe.
#@# Hence, I'm not sure the defering of this method to another
#@# thread is necessary or even safe.
#@# Avoiding that may also help you in knowing when to commit or not
#@# the operation then becomes more synchronous and you may have
#@# knowledge about when the larger operation of importing mail is
#@# complete.
#@# the try/except block above is as good as not having it since no
#@# exceptions are processed anyway.
return repMessage
def _addedMessage(self, mailMessage, flags):
# Called after a message has been added to the repository.
# If this mailbox is not 'Inbox', this method is where the
# message will actually be added to this mailbox.
msg = IMessageAnnotation(mailMessage)
flags.append("\Recent")
msg.setFlags(flags)
msg.mailbox = self
for listener in self._getListeners():
listener.newMessages(self.getMessageCount(), None)
def _uidMessageSetToSeqDict(self, messageSet):
'''
take a MessageSet object containing UIDs, and return a
dictionary mapping sequence numbers to IMessageAnnotations
'''
# if messageSet.last is None, it means 'the end', and needs
# to be set to a sane value before it can be iterated
# (or "in-ed")
if not messageSet.last:
messageSet.last = self.getUIDNext()
messageDict = {}
# Need to iterate through with sequence numbers. We're
# going to check every message to see if it one of the
# messages we want.
messages = list(self.messages)
for seqNumber in xrange(1, len(self.messages)+1):
msg = IMessageAnnotation(messages[seqNumber - 1])
if msg.uid in messageSet:
messageDict[seqNumber] = msg
return messageDict
def _seqMessageSetToSeqDict(self, messageSet):
'''
take a MessageSet object containing sequence numbers, and return
a dictionary mapping sequence numbers to IMessageAnnotations
'''
# if messageSet.last is None, it means 'the end', and needs
# to be set to a sane value before it can be iterated
# (or "in-ed")
if not messageSet.last:
messageSet.last = len(list(self.messages)) - 1
messageDict = {}
# For this case, we just look at the messages specified
# in the message set.
messages = list(self.messages)
for seq in messageSet:
messageDict[seq] = IMessageAnnotation(messages[seq - 1])
return messageDict
def _getListeners(self):
if not getattr(self, 'listeners', None):
self.listeners = []
return self.listeners
def consumeNextUID(self):
'''
Return the next UID and increment this number.
'''
self.uidNext += 1
return self.uidNext-1
#@# why not use UUIDs for UIDs ?
############ The one special method of this class ;) ############
def addMailMessageMixin(self, mailMessageMixin):
'''
Add a MailMessageMixin to this mailbox.
Annotate mailMessageMixin with IMessageAnnotation and adds it
to this mailbox.
@type mailMessageMixin: MailMessageMixin
@param mailMesssageMixin: the MailMessageMixin to add to this mailbox
'''
newMessage = IMessageAnnotation(mailMessageMixin)
newMessage.mailbox = self
class IMAPMailboxDict(schema.Item):
#XXX I would have liked to make this a subclass of dict, but
# when it was put into the repository it became just a normal dict
# is there any way to realize this dream?
boxes = schema.Mapping(
ChandlerIMailbox,
initialValue = {}
)
#@# YES, instead of making a dict (or a subclass) declare an attribute
#@# of cardinality 'list' and give it an inverse (or an otherName) so as
#@# to declare a ref collection (a collection of bi-directional
#@# references).
#@# A ref collection is backed in memory by a dict but is also a
#@# double-linked list. The dict's keys are the items' UUIDs but it is
#@# possible to use a second set of keys for some or all the member
#@# items, the key aliases (strings of your choice). When adding a
#@# mailbox into this ref collection, assign it a key alias as well.
#@#
#@# for example: self.boxes.append(box, alias)
#@# self.boxes.getByAlias(alias)
#@# where 'alias' is the key you intended to use in the current code
#@#
#@# To get more information about the ref collection API see the
#@# chandler/util/LinkedMap.py, chandler/item/RefCollections.py and
#@# internal/chandlerdb/chandlerdb/util/linkedmap.c files
#@#
#@# In order to scale and avoiding dangling references, collections of
#@# items should really use bi-directional references or be abstract.
#@# For this class, a ref collection seems the most appropriate.
def _processKey(self, key):
if not isinstance(key, str):
raise TypeError(
'Keywords for this object must be strings. You supplied %s.'
% type(key))
l = key.split(MAILBOXDELIMITER)
if l[0].lower() == 'inbox':
l[0] = 'INBOX'
return MAILBOXDELIMITER.join(l)
def __setitem__(self, key, value):
key = self._processKey(key)
self.boxes.__setitem__(key, value)
def __getitem__(self, key):
key = self._processKey(key)
return self.boxes.__getitem__(key)
def __contains__(self, key):
key = self._processKey(key)
return self.boxes.__contains__(key)
#@# to test 'in' with a ref collection you can use any of:
#@# box in self.boxes
#@# box.itsUUID in self.boxes
#@# self.boxes.resolveAlias(key) == box.itsUUID
def has_key(self, key):
key = self._processKey(key)
return self.boxes.has_key(key)
def get(self, key, default=None):
key = self._processKey(key)
return self.boxes.get(key, default)
def keys(self):
return self.boxes.keys()
#@# to get the keys (that is in your case, the aliases of a ref
#@# collection) use self.boxes.iteraliases()
def values(self):
return self.boxes.values()
#@# to get the values of a ref collection (the member items)
#@# use self.boxes.itervalues()
#@# you can also use self.boxes.values() but iteration scales better
#@# and is more responsive as member items are loaded on demand as
#@# opposed to being all loaded at once (if they weren't already loaded).
def items(self):
return self.boxes.items()
#@# same comments as above, use iteration instead of loading-all-at-once
class ChandlerMaildirectory(schema.Item):
mailboxes = schema.One(
IMAPMailboxDict,
doc = 'A list of IMAP mailboxes'
)
#@# it seems to me that the previous class can be eliminated entirely in
#@# favor of a ref collection attribute here
#@#
#@# mailboxes = \
#@# schema.Sequence(ChandlerIMailbox,
#@# otherName='pickOneAndDeclareItOnChandlerIMailbox',
#@# initialValue=[]) # optional
def __init__(self, *args, **kw):
super(ChandlerMaildirectory, self).__init__(*args, **kw)
self.mailboxes = IMAPMailboxDict("mailboxes", self.itsView)
#@# not necessary with the ref collection code
self.mailboxes['INBOX'] = ChandlerIMailbox("INBOX", self.itsView)
#@# replace with
#@# self.mailboxes.append(ChandlerIMailbox("INBOX", parent),
'INBOX')
#@# be sure that the 'parent' item of the mailbox item is something
#@# else than the view or else you're creating a bunch of repository
#@# roots (not recommended).
#@# I don't know how this works in parcel world, maybe 'parent' is
#@# defaulted to something else (question for PJE).
self.watchKind(MailMessageMixin.getKind(self.itsView), "_message_added")
#@# is this mail directory item a singleton ?
#@# if not, this code needs to move elsewhere or you're creating a
#@# bunch of watchers where probably only one is needed
def getMailbox(self, name):
return self.mailboxes[name]
def _message_added(self, op, kind, name):
'''
Called when a new MailMessageMixin enters Chandler.
Annotates the MailMessage with a IMessageAnnotation, gives it a UID,
and packs it into this mailbox.
'''
if op is 'add': #@# BUG
self.mailboxes['INBOX'].addMailMessageMixin(self.itsView.find(name))
#@# will only work randomly, if at all
#@# you need to use "if op == 'add'" instead of 'is'
#@# 'is' compares pointers, '==' compares values (string bytes here)
#@# as in Lisp's eq sv equals
#@# or java's String.equals vs '=='
#@#
#@# just to be sure: in python "foo" and 'foo' are exactly the same
#@# strings, there is no difference in meaning between " and '
#@# both can be used as wished, they're equivalent quotation marks.
#@# with the ref collection code, replace this with
#@# if op == 'add':
#@# self.mailboxes.getByAlias('INBOX').addMailMessageMixin(....)
#@#
#@# the call to self.itsView.find(name) shows that you're indeed
#@# using repository roots. There are several problems with this:
#@# - it's like throwing all your files into /
#@# - there can be only ONE item by a given name under a given
#@# parent (a parent is the view for a root or another item for
#@# everything else)
#@# - we decided a long time ago to not use item intrinsic names
#@# for normal repo operations because at some point you'll hit
#@# the an-item-name-and-location-is-unique problem with
#@# attaching semantics to a name
#@#
#@# Instead, at least use a container item (any item can have
#@# children) so that you're not creating roots
#@# Instead of using item names, use key aliases in a ref collection
#@# again to lookup an item by a name of your choice in a ref
#@# collection if you must use names for lookup.
#@# The advantage of using an alias in a ref collection is that with
#@# an alias you're naming a reference to an item instead of the
#@# item itself and thus are avoiding the name unicity constraint
#@# for the item since an item may have many references (aliased or
#@# not and with different aliases or not) to it but only one name.
def createMailbox(self, name):
if self.mailboxes.has_key(name):
raise imap4.MailboxException, name + " already exists."
#@# has_key is slowly being deprecated in python
#@# use 'in' instead, it's more pythonic
#@# if name in self.mailboxes: ....
#@# or, once you switch to a ref collection
#@# if self.mailboxes.resolveAlias(name): ...
self.mailboxes[name] = ChandlerIMailbox(name,self.itsView)
#@# replace with:
#@# self.mailboxes.append(ChandlerIMailbox(...), name)
#@# (and revisit this root business)
self.itsView.commit()
#@# I don't know enough about the cycle of operations to know
#@# whether this call to commit() belongs here or not
#@# Typically, you want to commit() once you're done with the
#@# highlevel operation in progress, when you're ready to have other
#@# views see your changes.
#@# To use an analogy, if you were writing a web app, you'd probably
#@# commit your changes just before/after sending the HTTP response
#@# back to the client.
#@# In other words, try to separate the committing from the rest.
#@# For example, if you were using a NullRepositoryView (a view that
#@# is useful for testing and is not backed by any persistence
#@# capabilities) your code would break because commit() is not
#@# implemented on it (not even as a NOOP, by design).
#@# Your application's harness should drive the committing and
#@# refreshing not the low-level code itself.
return True
class UserAccount(object):
'''
Implements imap4.IAccount interface to provide an interface into Chandler.
Currently, password handling it pretty poor. Any password is accepted,
and I'm pretty sure any user name is accepted. I'm not really even sure
what to do here. One option would be to make the username and password
settable in the Chandler GUI. This would provide some measure of
security, which might be nice.
Also, most of these operations are just not supported. The concept
of multiple mailboxes will add some complexity, and might want
to be tied in with some notion of organization in Chandler...
'''
implements(imap4.IAccount)
def __init__(self, maildir):
self.maildir = maildir
def _getMailbox(self, name, create=False):
# This function could potentially block due to commits in
# self.maildir.createMailbox()
try:
return self.maildir.getMailbox(name)
except KeyError, e:
if create:
self.maildir.createMailbox(name)
return self.maildir.getMailbox(name)
else:
returnString = "No mailbox " + str(name) + "\n"+ \
"Additionally, the following error was returned:\n" + \
str(e)
raise KeyError, returnString
def select(self, name, rw=False):
return self._getMailbox(name)
def addMailbox(self, name, mbox=None):
if mbox:
# We might want to make sure the mbox is persistable...
self.maildir.mailboxes[name] = mbox
else:
return self.create(name)
def create(self, path):
return self.maildir.createMailbox(path)
def delete(self, name):
raise imap4.MailboxException, "Delete not supported"
#@# with a ref collection, delete becomes trivial to implement since
#@# bi-directional references maintain both their endpoints. If one goes
#@# away, the other one does too
#@# assuming you want to delete the mailbox and its contents, the code
#@# could be as simple as this:
#@# mailbox = self.maildir.mailboxes.getByAlias(name)
#@# for msg in mailbox:
#@# msg.delete()
#@# mailbox.delete()
def rename(self, oldname, newname):
try:
self.maildir.mailboxes[newname] = self.maildir.mailboxes(oldname)
del self.maildir.mailboxes[oldname]
return True
except Exception, e:
raise imap4.MailboxException, e
#@# to rename an alias to a key in a ref collection:
#@# mailboxes = self.maildir.mailboxes
#@# mailboxes.setAlias(mailboxes.resolveAlias(oldName), newName)
def isSubscribed(self, name):
"return a true value if user is subscribed to the mailbox"
return self._getMailbox(name).subscribed
def subscribe(self, name):
self._getMailbox(name).subscribed = True
return True
def unsubscribe(self, name):
self._getMailbox(name).subscribed = False
return True
class DummyIMailbox(object):
implements(imap4.IMailboxInfo)
def getFlags(self):
return [r"\Noselect"]
def getHierarchicalDelimiter(self):
return MAILBOXDELIMITER
def listMailboxes(self, ref, wildcard):
# if ref and wildcard are both "", we should
# return a special dummy mailbox
if ref == "" and wildcard == "":
yield "", self.DummyIMailbox()
#XXX This seems like the right place for refreshing the current view,
# but I'm not really sure.
self.maildir.itsView.refresh()
# Process wildcard into a regular expression
delim = MAILBOXDELIMITER
pathParts = wildcard.split(delim)
if pathParts[0].lower() == "inbox":
pathParts[0] = "INBOX"
wildcard = delim.join(pathParts)
if delim in "?+|().^$[]{}\/":
wildcard = wildcard.replace(delim,
"\\"+ delim)
delim = "\\" + delim
wildcard = wildcard.replace("*", ".*")
wildcard = wildcard.replace("%", "[^"+delim+"]*")
wildcard = "^"+wildcard+"$"
# Create pattern object for matching below
pattern = re.compile(ref + wildcard)
#@# BUG
#@# there is no promise that the keys and the values of a dictionary
#@# are iterated in the same order
#@# Also, when iterating a dict, use the dict's iterators
for name, mailbox in zip(self.maildir.mailboxes.keys(),
self.maildir.mailboxes.values()):
if not pattern.match(name) == None:
yield name, mailbox
#@# instead use iteritems()
#@# for name, mailbox in self.maildir.mailboxes.iteritems():
#@# ....
#@# with a ref collection, this loop would become:
#@# for name in self.maildir.mailboxes.iteraliases():
#@# mailbox = self.maildir.mailboxes.getByAlias(name)
#@# OR better if all items are aliased (as they should be in this case)
#@# for uuid, mailbox in self.maildir.mailboxes.iteritems():
#@# name = mailbox.getAlias(uuid)
#@# OR best in your case here:
#@# mailboxes = self.maildir.mailboxes
#@# for name in mailboxes.iteraliases():
#@# if pattern.match(name) is not None:
#@# yield name, mailboxes.getByAlias(name)
class IMAPServerProtocol(imap4.IMAP4Server):
"""Subclass of imap4.IMAP4Server that adds debugging.
Taken directly from Fettig.
"""
debug = True
def lineReceived(self, line):
if self.debug:
log.debug("CLIENT:" + str(line))
imap4.IMAP4Server.lineReceived(self, line)
def sendLine(self, line):
imap4.IMAP4Server.sendLine(self, line)
if self.debug:
log.debug("SERVER:" + str(line))
class IMAPFactory(protocol.Factory):
"""
Subclass of protocol.Factory.
Taken directly from Fettig.
"""
protocol = IMAPServerProtocol
portal = None
def buildProtocol(self, address):
p = self.protocol()
p.portal = self.portal
p.factory = self
return p
class MailUserRealm(object):
"""
Implementation of portal.IRealm. Like ChandlerIAccount, this
is a dummy method.
Implementing security in this parcel would probably require
modifications of this class as well.
"""
implements(portal.IRealm)
avatarInterfaces = {
imap4.IAccount: UserAccount,
}
def __init__(self, mailDirectory):
self.mailDirectory = mailDirectory
def requestAvatar(self, avatarId, mind, *interfaces):
for requestedInterface in interfaces:
if self.avatarInterfaces.has_key(requestedInterface):
avatarClass = self.avatarInterfaces[requestedInterface]
avatar = avatarClass(self.mailDirectory)
#null logout function: take no arguments and do nothing
logout = lambda: None
return defer.succeed((imap4.IAccount, avatar, logout))
raise KeyError("None of the requested interfaces is supported")
class CredentialsChecker(object):
implements(checkers.ICredentialsChecker)
credentialInterfaces = (credentials.IUsernamePassword,
credentials.IUsernameHashedPassword)
def requestAvatarId(self, credentials):
"""
check to see if the supplied credentials authenticate.
if so, return an 'avatar id', in this case the name of
the IMAP user.
The supplied credentials will implement one of the classes
in self.credentialInterfaces. In this case both
IUsernamePassword and IUsernameHashedPassword have a
checkPassword method that takes the real password and checks
it against the supplied password.
Currently completely neutered.
"""
pwdict = {"chandler":"chandler"}
username = credentials.username
if pwdict.has_key(username):
#@# it's more pythonic to say
#@# if username in pwdict:
realPassword = pwdict[username]
checking = defer.maybeDeferred(
credentials.checkPassword, realPassword)
checking.addCallback(self._checkedPassword, username)
return checking
else:
raise credError.UnauthorizedLogin("No such user")
def _checkedPassword(self, matched, username):
if matched:
return username
else:
raise credError.UnauthorizedLogin("Bad password")
def installParcel(parcel, useVersion=None):
ChandlerMaildirectory.update(parcel, "DefaultMaildirectory")