view 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 source

#!/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)