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

Reply via email to