changeset 253:f45ffbf211e9

MAILDIR + IMAP plugins: first draft
author Goffi <goffi@goffi.org>
date Mon, 17 Jan 2011 00:15:50 +0100
parents c09aa319712e
children 9fc32d1d9046
files src/plugins/plugin_misc_imap.py src/plugins/plugin_misc_maildir.py
diffstat 2 files changed, 596 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/plugin_misc_imap.py	Mon Jan 17 00:15:50 2011 +0100
@@ -0,0 +1,373 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+SAT plugin for managing imap server
+Copyright (C) 2011  Jérôme Poisson (goffi@goffi.org)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+
+from logging import debug, info, error
+import warnings
+from twisted.internet import protocol
+from twisted.words.protocols.jabber import error as jab_error
+from twisted.cred import portal,checkers
+from twisted.mail import imap4
+from email.parser import Parser
+import email.message
+import os,os.path
+from cStringIO import StringIO
+from twisted.internet import reactor
+import pdb
+
+
+from zope.interface import implements
+
+
+PLUGIN_INFO = {
+"name": "IMAP server Plugin",
+"import_name": "IMAP",
+"type": "Misc",
+"protocols": [],
+"dependencies": ["Maildir"],
+"main": "IMAP_server",
+"handler": "no",
+"description": _("""Create an Imap server that you can use to read your "normal" type messages""")
+}
+
+class IMAP_server():
+    
+    params = """
+    <params>
+    <general>
+    <category name="IMAP Server">
+        <param name="Port" value="10143" type="string" />
+    </category>
+    </general>
+    </params>
+    """
+
+    def __init__(self, host):
+        info(_("Plugin Imap Server initialization"))
+        self.host = host
+        
+        #parameters
+        host.memory.importParams(self.params)
+
+        port = int(self.host.memory.getParamA("Port", "IMAP Server"))
+        info(_("Launching IMAP server on port %d"), port)
+        
+        self.server_factory = ImapServerFactory(self.host)
+        reactor.listenTCP(port, self.server_factory)
+
+class Message():
+    implements(imap4.IMessage)
+
+    def __init__(self, uid, mess_fp):
+        debug('Message Init')
+        self.uid=uid
+        self.mess_fp=mess_fp
+        self.message=Parser().parse(mess_fp)
+    
+    def getUID(self):
+        """Retrieve the unique identifier associated with this message.
+        """
+        debug('getUID (message)')
+        return self.uid
+
+    def getFlags(self):
+        """Retrieve the flags associated with this message.
+        @return: The flags, represented as strings.
+        """
+        debug('getFlags')
+        return []
+
+    def getInternalDate(self):
+        """Retrieve the date internally associated with this message.
+        @return: An RFC822-formatted date string.
+        """
+        debug('getInternalDate')
+        return self.message['Date']
+
+
+    def getHeaders(self, negate, *names):
+        """Retrieve a group of message headers.
+        @param names: The names of the headers to retrieve or omit.
+        @param negate: If True, indicates that the headers listed in names
+        should be omitted from the return value, rather than included.
+        @return: A mapping of header field names to header field values
+        """
+        debug('getHeaders %s - %s' % (negate, names))
+        final_dict={}
+        to_check=[name.lower() for name in names]
+        for header in self.message.keys():
+            if (negate and not header.lower() in to_check) or \
+              (not negate and header.lower() in to_check):
+                final_dict[header]=self.message[header]
+        return final_dict
+
+    def getBodyFile(self):
+        """Retrieve a file object containing only the body of this message.
+        """
+        debug('getBodyFile')
+        return StringIO(self.message.get_payload())
+
+    def getSize(self):
+        """Retrieve the total size, in octets, of this message.
+        """
+        debug('getSize')
+        self.mess_fp.seek(0,os.SEEK_END)
+        return self.mess_fp.tell()
+
+
+    def isMultipart(self):
+        """Indicate whether this message has subparts.
+        """
+        debug('isMultipart')
+        return False
+
+    def getSubPart(self,part):
+        """Retrieve a MIME sub-message
+        @param part: The number of the part to retrieve, indexed from 0.
+        @return: The specified sub-part.
+        """
+        debug('getSubPart')
+        return TypeError
+
+
+class SatMailbox:
+    implements(imap4.IMailbox)
+
+    def __init__(self,host,name):
+        self.host = host
+        self.listeners=set()
+        debug ('Mailbox init (%s)', name)
+        if name!="INBOX":
+            raise imap4.MailboxException("Only INBOX is managed for the moment")
+        self.name=name
+        self.mailbox=self.host.plugins["Maildir"].accessMessageBox("INBOX",self.newMessage)
+
+    def newMessage(self):
+        """Called when a new message is in the mailbox"""
+        debug ("newMessage signal received")
+        nb_messages=self.getMessageCount()
+        for listener in self.listeners:
+            listener.newMessages(nb_messages,None)
+
+    def getUIDValidity(self):
+        """Return the unique validity identifier for this mailbox.
+        """
+        debug ('getUIDValidity')
+        return 0
+
+    def getUIDNext(self):
+        """Return the likely UID for the next message added to this mailbox.
+        """
+        debug ('getUIDNext')
+        return self.getMessageCount()+1
+
+    def getUID(self,message):
+        """Return the UID of a message in the mailbox
+        @param message: The message sequence number
+        @return: The UID of the message.
+        """
+        debug ('getUID')
+        return self.mailbox.getId(message)
+
+    def getMessageCount(self):
+        """Return the number of messages in this mailbox.
+        """
+        debug('getMessageCount')
+        ret = self.mailbox.getMessageCount()
+        debug("count = %i" % ret)
+        return ret
+
+    def getRecentCount(self):
+        """Return the number of messages with the 'Recent' flag.
+        """
+        debug('getRecentCount')
+        return 0
+
+    def getUnseenCount(self):
+        """Return the number of messages with the 'Unseen' flag.
+        """
+        debug('getUnseenCount')
+        return 1
+
+    def isWriteable(self):
+        """Get the read/write status of the mailbox.
+        @return: A true value if write permission is allowed, a false value otherwise.
+        """
+        debug('isWriteable')
+        return True
+
+    def destroy(self):
+        """Called before this mailbox is deleted, permanently.
+        """
+        debug('destroy')
+        
+
+    def requestStatus(self, names):
+        """Return status information about this mailbox.
+        @param names: The status names to return information regarding.
+        The possible values for each name are: MESSAGES, RECENT, UIDNEXT,
+        UIDVALIDITY, UNSEEN.
+        @return: A dictionary containing status information about the
+        requested names is returned.  If the process of looking this
+        information up would be costly, a deferred whose callback will
+        eventually be passed this dictionary is returned instead.
+        """
+        debug('requestStatus')
+        return imap4.statusRequestHelper(self, names)
+
+    def addListener(self, listener):
+        """Add a mailbox change listener
+
+        @type listener: Any object which implements C{IMailboxListener}
+        @param listener: An object to add to the set of those which will
+        be notified when the contents of this mailbox change.
+        """
+        debug('addListener %s' % listener)
+        self.listeners.add(listener)
+
+    def removeListener(self, listener):
+        """Remove a mailbox change listener
+
+        @type listener: Any object previously added to and not removed from
+        this mailbox as a listener.
+        @param listener: The object to remove from the set of listeners.
+
+        @raise ValueError: Raised when the given object is not a listener for
+        this mailbox.
+        """
+        debug('removeListener')
+        if listener in self.listeners:
+            self.listeners.remove(listener)
+        else:
+            raise imap4.MailboxException('Trying to remove an unknown listener')
+
+    def addMessage(self, message, flags = (), date = None):
+        """Add the given message to this mailbox.
+        @param message: The RFC822 formatted message
+        @param flags: The flags to associate with this message
+        @param date: If specified, the date to associate with this
+        @return: A deferred whose callback is invoked with the message
+        id if the message is added successfully and whose errback is
+        invoked otherwise.
+        """
+        debug('addMessage')
+        raise NotImplementedError
+
+    def expunge(self):
+        """Remove all messages flagged \\Deleted.
+        @return: The list of message sequence numbers which were deleted,
+        or a Deferred whose callback will be invoked with such a list.
+        """
+        debug('expunge')
+        raise NotImplementedError
+
+    def fetch(self, messages, uid):
+        """Retrieve one or more messages.
+        @param messages: The identifiers of messages to retrieve information
+        about
+        @param uid: If true, the IDs specified in the query are UIDs;
+        """
+        debug('fetch (%s, %s)'%(messages,uid))
+        messages.last = self.getMessageCount()
+        for mes_idx in messages:
+            if mes_idx>self.getMessageCount():
+                continue
+            yield (mes_idx,Message(mes_idx,self.mailbox.getMessage(mes_idx-1)))
+
+    def store(self, messages, flags, mode, uid):
+        """Set the flags of one or more messages.
+        @param messages: The identifiers of the messages to set the flags of.
+        @param flags: The flags to set, unset, or add.
+        @param mode: If mode is -1, these flags should be removed from the
+        specified messages.  If mode is 1, these flags should be added to
+        the specified messages.  If mode is 0, all existing flags should be
+        cleared and these flags should be added.
+        @param uid: If true, the IDs specified in the query are UIDs;
+        otherwise they are message sequence IDs.
+        @return: A dict mapping message sequence numbers to sequences of str
+        representing the flags set on the message after this operation has
+        been performed, or a Deferred whose callback will be invoked with
+        such a dict.
+        """
+        debug('store')
+        raise NotImplementedError
+
+    def getFlags(self):
+        """Return the flags defined in this mailbox
+        Flags with the \\ prefix are reserved for use as system flags.
+        @return: A list of the flags that can be set on messages in this mailbox.
+        """
+        debug('getFlags')
+        #return ['\Seen','\Answered','\Flagged','\Deleted','\Draft', '\Recent']
+        return []
+
+    def getHierarchicalDelimiter(self):
+        """Get the character which delimits namespaces for in this mailbox.
+        """
+        debug('getHierarchicalDelimiter')
+        return '.'
+
+
+
+class ImapAccount(imap4.MemoryAccount):
+    #implements(imap4.IAccount)
+    # Actually implement the interface here
+
+    def __init__(self, host, name):
+        debug("ImapAccount init")
+        self.host=host
+        imap4.MemoryAccount.__init__(self,name)
+        self.addMailbox("Inbox") #We only manage Inbox for the moment
+        debug ('INBOX added')
+
+    def _emptyMailbox(self, name, id):
+        return SatMailbox(self.host,name)
+
+
+class ImapRealm:
+    implements(portal.IRealm)
+
+    def __init__(self,host):
+        self.host = host
+
+    def requestAvatar(self, avatarID, mind, *interfaces):
+        debug('requestAvatar')
+        if imap4.IAccount not in interfaces:
+            raise NotImplementedError
+        return imap4.IAccount, ImapAccount(self.host,avatarID), lambda:None
+
+class ImapServerFactory(protocol.ServerFactory):
+    protocol = imap4.IMAP4Server
+
+    def __init__(self, host):
+        self.host=host
+
+    def startedConnecting(self, connector):
+        debug (_("IMAP server connection started"))
+
+    def clientConnectionLost(self, connector, reason):
+        debug (_("IMAP server connection lost (reason: %s)"), reason)
+
+    def buildProtocol(self, addr):
+        debug ("Building protocole")
+        prot = protocol.ServerFactory.buildProtocol(self, addr)
+        prot.portal = portal.Portal(ImapRealm(self.host))
+        prot.portal.registerChecker(checkers.InMemoryUsernamePasswordDatabaseDontUse(goffi="toto"))
+        return prot
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/plugins/plugin_misc_maildir.py	Mon Jan 17 00:15:50 2011 +0100
@@ -0,0 +1,223 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+SAT plugin for managing imap server
+Copyright (C) 2011  Jérôme Poisson (goffi@goffi.org)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+
+from logging import debug, info, error
+import warnings
+warnings.filterwarnings('ignore','the MimeWriter',DeprecationWarning,'twisted' ) #FIXME: to be removed, see http://twistedmatrix.com/trac/ticket/4038
+from twisted.internet import protocol
+from twisted.words.protocols.jabber import error as jab_error
+from twisted.cred import portal,checkers
+from twisted.mail import imap4,maildir
+from email.parser import Parser
+import email.message
+from email.charset import Charset
+import os,os.path
+from cStringIO import StringIO
+from twisted.internet import reactor
+import pdb
+
+
+from zope.interface import implements
+
+
+PLUGIN_INFO = {
+"name": "Maildir Plugin",
+"import_name": "Maildir",
+"type": "Misc",
+"protocols": [],
+"dependencies": [],
+"main": "MaildirBox",
+"handler": "no",
+"description": _("""Intercept "normal" type messages, and put them in a Maildir type box""")
+}
+
+MAILDIR_PATH = "Maildir"
+
+class MaildirError(Exception):
+    pass
+
+class MaildirBox():
+    
+    def __init__(self, host):
+        info(_("Plugin Maildir initialization"))
+        self.host = host
+
+        self.__observed={}
+        self.__mailboxes={}
+
+        #the trigger
+        host.trigger.add("MessageReceived", self.MessageReceivedTrigger)
+
+    def accessMessageBox(self, boxname, observer=None):
+        """Create and return a MailboxUser instance
+        @param boxname: name of the box
+        @param observer: method to call when a NewMessage arrive"""
+        if not self.__mailboxes.has_key(boxname):
+            self.__mailboxes[boxname]=MailboxUser(self, boxname, observer)
+        else:
+            if observer:
+                self.addObserver(observer, boxname)
+        return self.__mailboxes[boxname]
+
+    def _removeBoxAccess(self, boxname, mailboxUser):
+        """Remove a reference to a box
+        @param name: name of the box
+        @param mailboxUser: MailboxUser instance"""
+        if not self.__mailboxes.has_key(boxname):
+            err_msg=_("Trying to remove an mailboxUser not referenced")
+            error(_("INTERNAL ERROR: ") + err_msg)
+            raise MaildirError(err_msg)
+        assert self.__mailboxes[boxname]==mailboxUser
+        del __mailboxes[boxname]
+
+    def _checkBoxReference(self, boxname):
+        """Check if there is a reference on a box, and return it
+        @param boxname: name of the box to check
+        @return: MailboxUser instance or None"""
+        if self.__mailboxes.has_key(boxname):
+            return self.__mailboxes[boxname]
+
+
+
+    def MessageReceivedTrigger(self, message):
+        """This trigger catch normal message and put the in the Maildir box.
+        If the message is not of "normal" type, do nothing
+        @param message: message xmlstrem
+        @return: False if it's a normal message, True else"""
+        for e in message.elements():
+            if e.name == "body":
+                type = message['type'] if message.hasAttribute('type') else 'chat' #FIXME: check specs
+                if message['type'] != 'normal':
+                    return True
+                self.accessMessageBox("INBOX").addMessage(message)
+                return False
+
+    def addObserver(self, callback, boxname, signal="NEW_MESSAGE"):
+        """Add an observer for maildir box changes
+        @param callback: method to call when the the box is updated
+        @param boxname: name of the box to observe
+        @param signal: which signal is observed by the caller"""
+        if not self.__observed.has_key(boxname):
+            self.__observed[boxname]={}
+        if not self.__observed[boxname].has_key(signal):
+            self.__observed[boxname][signal]=set()
+        self.__observed[boxname][signal].add(callback)
+
+    def removeObserver(self, callback, boxname, signal="NEW_MESSAGE"):
+        """Remove an observer of maildir box changes
+        @param callback: method to remove from obervers
+        @param boxname: name of the box which was observed
+        @param signal: which signal was observed by the caller"""
+        if not self.__observed.has_key(boxname):
+            err_msg=_("Trying to remove an observer for an inexistant mailbox")
+            error(_("INTERNAL ERROR: ") + err_msg)
+            raise MaildirError(err_msg)
+        if not self.__observed[boxname].has_key(signal):
+            err_msg=_("Trying to remove an inexistant observer, no observer for this signal")
+            error(_("INTERNAL ERROR: ") + err_msg)
+            raise MaildirError(err_msg)
+        if not callback in self.__observed[boxname][signal]:
+            err_msg=_("Trying to remove an inexistant observer")
+            error(_("INTERNAL ERROR: ") + err_msg)
+            raise MaildirError(err_msg)
+        self.__observed[boxname][signal].remove(callback)
+
+    def emitSignal(self, boxname, signal_name):
+        """Emit the signal to observer"""
+        debug('emitSignal %s %s' %(boxname, signal_name))
+        try:
+            for observer_cb in self.__observed[boxname][signal_name]:
+                observer_cb()
+        except KeyError:
+            pass
+
+
+class MailboxUser:
+    """This class is used to access a mailbox"""
+
+    def xmppMessage2mail(self, message):
+        """Convert the XMPP's XML message to a basic rfc2822 message
+        @param xml: domish.Element of the message
+        @return: string email"""
+        mail = email.message.Message()
+        mail['MIME-Version'] = "1.0"
+        mail['Content-Type'] = "text/plain; charset=UTF-8; format=flowed"
+        mail['Content-Transfer-Encoding'] = "8bit"
+        mail['From'] = message['from'].encode('utf-8')
+        mail['To'] = message['to'].encode('utf-8')
+        mail['Date'] = email.utils.formatdate().encode('utf-8')
+        #TODO: save thread id
+        for e in message.elements():
+            if e.name == "body":
+                mail.set_payload(e.children[0].encode('utf-8'))
+            elif e.name == "subject":
+                mail['Subject'] =  e.children[0].encode('utf-8')
+        return mail.as_string()
+    
+    def __init__(self, _maildir, name, observer=None):
+        """@param _maildir: the main MaildirBox instance
+           @param name: name of the mailbox
+           THIS OBJECT MUST NOT BE USED DIRECTLY: use MaildirBox.accessMessageBox instead"""
+        if _maildir._checkBoxReference(self):
+            error ("INTERNAL ERROR: MailboxUser MUST NOT be instancied directly")
+            raise MailboxException('double MailboxUser instanciation')
+        if name!="INBOX":
+            raise NotImplementedError
+        self.name=name
+        self.maildir=_maildir
+        mailbox_path = os.path.expanduser(os.path.join(self.maildir.host.get_const('local_dir'), MAILDIR_PATH))
+        self.mailbox_path=mailbox_path
+        self.mailbox = maildir.MaildirMailbox(mailbox_path)
+        self.observer=observer
+        if observer:
+            debug("adding observer for %s" % name)
+            self.maildir.addObserver(observer, name, "NEW_MESSAGE")
+
+    def __destroy__(self):
+        if observer:
+            debug("removing observer for %s" % self.name)
+            self._maildir.removeObserver(observer, self.name, "NEW_MESSAGE")
+        self._maildir._removeBoxAccess(self.name, self)
+
+    def addMessage(self, message):
+        """Add a message to the box
+        @param message: XMPP XML message"""
+        self.mailbox.appendMessage(self.xmppMessage2mail(message)).addCallback(self.emitSignal, "NEW_MESSAGE")
+
+    def emitSignal(self, ignore, signal):
+        """Emit the signal to the observers"""
+        print ('self: %s, mailbox: %s, count: %i' % (self, self.mailbox, self.getMessageCount()))
+        self.maildir.emitSignal(self.name, signal)
+
+    def getId(self, mess_idx):
+        """Return the Unique ID of the message
+        @mess_idx: message index"""
+        return self.mailbox.getUidl(mess_idx)
+
+    def getMessageCount(self):
+        """Return number of mails present in this box"""
+        print "count: %i" % len(self.mailbox.listMessages())
+        return len(self.mailbox.listMessages())
+
+    def getMessage(self, mess_idx):
+        """Return the full message
+        @mess_idx: message index"""
+        return self.mailbox.getMessage(mess_idx)