diff libervia/backend/plugins/plugin_misc_account.py @ 4071:4b842c1fb686

refactoring: renamed `sat` package to `libervia.backend`
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 11:49:51 +0200
parents sat/plugins/plugin_misc_account.py@524856bd7b19
children 0d7bb4df2343
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/backend/plugins/plugin_misc_account.py	Fri Jun 02 11:49:51 2023 +0200
@@ -0,0 +1,766 @@
+#!/usr/bin/env python3
+
+# Libervia plugin for account creation
+# Copyright (C) 2009-2021 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 libervia.backend.core.i18n import _, D_
+from libervia.backend.core.log import getLogger
+
+from libervia.backend.core import exceptions
+from libervia.backend.tools import xml_tools
+from libervia.backend.memory.memory import Sessions
+from libervia.backend.memory.crypto import PasswordHasher
+from libervia.backend.core.constants import Const as C
+import configparser
+from twisted.internet import defer
+from twisted.python.failure import Failure
+from twisted.words.protocols.jabber import jid
+from libervia.backend.tools.common import email as sat_email
+
+
+log = getLogger(__name__)
+
+
+#  FIXME: this plugin code is old and need a cleaning
+# TODO: account deletion/password change need testing
+
+
+PLUGIN_INFO = {
+    C.PI_NAME: "Account Plugin",
+    C.PI_IMPORT_NAME: "MISC-ACCOUNT",
+    C.PI_TYPE: "MISC",
+    C.PI_PROTOCOLS: [],
+    C.PI_DEPENDENCIES: ["XEP-0077"],
+    C.PI_RECOMMENDATIONS: ["GROUPBLOG"],
+    C.PI_MAIN: "MiscAccount",
+    C.PI_HANDLER: "no",
+    C.PI_DESCRIPTION: _("""Libervia account creation"""),
+}
+
+CONFIG_SECTION = "plugin account"
+
+# You need do adapt the following consts to your server
+# all theses values (key=option name, value=default) can (and should) be overriden
+# in libervia.conf in section CONFIG_SECTION
+
+default_conf = {
+    "email_from": "NOREPLY@example.net",
+    "email_server": "localhost",
+    "email_sender_domain": "",
+    "email_port": 25,
+    "email_username": "",
+    "email_password": "",
+    "email_starttls": "false",
+    "email_auth": "false",
+    "email_admins_list": [],
+    "admin_email": "",
+    "new_account_server": "localhost",
+    "new_account_domain": "",  #  use xmpp_domain if not found
+    "reserved_list": ["libervia"],  # profiles which can't be used
+}
+
+WELCOME_MSG = D_(
+    """Welcome to Libervia, the web interface of Salut à Toi.
+
+Your account on {domain} has been successfully created.
+This is a demonstration version to show you the current status of the project.
+It is still under development, please keep it in mind!
+
+Here is your connection information:
+
+Login on {domain}: {profile}
+Jabber ID (JID): {jid}
+Your password has been chosen by yourself during registration.
+
+In the beginning, you have nobody to talk to. To find some contacts, you may use the users' directory:
+    - make yourself visible in "Service / Directory subscription".
+    - search for people with "Contacts" / Search directory".
+
+Any feedback welcome. Thank you!
+
+Salut à Toi association
+https://www.salut-a-toi.org
+"""
+)
+
+DEFAULT_DOMAIN = "example.net"
+
+
+class MiscAccount(object):
+    """Account plugin: create a SàT + XMPP account, used by Libervia"""
+
+    # XXX: This plugin was initialy a Q&D one used for the demo.
+    # TODO: cleaning, separate email handling, more configuration/tests, fixes
+
+    def __init__(self, host):
+        log.info(_("Plugin Account initialization"))
+        self.host = host
+        host.bridge.add_method(
+            "libervia_account_register",
+            ".plugin",
+            in_sign="sss",
+            out_sign="",
+            method=self._register_account,
+            async_=True,
+        )
+        host.bridge.add_method(
+            "account_domain_new_get",
+            ".plugin",
+            in_sign="",
+            out_sign="s",
+            method=self.account_domain_new_get,
+            async_=False,
+        )
+        host.bridge.add_method(
+            "account_dialog_ui_get",
+            ".plugin",
+            in_sign="s",
+            out_sign="s",
+            method=self._get_account_dialog_ui,
+            async_=False,
+        )
+        host.bridge.add_method(
+            "credentials_xmpp_connect",
+            ".plugin",
+            in_sign="ss",
+            out_sign="b",
+            method=self.credentials_xmpp_connect,
+            async_=True,
+        )
+
+        self.fix_email_admins()
+        self._sessions = Sessions()
+
+        self.__account_cb_id = host.register_callback(
+            self._account_dialog_cb, with_data=True
+        )
+        self.__change_password_id = host.register_callback(
+            self.__change_password_cb, with_data=True
+        )
+
+        def delete_blog_callback(posts, comments):
+            return lambda data, profile: self.__delete_blog_posts_cb(
+                posts, comments, data, profile
+            )
+
+        self.__delete_posts_id = host.register_callback(
+            delete_blog_callback(True, False), with_data=True
+        )
+        self.__delete_comments_id = host.register_callback(
+            delete_blog_callback(False, True), with_data=True
+        )
+        self.__delete_posts_comments_id = host.register_callback(
+            delete_blog_callback(True, True), with_data=True
+        )
+
+        self.__delete_account_id = host.register_callback(
+            self.__delete_account_cb, with_data=True
+        )
+
+    # FIXME: remove this after some time, when the deprecated parameter is really abandoned
+    def fix_email_admins(self):
+        """Handle deprecated config option "admin_email" to fix the admin emails list"""
+        admin_email = self.config_get("admin_email")
+        if not admin_email:
+            return
+        log.warning(
+            "admin_email parameter is deprecated, please use email_admins_list instead"
+        )
+        param_name = "email_admins_list"
+        try:
+            section = ""
+            value = self.host.memory.config_get(section, param_name, Exception)
+        except (configparser.NoOptionError, configparser.NoSectionError):
+            section = CONFIG_SECTION
+            value = self.host.memory.config_get(
+                section, param_name, default_conf[param_name]
+            )
+
+        value = set(value)
+        value.add(admin_email)
+        self.host.memory.config.set(section, param_name, ",".join(value))
+
+    def config_get(self, name, section=CONFIG_SECTION):
+        if name.startswith("email_"):
+            # XXX: email_ parameters were first in [plugin account] section
+            #      but as it make more sense to have them in common with other plugins,
+            #      they can now be in [DEFAULT] section
+            try:
+                value = self.host.memory.config_get(None, name, Exception)
+            except (configparser.NoOptionError, configparser.NoSectionError):
+                pass
+            else:
+                return value
+
+        if section == CONFIG_SECTION:
+            default = default_conf[name]
+        else:
+            default = None
+        return self.host.memory.config_get(section, name, default)
+
+    def _register_account(self, email, password, profile):
+        return self.registerAccount(email, password, None, profile)
+
+    def registerAccount(self, email, password, jid_s, profile):
+        """Register a new profile, its associated XMPP account, send the confirmation emails.
+
+        @param email (unicode): where to send to confirmation email to
+        @param password (unicode): password chosen by the user
+            while be used for profile *and* XMPP account
+        @param jid_s (unicode): JID to re-use or to register:
+            - non empty value: bind this JID to the new sat profile
+            - None or "": register a new JID on the local XMPP server
+        @param profile
+        @return Deferred
+        """
+        d = self.create_profile(password, jid_s, profile)
+        d.addCallback(lambda __: self.send_emails(email, profile))
+        return d
+
+    def create_profile(self, password, jid_s, profile):
+        """Register a new profile and its associated XMPP account.
+
+        @param password (unicode): password chosen by the user
+            while be used for profile *and* XMPP account
+        @param jid_s (unicode): JID to re-use or to register:
+            - non empty value: bind this JID to the new sat profile
+            - None or "": register a new JID on the local XMPP server
+        @param profile
+        @return Deferred
+        """
+        if not password or not profile:
+            raise exceptions.DataError
+
+        if profile.lower() in self.config_get("reserved_list"):
+            return defer.fail(Failure(exceptions.ConflictError))
+
+        d = self.host.memory.create_profile(profile, password)
+        d.addCallback(lambda __: self.profile_created(password, jid_s, profile))
+        return d
+
+    def profile_created(self, password, jid_s, profile):
+        """Create the XMPP account and set the profile connection parameters.
+
+        @param password (unicode): password chosen by the user
+        @param jid_s (unicode): JID to re-use or to register:
+            - non empty value: bind this JID to the new sat profile
+            - None or empty: register a new JID on the local XMPP server
+        @param profile
+        @return: Deferred
+        """
+        if jid_s:
+            d = defer.succeed(None)
+            jid_ = jid.JID(jid_s)
+        else:
+            jid_s = profile + "@" + self.account_domain_new_get()
+            jid_ = jid.JID(jid_s)
+            d = self.host.plugins["XEP-0077"].register_new_account(jid_, password)
+
+        def setParams(__):
+            self.host.memory.param_set(
+                "JabberID", jid_s, "Connection", profile_key=profile
+            )
+            d = self.host.memory.param_set(
+                "Password", password, "Connection", profile_key=profile
+            )
+            return d
+
+        def remove_profile(failure):
+            self.host.memory.profile_delete_async(profile)
+            return failure
+
+        d.addCallback(lambda __: self.host.memory.start_session(password, profile))
+        d.addCallback(setParams)
+        d.addCallback(lambda __: self.host.memory.stop_session(profile))
+        d.addErrback(remove_profile)
+        return d
+
+    def _send_email_eb(self, failure_, email):
+        # TODO: return error code to user
+        log.error(
+            _("Failed to send account creation confirmation to {email}: {msg}").format(
+                email=email, msg=failure_
+            )
+        )
+
+    def send_emails(self, email, profile):
+        # time to send the email
+
+        domain = self.account_domain_new_get()
+
+        # email to the administrators
+        admins_emails = self.config_get("email_admins_list")
+        if not admins_emails:
+            log.warning(
+                "No known admin email, we can't send email to administrator(s).\n"
+                "Please fill email_admins_list parameter"
+            )
+            d_admin = defer.fail(exceptions.DataError("no admin email"))
+        else:
+            subject = _("New Libervia account created")
+            # there is no email when an existing XMPP account is used
+            body = f"New account created on {domain}: {profile} [{email or '<no email>'}]"
+            d_admin = sat_email.send_email(
+                self.host.memory.config, admins_emails, subject, body)
+
+        admins_emails_txt = ", ".join(["<" + addr + ">" for addr in admins_emails])
+        d_admin.addCallbacks(
+            lambda __: log.debug(
+                "Account creation notification sent to admin(s) {}".format(
+                    admins_emails_txt
+                )
+            ),
+            lambda __: log.error(
+                "Failed to send account creation notification to admin {}".format(
+                    admins_emails_txt
+                )
+            ),
+        )
+        if not email:
+            # TODO: if use register with an existing account, an XMPP message should be sent
+            return d_admin
+
+        jid_s = self.host.memory.param_get_a(
+            "JabberID", "Connection", profile_key=profile
+        )
+        subject = _("Your Libervia account has been created")
+        body = _(WELCOME_MSG).format(profile=profile, jid=jid_s, domain=domain)
+
+        # XXX: this will not fail when the email address doesn't exist
+        # FIXME: check email reception to validate email given by the user
+        # FIXME: delete the profile if the email could not been sent?
+        d_user = sat_email.send_email(self.host.memory.config, [email], subject, body)
+        d_user.addCallbacks(
+            lambda __: log.debug(
+                "Account creation confirmation sent to <{}>".format(email)
+            ),
+            self._send_email_eb,
+            errbackArgs=[email]
+        )
+        return defer.DeferredList([d_user, d_admin])
+
+    def account_domain_new_get(self):
+        """get the domain that will be set to new account"""
+
+        domain = self.config_get("new_account_domain") or self.config_get(
+            "xmpp_domain", None
+        )
+        if not domain:
+            log.warning(
+                _(
+                    'xmpp_domain needs to be set in sat.conf. Using "{default}" meanwhile'
+                ).format(default=DEFAULT_DOMAIN)
+            )
+            return DEFAULT_DOMAIN
+        return domain
+
+    def _get_account_dialog_ui(self, profile):
+        """Get the main dialog to manage your account
+        @param menu_data
+        @param profile: %(doc_profile)s
+        @return: XML of the dialog
+        """
+        form_ui = xml_tools.XMLUI(
+            "form",
+            "tabs",
+            title=D_("Manage your account"),
+            submit_id=self.__account_cb_id,
+        )
+        tab_container = form_ui.current_container
+
+        tab_container.add_tab(
+            "update", D_("Change your password"), container=xml_tools.PairsContainer
+        )
+        form_ui.addLabel(D_("Current profile password"))
+        form_ui.addPassword("current_passwd", value="")
+        form_ui.addLabel(D_("New password"))
+        form_ui.addPassword("new_passwd1", value="")
+        form_ui.addLabel(D_("New password (again)"))
+        form_ui.addPassword("new_passwd2", value="")
+
+        # FIXME: uncomment and fix these features
+        """
+        if 'GROUPBLOG' in self.host.plugins:
+            tab_container.add_tab("delete_posts", D_("Delete your posts"), container=xml_tools.PairsContainer)
+            form_ui.addLabel(D_("Current profile password"))
+            form_ui.addPassword("delete_posts_passwd", value="")
+            form_ui.addLabel(D_("Delete all your posts and their comments"))
+            form_ui.addBool("delete_posts_checkbox", "false")
+            form_ui.addLabel(D_("Delete all your comments on other's posts"))
+            form_ui.addBool("delete_comments_checkbox", "false")
+
+        tab_container.add_tab("delete", D_("Delete your account"), container=xml_tools.PairsContainer)
+        form_ui.addLabel(D_("Current profile password"))
+        form_ui.addPassword("delete_passwd", value="")
+        form_ui.addLabel(D_("Delete your account"))
+        form_ui.addBool("delete_checkbox", "false")
+        """
+
+        return form_ui.toXml()
+
+    @defer.inlineCallbacks
+    def _account_dialog_cb(self, data, profile):
+        """Called when the user submits the main account dialog
+        @param data
+        @param profile
+        """
+        sat_cipher = yield self.host.memory.param_get_a_async(
+            C.PROFILE_PASS_PATH[1], C.PROFILE_PASS_PATH[0], profile_key=profile
+        )
+
+        @defer.inlineCallbacks
+        def verify(attempt):
+            auth = yield PasswordHasher.verify(attempt, sat_cipher)
+            defer.returnValue(auth)
+
+        def error_ui(message=None):
+            if not message:
+                message = D_("The provided profile password doesn't match.")
+            error_ui = xml_tools.XMLUI("popup", title=D_("Attempt failure"))
+            error_ui.addText(message)
+            return {"xmlui": error_ui.toXml()}
+
+        # check for account deletion
+        # FIXME: uncomment and fix these features
+        """
+        delete_passwd = data[xml_tools.SAT_FORM_PREFIX + 'delete_passwd']
+        delete_checkbox = data[xml_tools.SAT_FORM_PREFIX + 'delete_checkbox']
+        if delete_checkbox == 'true':
+            verified = yield verify(delete_passwd)
+            assert isinstance(verified, bool)
+            if verified:
+                defer.returnValue(self.__delete_account(profile))
+            defer.returnValue(error_ui())
+
+        # check for blog posts deletion
+        if 'GROUPBLOG' in self.host.plugins:
+            delete_posts_passwd = data[xml_tools.SAT_FORM_PREFIX + 'delete_posts_passwd']
+            delete_posts_checkbox = data[xml_tools.SAT_FORM_PREFIX + 'delete_posts_checkbox']
+            delete_comments_checkbox = data[xml_tools.SAT_FORM_PREFIX + 'delete_comments_checkbox']
+            posts = delete_posts_checkbox == 'true'
+            comments = delete_comments_checkbox == 'true'
+            if posts or comments:
+                verified = yield verify(delete_posts_passwd)
+                assert isinstance(verified, bool)
+                if verified:
+                    defer.returnValue(self.__delete_blog_posts(posts, comments, profile))
+                defer.returnValue(error_ui())
+        """
+
+        # check for password modification
+        current_passwd = data[xml_tools.SAT_FORM_PREFIX + "current_passwd"]
+        new_passwd1 = data[xml_tools.SAT_FORM_PREFIX + "new_passwd1"]
+        new_passwd2 = data[xml_tools.SAT_FORM_PREFIX + "new_passwd2"]
+        if new_passwd1 or new_passwd2:
+            verified = yield verify(current_passwd)
+            assert isinstance(verified, bool)
+            if verified:
+                if new_passwd1 == new_passwd2:
+                    data = yield self.__change_password(new_passwd1, profile=profile)
+                    defer.returnValue(data)
+                else:
+                    defer.returnValue(
+                        error_ui(
+                            D_("The values entered for the new password are not equal.")
+                        )
+                    )
+            defer.returnValue(error_ui())
+
+        defer.returnValue({})
+
+    def __change_password(self, password, profile):
+        """Ask for a confirmation before changing the XMPP account and SàT profile passwords.
+
+        @param password (str): the new password
+        @param profile (str): %(doc_profile)s
+        """
+        session_id, __ = self._sessions.new_session(
+            {"new_password": password}, profile=profile
+        )
+        form_ui = xml_tools.XMLUI(
+            "form",
+            title=D_("Change your password?"),
+            submit_id=self.__change_password_id,
+            session_id=session_id,
+        )
+        form_ui.addText(
+            D_(
+                "Note for advanced users: this will actually change both your SàT profile password AND your XMPP account password."
+            )
+        )
+        form_ui.addText(D_("Continue with changing the password?"))
+        return {"xmlui": form_ui.toXml()}
+
+    def __change_password_cb(self, data, profile):
+        """Actually change the user XMPP account and SàT profile password
+        @param data (dict)
+        @profile (str): %(doc_profile)s
+        """
+        client = self.host.get_client(profile)
+        password = self._sessions.profile_get(data["session_id"], profile)["new_password"]
+        del self._sessions[data["session_id"]]
+
+        def password_changed(__):
+            d = self.host.memory.param_set(
+                C.PROFILE_PASS_PATH[1],
+                password,
+                C.PROFILE_PASS_PATH[0],
+                profile_key=profile,
+            )
+            d.addCallback(
+                lambda __: self.host.memory.param_set(
+                    "Password", password, "Connection", profile_key=profile
+                )
+            )
+            confirm_ui = xml_tools.XMLUI("popup", title=D_("Confirmation"))
+            confirm_ui.addText(D_("Your password has been changed."))
+            return defer.succeed({"xmlui": confirm_ui.toXml()})
+
+        def errback(failure):
+            error_ui = xml_tools.XMLUI("popup", title=D_("Error"))
+            error_ui.addText(
+                D_("Your password could not be changed: %s") % failure.getErrorMessage()
+            )
+            return defer.succeed({"xmlui": error_ui.toXml()})
+
+        d = self.host.plugins["XEP-0077"].change_password(client, password)
+        d.addCallbacks(password_changed, errback)
+        return d
+
+    def __delete_account(self, profile):
+        """Ask for a confirmation before deleting the XMPP account and SàT profile
+        @param profile
+        """
+        form_ui = xml_tools.XMLUI(
+            "form", title=D_("Delete your account?"), submit_id=self.__delete_account_id
+        )
+        form_ui.addText(
+            D_(
+                "If you confirm this dialog, you will be disconnected and then your XMPP account AND your SàT profile will both be DELETED."
+            )
+        )
+        target = D_(
+            "contact list, messages history, blog posts and comments"
+            if "GROUPBLOG" in self.host.plugins
+            else D_("contact list and messages history")
+        )
+        form_ui.addText(
+            D_(
+                "All your data stored on %(server)s, including your %(target)s will be erased."
+            )
+            % {"server": self.account_domain_new_get(), "target": target}
+        )
+        form_ui.addText(
+            D_(
+                "There is no other confirmation dialog, this is the very last one! Are you sure?"
+            )
+        )
+        return {"xmlui": form_ui.toXml()}
+
+    def __delete_account_cb(self, data, profile):
+        """Actually delete the XMPP account and SàT profile
+
+        @param data
+        @param profile
+        """
+        client = self.host.get_client(profile)
+
+        def user_deleted(__):
+
+            # FIXME: client should be disconnected at this point, so 2 next loop should be removed (to be confirmed)
+            for jid_ in client.roster._jids:  # empty roster
+                client.presence.unsubscribe(jid_)
+
+            for jid_ in self.host.memory.sub_waiting_get(
+                profile
+            ):  # delete waiting subscriptions
+                self.host.memory.del_waiting_sub(jid_)
+
+            delete_profile = lambda: self.host.memory.profile_delete_async(
+                profile, force=True
+            )
+            if "GROUPBLOG" in self.host.plugins:
+                d = self.host.plugins["GROUPBLOG"].deleteAllGroupBlogsAndComments(
+                    profile_key=profile
+                )
+                d.addCallback(lambda __: delete_profile())
+            else:
+                delete_profile()
+
+            return defer.succeed({})
+
+        def errback(failure):
+            error_ui = xml_tools.XMLUI("popup", title=D_("Error"))
+            error_ui.addText(
+                D_("Your XMPP account could not be deleted: %s")
+                % failure.getErrorMessage()
+            )
+            return defer.succeed({"xmlui": error_ui.toXml()})
+
+        d = self.host.plugins["XEP-0077"].unregister(client, jid.JID(client.jid.host))
+        d.addCallbacks(user_deleted, errback)
+        return d
+
+    def __delete_blog_posts(self, posts, comments, profile):
+        """Ask for a confirmation before deleting the blog posts
+        @param posts: delete all posts of the user (and their comments)
+        @param comments: delete all the comments of the user on other's posts
+        @param data
+        @param profile
+        """
+        if posts:
+            if comments:  # delete everything
+                form_ui = xml_tools.XMLUI(
+                    "form",
+                    title=D_("Delete all your (micro-)blog posts and comments?"),
+                    submit_id=self.__delete_posts_comments_id,
+                )
+                form_ui.addText(
+                    D_(
+                        "If you confirm this dialog, all the (micro-)blog data you submitted will be erased."
+                    )
+                )
+                form_ui.addText(
+                    D_(
+                        "These are the public and private posts and comments you sent to any group."
+                    )
+                )
+                form_ui.addText(
+                    D_(
+                        "There is no other confirmation dialog, this is the very last one! Are you sure?"
+                    )
+                )
+            else:  # delete only the posts
+                form_ui = xml_tools.XMLUI(
+                    "form",
+                    title=D_("Delete all your (micro-)blog posts?"),
+                    submit_id=self.__delete_posts_id,
+                )
+                form_ui.addText(
+                    D_(
+                        "If you confirm this dialog, all the public and private posts you sent to any group will be erased."
+                    )
+                )
+                form_ui.addText(
+                    D_(
+                        "There is no other confirmation dialog, this is the very last one! Are you sure?"
+                    )
+                )
+        elif comments:  # delete only the comments
+            form_ui = xml_tools.XMLUI(
+                "form",
+                title=D_("Delete all your (micro-)blog comments?"),
+                submit_id=self.__delete_comments_id,
+            )
+            form_ui.addText(
+                D_(
+                    "If you confirm this dialog, all the public and private comments you made on other people's posts will be erased."
+                )
+            )
+            form_ui.addText(
+                D_(
+                    "There is no other confirmation dialog, this is the very last one! Are you sure?"
+                )
+            )
+
+        return {"xmlui": form_ui.toXml()}
+
+    def __delete_blog_posts_cb(self, posts, comments, data, profile):
+        """Actually delete the XMPP account and SàT profile
+        @param posts: delete all posts of the user (and their comments)
+        @param comments: delete all the comments of the user on other's posts
+        @param profile
+        """
+        if posts:
+            if comments:
+                target = D_("blog posts and comments")
+                d = self.host.plugins["GROUPBLOG"].deleteAllGroupBlogsAndComments(
+                    profile_key=profile
+                )
+            else:
+                target = D_("blog posts")
+                d = self.host.plugins["GROUPBLOG"].deleteAllGroupBlogs(
+                    profile_key=profile
+                )
+        elif comments:
+            target = D_("comments")
+            d = self.host.plugins["GROUPBLOG"].deleteAllGroupBlogsComments(
+                profile_key=profile
+            )
+
+        def deleted(result):
+            ui = xml_tools.XMLUI("popup", title=D_("Deletion confirmation"))
+            # TODO: change the message when delete/retract notifications are done with XEP-0060
+            ui.addText(D_("Your %(target)s have been deleted.") % {"target": target})
+            ui.addText(
+                D_(
+                    "Known issue of the demo version: you need to refresh the page to make the deleted posts actually disappear."
+                )
+            )
+            return defer.succeed({"xmlui": ui.toXml()})
+
+        def errback(failure):
+            error_ui = xml_tools.XMLUI("popup", title=D_("Error"))
+            error_ui.addText(
+                D_("Your %(target)s could not be deleted: %(message)s")
+                % {"target": target, "message": failure.getErrorMessage()}
+            )
+            return defer.succeed({"xmlui": error_ui.toXml()})
+
+        d.addCallbacks(deleted, errback)
+        return d
+
+    def credentials_xmpp_connect(self, jid_s, password):
+        """Create and connect a new SàT profile using the given XMPP credentials.
+
+        Re-use given JID and XMPP password for the profile name and profile password.
+        @param jid_s (unicode): JID
+        @param password (unicode): XMPP password
+        @return Deferred(bool)
+        @raise exceptions.PasswordError, exceptions.ConflictError
+        """
+        try:  # be sure that the profile doesn't exist yet
+            self.host.memory.get_profile_name(jid_s)
+        except exceptions.ProfileUnknownError:
+            pass
+        else:
+            raise exceptions.ConflictError
+
+        d = self.create_profile(password, jid_s, jid_s)
+        d.addCallback(
+            lambda __: self.host.memory.get_profile_name(jid_s)
+        )  # checks if the profile has been successfuly created
+        d.addCallback(lambda profile: defer.ensureDeferred(
+            self.host.connect(profile, password, {}, 0)))
+
+        def connected(result):
+            self.send_emails(None, profile=jid_s)
+            return result
+
+        def remove_profile(
+            failure
+        ):  # profile has been successfully created but the XMPP credentials are wrong!
+            log.debug(
+                "Removing previously auto-created profile: %s" % failure.getErrorMessage()
+            )
+            self.host.memory.profile_delete_async(jid_s)
+            raise failure
+
+        # FIXME: we don't catch the case where the JID host is not an XMPP server, and the user
+        # has to wait until the DBUS timeout ; as a consequence, emails are sent to the admins
+        # and the profile is not deleted. When the host exists, remove_profile is well called.
+        d.addCallbacks(connected, remove_profile)
+        return d