Mercurial > libervia-backend
view src/plugins/plugin_misc_maildir.py @ 254:9fc32d1d9046
Plugin IMAP, plugin MAILDIR: added IMAP's UID management, mailbox data persistence
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 17 Jan 2011 04:23:31 +0100 |
parents | f45ffbf211e9 |
children | 55b750017b71 |
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={} self.data=host.memory.getPrivate("MAILDIR_data") or {"INBOX":{"cur_idx":0}} #a value in the dictionnary for a mailbox is a dictionnary with the following value # - cur_idx: value of the current unique integer increment (UID) # - message_id (as returned by MaildirMailbox): a tuple of (UID, [flag1, flag2, ...]) #the trigger host.trigger.add("MessageReceived", self.MessageReceivedTrigger) def __destroy__(self): debug('Destroying MaildirBox') self.host.memory.setPrivate('MAILDIR_data',self.data) 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 getUid(self, boxname, message_id): """Return an unique integer, always ascending, for a message This is mainly needed for the IMAP protocol @param boxname: name of the box where the message is @param message_id: unique id of the message as given by MaildirMailbox @return: Integer UID""" try: box_data = self.data[boxname] #the boxname MUST exist in the data except KeyError: err_msg=_("Boxname doesn't exist in internal data") error(_("INTERNAL ERROR: ") + err_msg) raise MaildirError(err_msg) if box_data.has_key(message_id): ret = box_data[message_id][0] else: box_data['cur_idx']+=1 box_data[message_id]=(box_data['cur_idx'],[]) ret = box_data[message_id] self.host.memory.setPrivate('MAILDIR_data',self.data) return ret def getNextUid(self, boxname): """Return next unique integer that will generated This is mainly needed for the IMAP protocol @param boxname: name of the box where the message is @return: Integer UID""" try: box_data = self.data[boxname] #the boxname MUST exist in the data except KeyError: err_msg=_("Boxname doesn't exist in internal data") error(_("INTERNAL ERROR: ") + err_msg) raise MaildirError(err_msg) return box_data['cur_idx']+1 def getNextExistingUid(self, boxname, uid): """Give the next uid of existing message @param boxname: name of the box where the message is @param uid: uid to start from @return: uid or None if the is no more message""" try: box_data = self.data[boxname] #the boxname MUST exist in the data except KeyError: err_msg=_("Boxname doesn't exist in internal data") error(_("INTERNAL ERROR: ") + err_msg) raise MaildirError(err_msg) idx=uid+1 while self.getIdfromUid(boxname, idx) == None: #TODO: this is highly inefficient because getIdfromUid is inefficient, fix this idx+=1 if idx>box_data['cur_idx']: return None return idx def getMaxUid(self, boxname): """Give the max existing uid @param boxname: name of the box where the message is @return: uid""" try: box_data = self.data[boxname] #the boxname MUST exist in the data except KeyError: err_msg=_("Boxname doesn't exist in internal data") error(_("INTERNAL ERROR: ") + err_msg) raise MaildirError(err_msg) return box_data['cur_idx'] def getIdfromUid(self, boxname, message_uid): """Return the message unique id from it's integer UID @param boxname: name of the box where the message is @param message_uid: unique integer identifier @return: unique id of the message as given by MaildirMailbox or None if not found""" try: box_data = self.data[boxname] #the boxname MUST exist in the data except KeyError: err_msg=_("Boxname doesn't exist in internal data") error(_("INTERNAL ERROR: ") + err_msg) raise MaildirError(err_msg) for message_id in box_data.keys(): #TODO: this is highly inefficient on big mailbox, must be replaced in the future if message_id == 'cur_idx': continue if box_data[message_id][0] == message_uid: return message_id return None def cleanTable(self, boxname, existant_id): """Remove mails which no longuer exist from the table @param boxname: name of the box to clean @param existant_id: list of id which actually exist""" try: box_data = self.data[boxname] #the boxname MUST exist in the data except KeyError: err_msg=_("Boxname doesn't exist in internal data") error(_("INTERNAL ERROR: ") + err_msg) raise MaildirError(err_msg) to_remove=[] for key in box_data: if key not in existant_id and key!="cur_idx": to_remove.append(key) for key in to_remove: del box_data[key] 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 self.__uid_table_update() if observer: debug("adding observer for %s" % name) self.maildir.addObserver(observer, name, "NEW_MESSAGE") def __uid_table_update(self): existant_id=[] for mess_idx in range (self.getMessageCount()): #we update the uid table existant_id.append(self.getId(mess_idx)) self.getUid(mess_idx) self.maildir.cleanTable(self.name, existant_id) 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""" self.getUid(self.getMessageCount()-1) #we make an uid for the last message added 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 getUid(self, mess_idx): """Return a unique interger id for the message, always ascending""" mess_id=self.getId(mess_idx) return self.maildir.getUid(self.name,mess_id) def getNextUid(self): return self.maildir.getNextUid(self.name) def getNextExistingUid(self, uid): return self.maildir.getNextExistingUid(self.name, uid) def getMaxUid(self): return self.maildir.getMaxUid(self.name) def getMessageCount(self): """Return number of mails present in this box""" return len(self.mailbox.list) def getMessageIdx(self, mess_idx): """Return the full message @mess_idx: message index""" return self.mailbox.getMessage(mess_idx) def getMessage(self, mess_idx): """Return the full message @mess_idx: message index""" return self.mailbox.getMessage(mess_idx) def getMessageUid(self, mess_uid): """Return the full message @mess_idx: message unique identifier""" for mess_idx in range (self.getMessageCount()): if self.getUid(mess_idx) == mess_uid: return self.mailbox.getMessage(mess_idx) raise IndexError