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 [EMAIL PROTECTED] 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 [EMAIL PROTECTED] 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 [EMAIL PROTECTED] 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)
[EMAIL PROTECTED] 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),
) [EMAIL PROTECTED] why not use a UUID ?
[EMAIL PROTECTED] from chandlerdb.util.c import UUID()
[EMAIL PROTECTED] initialValue = UUID()
[EMAIL PROTECTED] also, maybe this should be in __init__() so that
all mailboxes
[EMAIL PROTECTED] don't endup with the same UID
[EMAIL PROTECTED] (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,
)
[EMAIL PROTECTED] 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.""")
[EMAIL PROTECTED] 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
[EMAIL PROTECTED] while committing in another thread IN THE SAME view,
your
[EMAIL PROTECTED] view should really be doing nothing else. Concurrently
[EMAIL PROTECTED] committing in and working with a view is not
supported. In other
[EMAIL PROTECTED] words, views are not really thread safe.
[EMAIL PROTECTED] Hence, I'm not sure the defering of this method to
another
[EMAIL PROTECTED] thread is necessary or even safe.
[EMAIL PROTECTED] Avoiding that may also help you in knowing when to
commit or not
[EMAIL PROTECTED] the operation then becomes more synchronous and you
may have
[EMAIL PROTECTED] knowledge about when the larger operation of
importing mail is
[EMAIL PROTECTED] complete.
[EMAIL PROTECTED] the try/except block above is as good as not having
it since no
[EMAIL PROTECTED] 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
[EMAIL PROTECTED] 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 = {}
)
[EMAIL PROTECTED] YES, instead of making a dict (or a subclass) declare an
attribute
[EMAIL PROTECTED] of cardinality 'list' and give it an inverse (or an
otherName) so as
[EMAIL PROTECTED] to declare a ref collection (a collection of
bi-directional
[EMAIL PROTECTED] references).
[EMAIL PROTECTED] A ref collection is backed in memory by a dict but is
also a
[EMAIL PROTECTED] double-linked list. The dict's keys are the items' UUIDs
but it is
[EMAIL PROTECTED] possible to use a second set of keys for some or all the
member
[EMAIL PROTECTED] items, the key aliases (strings of your choice). When
adding a
[EMAIL PROTECTED] mailbox into this ref collection, assign it a key alias
as well.
[EMAIL PROTECTED]
[EMAIL PROTECTED] for example: self.boxes.append(box, alias)
[EMAIL PROTECTED] self.boxes.getByAlias(alias)
[EMAIL PROTECTED] where 'alias' is the key you intended to use in the
current code
[EMAIL PROTECTED]
[EMAIL PROTECTED] To get more information about the ref collection API see
the
[EMAIL PROTECTED] chandler/util/LinkedMap.py,
chandler/item/RefCollections.py and
[EMAIL PROTECTED] internal/chandlerdb/chandlerdb/util/linkedmap.c files
[EMAIL PROTECTED]
[EMAIL PROTECTED] In order to scale and avoiding dangling references,
collections of
[EMAIL PROTECTED] items should really use bi-directional references or be
abstract.
[EMAIL PROTECTED] 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)
[EMAIL PROTECTED] to test 'in' with a ref collection you can use any of:
[EMAIL PROTECTED] box in self.boxes
[EMAIL PROTECTED] box.itsUUID in self.boxes
[EMAIL PROTECTED] 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()
[EMAIL PROTECTED] to get the keys (that is in your case, the aliases of a
ref
[EMAIL PROTECTED] collection) use self.boxes.iteraliases()
def values(self):
return self.boxes.values()
[EMAIL PROTECTED] to get the values of a ref collection (the member items)
[EMAIL PROTECTED] use self.boxes.itervalues()
[EMAIL PROTECTED] you can also use self.boxes.values() but iteration scales
better
[EMAIL PROTECTED] and is more responsive as member items are loaded on
demand as
[EMAIL PROTECTED] opposed to being all loaded at once (if they weren't
already loaded).
def items(self):
return self.boxes.items()
[EMAIL PROTECTED] 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'
)
[EMAIL PROTECTED] it seems to me that the previous class can be eliminated
entirely in
[EMAIL PROTECTED] favor of a ref collection attribute here
[EMAIL PROTECTED]
[EMAIL PROTECTED] mailboxes = \
[EMAIL PROTECTED] schema.Sequence(ChandlerIMailbox,
[EMAIL PROTECTED]
otherName='pickOneAndDeclareItOnChandlerIMailbox',
[EMAIL PROTECTED] initialValue=[]) # optional
def __init__(self, *args, **kw):
super(ChandlerMaildirectory, self).__init__(*args, **kw)
self.mailboxes = IMAPMailboxDict("mailboxes", self.itsView)
[EMAIL PROTECTED] not necessary with the ref collection code
self.mailboxes['INBOX'] = ChandlerIMailbox("INBOX", self.itsView)
[EMAIL PROTECTED] replace with
[EMAIL PROTECTED] self.mailboxes.append(ChandlerIMailbox("INBOX",
parent),
'INBOX')
[EMAIL PROTECTED] be sure that the 'parent' item of the mailbox item is
something
[EMAIL PROTECTED] else than the view or else you're creating a bunch of
repository
[EMAIL PROTECTED] roots (not recommended).
[EMAIL PROTECTED] I don't know how this works in parcel world, maybe
'parent' is
[EMAIL PROTECTED] defaulted to something else (question for PJE).
self.watchKind(MailMessageMixin.getKind(self.itsView), "_message_added")
[EMAIL PROTECTED] is this mail directory item a singleton ?
[EMAIL PROTECTED] if not, this code needs to move elsewhere or you're
creating a
[EMAIL PROTECTED] 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': [EMAIL PROTECTED] BUG
self.mailboxes['INBOX'].addMailMessageMixin(self.itsView.find(name))
[EMAIL PROTECTED] will only work randomly, if at all
[EMAIL PROTECTED] you need to use "if op == 'add'" instead of 'is'
[EMAIL PROTECTED] 'is' compares pointers, '==' compares values (string
bytes here)
[EMAIL PROTECTED] as in Lisp's eq sv equals
[EMAIL PROTECTED] or java's String.equals vs '=='
[EMAIL PROTECTED]
[EMAIL PROTECTED] just to be sure: in python "foo" and 'foo' are
exactly the same
[EMAIL PROTECTED] strings, there is no difference in meaning between "
and '
[EMAIL PROTECTED] both can be used as wished, they're equivalent
quotation marks.
[EMAIL PROTECTED] with the ref collection code, replace this with
[EMAIL PROTECTED] if op == 'add':
[EMAIL PROTECTED]
self.mailboxes.getByAlias('INBOX').addMailMessageMixin(....)
[EMAIL PROTECTED]
[EMAIL PROTECTED] the call to self.itsView.find(name) shows that you're
indeed
[EMAIL PROTECTED] using repository roots. There are several problems
with this:
[EMAIL PROTECTED] - it's like throwing all your files into /
[EMAIL PROTECTED] - there can be only ONE item by a given name under
a given
[EMAIL PROTECTED] parent (a parent is the view for a root or
another item for
[EMAIL PROTECTED] everything else)
[EMAIL PROTECTED] - we decided a long time ago to not use item
intrinsic names
[EMAIL PROTECTED] for normal repo operations because at some point
you'll hit
[EMAIL PROTECTED] the an-item-name-and-location-is-unique problem
with
[EMAIL PROTECTED] attaching semantics to a name
[EMAIL PROTECTED]
[EMAIL PROTECTED] Instead, at least use a container item (any item can
have
[EMAIL PROTECTED] children) so that you're not creating roots
[EMAIL PROTECTED] Instead of using item names, use key aliases in a ref
collection
[EMAIL PROTECTED] again to lookup an item by a name of your choice in a
ref
[EMAIL PROTECTED] collection if you must use names for lookup.
[EMAIL PROTECTED] The advantage of using an alias in a ref collection
is that with
[EMAIL PROTECTED] an alias you're naming a reference to an item instead
of the
[EMAIL PROTECTED] item itself and thus are avoiding the name unicity
constraint
[EMAIL PROTECTED] for the item since an item may have many references
(aliased or
[EMAIL PROTECTED] 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."
[EMAIL PROTECTED] has_key is slowly being deprecated in python
[EMAIL PROTECTED] use 'in' instead, it's more pythonic
[EMAIL PROTECTED] if name in self.mailboxes: ....
[EMAIL PROTECTED] or, once you switch to a ref collection
[EMAIL PROTECTED] if self.mailboxes.resolveAlias(name): ...
self.mailboxes[name] = ChandlerIMailbox(name,self.itsView)
[EMAIL PROTECTED] replace with:
[EMAIL PROTECTED] self.mailboxes.append(ChandlerIMailbox(...), name)
[EMAIL PROTECTED] (and revisit this root business)
self.itsView.commit()
[EMAIL PROTECTED] I don't know enough about the cycle of operations to
know
[EMAIL PROTECTED] whether this call to commit() belongs here or not
[EMAIL PROTECTED] Typically, you want to commit() once you're done with
the
[EMAIL PROTECTED] highlevel operation in progress, when you're ready to
have other
[EMAIL PROTECTED] views see your changes.
[EMAIL PROTECTED] To use an analogy, if you were writing a web app,
you'd probably
[EMAIL PROTECTED] commit your changes just before/after sending the
HTTP response
[EMAIL PROTECTED] back to the client.
[EMAIL PROTECTED] In other words, try to separate the committing from
the rest.
[EMAIL PROTECTED] For example, if you were using a NullRepositoryView
(a view that
[EMAIL PROTECTED] is useful for testing and is not backed by any
persistence
[EMAIL PROTECTED] capabilities) your code would break because commit()
is not
[EMAIL PROTECTED] implemented on it (not even as a NOOP, by design).
[EMAIL PROTECTED] Your application's harness should drive the
committing and
[EMAIL PROTECTED] 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"
[EMAIL PROTECTED] with a ref collection, delete becomes trivial to
implement since
[EMAIL PROTECTED] bi-directional references maintain both their endpoints.
If one goes
[EMAIL PROTECTED] away, the other one does too
[EMAIL PROTECTED] assuming you want to delete the mailbox and its contents,
the code
[EMAIL PROTECTED] could be as simple as this:
[EMAIL PROTECTED] mailbox = self.maildir.mailboxes.getByAlias(name)
[EMAIL PROTECTED] for msg in mailbox:
[EMAIL PROTECTED] msg.delete()
[EMAIL PROTECTED] 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
[EMAIL PROTECTED] to rename an alias to a key in a ref collection:
[EMAIL PROTECTED] mailboxes = self.maildir.mailboxes
[EMAIL PROTECTED]
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)
[EMAIL PROTECTED] BUG
[EMAIL PROTECTED] there is no promise that the keys and the values of a
dictionary
[EMAIL PROTECTED] are iterated in the same order
[EMAIL PROTECTED] 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
[EMAIL PROTECTED] instead use iteritems()
[EMAIL PROTECTED] for name, mailbox in
self.maildir.mailboxes.iteritems():
[EMAIL PROTECTED] ....
[EMAIL PROTECTED] with a ref collection, this loop would become:
[EMAIL PROTECTED] for name in self.maildir.mailboxes.iteraliases():
[EMAIL PROTECTED] mailbox = self.maildir.mailboxes.getByAlias(name)
[EMAIL PROTECTED] OR better if all items are aliased (as they should be
in this case)
[EMAIL PROTECTED] for uuid, mailbox in
self.maildir.mailboxes.iteritems():
[EMAIL PROTECTED] name = mailbox.getAlias(uuid)
[EMAIL PROTECTED] OR best in your case here:
[EMAIL PROTECTED] mailboxes = self.maildir.mailboxes
[EMAIL PROTECTED] for name in mailboxes.iteraliases():
[EMAIL PROTECTED] if pattern.match(name) is not None:
[EMAIL PROTECTED] 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):
[EMAIL PROTECTED] it's more pythonic to say
[EMAIL PROTECTED] 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")
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
Open Source Applications Foundation "chandler-dev" mailing list
http://lists.osafoundation.org/mailman/listinfo/chandler-dev