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
This commit is contained in:
Goffi 2011-01-18 00:57:26 +01:00
commit bf514891c2
3 changed files with 159 additions and 99 deletions

View file

@ -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 @@ import os,os.path
from cStringIO import StringIO
from twisted.internet import reactor
import pdb
from sat.exceptions import *
from zope.interface import implements
@ -61,8 +62,9 @@ class MaildirBox():
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 @@ class MaildirBox():
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 @@ class MaildirBox():
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 @@ class MaildirBox():
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 @@ class MaildirBox():
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 @@ class MaildirBox():
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 @@ class MaildirBox():
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 @@ class MaildirBox():
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 @@ class MailboxUser:
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 @@ class MailboxUser:
#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 @@ class MailboxUser:
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 @@ class MailboxUser:
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 @@ class MailboxUser:
@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 @@ class MailboxUser:
@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 @@ class MailboxUser:
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"""