# HG changeset patch # User Goffi # Date 1295219750 -3600 # Node ID f45ffbf211e945baffe9fa03a8c363f992fe91ab # Parent c09aa319712e2786e50507404aa832fcb418491c MAILDIR + IMAP plugins: first draft diff -r c09aa319712e -r f45ffbf211e9 src/plugins/plugin_misc_imap.py --- /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 . +""" + +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 = """ + + + + + + + + """ + + 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 diff -r c09aa319712e -r f45ffbf211e9 src/plugins/plugin_misc_maildir.py --- /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 . +""" + +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)