diff --git a/src/plugins/plugin_misc_imap.py b/src/plugins/plugin_misc_imap.py
new file mode 100755
index 00000000..8a8dcb24
--- /dev/null
+++ b/src/plugins/plugin_misc_imap.py
@@ -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 --git a/src/plugins/plugin_misc_maildir.py b/src/plugins/plugin_misc_maildir.py
new file mode 100755
index 00000000..6e26089c
--- /dev/null
+++ b/src/plugins/plugin_misc_maildir.py
@@ -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)