changeset 3081:fd593b448bee

plugins (imap, maildir, smtp): removed plugins IMAP, Maildir and SMTP: they were experimental plugins, are not used and Maildir has not been ported to Python 3
author Goffi <goffi@goffi.org>
date Fri, 06 Dec 2019 15:02:02 +0100
parents 16925f494820
children 9211c0d8b50c
files CHANGELOG sat/plugins/plugin_misc_imap.py sat/plugins/plugin_misc_maildir.py sat/plugins/plugin_misc_smtp.py
diffstat 4 files changed, 2 insertions(+), 1207 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGELOG	Thu Dec 05 23:05:16 2019 +0100
+++ b/CHANGELOG	Fri Dec 06 15:02:02 2019 +0100
@@ -2,6 +2,8 @@
 
 v 0.8.0 « La Cecília » (NOT RELEASED YET):
 	- Python 3 port
+	- removed plugins IMAP, Maildir and SMTP: they were experimental plugins, are not used
+	  and Maildir has not been ported to Python 3
 	- Cagou:
 		- new "share" widget
 	- Cagou (Android):
--- a/sat/plugins/plugin_misc_imap.py	Thu Dec 05 23:05:16 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,480 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-# 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
-# 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 _
-from sat.core.constants import Const as C
-from sat.core.log import getLogger
-
-log = getLogger(__name__)
-from twisted.internet import protocol, defer
-from twisted.cred import portal, checkers, credentials
-from twisted.cred import error as cred_error
-from twisted.mail import imap4
-from twisted.python import failure
-from email.parser import Parser
-import os
-from io import StringIO
-from twisted.internet import reactor
-
-from zope.interface import implementer
-
-PLUGIN_INFO = {
-    C.PI_NAME: "IMAP server Plugin",
-    C.PI_IMPORT_NAME: "IMAP",
-    C.PI_TYPE: "Misc",
-    C.PI_PROTOCOLS: [],
-    C.PI_DEPENDENCIES: ["Maildir"],
-    C.PI_MAIN: "IMAP_server",
-    C.PI_HANDLER: "no",
-    C.PI_DESCRIPTION: _(
-        """Create an Imap server that you can use to read your "normal" type messages"""
-    ),
-}
-
-
-class IMAP_server(object):
-    # TODO: connect profile on mailbox request, once password is accepted
-
-    params = """
-    <params>
-    <general>
-    <category name="Mail Server">
-        <param name="IMAP Port" value="10143" type="int" constraint="1;65535" />
-    </category>
-    </general>
-    </params>
-    """
-
-    def __init__(self, host):
-        log.info(_("Plugin Imap Server initialization"))
-        self.host = host
-
-        # parameters
-        host.memory.updateParams(self.params)
-
-        port = int(self.host.memory.getParamA("IMAP Port", "Mail Server"))
-        log.info(_("Launching IMAP server on port %d") % port)
-
-        self.server_factory = ImapServerFactory(self.host)
-        reactor.listenTCP(port, self.server_factory)
-
-
-@implementer(imap4.IMessage)
-class Message(object):
-
-    def __init__(self, uid, flags, mess_fp):
-        log.debug("Message Init")
-        self.uid = uid
-        self.flags = flags
-        self.mess_fp = mess_fp
-        self.message = Parser().parse(mess_fp)
-
-    def getUID(self):
-        """Retrieve the unique identifier associated with this message.
-        """
-        log.debug("getUID (message)")
-        return self.uid
-
-    def getFlags(self):
-        """Retrieve the flags associated with this message.
-        @return: The flags, represented as strings.
-        """
-        log.debug("getFlags")
-        return self.flags
-
-    def getInternalDate(self):
-        """Retrieve the date internally associated with this message.
-        @return: An RFC822-formatted date string.
-        """
-        log.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
-        """
-        log.debug("getHeaders %s - %s" % (negate, names))
-        final_dict = {}
-        to_check = [name.lower() for name in names]
-        for header in list(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.
-        """
-        log.debug("getBodyFile")
-        return StringIO(self.message.get_payload())
-
-    def getSize(self):
-        """Retrieve the total size, in octets, of this message.
-        """
-        log.debug("getSize")
-        self.mess_fp.seek(0, os.SEEK_END)
-        return self.mess_fp.tell()
-
-    def isMultipart(self):
-        """Indicate whether this message has subparts.
-        """
-        log.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.
-        """
-        log.debug("getSubPart")
-        return TypeError
-
-
-@implementer(imap4.IMailbox)
-class SatMailbox(object):
-
-    def __init__(self, host, name, profile):
-        self.host = host
-        self.listeners = set()
-        log.debug("Mailbox init (%s)" % name)
-        if name != "INBOX":
-            raise imap4.MailboxException("Only INBOX is managed for the moment")
-        self.mailbox = self.host.plugins["Maildir"].accessMessageBox(
-            name, self.messageNew, profile
-        )
-
-    def messageNew(self):
-        """Called when a new message is in the mailbox"""
-        log.debug("messageNew 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.
-        """
-        log.debug("getUIDValidity")
-        return 0
-
-    def getUIDNext(self):
-        """Return the likely UID for the next message added to this mailbox.
-        """
-        log.debug("getUIDNext")
-        return self.mailbox.getNextUid()
-
-    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.
-        """
-        log.debug("getUID (%i)" % message)
-        # return self.mailbox.getUid(message-1) #XXX: it seems that this method get uid and not message sequence number
-        return message
-
-    def getMessageCount(self):
-        """Return the number of messages in this mailbox.
-        """
-        log.debug("getMessageCount")
-        ret = self.mailbox.getMessageCount()
-        log.debug("count = %i" % ret)
-        return ret
-
-    def getRecentCount(self):
-        """Return the number of messages with the 'Recent' flag.
-        """
-        log.debug("getRecentCount")
-        return len(self.mailbox.getMessageIdsWithFlag("\\Recent"))
-
-    def getUnseenCount(self):
-        """Return the number of messages with the 'Unseen' flag.
-        """
-        log.debug("getUnseenCount")
-        return self.getMessageCount() - len(self.mailbox.getMessageIdsWithFlag("\\SEEN"))
-
-    def isWriteable(self):
-        """Get the read/write status of the mailbox.
-        @return: A true value if write permission is allowed, a false value otherwise.
-        """
-        log.debug("isWriteable")
-        return True
-
-    def destroy(self):
-        """Called before this mailbox is deleted, permanently.
-        """
-        log.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.
-        """
-        log.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.
-        """
-        log.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.
-        """
-        log.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.
-        """
-        log.debug("addMessage")
-        raise imap4.MailboxException("Client message addition not implemented yet")
-
-    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.
-        """
-        log.debug("expunge")
-        self.mailbox.removeDeleted()
-
-    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;
-        """
-        log.debug("fetch (%s, %s)" % (messages, uid))
-        if uid:
-            messages.last = self.mailbox.getMaxUid()
-            messages.getnext = self.mailbox.getNextExistingUid
-            for mess_uid in messages:
-                if mess_uid is None:
-                    log.debug("stopping iteration")
-                    raise StopIteration
-                try:
-                    yield (
-                        mess_uid,
-                        Message(
-                            mess_uid,
-                            self.mailbox.getFlagsUid(mess_uid),
-                            self.mailbox.getMessageUid(mess_uid),
-                        ),
-                    )
-                except IndexError:
-                    continue
-        else:
-            messages.last = self.getMessageCount()
-            for mess_idx in messages:
-                if mess_idx > self.getMessageCount():
-                    raise StopIteration
-                yield (
-                    mess_idx,
-                    Message(
-                        mess_idx,
-                        self.mailbox.getFlags(mess_idx),
-                        self.mailbox.getMessage(mess_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.
-        """
-        log.debug("store")
-
-        flags = [flag.upper() for flag in flags]
-
-        def updateFlags(getF, setF):
-            ret = {}
-            for mess_id in messages:
-                if (uid and mess_id is None) or (
-                    not uid and mess_id > self.getMessageCount()
-                ):
-                    break
-                _flags = set(getF(mess_id) if mode else [])
-                if mode == -1:
-                    _flags.difference_update(set(flags))
-                else:
-                    _flags.update(set(flags))
-                new_flags = list(_flags)
-                setF(mess_id, new_flags)
-                ret[mess_id] = tuple(new_flags)
-            return ret
-
-        if uid:
-            messages.last = self.mailbox.getMaxUid()
-            messages.getnext = self.mailbox.getNextExistingUid
-            ret = updateFlags(self.mailbox.getFlagsUid, self.mailbox.setFlagsUid)
-            for listener in self.listeners:
-                listener.flagsChanged(ret)
-            return ret
-
-        else:
-            messages.last = self.getMessageCount()
-            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
-        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.
-        """
-        log.debug("getFlags")
-        return [
-            "\\SEEN",
-            "\\ANSWERED",
-            "\\FLAGGED",
-            "\\DELETED",
-            "\\DRAFT",
-        ]  # TODO: add '\\RECENT'
-
-    def getHierarchicalDelimiter(self):
-        """Get the character which delimits namespaces for in this mailbox.
-        """
-        log.debug("getHierarchicalDelimiter")
-        return "."
-
-
-class ImapSatAccount(imap4.MemoryAccount):
-    # implements(imap4.IAccount)
-
-    def __init__(self, host, profile):
-        log.debug("ImapAccount init")
-        self.host = host
-        self.profile = profile
-        imap4.MemoryAccount.__init__(self, profile)
-        self.addMailbox("Inbox")  # We only manage Inbox for the moment
-        log.debug("INBOX added")
-
-    def _emptyMailbox(self, name, id):
-        return SatMailbox(self.host, name, self.profile)
-
-
-@implementer(portal.IRealm)
-class ImapRealm(object):
-
-    def __init__(self, host):
-        self.host = host
-
-    def requestAvatar(self, avatarID, mind, *interfaces):
-        log.debug("requestAvatar")
-        profile = avatarID
-        if imap4.IAccount not in interfaces:
-            raise NotImplementedError
-        return imap4.IAccount, ImapSatAccount(self.host, profile), lambda: None
-
-
-@implementer(checkers.ICredentialsChecker)
-class SatProfileCredentialChecker(object):
-    """
-    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
-    """
-
-    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())
-        d = self.host.memory.asyncGetParamA(
-            "Password", "Connection", profile_key=credentials.username
-        )
-        d.addCallback(lambda password: credentials.checkPassword(password))
-        d.addCallback(self._cbPasswordMatch, credentials.username)
-        return d
-
-
-class ImapServerFactory(protocol.ServerFactory):
-    protocol = imap4.IMAP4Server
-
-    def __init__(self, host):
-        self.host = host
-
-    def startedConnecting(self, connector):
-        log.debug(_("IMAP server connection started"))
-
-    def clientConnectionLost(self, connector, reason):
-        log.debug(_("IMAP server connection lost (reason: %s)"), reason)
-
-    def buildProtocol(self, addr):
-        log.debug("Building protocol")
-        prot = protocol.ServerFactory.buildProtocol(self, addr)
-        prot.portal = portal.Portal(ImapRealm(self.host))
-        prot.portal.registerChecker(SatProfileCredentialChecker(self.host))
-        return prot
--- a/sat/plugins/plugin_misc_maildir.py	Thu Dec 05 23:05:16 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,500 +0,0 @@
-#!/usr/bin/env python3
-# -*- 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(_("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(_("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 list(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, str))
-        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 = _("Trying to remove an observer for an inexistant mailbox")
-            log.error(_("INTERNAL ERROR: ") + err_msg)
-            raise MaildirError(err_msg)
-        if signal not in self.__observed[(profile, boxname)]:
-            err_msg = _("Trying to remove an inexistant observer, no observer for this signal")
-            log.error(_("INTERNAL ERROR: ") + err_msg)
-            raise MaildirError(err_msg)
-        if not callback in self.__observed[(profile, boxname)][signal]:
-            err_msg = _("Trying to remove an inexistant observer")
-            log.error(_("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('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("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, 0o700)
-        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("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("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
--- a/sat/plugins/plugin_misc_smtp.py	Thu Dec 05 23:05:16 2019 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,227 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-# SàT plugin for managing smtp 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 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 _
-from sat.core.constants import Const as C
-from sat.core.log import getLogger
-
-log = getLogger(__name__)
-from twisted.internet import defer
-from twisted.cred import portal, checkers, credentials
-from twisted.cred import error as cred_error
-from twisted.mail import smtp
-from twisted.python import failure
-from email.parser import Parser
-from email.utils import parseaddr
-from twisted.mail.imap4 import LOGINCredentials, PLAINCredentials
-from twisted.internet import reactor
-import sys
-
-from zope.interface import implementer
-
-PLUGIN_INFO = {
-    C.PI_NAME: "SMTP server Plugin",
-    C.PI_IMPORT_NAME: "SMTP",
-    C.PI_TYPE: "Misc",
-    C.PI_PROTOCOLS: [],
-    C.PI_DEPENDENCIES: ["Maildir"],
-    C.PI_MAIN: "SMTP_server",
-    C.PI_HANDLER: "no",
-    C.PI_DESCRIPTION: _(
-        """Create a SMTP server that you can use to send your "normal" type messages"""
-    ),
-}
-
-
-class SMTP_server(object):
-
-    params = """
-    <params>
-    <general>
-    <category name="Mail Server">
-        <param name="SMTP Port" value="10125" type="int" constraint="1;65535" />
-    </category>
-    </general>
-    </params>
-    """
-
-    def __init__(self, host):
-        log.info(_("Plugin SMTP Server initialization"))
-        self.host = host
-
-        # parameters
-        host.memory.updateParams(self.params)
-
-        port = int(self.host.memory.getParamA("SMTP Port", "Mail Server"))
-        log.info(_("Launching SMTP server on port %d") % port)
-
-        self.server_factory = SmtpServerFactory(self.host)
-        reactor.listenTCP(port, self.server_factory)
-
-
-@implementer(smtp.IMessage)
-class SatSmtpMessage(object):
-
-    def __init__(self, host, profile):
-        self.host = host
-        self.profile = profile
-        self.message = []
-
-    def lineReceived(self, line):
-        """handle another line"""
-        self.message.append(line)
-
-    def eomReceived(self):
-        """handle end of message"""
-        mail = Parser().parsestr("\n".join(self.message))
-        try:
-            self.host._sendMessage(
-                parseaddr(mail["to"].decode("utf-8", "replace"))[1],
-                mail.get_payload().decode(
-                    "utf-8", "replace"
-                ),  # TODO: manage other charsets
-                subject=mail["subject"].decode("utf-8", "replace"),
-                mess_type="normal",
-                profile_key=self.profile,
-            )
-        except:
-            exc_type, exc_value, exc_traceback = sys.exc_info()
-            log.error(
-                _("Can't send message: %s") % exc_value
-            )  # The email is invalid or incorreclty parsed
-            return defer.fail()
-        self.message = None
-        return defer.succeed(None)
-
-    def connectionLost(self):
-        """handle message truncated"""
-        raise smtp.SMTPError
-
-
-@implementer(smtp.IMessageDelivery)
-class SatSmtpDelivery(object):
-
-    def __init__(self, host, profile):
-        self.host = host
-        self.profile = profile
-
-    def receivedHeader(self, helo, origin, recipients):
-        """
-        Generate the Received header for a message
-        @param helo: The argument to the HELO command and the client's IP
-        address.
-        @param origin: The address the message is from
-        @param recipients: A list of the addresses for which this message
-        is bound.
-        @return: The full \"Received\" header string.
-        """
-        return "Received:"
-
-    def validateTo(self, user):
-        """
-        Validate the address for which the message is destined.
-        @param user: The address to validate.
-        @return: A Deferred which becomes, or a callable which
-        takes no arguments and returns an object implementing IMessage.
-        This will be called and the returned object used to deliver the
-        message when it arrives.
-        """
-        return lambda: SatSmtpMessage(self.host, self.profile)
-
-    def validateFrom(self, helo, origin):
-        """
-        Validate the address from which the message originates.
-        @param helo: The argument to the HELO command and the client's IP
-        address.
-        @param origin: The address the message is from
-        @return: origin or a Deferred whose callback will be
-        passed origin.
-        """
-        return origin
-
-
-@implementer(portal.IRealm)
-class SmtpRealm(object):
-
-    def __init__(self, host):
-        self.host = host
-
-    def requestAvatar(self, avatarID, mind, *interfaces):
-        log.debug("requestAvatar")
-        profile = avatarID
-        if smtp.IMessageDelivery not in interfaces:
-            raise NotImplementedError
-        return smtp.IMessageDelivery, SatSmtpDelivery(self.host, profile), lambda: None
-
-
-@implementer(checkers.ICredentialsChecker)
-class SatProfileCredentialChecker(object):
-    """
-    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
-    """
-
-    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())
-        d = self.host.memory.asyncGetParamA(
-            "Password", "Connection", profile_key=credentials.username
-        )
-        d.addCallback(credentials.checkPassword)
-        d.addCallback(self._cbPasswordMatch, credentials.username)
-        return d
-
-
-class SmtpServerFactory(smtp.SMTPFactory):
-    def __init__(self, host):
-        self.protocol = smtp.ESMTP
-        self.host = host
-        _portal = portal.Portal(SmtpRealm(self.host))
-        _portal.registerChecker(SatProfileCredentialChecker(self.host))
-        smtp.SMTPFactory.__init__(self, _portal)
-
-    def startedConnecting(self, connector):
-        log.debug(_("SMTP server connection started"))
-        smtp.SMTPFactory.startedConnecting(self, connector)
-
-    def clientConnectionLost(self, connector, reason):
-        log.debug(_("SMTP server connection lost (reason: %s)"), reason)
-        smtp.SMTPFactory.clientConnectionLost(self, connector, reason)
-
-    def buildProtocol(self, addr):
-        p = smtp.SMTPFactory.buildProtocol(self, addr)
-        # add the challengers from imap4, more secure and complicated challengers are available
-        p.challengers = {"LOGIN": LOGINCredentials, "PLAIN": PLAINCredentials}
-        return p