Mercurial > libervia-backend
diff sat/plugins/plugin_misc_maildir.py @ 2562:26edcf3a30eb
core, setup: huge cleaning:
- moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention
- move twisted directory to root
- removed all hacks from setup.py, and added missing dependencies, it is now clean
- use https URL for website in setup.py
- removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed
- renamed sat.sh to sat and fixed its installation
- added python_requires to specify Python version needed
- replaced glib2reactor which use deprecated code by gtk3reactor
sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Apr 2018 19:44:50 +0200 |
parents | src/plugins/plugin_misc_maildir.py@33c8c4973743 |
children | ab2696e34d29 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat/plugins/plugin_misc_maildir.py Mon Apr 02 19:44:50 2018 +0200 @@ -0,0 +1,500 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +# 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 +# it under the terms of the GNU Affero 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from sat.core.i18n import D_, _ +from sat.core.constants import Const as C +from sat.core.log import getLogger +log = getLogger(__name__) +import warnings +warnings.filterwarnings('ignore', 'the MimeWriter', DeprecationWarning, 'twisted') # FIXME: to be removed, see http://twistedmatrix.com/trac/ticket/4038 +from twisted.mail import maildir +import email.message +import email.utils +import os +from sat.core.exceptions import ProfileUnknownError +from sat.memory.persistent import PersistentBinaryDict + + +PLUGIN_INFO = { + C.PI_NAME: "Maildir Plugin", + C.PI_IMPORT_NAME: "Maildir", + C.PI_TYPE: "Misc", + C.PI_PROTOCOLS: [], + C.PI_DEPENDENCIES: [], + C.PI_MAIN: "MaildirBox", + C.PI_HANDLER: "no", + C.PI_DESCRIPTION: _("""Intercept "normal" type messages, and put them in a Maildir type box""") +} + +MAILDIR_PATH = "Maildir" +CATEGORY = D_("Mail Server") +NAME = D_('Block "normal" messages propagation') +# FIXME: (very) old and (very) experimental code, need a big cleaning/review or to be deprecated + + +class MaildirError(Exception): + pass + + +class MaildirBox(object): + params = """ + <params> + <individual> + <category name='{category_name}' label='{category_label}'> + <param name='{name}' label='{label}' value="false" type="bool" security="4" /> + </category> + </individual> + </params> + """.format(category_name=CATEGORY, + category_label=_(CATEGORY), + name=NAME, + label=_(NAME), + ) + + def __init__(self, host): + log.info(_("Plugin Maildir initialization")) + self.host = host + host.memory.updateParams(self.params) + + self.__observed = {} + self.data = {} # list of profile spectific data. key = profile, value = PersistentBinaryDict where key=mailbox name, + # and value 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, ...]) + self.__mailboxes = {} # key: profile, value: {boxname: MailboxUser instance} + + #the triggers + host.trigger.add("MessageReceived", self.messageReceivedTrigger) + + def profileConnected(self, client): + """Called on client connection, create profile data""" + profile = client.profile + self.data[profile] = PersistentBinaryDict("plugin_maildir", profile) + self.__mailboxes[profile] = {} + + def dataLoaded(ignore): + if not self.data[profile]: + #the mailbox is new, we initiate the data + self.data[profile]["INBOX"] = {"cur_idx": 0} + self.data[profile].load().addCallback(dataLoaded) + + def profileDisconnected(self, client): + """Called on profile disconnection, free profile's resources""" + profile = client.profile + del self.__mailboxes[profile] + del self.data[profile] + + def messageReceivedTrigger(self, client, message, post_treat): + """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""" + profile = client.profile + for e in message.elements(C.NS_CLIENT, 'body'): + mess_type = message.getAttribute('type', 'normal') + if mess_type != 'normal': + return True + self.accessMessageBox("INBOX", profile_key=profile).addMessage(message) + return not self.host.memory.getParamA(NAME, CATEGORY, profile_key=profile) + return True + + def accessMessageBox(self, boxname, observer=None, profile_key=C.PROF_KEY_NONE): + """Create and return a MailboxUser instance + @param boxname: name of the box + @param observer: method to call when a NewMessage arrive""" + profile = self.host.memory.getProfileName(profile_key) + if not profile: + raise ProfileUnknownError(profile_key) + if boxname not in self.__mailboxes[profile]: + self.__mailboxes[profile][boxname] = MailboxUser(self, boxname, observer, profile=profile) + else: + if observer: + self.addObserver(observer, profile, boxname) + return self.__mailboxes[profile][boxname] + + 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""" + if boxname not in self.__mailboxes: + err_msg = _("Trying to remove an mailboxUser not referenced") + log.error(_(u"INTERNAL ERROR: ") + err_msg) + raise MaildirError(err_msg) + assert self.__mailboxes[profile][boxname] == mailboxUser + del self.__mailboxes[profile][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 profile in self.__mailboxes: + if boxname in self.__mailboxes[profile]: + return self.__mailboxes[profile][boxname] + + def __getBoxData(self, boxname, profile): + """Return the date of a box""" + try: + return self.data[profile][boxname] # the boxname MUST exist in the data + except KeyError: + err_msg = _("Boxname doesn't exist in internal data") + log.error(_(u"INTERNAL ERROR: ") + err_msg) + raise MaildirError(err_msg) + + 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, profile) + if message_id in box_data: + 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.data[profile].force(boxname) + return ret + + 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, profile) + return box_data['cur_idx'] + 1 + + 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, profile) + idx = uid + 1 + while self.getIdFromUid(boxname, idx, profile) is 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, profile): + """Give the max existing uid + @param boxname: name of the box where the message is + @return: uid""" + box_data = self.__getBoxData(boxname, profile) + return box_data['cur_idx'] + + 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, 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 + if box_data[message_id][0] == message_uid: + return message_id + return None + + 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, profile) + if mess_id not in box_data: + raise MaildirError("Trying to get flags from an unexisting message") + return box_data[mess_id][1] + + 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, profile) + assert(type(flags) == list) + flags = [flag.upper() for flag in flags] # we store every flag UPPERCASE + if mess_id not in box_data: + raise MaildirError("Trying to set flags for an unexisting message") + box_data[mess_id][1] = flags + self.data[profile].force(boxname) + + 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, profile) + assert(isinstance(flag, basestring)) + flag = flag.upper() + result = [] + for key in box_data: + if key == 'cur_idx': + continue + if flag in box_data[key][1]: + result.append(key) + return result + + 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, profile) + for mess_id in self.getMessageIdsWithFlag(boxname, "\\Deleted", profile): + del(box_data[mess_id]) + self.data[profile].force(boxname) + + 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, profile) + 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 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 (profile, boxname) not in self.__observed: + self.__observed[(profile, boxname)] = {} + if signal not in self.__observed[(profile, boxname)]: + self.__observed[(profile, boxname)][signal] = set() + self.__observed[(profile, boxname)][signal].add(callback) + + 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 (profile, boxname) not in self.__observed: + err_msg = _(u"Trying to remove an observer for an inexistant mailbox") + log.error(_(u"INTERNAL ERROR: ") + err_msg) + raise MaildirError(err_msg) + if signal not in self.__observed[(profile, boxname)]: + err_msg = _(u"Trying to remove an inexistant observer, no observer for this signal") + log.error(_(u"INTERNAL ERROR: ") + err_msg) + raise MaildirError(err_msg) + if not callback in self.__observed[(profile, boxname)][signal]: + err_msg = _(u"Trying to remove an inexistant observer") + log.error(_(u"INTERNAL ERROR: ") + err_msg) + raise MaildirError(err_msg) + self.__observed[(profile, boxname)][signal].remove(callback) + + def emitSignal(self, profile, boxname, signal_name): + """Emit the signal to observer""" + log.debug(u'emitSignal %s %s %s' % (profile, boxname, signal_name)) + try: + for observer_cb in self.__observed[(profile, boxname)][signal_name]: + observer_cb() + except KeyError: + pass + + +class MailboxUser(object): + """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, profile=C.PROF_KEY_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(name, profile): + log.error(u"INTERNAL ERROR: MailboxUser MUST NOT be instancied directly") + raise MaildirError('double MailboxUser instanciation') + if name != "INBOX": + raise NotImplementedError + self.name = name + self.profile = profile + self.maildir = _maildir + profile_path = self.maildir._getProfilePath(profile) + full_profile_path = os.path.join(self.maildir.host.memory.getConfig('', 'local_dir'), 'maildir', 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: + log.debug(u"adding observer for %s (%s)" % (name, profile)) + self.maildir.addObserver(observer, profile, 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, profile=self.profile) + + def __del__(self): + if self.observer: + log.debug(u"removing observer for %s" % self.name) + self._maildir.removeObserver(self.observer, self.name, "NEW_MESSAGE") + self.maildir._removeBoxAccess(self.name, self, profile=self.profile) + + 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""" + 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 + @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, profile=self.profile) + + def getNextUid(self): + return self.maildir.getNextUid(self.name, profile=self.profile) + + def getNextExistingUid(self, uid): + return self.maildir.getNextExistingUid(self.name, uid, profile=self.profile) + + def getMaxUid(self): + return self.maildir.getMaxUid(self.name, profile=self.profile) + + 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 getIdxFromUid(self, mess_uid): + """Return the message index from the uid + @param mess_uid: message unique identifier + @return: message index, as managed by MaildirMailbox""" + for mess_idx in range(self.getMessageCount()): + if self.getUid(mess_idx) == mess_uid: + return mess_idx + raise IndexError + + def getIdxFromId(self, mess_id): + """Return the message index from the unique index + @param mess_id: message unique index as given by MaildirMailbox + @return: message sequence index""" + for mess_idx in range(self.getMessageCount()): + if self.mailbox.getUidl(mess_idx) == mess_id: + return mess_idx + raise IndexError + + def getMessage(self, mess_idx): + """Return the full message + @param mess_idx: message index""" + return self.mailbox.getMessage(mess_idx) + + def getMessageUid(self, mess_uid): + """Return the full message + @param mess_idx: message unique identifier""" + return self.mailbox.getMessage(self.getIdxFromUid(mess_uid)) + + def getFlags(self, mess_idx): + """Return the flags of the message + @param mess_idx: message index + @return: list of strings""" + id = self.getId(mess_idx) + 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, 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 + @param mess_idx: message index + @param flags: list of strings + """ + id = self.getId(mess_idx) + 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, 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, profile=self.profile) + + def removeDeleted(self): + """Actually delete message flagged "\\Deleted" + Also purge the internal data of these messages + """ + for mess_id in self.getMessageIdsWithFlag("\\Deleted"): + 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, profile=self.profile) + + def emptyTrash(self): + """Delete everything in the .Trash dir""" + pass #TODO