# HG changeset patch # User Goffi # Date 1295308646 -3600 # Node ID 012c38b56cddf08872067e6b128df11155e8a0d4 # Parent f5181f6dd98faf504732137b04f38098486f4763 plugin IMAP, plugin Maildir: profile management - IMAP Login/pass is now checked against profile name/jabber pass - Mailboxes are now per-profile - flags are now checked without case sensitiveness diff -r f5181f6dd98f -r 012c38b56cdd src/plugins/plugin_misc_imap.py --- a/src/plugins/plugin_misc_imap.py Tue Jan 18 00:51:47 2011 +0100 +++ b/src/plugins/plugin_misc_imap.py Tue Jan 18 00:57:26 2011 +0100 @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ -SAT plugin for managing imap server +SàT 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 @@ -21,9 +21,10 @@ from logging import debug, info, error import warnings -from twisted.internet import protocol +from twisted.internet import protocol,defer from twisted.words.protocols.jabber import error as jab_error -from twisted.cred import portal,checkers +from twisted.cred import portal,checkers,credentials +from twisted.cred import error as cred_error from twisted.mail import imap4 from email.parser import Parser import email.message @@ -86,7 +87,6 @@ """Retrieve the unique identifier associated with this message. """ debug('getUID (message)') - debug ('===>%i', self.uid) return self.uid def getFlags(self): @@ -152,14 +152,13 @@ class SatMailbox: implements(imap4.IMailbox) - def __init__(self,host,name): + def __init__(self,host,name,profile): 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) + self.mailbox=self.host.plugins["Maildir"].accessMessageBox(name,self.newMessage, profile) def newMessage(self): """Called when a new message is in the mailbox""" @@ -207,7 +206,7 @@ """Return the number of messages with the 'Unseen' flag. """ debug('getUnseenCount') - return self.getMessageCount()-len(self.mailbox.getMessageIdsWithFlag('\\Seen')) + return self.getMessageCount()-len(self.mailbox.getMessageIdsWithFlag('\\SEEN')) def isWriteable(self): """Get the read/write status of the mailbox. @@ -271,7 +270,7 @@ invoked otherwise. """ debug('addMessage') - raise NotImplementedError + raise imap4.MailboxException("Client message addition not implemented yet") def expunge(self): """Remove all messages flagged \\Deleted. @@ -323,6 +322,8 @@ """ debug('store') + flags=[flag.upper() for flag in flags] + def updateFlags(getF,setF): ret = {} for mess_id in messages: @@ -335,16 +336,27 @@ _flags.update(set(flags)) new_flags=list(_flags) setF(mess_id, new_flags) - ret[mess_id] = new_flags + ret[mess_id] = tuple(new_flags) return ret if uid: messages.last = self.mailbox.getMaxUid() messages.getnext = self.mailbox.getNextExistingUid - return updateFlags(self.mailbox.getFlagsUid,self.mailbox.setFlagsUid) + ret = updateFlags(self.mailbox.getFlagsUid,self.mailbox.setFlagsUid) + for listener in self.listeners: + listener.flagsChanged(ret) + return ret + else: messages.last = self.getMessageCount() - return updateFlags(self.mailbox.getFlags,self.mailbox.setFlags) + ret = updateFlags(self.mailbox.getFlags,self.mailbox.setFlags) + newFlags={} + for idx in ret: + #we have to convert idx to uid for the listeners + newFlags[self.mailbox.getUid(idx)] = ret[idx] + for listener in self.listeners: + listener.flagsChanged(newFlags) + return ret def getFlags(self): """Return the flags defined in this mailbox @@ -352,7 +364,7 @@ @return: A list of the flags that can be set on messages in this mailbox. """ debug('getFlags') - return ['\Seen','\Answered','\Flagged','\Deleted','\Draft'] #TODO: add '\Recent' + return ['\\SEEN','\\ANSWERED','\\FLAGGED','\\DELETED','\\DRAFT'] #TODO: add '\\RECENT' def getHierarchicalDelimiter(self): """Get the character which delimits namespaces for in this mailbox. @@ -360,21 +372,19 @@ debug('getHierarchicalDelimiter') return '.' - - -class ImapAccount(imap4.MemoryAccount): +class ImapSatAccount(imap4.MemoryAccount): #implements(imap4.IAccount) - # Actually implement the interface here - def __init__(self, host, name): + def __init__(self, host, profile): debug("ImapAccount init") self.host=host - imap4.MemoryAccount.__init__(self,name) + self.profile=profile + imap4.MemoryAccount.__init__(self,profile) self.addMailbox("Inbox") #We only manage Inbox for the moment debug ('INBOX added') def _emptyMailbox(self, name, id): - return SatMailbox(self.host,name) + return SatMailbox(self.host,name,self.profile) class ImapRealm: @@ -385,9 +395,40 @@ def requestAvatar(self, avatarID, mind, *interfaces): debug('requestAvatar') + profile=avatarID.decode('utf-8') if imap4.IAccount not in interfaces: raise NotImplementedError - return imap4.IAccount, ImapAccount(self.host,avatarID), lambda:None + return imap4.IAccount, ImapSatAccount(self.host,profile), lambda:None + +class SatProfileCredentialChecker: + """ + This credential checker check against SàT's profile and associated jabber's password + Check if the profile exists, and if the password is OK + Return the profile as avatarId + """ + implements(checkers.ICredentialsChecker) + credentialInterfaces = (credentials.IUsernamePassword, + credentials.IUsernameHashedPassword) + + + def __init__(self, host): + self.host = host + + def _cbPasswordMatch(self, matched, profile): + if matched: + return profile.encode('utf-8') + else: + return failure.Failure(cred_error.UnauthorizedLogin()) + + def requestAvatarId(self, credentials): + profiles = self.host.memory.getProfilesList() + if not credentials.username in profiles: + return defer.fail(cred_error.UnauthorizedLogin()) + password = self.host.memory.getParamA("Password", "Connection", profile_key=credentials.username) + return defer.maybeDeferred( + credentials.checkPassword, + password).addCallback( + self._cbPasswordMatch, credentials.username) class ImapServerFactory(protocol.ServerFactory): protocol = imap4.IMAP4Server @@ -405,5 +446,5 @@ debug ("Building protocole") prot = protocol.ServerFactory.buildProtocol(self, addr) prot.portal = portal.Portal(ImapRealm(self.host)) - prot.portal.registerChecker(checkers.InMemoryUsernamePasswordDatabaseDontUse(goffi="toto")) + prot.portal.registerChecker(SatProfileCredentialChecker(self.host)) return prot diff -r f5181f6dd98f -r 012c38b56cdd src/plugins/plugin_misc_maildir.py --- a/src/plugins/plugin_misc_maildir.py Tue Jan 18 00:51:47 2011 +0100 +++ b/src/plugins/plugin_misc_maildir.py Tue Jan 18 00:57:26 2011 +0100 @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ -SAT plugin for managing imap server +SàT plugin for managing Maildir type mail boxes Copyright (C) 2011 Jérôme Poisson (goffi@goffi.org) This program is free software: you can redistribute it and/or modify @@ -33,6 +33,7 @@ from cStringIO import StringIO from twisted.internet import reactor import pdb +from sat.exceptions import * from zope.interface import implements @@ -61,8 +62,9 @@ self.host = host self.__observed={} - self.__mailboxes={} - self.data=host.memory.getPrivate("MAILDIR_data") or {"INBOX":{"cur_idx":0}} + pList=host.memory.getProfilesList #shorter :) + self.__mailboxes=dict(zip(pList(),len(pList())*[{}])) + self.data=host.memory.getPrivate("MAILDIR_data") or dict(zip(pList(),len(pList())*[{"INBOX":{"cur_idx":0}}])) #Create empty box for each profile #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, ...]) @@ -74,18 +76,26 @@ debug('Destroying MaildirBox') self.host.memory.setPrivate('MAILDIR_data',self.data) - def accessMessageBox(self, boxname, observer=None): + def accessMessageBox(self, boxname, observer=None, profile_key='@DEFAULT@'): """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) + profile = self.host.memory.getProfileName(profile_key) + if not profile: + raise ProfileUnknownError + if not self.__mailboxes[profile].has_key(boxname): + self.__mailboxes[profile][boxname]=MailboxUser(self, boxname, observer, profile=profile) else: if observer: - self.addObserver(observer, boxname) - return self.__mailboxes[boxname] + self.addObserver(observer, profile, boxname) + return self.__mailboxes[profile][boxname] - def _removeBoxAccess(self, boxname, mailboxUser): + def _getProfilePath(self, profile): + """Return a unique path for profile's mailbox + The path must be unique, usable as a dir name, and bijectional""" + return profile.replace('/','_').replace('..','_') #FIXME: this is too naive to work well, must be improved + + def _removeBoxAccess(self, boxname, mailboxUser, profile): """Remove a reference to a box @param name: name of the box @param mailboxUser: MailboxUser instance""" @@ -93,32 +103,33 @@ 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] + assert self.__mailboxes[profile][boxname]==mailboxUser + del __mailboxes[profile][boxname] - def _checkBoxReference(self, boxname): + def _checkBoxReference(self, boxname, profile): """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] + if self.__mailboxes.has_key(profile): + if self.__mailboxes[profile].has_key(boxname): + return self.__mailboxes[profile][boxname] - def __getBoxData(self, boxname): + def __getBoxData(self, boxname, profile): """Return the date of a box""" try: - return self.data[boxname] #the boxname MUST exist in the data + return self.data[profile][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) - def getUid(self, boxname, message_id): + def getUid(self, boxname, message_id, profile): """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""" - box_data = self.__getBoxData(boxname) + box_data = self.__getBoxData(boxname, profile) if box_data.has_key(message_id): ret = box_data[message_id][0] else: @@ -128,40 +139,40 @@ self.host.memory.setPrivate('MAILDIR_data',self.data) return ret - def getNextUid(self, boxname): + def getNextUid(self, boxname, profile): """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""" - box_data = self.__getBoxData(boxname) + box_data = self.__getBoxData(boxname, profile) return box_data['cur_idx']+1 - def getNextExistingUid(self, boxname, uid): + def getNextExistingUid(self, boxname, uid, profile): """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""" - box_data = self.__getBoxData(boxname) + box_data = self.__getBoxData(boxname, profile) idx=uid+1 - while self.getIdFromUid(boxname, idx) == None: #TODO: this is highly inefficient because getIdfromUid is inefficient, fix this + while self.getIdFromUid(boxname, idx, profile) == 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): + def getMaxUid(self, boxname, profile): """Give the max existing uid @param boxname: name of the box where the message is @return: uid""" - box_data = self.__getBoxData(boxname) + box_data = self.__getBoxData(boxname, profile) return box_data['cur_idx'] - def getIdFromUid(self, boxname, message_uid): + def getIdFromUid(self, boxname, message_uid, profile): """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""" - box_data = self.__getBoxData(boxname) + box_data = self.__getBoxData(boxname, profile) 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 @@ -169,36 +180,38 @@ return message_id return None - def getFlags(self, boxname, mess_id): + def getFlags(self, boxname, mess_id, profile): """Return the messages flags @param boxname: name of the box where the message is @param message_idx: message id as given by MaildirMailbox @return: list of strings""" - box_data = self.__getBoxData(boxname) + box_data = self.__getBoxData(boxname, profile) if not box_data.has_key(mess_id): raise MailboxException("Trying to get flags from an unexisting message") return box_data[mess_id][1] - def setFlags(self, boxname, mess_id, flags): + def setFlags(self, boxname, mess_id, flags, profile): """Change the flags of the message @param boxname: name of the box where the message is @param message_idx: message id as given by MaildirMailbox @param flags: list of strings """ - box_data = self.__getBoxData(boxname) + box_data = self.__getBoxData(boxname, profile) assert(type(flags)==list) + flags=[flag.upper() for flag in flags] #we store every flag UPPERCASE if not box_data.has_key(mess_id): raise MailboxException("Trying to set flags for an unexisting message") box_data[mess_id][1]=flags self.host.memory.setPrivate('MAILDIR_data',self.data) - def getMessageIdsWithFlag(self, boxname, flag): + def getMessageIdsWithFlag(self, boxname, flag, profile): """Return ids of messages where a flag is set @param boxname: name of the box where the message is @param flag: flag to check @return: list of id (as given by MaildirMailbox)""" - box_data = self.__getBoxData(boxname) + box_data = self.__getBoxData(boxname, profile) assert(isinstance(flag,basestring)) + flag=flag.upper() result = [] for key in box_data: if key=='cur_idx': @@ -207,20 +220,20 @@ result.append(key) return result - def purgeDeleted(self, boxname): + def purgeDeleted(self, boxname, profile): """Remove data for messages with flag "\\Deleted" @param boxname: name of the box where the message is """ - box_data = self.__getBoxData(boxname) - for mess_id in self.getMessageIdsWithFlag(boxname,"\\Deleted"): + box_data = self.__getBoxData(boxname, profile) + for mess_id in self.getMessageIdsWithFlag(boxname,"\\Deleted", profile): del(box_data[mess_id]) self.host.memory.setPrivate('MAILDIR_data',self.data) - def cleanTable(self, boxname, existant_id): + def cleanTable(self, boxname, existant_id, profile): """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""" - box_data = self.__getBoxData(boxname) + box_data = self.__getBoxData(boxname, profile) to_remove=[] for key in box_data: if key not in existant_id and key!="cur_idx": @@ -229,7 +242,7 @@ del box_data[key] - def MessageReceivedTrigger(self, message): + def MessageReceivedTrigger(self, message, profile): """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 @@ -239,44 +252,44 @@ type = message['type'] if message.hasAttribute('type') else 'chat' #FIXME: check specs if message['type'] != 'normal': return True - self.accessMessageBox("INBOX").addMessage(message) + self.accessMessageBox("INBOX", profile_key=profile).addMessage(message) return False - def addObserver(self, callback, boxname, signal="NEW_MESSAGE"): + def addObserver(self, callback, profile, 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) + if not self.__observed.has_key((profile,boxname)): + self.__observed[(profile,boxname)]={} + if not self.__observed[(profile,boxname)].has_key(signal): + self.__observed[(profile,boxname)][signal]=set() + self.__observed[(profile,boxname)][signal].add(callback) - def removeObserver(self, callback, boxname, signal="NEW_MESSAGE"): + def removeObserver(self, callback, profile, 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): + if not self.__observed.has_key((profile,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): + if not self.__observed[(profile,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]: + if not callback in self.__observed[(profile,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) + self.__observed[(profile,boxname)][signal].remove(callback) - def emitSignal(self, boxname, signal_name): + def emitSignal(self, profile, boxname, signal_name): """Emit the signal to observer""" - debug('emitSignal %s %s' %(boxname, signal_name)) + debug('emitSignal %s %s %s' %(profile, boxname, signal_name)) try: - for observer_cb in self.__observed[boxname][signal_name]: + for observer_cb in self.__observed[(profile, boxname)][signal_name]: observer_cb() except KeyError: pass @@ -304,27 +317,32 @@ mail['Subject'] = e.children[0].encode('utf-8') return mail.as_string() - def __init__(self, _maildir, name, observer=None): + def __init__(self, _maildir, name, observer=None, profile="@NONE@"): """@param _maildir: the main MaildirBox instance @param name: name of the mailbox + @param profile: real profile (ie not a profile_key) THIS OBJECT MUST NOT BE USED DIRECTLY: use MaildirBox.accessMessageBox instead""" - if _maildir._checkBoxReference(self): + if _maildir._checkBoxReference(name, profile): error ("INTERNAL ERROR: MailboxUser MUST NOT be instancied directly") raise MailboxException('double MailboxUser instanciation') if name!="INBOX": raise NotImplementedError self.name=name + self.profile=profile self.maildir=_maildir - mailbox_path = os.path.expanduser(os.path.join(self.maildir.host.get_const('local_dir'), MAILDIR_PATH)) + profile_path = self.maildir._getProfilePath(profile) + full_profile_path = os.path.join(os.path.expanduser(self.maildir.host.get_const('local_dir')),profile_path) + if not os.path.exists(full_profile_path): + os.makedirs(full_profile_path,0700) + mailbox_path = os.path.join(full_profile_path, 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") + debug("adding observer for %s (%s)" % (name,profile)) + self.maildir.addObserver(observer, profile, name, "NEW_MESSAGE") def __uid_table_update(self): existant_id=[] @@ -332,14 +350,14 @@ #we update the uid table existant_id.append(self.getId(mess_idx)) self.getUid(mess_idx) - self.maildir.cleanTable(self.name, existant_id) + self.maildir.cleanTable(self.name, existant_id, profile=self.profile) def __del__(self): if observer: debug("removing observer for %s" % self.name) self._maildir.removeObserver(observer, self.name, "NEW_MESSAGE") - self._maildir._removeBoxAccess(self.name, self) + self.maildir._removeBoxAccess(self.name, self, profile=self.profile) def addMessage(self, message): """Add a message to the box @@ -348,8 +366,9 @@ 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) + if signal=="NEW_MESSAGE": + self.getUid(self.getMessageCount()-1) #XXX: we make an uid for the last message added + self.maildir.emitSignal(self.profile, self.name, signal) def getId(self, mess_idx): """Return the Unique ID of the message @@ -359,16 +378,16 @@ 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) + return self.maildir.getUid(self.name,mess_id, profile=self.profile) def getNextUid(self): - return self.maildir.getNextUid(self.name) + return self.maildir.getNextUid(self.name, profile=self.profile) def getNextExistingUid(self, uid): - return self.maildir.getNextExistingUid(self.name, uid) + return self.maildir.getNextExistingUid(self.name, uid, profile=self.profile) def getMaxUid(self): - return self.maildir.getMaxUid(self.name) + return self.maildir.getMaxUid(self.name, profile=self.profile) def getMessageCount(self): """Return number of mails present in this box""" @@ -412,14 +431,14 @@ @param mess_idx: message index @return: list of strings""" id = self.getId(mess_idx) - return self.maildir.getFlags(self.name, id) + return self.maildir.getFlags(self.name, id, profile=self.profile) def getFlagsUid(self, mess_uid): """Return the flags of the message @param mess_uid: message unique identifier @return: list of strings""" - id = self.maildir.getIdFromUid(self.name,mess_uid) - return self.maildir.getFlags(self.name, id) + id = self.maildir.getIdFromUid(self.name,mess_uid, profile=self.profile) + return self.maildir.getFlags(self.name, id, profile=self.profile) def setFlags(self, mess_idx, flags): """Change the flags of the message @@ -427,21 +446,21 @@ @param flags: list of strings """ id = self.getId(mess_idx) - return self.maildir.setFlags(self.name, id, flags) + self.maildir.setFlags(self.name, id, flags, profile=self.profile) def setFlagsUid(self, mess_uid, flags): """Change the flags of the message @param mess_uid: message unique identifier @param flags: list of strings """ - id = self.maildir.getIdFromUid(self.name,mess_uid) - return self.maildir.setFlags(self.name, id, flags) + id = self.maildir.getIdFromUid(self.name,mess_uid, profile=self.profile) + return self.maildir.setFlags(self.name, id, flags, profile=self.profile) def getMessageIdsWithFlag(self, flag): """Return ids of messages where a flag is set @param flag: flag to check @return: list of id (as given by MaildirMailbox)""" - return self.maildir.getMessageIdsWithFlag(self.name,flag) + return self.maildir.getMessageIdsWithFlag(self.name,flag, profile=self.profile) def removeDeleted(self): """Actually delete message flagged "\\Deleted" @@ -451,7 +470,7 @@ print ("Deleting %s", mess_id) self.mailbox.deleteMessage(self.getIdxFromId(mess_id)) self.mailbox = maildir.MaildirMailbox(self.mailbox_path) #We need to reparse the dir to have coherent indexing - self.maildir.purgeDeleted(self.name) + self.maildir.purgeDeleted(self.name, profile=self.profile) def emptyTrash(self): """Delete everything in the .Trash dir""" diff -r f5181f6dd98f -r 012c38b56cdd src/sat.tac --- a/src/sat.tac Tue Jan 18 00:51:47 2011 +0100 +++ b/src/sat.tac Tue Jan 18 00:57:26 2011 +0100 @@ -127,7 +127,7 @@ def onMessage(self, message): debug (_(u"got message from: %s"), message["from"]) - if not self.host.trigger.point("MessageReceived",message): + if not self.host.trigger.point("MessageReceived",message, profile=self.parent.profile): return for e in message.elements(): if e.name == "body":