diff src/plugins/plugin_misc_maildir.py @ 253:f45ffbf211e9

MAILDIR + IMAP plugins: first draft
author Goffi <goffi@goffi.org>
date Mon, 17 Jan 2011 00:15:50 +0100
parents
children 9fc32d1d9046
line wrap: on
line diff
--- /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)