Skip to content
Snippets Groups Projects
Select Git revision
  • 93abfeaf7fcad96ad0919252ff412ccbe031e35a
  • master default protected
  • release/201811
  • release/201812
  • release/201901
  • release/201902
  • release/201903
  • release/201904
  • release/201905
  • release/201906
  • release/201908
  • release/201912
  • release/202001
  • release/202005
  • release/windows-test/201910
  • release/201808
  • wip/smartlist_refacto
  • wip/patches_poly_2017/JimmyHamel/MathieuGirouxHuppe
18 results

RingConsolePanel.xaml

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    jamiaccount.cpp NaN GiB
    /*
     *  Copyright (C) 2004-2020 Savoir-faire Linux Inc.
     *
     *  Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
     *  Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com>
     *  Author: Simon Désaulniers <simon.desaulniers@savoirfairelinux.com>
     *  Author: Nicolas Jäger <nicolas.jager@savoirfairelinux.com>
     *  Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
     *
     *  This program is free software; you can redistribute it and/or modify
     *  it under the terms of the GNU 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 General Public License for more details.
     *
     *  You should have received a copy of the GNU General Public License
     *  along with this program; if not, write to the Free Software
     *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
     */
    
    #ifdef HAVE_CONFIG_H
    #include "config.h"
    #endif
    
    #include "jamiaccount.h"
    
    #include "logger.h"
    
    #include "accountarchive.h"
    #include "jami_contact.h"
    #include "configkeys.h"
    #include "contact_list.h"
    #include "archive_account_manager.h"
    #include "server_account_manager.h"
    #include "jamidht/channeled_transport.h"
    #include "multiplexed_socket.h"
    
    #include "sip/sdp.h"
    #include "sip/sipvoiplink.h"
    #include "sip/sipcall.h"
    #include "sip/siptransport.h"
    #include "sip/sip_utils.h"
    
    #include "sips_transport_ice.h"
    #include "ice_transport.h"
    
    #include "p2p.h"
    #include "connectionmanager.h"
    
    #include "client/ring_signal.h"
    #include "dring/call_const.h"
    #include "dring/account_const.h"
    
    #include "upnp/upnp_control.h"
    #include "system_codec_container.h"
    
    #include "account_schema.h"
    #include "manager.h"
    #include "utf8_utils.h"
    
    #ifdef ENABLE_VIDEO
    #include "libav_utils.h"
    #endif
    #include "fileutils.h"
    #include "string_utils.h"
    #include "array_size.h"
    #include "archiver.h"
    #include "data_transfer.h"
    
    #include "config/yamlparser.h"
    #include "security/certstore.h"
    #include "libdevcrypto/Common.h"
    #include "base64.h"
    #include "im/instant_messaging.h"
    
    #include <opendht/thread_pool.h>
    #include <opendht/peer_discovery.h>
    #include <opendht/http.h>
    
    #include <yaml-cpp/yaml.h>
    #include <json/json.h>
    
    #include <unistd.h>
    
    #include <algorithm>
    #include <array>
    #include <cctype>
    #include <cinttypes>
    #include <cstdarg>
    #include <initializer_list>
    #include <memory>
    #include <regex>
    #include <sstream>
    #include <string>
    #include <system_error>
    
    using namespace std::placeholders;
    
    namespace jami {
    
    constexpr pj_str_t STR_MESSAGE_ID = jami::sip_utils::CONST_PJ_STR("Message-ID");
    
    struct PendingConfirmation {
        std::mutex lock;
        bool replied {false};
        std::map<dht::InfoHash, std::future<size_t>> listenTokens {};
    };
    
    // Used to pass infos to a pjsip callback (pjsip_endpt_send_request)
    struct TextMessageCtx {
        std::weak_ptr<JamiAccount> acc;
        std::string to;
        std::string deviceId;
        uint64_t id;
        bool retryOnTimeout;
        std::shared_ptr<PendingConfirmation> confirmation;
    };
    
    struct VCardMessageCtx {
        std::shared_ptr<std::atomic_int> success;
        int total;
        std::string path;
    };
    
    namespace Migration {
    
    enum class State { // Contains all the Migration states
        SUCCESS,
        INVALID
    };
    
    std::string
    mapStateNumberToString(const State migrationState)
    {
    #define CASE_STATE(X) case Migration::State::X: \
                               return #X
    
        switch (migrationState) {
            CASE_STATE(INVALID);
            CASE_STATE(SUCCESS);
        }
        return {};
    }
    
    void
    setState (const std::string& accountID,
              const State migrationState)
    {
        emitSignal<DRing::ConfigurationSignal::MigrationEnded>(accountID,
            mapStateNumberToString(migrationState));
    }
    
    } // namespace jami::Migration
    
    struct JamiAccount::BuddyInfo
    {
        /* the buddy id */
        dht::InfoHash id;
    
        /* number of devices connected on the DHT */
        uint32_t devices_cnt {};
    
        /* The disposable object to update buddy info */
        std::future<size_t> listenToken;
    
        BuddyInfo(dht::InfoHash id) : id(id) {}
    };
    
    struct JamiAccount::PendingCall
    {
        std::chrono::steady_clock::time_point start;
        std::shared_ptr<IceTransport> ice_sp;
        std::shared_ptr<IceTransport> ice_tcp_sp;
        std::weak_ptr<SIPCall> call;
        std::future<size_t> listen_key;
        dht::InfoHash call_key;
        dht::InfoHash from;
        dht::InfoHash from_account;
        std::shared_ptr<dht::crypto::Certificate> from_cert;
    };
    
    struct JamiAccount::PendingMessage
    {
        std::set<dht::InfoHash> to;
    };
    
    struct AccountPeerInfo
    {
        dht::InfoHash accountId;
        std::string displayName;
        MSGPACK_DEFINE(accountId, displayName)
    };
    
    struct JamiAccount::DiscoveredPeer
    {
        std::string displayName;
        std::shared_ptr<Task> cleanupTask;
    };
    
    static constexpr int ICE_COMPONENTS {1};
    static constexpr int ICE_COMP_SIP_TRANSPORT {0};
    static constexpr auto ICE_NEGOTIATION_TIMEOUT = std::chrono::seconds(60);
    static constexpr auto TLS_TIMEOUT = std::chrono::seconds(30);
    const constexpr auto EXPORT_KEY_RENEWAL_TIME = std::chrono::minutes(20);
    
    static constexpr const char * const RING_URI_PREFIX = "ring:";
    static constexpr const char * const JAMI_URI_PREFIX = "jami:";
    static constexpr const char * DEFAULT_TURN_SERVER = "turn.jami.net";
    static constexpr const char * DEFAULT_TURN_USERNAME = "ring";
    static constexpr const char * DEFAULT_TURN_PWD = "ring";
    static constexpr const char * DEFAULT_TURN_REALM = "ring";
    static const auto PROXY_REGEX = std::regex("(https?://)?([\\w\\.]+)(:(\\d+)|:\\[(.+)-(.+)\\])?");
    static const std::string PEER_DISCOVERY_JAMI_SERVICE = "jami";
    const constexpr auto PEER_DISCOVERY_EXPIRATION = std::chrono::minutes(1);
    
    constexpr const char* const JamiAccount::ACCOUNT_TYPE;
    /* constexpr */ const std::pair<uint16_t, uint16_t> JamiAccount::DHT_PORT_RANGE {4000, 8888};
    
    using ValueIdDist = std::uniform_int_distribution<dht::Value::Id>;
    
    static const std::string
    stripPrefix(const std::string& toUrl)
    {
        auto dhtf = toUrl.find(RING_URI_PREFIX);
        if (dhtf != std::string::npos) {
            dhtf = dhtf+5;
        } else {
            dhtf = toUrl.find(JAMI_URI_PREFIX);
            if (dhtf != std::string::npos) {
                dhtf = dhtf+5;
            } else {
                dhtf = toUrl.find("sips:");
                dhtf = (dhtf == std::string::npos) ? 0 : dhtf+5;
            }
        }
        while (dhtf < toUrl.length() && toUrl[dhtf] == '/')
            dhtf++;
        return toUrl.substr(dhtf);
    }
    
    static const std::string
    parseJamiUri(const std::string& toUrl)
    {
        auto sufix = stripPrefix(toUrl);
        if (sufix.length() < 40)
            throw std::invalid_argument("id must be a Jami infohash");
    
        const std::string toUri = sufix.substr(0, 40);
        if (std::find_if_not(toUri.cbegin(), toUri.cend(), ::isxdigit) != toUri.cend())
            throw std::invalid_argument("id must be a Jami infohash");
        return toUri;
    }
    
    static constexpr const char*
    dhtStatusStr(dht::NodeStatus status) {
        return status == dht::NodeStatus::Connected  ? "connected"  : (
               status == dht::NodeStatus::Connecting ? "connecting" :
                                                       "disconnected");
    }
    
    /**
     * Local ICE Transport factory helper
     *
     * JamiAccount must use this helper than direct IceTranportFactory API
     */
    template <class... Args>
    std::shared_ptr<IceTransport>
    JamiAccount::createIceTransport(const Args&... args)
    {
        auto ice = Manager::instance().getIceTransportFactory().createTransport(args...);
        if (!ice)
            throw std::runtime_error("ICE transport creation failed");
    
        return ice;
    }
    
    JamiAccount::JamiAccount(const std::string& accountID, bool /* presenceEnabled */)
        : SIPAccountBase(accountID)
        , dht_(new dht::DhtRunner)
        , idPath_(fileutils::get_data_dir()+DIR_SEPARATOR_STR+getAccountID())
        , cachePath_(fileutils::get_cache_dir()+DIR_SEPARATOR_STR+getAccountID())
        , dataPath_(cachePath_ + DIR_SEPARATOR_STR "values")
        , dhtPeerConnector_ {}
        , connectionManager_ {}
    {
        // Force the SFL turn server if none provided yet
        turnServer_ = DEFAULT_TURN_SERVER;
        turnServerUserName_ = DEFAULT_TURN_USERNAME;
        turnServerPwd_ = DEFAULT_TURN_PWD;
        turnServerRealm_ = DEFAULT_TURN_REALM;
        turnEnabled_ = true;
    
        proxyListUrl_ = DHT_DEFAULT_PROXY_LIST_URL;
        proxyServer_ = DHT_DEFAULT_PROXY;
    
        try {
            std::istringstream is(fileutils::loadCacheTextFile(cachePath_ + DIR_SEPARATOR_STR "dhtproxy", std::chrono::hours(24 * 7)));
            std::getline(is, proxyServerCached_);
        } catch (const std::exception& e) {
            JAMI_DBG("Can't load proxy URL from cache: %s", e.what());
        }
    
        setActiveCodecs({});
    }
    
    JamiAccount::~JamiAccount()
    {
        shutdownConnections();
        if(peerDiscovery_){
            peerDiscovery_->stopPublish(PEER_DISCOVERY_JAMI_SERVICE);
            peerDiscovery_->stopDiscovery(PEER_DISCOVERY_JAMI_SERVICE);
        }
        if (auto dht = dht_)
            dht->join();
    }
    
    void
    JamiAccount::shutdownConnections()
    {
        connectionManager_.reset();
        dhtPeerConnector_.reset();
        std::lock_guard<std::mutex> lk(sipConnectionsMtx_);
        sipConnections_.clear();
        pendingSipConnections_.clear();
    }
    
    void
    JamiAccount::flush()
    {
        // Class base method
        SIPAccountBase::flush();
    
        fileutils::removeAll(dataPath_);
        fileutils::removeAll(cachePath_);
        fileutils::removeAll(idPath_, true);
    }
    
    std::shared_ptr<SIPCall>
    JamiAccount::newIncomingCall(const std::string& from, const std::map<std::string, std::string>& details, const std::shared_ptr<SipTransport>& sipTr)
    {
        std::lock_guard<std::mutex> lock(callsMutex_);
    
        if (sipTr) {
            std::unique_lock<std::mutex> lk(sipConnectionsMtx_);
            auto sipConnIt = sipConnections_.find(from);
            if (sipConnIt != sipConnections_.end() && !sipConnIt->second.empty()) {
                for (auto dit = sipConnIt->second.rbegin(); dit != sipConnIt->second.rend(); ++dit) {
                    for (auto it = dit->second.rbegin(); it != dit->second.rend(); ++it) {
                        // Search linked Sip Transport
                        if (it->transport != sipTr) continue;
    
                        auto call = Manager::instance().callFactory.newCall<SIPCall, JamiAccount>(*this, Manager::instance().getNewCallID(), Call::CallType::INCOMING);
                        if (!call) return {};
    
                        std::weak_ptr<SIPCall> wcall = call;
                        call->setPeerUri(RING_URI_PREFIX + from);
                        call->setPeerNumber(from);
    
                        call->updateDetails(details);
                        return call;
                    }
                }
            }
            lk.unlock();
        }
    
        auto call_it = pendingSipCalls_.begin();
        while (call_it != pendingSipCalls_.end()) {
            auto call = call_it->call.lock();
            if (not call) {
                JAMI_WARN("newIncomingCall: discarding deleted call");
                call_it = pendingSipCalls_.erase(call_it);
            } else if (call->getPeerNumber() == from || (call_it->from_cert and
                                                         call_it->from_cert->issuer and
                                                         call_it->from_cert->issuer->getId().toString() == from)) {
                JAMI_DBG("newIncomingCall: found matching call for %s", from.c_str());
                pendingSipCalls_.erase(call_it);
                call->updateDetails(details);
                return call;
            } else {
                ++call_it;
            }
        }
        JAMI_ERR("newIncomingCall: can't find matching call for %s", from.c_str());
        return nullptr;
    }
    
    template <>
    std::shared_ptr<SIPCall>
    JamiAccount::newOutgoingCall(const std::string& toUrl,
                                 const std::map<std::string, std::string>& volatileCallDetails)
    {
        auto suffix = stripPrefix(toUrl);
        JAMI_DBG() << *this << "Calling DHT peer " << suffix;
        auto& manager = Manager::instance();
        auto call = manager.callFactory.newCall<SIPCall, JamiAccount>(*this, manager.getNewCallID(),
                                                                      Call::CallType::OUTGOING,
                                                                      volatileCallDetails);
    
        call->setIPToIP(true);
        call->setSecure(isTlsEnabled());
    
        try {
            const std::string toUri = parseJamiUri(suffix);
            startOutgoingCall(call, toUri);
        } catch (...) {
    #if HAVE_RINGNS
            NameDirectory::lookupUri(suffix, nameServer_, [wthis_=weak(), call](const std::string& result,
                                                                       NameDirectory::Response response) {
                // we may run inside an unknown thread, but following code must be called in main thread
                runOnMainThread([wthis_, result, response, call]() {
                    if (response != NameDirectory::Response::found) {
                        call->onFailure(EINVAL);
                        return;
                    }
                    if (auto sthis = wthis_.lock()) {
                        try {
                            const std::string toUri = parseJamiUri(result);
                            sthis->startOutgoingCall(call, toUri);
                        } catch (...) {
                            call->onFailure(ENOENT);
                        }
                    } else {
                        call->onFailure();
                    }
                });
            });
    #else
            call->onFailure(ENOENT);
    #endif
        }
    
        return call;
    }
    
    void
    initICE(const std::vector<uint8_t> &msg, const std::shared_ptr<IceTransport> &ice,
            const std::shared_ptr<IceTransport> &ice_tcp, bool &udp_failed, bool &tcp_failed)
    {
        auto sdp_list = IceTransport::parseSDPList(msg);
        for (const auto &sdp : sdp_list) {
            if (sdp.candidates.size() > 0) {
                if (sdp.candidates[0].find("TCP") != std::string::npos) {
                    // It is a SDP for the TCP component
                    tcp_failed = (ice_tcp && !ice_tcp->start(sdp));
                } else {
                    // For UDP
                    udp_failed = (ice && !ice->start(sdp));
                }
            }
        }
    
        // During the ICE reply we can start the ICE negotiation
        if (tcp_failed && ice_tcp) {
            ice_tcp->stop();
            JAMI_WARN("ICE over TCP not started, will only use UDP");
        }
    }
    
    void
    JamiAccount::startOutgoingCall(const std::shared_ptr<SIPCall>& call, const std::string& toUri)
    {
        if (not accountManager_) {
            call->onFailure(ENETDOWN);
            return;
        }
        // TODO: for now, we automatically trust all explicitly called peers
        setCertificateStatus(toUri, tls::TrustStore::PermissionStatus::ALLOWED);
    
        call->setPeerNumber(toUri + "@ring.dht");
        call->setPeerUri(RING_URI_PREFIX + toUri);
        call->setState(Call::ConnectionState::TRYING);
        std::weak_ptr<SIPCall> wCall = call;
    
    #if HAVE_RINGNS
        accountManager_->lookupAddress(toUri, [wCall](const std::string& result, const NameDirectory::Response& response){
            if (response == NameDirectory::Response::found)
                if (auto call = wCall.lock()) {
                    call->setPeerRegistredName(result);
                    call->setPeerUri(RING_URI_PREFIX + result);
                }
        });
    #endif
    
        dht::InfoHash peer_account(toUri);
        auto sendDhtRequest = [this, wCall, toUri, peer_account](const std::string& deviceId) {
            auto call = wCall.lock();
            if (not call) return;
            JAMI_DBG("[call %s] calling device %s", call->getCallId().c_str(), deviceId.c_str());
    
            auto& manager = Manager::instance();
            auto dev_call = manager.callFactory.newCall<SIPCall, JamiAccount>(*this, manager.getNewCallID(),
                                                                              Call::CallType::OUTGOING,
                                                                              call->getDetails());
    
            auto callId = dev_call->getCallId();
            auto onNegoDone = [callId, w=weak()](bool) {
                runOnMainThread([callId, w]() {
                    if (auto shared = w.lock())
                        shared->checkPendingCall(callId);
                });
            };
    
            std::weak_ptr<SIPCall> weak_dev_call = dev_call;
            auto iceOptions = getIceOptions();
            iceOptions.onNegoDone = onNegoDone;
            dev_call->setIPToIP(true);
            dev_call->setSecure(isTlsEnabled());
            auto ice = createIceTransport(("sip:" + dev_call->getCallId()).c_str(),
                                                 ICE_COMPONENTS, true, iceOptions);
            if (not ice) {
                JAMI_WARN("[call %s] Can't create ICE", call->getCallId().c_str());
                dev_call->removeCall();
                return;
            }
    
            iceOptions.tcpEnable = true;
            auto ice_tcp = createIceTransport(("sip:" + dev_call->getCallId()).c_str(), ICE_COMPONENTS, true, iceOptions);
            if (not ice_tcp) {
                JAMI_WARN("Can't create ICE over TCP, will only use UDP");
            }
            call->addSubCall(*dev_call);
    
            manager.addTask([w=weak(), weak_dev_call, ice, ice_tcp, deviceId, toUri, peer_account] {
                auto sthis = w.lock();
                if (not sthis) {
                    dht::ThreadPool::io().run([ice=std::move(ice), ice_tcp=std::move(ice_tcp)](){});
                    return false;
                }
                auto call = weak_dev_call.lock();
    
                // call aborted?
                if (not call) {
                    dht::ThreadPool::io().run([ice=std::move(ice), ice_tcp=std::move(ice_tcp)](){});
                    return false;
                }
    
                if (ice->isFailed()) {
                    JAMI_ERR("[call:%s] ice init failed", call->getCallId().c_str());
                    call->onFailure(EIO);
                    dht::ThreadPool::io().run([ice=std::move(ice), ice_tcp=std::move(ice_tcp)](){});
                    return false;
                }
    
                if (ice_tcp && ice_tcp->isFailed()) {
                    JAMI_WARN("[call:%s] ice tcp init failed, will only use UDP", call->getCallId().c_str());
                }
    
                // Loop until ICE transport is initialized.
                // Note: we suppose that ICE init routine has a an internal timeout (bounded in time)
                // and we let upper layers decide when the call shall be aborded (our first check upper).
                if ((not ice->isInitialized()) || (ice_tcp && !ice_tcp->isInitialized()))
                    return true;
    
                sthis->registerDhtAddress(*ice);
                if (ice_tcp) sthis->registerDhtAddress(*ice_tcp);
                // Next step: sent the ICE data to peer through DHT
                const dht::Value::Id callvid  = ValueIdDist()(sthis->rand);
                const auto callkey = dht::InfoHash::get("callto:" + deviceId);
                auto blob = ice->packIceMsg();
                if (ice_tcp)  {
                    auto ice_tcp_msg = ice_tcp->packIceMsg(2);
                    blob.insert(blob.end(), ice_tcp_msg.begin(), ice_tcp_msg.end());
                }
                dht::Value val { dht::IceCandidates(callvid,  blob) };
    
                dht::InfoHash dev(deviceId);
                sthis->dht_->putEncrypted(
                    callkey, dev,
                    std::move(val),
                    [weak_dev_call](bool ok) { // Put complete callback
                        if (!ok) {
                            JAMI_WARN("Can't put ICE descriptor on DHT");
                            if (auto call = weak_dev_call.lock())
                                call->onFailure();
                        } else
                            JAMI_DBG("Successfully put ICE descriptor on DHT");
                    }
                );
    
                auto listenKey = sthis->dht_->listen<dht::IceCandidates>(
                    callkey,
                    [weak_dev_call, ice, ice_tcp, callvid, deviceId] (dht::IceCandidates&& msg) {
                        if (msg.id != callvid or msg.from.toString() != deviceId)
                            return true;
                        auto call = weak_dev_call.lock();
                        if (!call)
                            return false;
                        // remove unprintable characters
                        auto iceData = std::string(msg.ice_data.cbegin(), msg.ice_data.cend());
                        iceData.erase(std::remove_if(iceData.begin(), iceData.end(),
                                                     [](unsigned char c){ return !std::isprint(c) && !std::isspace(c); }
                                                    ), iceData.end());
                        JAMI_WARN("ICE request for call %s replied from DHT peer %s\nData: %s", call->getCallId().c_str(), deviceId.c_str(), iceData.c_str());
                        call->setState(Call::ConnectionState::PROGRESSING);
    
                        auto udp_failed = true, tcp_failed = true;
                        initICE(msg.ice_data, ice, ice_tcp, udp_failed, tcp_failed);
                        if (udp_failed && tcp_failed) {
                            call->onFailure();
                            return true;
                        }
                        return false;
                    }
                );
    
                std::lock_guard<std::mutex> lock(sthis->callsMutex_);
                sthis->pendingCalls_.emplace(call->getCallId(), PendingCall {
                    std::chrono::steady_clock::now(),
                    std::move(ice), std::move(ice_tcp), weak_dev_call,
                    std::move(listenKey),
                    callkey,
                    dev,
                    peer_account,
                    tls::CertificateStore::instance().getCertificate(toUri)
                });
    
                Manager::instance().scheduleTask([w, callId=call->getCallId()]() {
                    if (auto shared = w.lock())
                        shared->checkPendingCall(callId);
                }, std::chrono::steady_clock::now() + ICE_NEGOTIATION_TIMEOUT);
                return false;
            });
        };
    
        // Call connected devices
        std::set<std::string> devices;
        std::unique_lock<std::mutex> lk(sipConnectionsMtx_);
        auto& sipConns = sipConnections_[toUri];
        // NOTE: dummyCall is a call used to avoid to mark the call as failed if the
        // cached connection is failing with ICE (close event still not detected).
        auto& manager = Manager::instance();
        auto dummyCall = manager.callFactory.newCall<SIPCall, JamiAccount>(*this, manager.getNewCallID(),
                                                                                Call::CallType::OUTGOING,
                                                                                call->getDetails());
        dummyCall->setIPToIP(true);
        dummyCall->setSecure(isTlsEnabled());
        call->addSubCall(*dummyCall);
        for (auto deviceConnIt = sipConns.begin(); deviceConnIt != sipConns.end(); ++deviceConnIt) {
            if (deviceConnIt->second.empty()) continue;
            auto& it = deviceConnIt->second.back();
    
            auto transport = it.transport;
            if (!it.channel->underlyingICE()) {
                JAMI_WARN("A SIP transport exists without Channel, this is a bug. Please report");
                continue;
            }
            if (!transport) continue;
    
            JAMI_WARN("[call %s] A channeled socket is detected with this peer.", call->getCallId().c_str());
    
            auto dev_call = manager.callFactory.newCall<SIPCall, JamiAccount>(*this, manager.getNewCallID(),
                                                                              Call::CallType::OUTGOING,
                                                                              call->getDetails());
            dev_call->setIPToIP(true);
            dev_call->setSecure(isTlsEnabled());
            dev_call->setTransport(transport);
            call->addSubCall(*dev_call);
            // Set the call in PROGRESSING State because the ICE session
            // is already ready. Note that this line should be after
            // addSubcall() to change the state of the main call
            // and avoid to get an active call in a TRYING state.
            dev_call->setState(Call::ConnectionState::PROGRESSING);
    
            auto remoted_address = it.channel->underlyingICE()->getRemoteAddress(ICE_COMP_SIP_TRANSPORT);
            try {
                onConnectedOutgoingCall(*dev_call, toUri, remoted_address);
            } catch (const VoipLinkException&) {
                // In this case, the main scenario is that SIPStartCall failed because
                // the ICE is dead and the TLS session didn't send any packet on that dead
                // link (connectivity change, killed by the os, etc)
                // Here, we don't need to do anything, the TLS will fail and will delete
                // the cached transport
                continue;
            }
            devices.emplace(deviceConnIt->first);
    
            call->setOnNeedFallback([sendDhtRequest, deviceId = deviceConnIt->first]() {
                sendDhtRequest(deviceId);
            });
        }
    
        // Find listening devices for this account
        accountManager_->forEachDevice(peer_account, [this, toUri, devices, sendDhtRequest, callId=call->getCallId()](const dht::InfoHash& dev)
        {
            // Test if already sent via a SIP transport
            if (devices.find(dev.toString()) != devices.end()) return;
    
            JAMI_WARN("[call %s] No channeled socket with this peer. Send request + DHT request", callId.c_str());
            // Else, ask for a channel (for future calls/text messages) and send a DHT message
            requestSIPConnection(toUri, dev.toString());
    
            sendDhtRequest(dev.toString());
        }, [wCall, dummyCall] (bool ok) {
            // Mark the temp call as failed to stop the main call if necessary
            if (dummyCall) dummyCall->onFailure(static_cast<int>(std::errc::no_such_device_or_address));
            if (not ok) {
                if (auto call = wCall.lock()) {
                    JAMI_WARN("[call:%s] no devices found", call->getCallId().c_str());
                    call->onFailure(static_cast<int>(std::errc::no_such_device_or_address));
                }
            }
        });
    
    }
    
    void
    JamiAccount::onConnectedOutgoingCall(SIPCall& call, const std::string& to_id, IpAddr target)
    {
        JAMI_DBG("[call:%s] outgoing call connected to %s", call.getCallId().c_str(), to_id.c_str());
    
        call.initIceMediaTransport(true);
        call.setIPToIP(true);
        call.setPeerNumber(to_id);
    
        const auto localAddress = ip_utils::getInterfaceAddr(getLocalInterface(), target.getFamily());
    
        IpAddr addrSdp;
        if (getUPnPActive()) {
            /* use UPnP addr, or published addr if its set */
            addrSdp = getPublishedSameasLocal() ?
                getUPnPIpAddress() : getPublishedIpAddress();
        } else {
            addrSdp = isStunEnabled() or (not getPublishedSameasLocal()) ?
                getPublishedIpAddress() : localAddress;
        }
    
        /* fallback on local address */
        if (not addrSdp) addrSdp = localAddress;
    
        // Initialize the session using ULAW as default codec in case of early media
        // The session should be ready to receive media once the first INVITE is sent, before
        // the session initialization is completed
        if (!getSystemCodecContainer()->searchCodecByName("PCMA", jami::MEDIA_AUDIO))
            throw VoipLinkException("Could not instantiate codec for early media");
    
        // Building the local SDP offer
        auto& sdp = call.getSDP();
    
        sdp.setPublishedIP(addrSdp);
        const bool created = sdp.createOffer(
                                getActiveAccountCodecInfoList(MEDIA_AUDIO),
                                getActiveAccountCodecInfoList(videoEnabled_ and not call.isAudioOnly() ? MEDIA_VIDEO
                                                                                                       : MEDIA_NONE),
                                getSrtpKeyExchange()
                             );
    
        if (not created or not SIPStartCall(call, target))
            throw VoipLinkException("Could not send outgoing INVITE request for new call");
    }
    
    std::shared_ptr<Call>
    JamiAccount::newOutgoingCall(const std::string& toUrl, const std::map<std::string, std::string>& volatileCallDetails)
    {
        return newOutgoingCall<SIPCall>(toUrl, volatileCallDetails);
    }
    
    bool
    JamiAccount::SIPStartCall(SIPCall& call, IpAddr target)
    {
        call.setupLocalSDPFromIce();
        std::string toUri(getToUri(call.getPeerNumber()+"@"+target.toString(true).c_str())); // expecting a fully well formed sip uri
    
        pj_str_t pjTo = pj_str((char*) toUri.c_str());
    
        // Create the from header
        std::string from(getFromUri());
        pj_str_t pjFrom = pj_str((char*) from.c_str());
    
        std::string targetStr = getToUri(target.toString(true)/*+";transport=ICE"*/);
        pj_str_t pjTarget = pj_str((char*) targetStr.c_str());
    
        pj_str_t pjContact;
        {
            auto transport = call.getTransport();
            pjContact = getContactHeader(transport ? transport->get() : nullptr);
        }
    
        JAMI_DBG("contact header: %.*s / %s -> %s / %.*s",
                 (int)pjContact.slen, pjContact.ptr, from.c_str(), toUri.c_str(),
                 (int)pjTarget.slen, pjTarget.ptr);
    
        auto local_sdp = call.getSDP().getLocalSdpSession();
        pjsip_dialog* dialog {nullptr};
        pjsip_inv_session* inv {nullptr};
        if (!CreateClientDialogAndInvite(&pjFrom, &pjContact, &pjTo, &pjTarget, local_sdp, &dialog, &inv))
            return false;
    
        inv->mod_data[link_.getModId()] = &call;
        call.inv.reset(inv);
    
    /*
        updateDialogViaSentBy(dialog);
        if (hasServiceRoute())
            pjsip_dlg_set_route_set(dialog, sip_utils::createRouteSet(getServiceRoute(), call->inv->pool));
    */
    
        pjsip_tx_data *tdata;
    
        if (pjsip_inv_invite(call.inv.get(), &tdata) != PJ_SUCCESS) {
            JAMI_ERR("Could not initialize invite messager for this call");
            return false;
        }
    
        pjsip_tpselector tp_sel;
        tp_sel.type = PJSIP_TPSELECTOR_TRANSPORT;
        if (!call.getTransport()) {
            JAMI_ERR("Could not get transport for this call");
            return false;
        }
        tp_sel.u.transport = call.getTransport()->get();
        if (pjsip_dlg_set_transport(dialog, &tp_sel) != PJ_SUCCESS) {
            JAMI_ERR("Unable to associate transport for invite session dialog");
            return false;
        }
    
        JAMI_DBG("[call:%s] Sending SIP invite", call.getCallId().c_str());
        if (pjsip_inv_send_msg(call.inv.get(), tdata) != PJ_SUCCESS) {
            JAMI_ERR("Unable to send invite message for this call");
            return false;
        }
    
        call.setState(Call::CallState::ACTIVE, Call::ConnectionState::PROGRESSING);
    
        return true;
    }
    
    void JamiAccount::saveConfig() const
    {
        try {
            YAML::Emitter accountOut;
            serialize(accountOut);
            auto accountConfig = getPath() + DIR_SEPARATOR_STR + "config.yml";
    
            std::lock_guard<std::mutex> lock(fileutils::getFileLock(accountConfig));
            std::ofstream fout = fileutils::ofstream(accountConfig);
            fout << accountOut.c_str();
            JAMI_DBG("Exported account to %s", accountConfig.c_str());
        } catch (const std::exception& e) {
            JAMI_ERR("Error exporting account: %s", e.what());
        }
    }
    
    void JamiAccount::serialize(YAML::Emitter &out) const
    {
        std::lock_guard<std::mutex> lock(configurationMutex_);
    
        if (registrationState_ == RegistrationState::INITIALIZING)
            return;
    
        out << YAML::BeginMap;
        SIPAccountBase::serialize(out);
        out << YAML::Key << Conf::DHT_PORT_KEY << YAML::Value << dhtPort_;
        out << YAML::Key << Conf::DHT_PUBLIC_IN_CALLS << YAML::Value << dhtPublicInCalls_;
        out << YAML::Key << Conf::DHT_ALLOW_PEERS_FROM_HISTORY << YAML::Value << allowPeersFromHistory_;
        out << YAML::Key << Conf::DHT_ALLOW_PEERS_FROM_CONTACT << YAML::Value << allowPeersFromContact_;
        out << YAML::Key << Conf::DHT_ALLOW_PEERS_FROM_TRUSTED << YAML::Value << allowPeersFromTrusted_;
        out << YAML::Key << DRing::Account::ConfProperties::DHT_PEER_DISCOVERY << YAML::Value << dhtPeerDiscovery_;
        out << YAML::Key << DRing::Account::ConfProperties::ACCOUNT_PEER_DISCOVERY << YAML::Value << accountPeerDiscovery_;
        out << YAML::Key << DRing::Account::ConfProperties::ACCOUNT_PUBLISH << YAML::Value << accountPublish_;
    
        out << YAML::Key << Conf::PROXY_ENABLED_KEY << YAML::Value << proxyEnabled_;
        out << YAML::Key << Conf::PROXY_SERVER_KEY << YAML::Value << proxyServer_;
        out << YAML::Key << Conf::PROXY_PUSH_TOKEN_KEY << YAML::Value << deviceKey_;
        out << YAML::Key << DRing::Account::ConfProperties::DHT_PROXY_LIST_URL << YAML::Value << proxyListUrl_;
    
    #if HAVE_RINGNS
        out << YAML::Key << DRing::Account::ConfProperties::RingNS::URI << YAML::Value <<  nameServer_;
        if (not registeredName_.empty())
            out << YAML::Key << DRing::Account::VolatileProperties::REGISTERED_NAME << YAML::Value << registeredName_;
    #endif
    
        out << YAML::Key << DRing::Account::ConfProperties::ARCHIVE_PATH << YAML::Value << archivePath_;
        out << YAML::Key << DRing::Account::ConfProperties::ARCHIVE_HAS_PASSWORD << YAML::Value << archiveHasPassword_;
        out << YAML::Key << Conf::RING_ACCOUNT_RECEIPT << YAML::Value << receipt_;
        if (receiptSignature_.size() > 0)
            out << YAML::Key << Conf::RING_ACCOUNT_RECEIPT_SIG << YAML::Value << YAML::Binary(receiptSignature_.data(), receiptSignature_.size());
        out << YAML::Key << DRing::Account::ConfProperties::RING_DEVICE_NAME << YAML::Value << ringDeviceName_;
        out << YAML::Key << DRing::Account::ConfProperties::MANAGER_URI << YAML::Value << managerUri_;
        out << YAML::Key << DRing::Account::ConfProperties::MANAGER_USERNAME << YAML::Value << managerUsername_;
    
        // tls submap
        out << YAML::Key << Conf::TLS_KEY << YAML::Value << YAML::BeginMap;
        SIPAccountBase::serializeTls(out);
        out << YAML::EndMap;
    
        out << YAML::EndMap;
    }
    
    void JamiAccount::unserialize(const YAML::Node &node)
    {
        std::lock_guard<std::mutex> lock(configurationMutex_);
    
        using yaml_utils::parseValue;
        using yaml_utils::parseValueOptional;
        using yaml_utils::parsePath;
    
        SIPAccountBase::unserialize(node);
    
        // get tls submap
        const auto &tlsMap = node[Conf::TLS_KEY];
        parsePath(tlsMap, Conf::CERTIFICATE_KEY, tlsCertificateFile_, idPath_);
        parsePath(tlsMap, Conf::CALIST_KEY, tlsCaListFile_, idPath_);
        parseValue(tlsMap, Conf::TLS_PASSWORD_KEY, tlsPassword_);
        parsePath(tlsMap, Conf::PRIVATE_KEY_KEY, tlsPrivateKeyFile_, idPath_);
    
        parseValue(node, Conf::DHT_ALLOW_PEERS_FROM_HISTORY, allowPeersFromHistory_);
        parseValue(node, Conf::DHT_ALLOW_PEERS_FROM_CONTACT, allowPeersFromContact_);
        parseValue(node, Conf::DHT_ALLOW_PEERS_FROM_TRUSTED, allowPeersFromTrusted_);
    
        parseValue(node, Conf::PROXY_ENABLED_KEY, proxyEnabled_);
        parseValue(node, Conf::PROXY_SERVER_KEY, proxyServer_);
        parseValue(node, Conf::PROXY_PUSH_TOKEN_KEY, deviceKey_);
        try {
            parseValue(node, DRing::Account::ConfProperties::DHT_PROXY_LIST_URL, proxyListUrl_);
        } catch (const std::exception& e) {
            proxyListUrl_ = DHT_DEFAULT_PROXY_LIST_URL;
        }
    
        parseValueOptional(node, DRing::Account::ConfProperties::RING_DEVICE_NAME, ringDeviceName_);
        parseValueOptional(node, DRing::Account::ConfProperties::MANAGER_URI, managerUri_);
        parseValueOptional(node, DRing::Account::ConfProperties::MANAGER_USERNAME, managerUsername_);
    
        try {
            parsePath(node, DRing::Account::ConfProperties::ARCHIVE_PATH, archivePath_, idPath_);
            parseValue(node, DRing::Account::ConfProperties::ARCHIVE_HAS_PASSWORD, archiveHasPassword_);
        } catch (const std::exception& e) {
            JAMI_WARN("can't read archive path: %s", e.what());
            archiveHasPassword_ = true;
        }
    
        try {
            parseValue(node, Conf::RING_ACCOUNT_RECEIPT, receipt_);
            auto receipt_sig = node[Conf::RING_ACCOUNT_RECEIPT_SIG].as<YAML::Binary>();
            receiptSignature_ = {receipt_sig.data(), receipt_sig.data()+receipt_sig.size()};
        } catch (const std::exception& e) {
            JAMI_WARN("can't read receipt: %s", e.what());
        }
    
        // HACK
        // MacOS doesn't seems to close the DHT port sometimes, so re-using the DHT port seems
        // to make the DHT unusable (Address already in use, and SO_REUSEADDR & SO_REUSEPORT
        // doesn't seems to work). For now, use a random port
        // See https://git.jami.net/savoirfairelinux/ring-client-macosx/issues/221
        // TODO: parseValueOptional(node, Conf::DHT_PORT_KEY, dhtPort_);
        if (not dhtPort_)
            dhtPort_ = getRandomEvenPort(DHT_PORT_RANGE);
        dhtPortUsed_ = dhtPort_;
    
        parseValueOptional(node, DRing::Account::ConfProperties::DHT_PEER_DISCOVERY, dhtPeerDiscovery_);
        parseValueOptional(node, DRing::Account::ConfProperties::ACCOUNT_PEER_DISCOVERY, accountPeerDiscovery_);
        parseValueOptional(node, DRing::Account::ConfProperties::ACCOUNT_PUBLISH, accountPublish_);
    
    #if HAVE_RINGNS
        parseValueOptional(node, DRing::Account::ConfProperties::RingNS::URI, nameServer_);
        if (registeredName_.empty()) {
            parseValueOptional(node, DRing::Account::VolatileProperties::REGISTERED_NAME, registeredName_);
        }
    #endif
    
        parseValue(node, Conf::DHT_PUBLIC_IN_CALLS, dhtPublicInCalls_);
    
        loadAccount();
    }
    
    bool
    JamiAccount::changeArchivePassword(const std::string& password_old, const std::string& password_new)
    {
        try {
            if (!accountManager_->changePassword(password_old, password_new)) {
                JAMI_ERR("[Account %s] Can't change archive password", getAccountID().c_str());
                return false;
            }
            archiveHasPassword_ = not password_new.empty();
        } catch (const std::exception& ex) {
            JAMI_ERR("[Account %s] Can't change archive password: %s", getAccountID().c_str(), ex.what());
            if (password_old.empty()) {
                archiveHasPassword_ = true;
                emitSignal<DRing::ConfigurationSignal::AccountDetailsChanged>(getAccountID(), getAccountDetails());
            }
            return false;
        }
        if (password_old != password_new)
            emitSignal<DRing::ConfigurationSignal::AccountDetailsChanged>(getAccountID(), getAccountDetails());
        return true;
    }
    
    bool
    JamiAccount::isPasswordValid(const std::string& password)
    {
        return accountManager_->isPasswordValid(password);
    }
    
    void
    JamiAccount::addDevice(const std::string& password)
    {
        accountManager_->addDevice(password, [this](AccountManager::AddDeviceResult result, std::string pin){
            switch(result) {
            case AccountManager::AddDeviceResult::SUCCESS_SHOW_PIN:
                emitSignal<DRing::ConfigurationSignal::ExportOnRingEnded>(getAccountID(), 0, pin);
                break;
            case AccountManager::AddDeviceResult::ERROR_CREDENTIALS:
                emitSignal<DRing::ConfigurationSignal::ExportOnRingEnded>(getAccountID(), 1, "");
                break;
            case AccountManager::AddDeviceResult::ERROR_NETWORK:
                emitSignal<DRing::ConfigurationSignal::ExportOnRingEnded>(getAccountID(), 2, "");
                break;
            }
        });
    }
    
    bool
    JamiAccount::exportArchive(const std::string& destinationPath, const std::string& password)
    {
        if (auto manager = dynamic_cast<ArchiveAccountManager*>(accountManager_.get())) {
            return manager->exportArchive(destinationPath, password);
        }
        return false;
    }
    
    bool
    JamiAccount::revokeDevice(const std::string& password, const std::string& device)
    {
        if (not accountManager_)
            return false;
        return accountManager_->revokeDevice(password, device, [this, device](AccountManager::RevokeDeviceResult result){
            switch(result) {
            case AccountManager::RevokeDeviceResult::SUCCESS:
                emitSignal<DRing::ConfigurationSignal::DeviceRevocationEnded>(getAccountID(), device, 0);
                break;
            case AccountManager::RevokeDeviceResult::ERROR_CREDENTIALS:
                emitSignal<DRing::ConfigurationSignal::DeviceRevocationEnded>(getAccountID(), device, 1);
                break;
            case AccountManager::RevokeDeviceResult::ERROR_NETWORK:
                emitSignal<DRing::ConfigurationSignal::DeviceRevocationEnded>(getAccountID(), device, 2);
                break;
            }
        });
        return true;
    }
    
    std::pair<std::string, std::string>
    JamiAccount::saveIdentity(const dht::crypto::Identity id, const std::string& path, const std::string& name)
    {
        auto names = std::make_pair(name + ".key", name + ".crt");
        if (id.first)
            fileutils::saveFile(path + DIR_SEPARATOR_STR + names.first, id.first->serialize(), 0600);
        if (id.second)
            fileutils::saveFile(path + DIR_SEPARATOR_STR + names.second, id.second->getPacked(), 0600);
        return names;
    }
    
    // must be called while configurationMutex_ is locked
    void
    JamiAccount::loadAccount(const std::string& archive_password, const std::string& archive_pin, const std::string& archive_path)
    {
        if (registrationState_ == RegistrationState::INITIALIZING)
            return;
    
        JAMI_DBG("[Account %s] loading account", getAccountID().c_str());
        AccountManager::OnChangeCallback callbacks {
            [this](const std::string& uri, bool confirmed) {
                dht::ThreadPool::computation().run([id=getAccountID(), uri, confirmed] {
                    emitSignal<DRing::ConfigurationSignal::ContactAdded>(id, uri, confirmed);
                });
            },
            [this](const std::string& uri, bool banned) {
                dht::ThreadPool::computation().run([id=getAccountID(), uri, banned] {
                    emitSignal<DRing::ConfigurationSignal::ContactRemoved>(id, uri, banned);
                });
            },
            [this](const std::string& uri, const std::vector<uint8_t>& payload, time_t received) {
                dht::ThreadPool::computation().run([id=getAccountID(), uri, payload = std::move(payload), received] {
                    emitSignal<DRing::ConfigurationSignal::IncomingTrustRequest>(id, uri, payload, received);
                });
            },
            [this]() {
                dht::ThreadPool::computation().run([id=getAccountID(), devices=getKnownDevices()] {
                    emitSignal<DRing::ConfigurationSignal::KnownDevicesChanged>(id, devices);
                });
            },
        };
    
        try {
            auto onAsync = [w = weak()](AccountManager::AsyncUser&& cb) {
                if (auto this_ = w.lock())
                    cb(*this_->accountManager_);
            };
            if (managerUri_.empty()) {
                accountManager_.reset(new ArchiveAccountManager(getPath(),
                    onAsync,
                    [this]() { return getAccountDetails(); },
                    archivePath_.empty() ? "archive.gz" : archivePath_,
                    nameServer_));
            } else {
                accountManager_.reset(new ServerAccountManager(getPath(),
                    onAsync,
                    managerUri_,
                    nameServer_));
            }
    
            auto id = accountManager_->loadIdentity(tlsCertificateFile_, tlsPrivateKeyFile_, tlsPassword_);
            if (auto info = accountManager_->useIdentity(id, receipt_, receiptSignature_, managerUsername_, std::move(callbacks))) {
                // normal loading path
                id_ = std::move(id);
                username_ = RING_URI_PREFIX+info->accountId;
                JAMI_WARN("[Account %s] loaded account identity", getAccountID().c_str());
                if (not isEnabled()) {
                    setRegistrationState(RegistrationState::UNREGISTERED);
                }
            }
            else if (isEnabled()) {
                if (not managerUri_.empty() and archive_password.empty()) {
                    Migration::setState(accountID_, Migration::State::INVALID);
                    setRegistrationState(RegistrationState::ERROR_NEED_MIGRATION);
                    return;
                }
    
                bool migrating = registrationState_ == RegistrationState::ERROR_NEED_MIGRATION;
                setRegistrationState(RegistrationState::INITIALIZING);
                auto fDeviceKey = dht::ThreadPool::computation().getShared<std::shared_ptr<dht::crypto::PrivateKey>>([](){
                    return std::make_shared<dht::crypto::PrivateKey>(dht::crypto::PrivateKey::generate());
                });
    
                std::unique_ptr<AccountManager::AccountCredentials> creds;
                if (managerUri_.empty()) {
                    auto acreds = std::make_unique<ArchiveAccountManager::ArchiveAccountCredentials>();
                    if (archivePath_.empty()) {
                        archivePath_ = "archive.gz";
                    }
                    auto archivePath = fileutils::getFullPath(idPath_, archivePath_);
                    bool hasArchive = fileutils::isFile(archivePath);
                    if (not archive_path.empty()) {
                        // Importing external archive
                        acreds->scheme = "file";
                        acreds->uri = archive_path;
                    }
                    else if (not archive_pin.empty()) {
                        // Importing from DHT
                        acreds->scheme = "dht";
                        acreds->uri = archive_pin;
                        acreds->dhtBootstrap = loadBootstrap();
                        acreds->dhtPort = (in_port_t)dhtPortUsed_;
                    } else if (hasArchive) {
                        // Migrating local account
                        acreds->scheme = "local";
                        acreds->uri = std::move(archivePath);
                        acreds->updateIdentity = id;
                        migrating = true;
                    }
                    creds = std::move(acreds);
                } else {
                    auto screds = std::make_unique<ServerAccountManager::ServerAccountCredentials>();
                    screds->username = managerUsername_;
                    creds = std::move(screds);
                }
                creds->password = archive_password;
                archiveHasPassword_ = !archive_password.empty();
    
                accountManager_->initAuthentication(
                    fDeviceKey,
                    ip_utils::getDeviceName(),
                    std::move(creds),
                    [this, fDeviceKey, migrating](const AccountInfo& info,
                        const std::map<std::string, std::string>& config,
                        std::string&& receipt,
                        std::vector<uint8_t>&& receipt_signature)
                {
                    JAMI_WARN("[Account %s] Auth success !", getAccountID().c_str());
    
                    fileutils::check_dir(idPath_.c_str(), 0700);
    
                    emitSignal<DRing::ConfigurationSignal::AccountAvatarReceived>(getAccountID(), info.photo);
    
                    // save the chain including CA
                    auto id = info.identity;
                    id.first = std::move(fDeviceKey.get());
                    std::tie(tlsPrivateKeyFile_, tlsCertificateFile_) = saveIdentity(id, idPath_, "ring_device");
                    id_ = std::move(id);
                    tlsPassword_ = {};
    
                    username_ = RING_URI_PREFIX+info.accountId;
                    registeredName_ = managerUsername_;
                    ringDeviceName_ = accountManager_->getAccountDeviceName();
    
                    auto nameServerIt = config.find(DRing::Account::ConfProperties::RingNS::URI);
                    if (nameServerIt != config.end() && !nameServerIt->second.empty()) {
                        nameServer_ = nameServerIt->second;
                    }
                    auto displayNameIt = config.find(DRing::Account::ConfProperties::DISPLAYNAME);
                    if (displayNameIt != config.end() && !displayNameIt->second.empty()) {
                        displayName_ = displayNameIt->second;
                    }
    
                    receipt_ = std::move(receipt);
                    receiptSignature_ = std::move(receipt_signature);
                    if (migrating) {
                        Migration::setState(getAccountID(), Migration::State::SUCCESS);
                    }
                    setRegistrationState(RegistrationState::UNREGISTERED);
                    saveConfig();
                    doRegister();
                }, [w = weak(), id, accountId = getAccountID(), migrating](AccountManager::AuthError error, const std::string& message) {
                    JAMI_WARN("[Account %s] Auth error: %d %s", accountId.c_str(), (int)error, message.c_str());
                    if ((id.first || migrating) && error == AccountManager::AuthError::INVALID_ARGUMENTS) {
                        // In cast of a migration or manager connexion failure stop the migration and block the account
                        Migration::setState(accountId, Migration::State::INVALID);
                        if (auto acc = w.lock())
                            acc->setRegistrationState(RegistrationState::ERROR_NEED_MIGRATION);
                    } else {
                        // In case of a DHT or backup import failure, just remove the account
                        if (auto acc = w.lock())
                            acc->setRegistrationState(RegistrationState::ERROR_GENERIC);
                        runOnMainThread([accountId = std::move(accountId)] {
                            Manager::instance().removeAccount(accountId, true);
                        });
                    }
                }, std::move(callbacks));
            }
        }
        catch (const std::exception& e) {
            JAMI_WARN("[Account %s] error loading account: %s", getAccountID().c_str(), e.what());
            accountManager_.reset();
            setRegistrationState(RegistrationState::ERROR_GENERIC);
        }
    }
    
    void
    JamiAccount::setAccountDetails(const std::map<std::string, std::string>& details)
    {
        std::lock_guard<std::mutex> lock(configurationMutex_);
        SIPAccountBase::setAccountDetails(details);
    
        // TLS
        parsePath(details, Conf::CONFIG_TLS_CA_LIST_FILE, tlsCaListFile_, idPath_);
        parsePath(details, Conf::CONFIG_TLS_CERTIFICATE_FILE, tlsCertificateFile_, idPath_);
        parsePath(details, Conf::CONFIG_TLS_PRIVATE_KEY_FILE, tlsPrivateKeyFile_, idPath_);
        parseString(details, Conf::CONFIG_TLS_PASSWORD, tlsPassword_);
    
        if (hostname_.empty())
            hostname_ = DHT_DEFAULT_BOOTSTRAP;
        parseString(details, DRing::Account::ConfProperties::BOOTSTRAP_LIST_URL, bootstrapListUrl_);
        parseInt(details, Conf::CONFIG_DHT_PORT, dhtPort_);
        parseBool(details, Conf::CONFIG_DHT_PUBLIC_IN_CALLS, dhtPublicInCalls_);
        parseBool(details, DRing::Account::ConfProperties::DHT_PEER_DISCOVERY, dhtPeerDiscovery_);
        parseBool(details, DRing::Account::ConfProperties::ACCOUNT_PEER_DISCOVERY, accountPeerDiscovery_);
        parseBool(details, DRing::Account::ConfProperties::ACCOUNT_PUBLISH, accountPublish_);
        parseBool(details, DRing::Account::ConfProperties::ALLOW_CERT_FROM_HISTORY, allowPeersFromHistory_);
        parseBool(details, DRing::Account::ConfProperties::ALLOW_CERT_FROM_CONTACT, allowPeersFromContact_);
        parseBool(details, DRing::Account::ConfProperties::ALLOW_CERT_FROM_TRUSTED, allowPeersFromTrusted_);
        if (not dhtPort_)
            dhtPort_ = getRandomEvenPort(DHT_PORT_RANGE);
        dhtPortUsed_ = dhtPort_;
    
        parseString(details, DRing::Account::ConfProperties::MANAGER_URI, managerUri_);
        parseString(details, DRing::Account::ConfProperties::MANAGER_USERNAME, managerUsername_);
        parseString(details, DRing::Account::ConfProperties::USERNAME, username_);
    
        std::string archive_password;
        std::string archive_pin;
        std::string archive_path;
        parseString(details, DRing::Account::ConfProperties::ARCHIVE_PASSWORD, archive_password);
        parseString(details, DRing::Account::ConfProperties::ARCHIVE_PIN,      archive_pin);
        std::transform(archive_pin.begin(), archive_pin.end(), archive_pin.begin(), ::toupper);
        parsePath(details, DRing::Account::ConfProperties::ARCHIVE_PATH,     archive_path, idPath_);
        parseString(details, DRing::Account::ConfProperties::RING_DEVICE_NAME, ringDeviceName_);
    
        auto oldProxyServer = proxyServer_, oldProxyServerList = proxyListUrl_;
        parseString(details, DRing::Account::ConfProperties::DHT_PROXY_LIST_URL, proxyListUrl_);
        parseBool(details, DRing::Account::ConfProperties::PROXY_ENABLED, proxyEnabled_);
        parseString(details, DRing::Account::ConfProperties::PROXY_SERVER, proxyServer_);
        parseString(details, DRing::Account::ConfProperties::PROXY_PUSH_TOKEN, deviceKey_);
        // Migrate from old versions
        if (proxyServer_.empty()
        || ((proxyServer_ == "dhtproxy.jami.net"
        ||  proxyServer_ == "dhtproxy.ring.cx")
        &&  proxyServerCached_.empty()))
            proxyServer_ = DHT_DEFAULT_PROXY;
        if (proxyServer_ != oldProxyServer || oldProxyServerList != proxyListUrl_) {
            JAMI_DBG("DHT Proxy configuration changed, resetting cache");
            proxyServerCached_ = {};
            auto proxyCachePath = cachePath_ + DIR_SEPARATOR_STR "dhtproxy";
            auto proxyListCachePath = cachePath_ + DIR_SEPARATOR_STR "dhtproxylist";
            std::remove(proxyCachePath.c_str());
            std::remove(proxyListCachePath.c_str());
        }
    
    #if HAVE_RINGNS
        parseString(details, DRing::Account::ConfProperties::RingNS::URI,     nameServer_);
    #endif
    
        loadAccount(archive_password, archive_pin, archive_path);
    
        // update device name if necessary
        if (accountManager_)
            accountManager_->setAccountDeviceName(ringDeviceName_);
    }
    
    std::map<std::string, std::string>
    JamiAccount::getAccountDetails() const
    {
        std::lock_guard<std::mutex> lock(configurationMutex_);
        std::map<std::string, std::string> a = SIPAccountBase::getAccountDetails();
        a.emplace(Conf::CONFIG_DHT_PORT, std::to_string(dhtPort_));
        a.emplace(Conf::CONFIG_DHT_PUBLIC_IN_CALLS, dhtPublicInCalls_ ? TRUE_STR : FALSE_STR);
        a.emplace(DRing::Account::ConfProperties::DHT_PEER_DISCOVERY, dhtPeerDiscovery_ ? TRUE_STR : FALSE_STR);
        a.emplace(DRing::Account::ConfProperties::ACCOUNT_PEER_DISCOVERY, accountPeerDiscovery_ ? TRUE_STR : FALSE_STR);
        a.emplace(DRing::Account::ConfProperties::ACCOUNT_PUBLISH, accountPublish_ ? TRUE_STR : FALSE_STR);
        if (accountManager_) {
            if (auto info = accountManager_->getInfo()) {
                a.emplace(DRing::Account::ConfProperties::RING_DEVICE_ID,  info->deviceId);
                a.emplace(DRing::Account::ConfProperties::RingNS::ACCOUNT, info->ethAccount);
            }
        }
        a.emplace(DRing::Account::ConfProperties::RING_DEVICE_NAME, ringDeviceName_);
        a.emplace(DRing::Account::ConfProperties::Presence::SUPPORT_SUBSCRIBE, TRUE_STR);
        if (not archivePath_.empty() or not managerUri_.empty())
            a.emplace(DRing::Account::ConfProperties::ARCHIVE_HAS_PASSWORD, archiveHasPassword_ ? TRUE_STR : FALSE_STR);
    
        /* these settings cannot be changed (read only), but clients should still be
         * able to read what they are */
        a.emplace(Conf::CONFIG_SRTP_KEY_EXCHANGE, sip_utils::getKeyExchangeName(getSrtpKeyExchange()));
        a.emplace(Conf::CONFIG_SRTP_ENABLE,       isSrtpEnabled() ? TRUE_STR : FALSE_STR);
        a.emplace(Conf::CONFIG_SRTP_RTP_FALLBACK, getSrtpFallback() ? TRUE_STR : FALSE_STR);
    
        a.emplace(Conf::CONFIG_TLS_CA_LIST_FILE,        fileutils::getFullPath(idPath_, tlsCaListFile_));
        a.emplace(Conf::CONFIG_TLS_CERTIFICATE_FILE,    fileutils::getFullPath(idPath_, tlsCertificateFile_));
        a.emplace(Conf::CONFIG_TLS_PRIVATE_KEY_FILE,    fileutils::getFullPath(idPath_, tlsPrivateKeyFile_));
        a.emplace(Conf::CONFIG_TLS_PASSWORD,            tlsPassword_);
        a.emplace(Conf::CONFIG_TLS_METHOD,                     "Automatic");
        a.emplace(Conf::CONFIG_TLS_CIPHERS,                    "");
        a.emplace(Conf::CONFIG_TLS_SERVER_NAME,                "");
        a.emplace(Conf::CONFIG_TLS_VERIFY_SERVER,              TRUE_STR);
        a.emplace(Conf::CONFIG_TLS_VERIFY_CLIENT,              TRUE_STR);
        a.emplace(Conf::CONFIG_TLS_REQUIRE_CLIENT_CERTIFICATE, TRUE_STR);
        a.emplace(DRing::Account::ConfProperties::ALLOW_CERT_FROM_HISTORY, allowPeersFromHistory_?TRUE_STR:FALSE_STR);
        a.emplace(DRing::Account::ConfProperties::ALLOW_CERT_FROM_CONTACT, allowPeersFromContact_?TRUE_STR:FALSE_STR);
        a.emplace(DRing::Account::ConfProperties::ALLOW_CERT_FROM_TRUSTED, allowPeersFromTrusted_?TRUE_STR:FALSE_STR);
        /* GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT is defined as -1 */
        a.emplace(Conf::CONFIG_TLS_NEGOTIATION_TIMEOUT_SEC,    "-1");
        a.emplace(DRing::Account::ConfProperties::PROXY_ENABLED,    proxyEnabled_ ? TRUE_STR : FALSE_STR);
        a.emplace(DRing::Account::ConfProperties::PROXY_SERVER,     proxyServer_);
        a.emplace(DRing::Account::ConfProperties::DHT_PROXY_LIST_URL,     proxyListUrl_);
        a.emplace(DRing::Account::ConfProperties::PROXY_PUSH_TOKEN, deviceKey_);
        a.emplace(DRing::Account::ConfProperties::MANAGER_URI, managerUri_);
        a.emplace(DRing::Account::ConfProperties::MANAGER_USERNAME, managerUsername_);
    #if HAVE_RINGNS
        a.emplace(DRing::Account::ConfProperties::RingNS::URI,                   nameServer_);
    #endif
    
        return a;
    }
    
    std::map<std::string, std::string>
    JamiAccount::getVolatileAccountDetails() const
    {
        auto a = SIPAccountBase::getVolatileAccountDetails();
        a.emplace(DRing::Account::VolatileProperties::InstantMessaging::OFF_CALL, TRUE_STR);
    #if HAVE_RINGNS
        if (not registeredName_.empty())
            a.emplace(DRing::Account::VolatileProperties::REGISTERED_NAME, registeredName_);
    #endif
        return a;
    }
    
    #if HAVE_RINGNS
    void
    JamiAccount::lookupName(const std::string& name)
    {
        if (accountManager_)
            accountManager_->lookupUri(name, nameServer_, [acc = getAccountID(),name](const std::string& result, NameDirectory::Response response) {
                emitSignal<DRing::ConfigurationSignal::RegisteredNameFound>(acc, (int)response, result, name);
            });
    }
    
    void
    JamiAccount::lookupAddress(const std::string& addr)
    {
        auto acc = getAccountID();
        if (accountManager_)
            accountManager_->lookupAddress(addr, [acc,addr](const std::string& result, NameDirectory::Response response) {
                emitSignal<DRing::ConfigurationSignal::RegisteredNameFound>(acc, (int)response, addr, result);
            });
    }
    
    void
    JamiAccount::registerName(const std::string& password, const std::string& name)
    {
        if (accountManager_)
            accountManager_->registerName(password, name, [acc=getAccountID(), name, w=weak()](NameDirectory::RegistrationResponse response){
                int res = (response == NameDirectory::RegistrationResponse::success)      ? 0 : (
                        (response == NameDirectory::RegistrationResponse::invalidCredentials)  ? 1 : (
                        (response == NameDirectory::RegistrationResponse::invalidName)  ? 2 : (
                        (response == NameDirectory::RegistrationResponse::alreadyTaken) ? 3 : 4)));
                if (response == NameDirectory::RegistrationResponse::success) {
                    if (auto this_ = w.lock())
                        this_->registeredName_ = name;
                }
                emitSignal<DRing::ConfigurationSignal::NameRegistrationEnded>(acc, res, name);
            });
    }
    #endif
    
    bool
    JamiAccount::searchUser(const std::string& query)
    {
        if (accountManager_)
            return accountManager_->searchUser(query, [acc = getAccountID(), query](const jami::NameDirectory::SearchResult& result, jami::NameDirectory::Response response) {
                jami::emitSignal<DRing::ConfigurationSignal::UserSearchEnded>(acc, (int)response, query, result);
            });
        return false;
    }
    
    void
    JamiAccount::checkPendingCall(const std::string& callId)
    {
        // Note only one check at a time. In fact, the UDP and TCP negotiation
        // can finish at the same time and we need to avoid potential race conditions.
        std::lock_guard<std::mutex> lk(callsMutex_);
        auto it = pendingCalls_.find(callId);
        if (it == pendingCalls_.end()) return;
    
        bool incoming = !it->second.call_key;
        bool handled;
        try {
            handled = handlePendingCall(it->second, incoming);
        } catch (const std::exception& e) {
            JAMI_ERR("[DHT] exception during pending call handling: %s", e.what());
            handled = true; // drop from pending list
        }
    
        if (handled) {
            if (not incoming) {
                // Cancel pending listen (outgoing call)
                dht_->cancelListen(it->second.call_key, std::move(it->second.listen_key));
            }
            pendingCalls_.erase(it);
        }
    }
    
    pj_status_t
    JamiAccount::checkPeerTlsCertificate(dht::InfoHash from,
                            dht::InfoHash from_account,
                            unsigned status,
                            const gnutls_datum_t* cert_list,
                            unsigned cert_num,
                            std::shared_ptr<dht::crypto::Certificate>& cert_out)
    {
        if (cert_num == 0) {
            JAMI_ERR("[peer:%s] No certificate", from.toString().c_str());
            return PJ_SSL_CERT_EUNKNOWN;
        }
        if (status & GNUTLS_CERT_EXPIRED or status & GNUTLS_CERT_NOT_ACTIVATED) {
            JAMI_ERR("[peer:%s] Expired certificate", from.toString().c_str());
            return PJ_SSL_CERT_EVALIDITY_PERIOD;
        }
    
        // Unserialize certificate chain
        std::vector<std::pair<uint8_t*, uint8_t*>> crt_data;
        crt_data.reserve(cert_num);
        for (unsigned i=0; i<cert_num; i++)
            crt_data.emplace_back(cert_list[i].data, cert_list[i].data + cert_list[i].size);
        auto crt = std::make_shared<dht::crypto::Certificate>(crt_data);
    
        // Check expected peer identity
        dht::InfoHash tls_account_id;
        if (not accountManager_->onPeerCertificate(crt, dhtPublicInCalls_, tls_account_id)) {
            JAMI_ERR("[peer:%s] Discarding message from invalid peer certificate.", from.toString().c_str());
            return PJ_SSL_CERT_EUNKNOWN;
        }
        if (from_account != tls_account_id) {
            JAMI_ERR("[peer:%s] Discarding message from wrong peer account %s.", from.toString().c_str(), tls_account_id.toString().c_str());
            return PJ_SSL_CERT_EUNTRUSTED;
        }
    
        const auto tls_id = crt->getId();
        if (crt->getUID() != tls_id.toString()) {
            JAMI_ERR("[peer:%s] Certificate UID must be the public key ID", from.toString().c_str());
            return PJ_SSL_CERT_EUNTRUSTED;
        }
        if (tls_id != from) {
            JAMI_ERR("[peer:%s] Certificate public key ID doesn't match (%s)",
                     from.toString().c_str(), tls_id.toString().c_str());
            return PJ_SSL_CERT_EUNTRUSTED;
        }
    
        JAMI_DBG("[peer:%s] Certificate verified", from.toString().c_str());
        cert_out = std::move(crt);
        return PJ_SUCCESS;
    }
    
    bool
    JamiAccount::handlePendingCall(PendingCall& pc, bool incoming)
    {
        auto call = pc.call.lock();
        // Cleanup pending call if call is over (cancelled by user or any other reason)
        if (not call || call->getState() == Call::CallState::OVER)
            return true;
    
        if ((std::chrono::steady_clock::now() - pc.start) >= ICE_NEGOTIATION_TIMEOUT) {
            JAMI_WARN("[call:%s] Timeout on ICE negotiation", call->getCallId().c_str());
            call->onFailure();
            return true;
        }
    
        auto ice_tcp = pc.ice_tcp_sp.get();
        auto ice = pc.ice_sp.get();
    
        bool tcp_finished = ice_tcp == nullptr || ice_tcp->isStopped();
        bool udp_finished = ice == nullptr || ice->isStopped();
    
        if (not udp_finished and ice->isFailed()) {
            udp_finished = true;
        }
    
        if (not tcp_finished and ice_tcp->isFailed()) {
            tcp_finished = true;
        }
    
        // At least wait for TCP
        if (not tcp_finished and not ice_tcp->isRunning()) {
            return false;
        } else if (tcp_finished and (not ice_tcp or not ice_tcp->isRunning())) {
            // If TCP is finished but not running, wait for UDP
            if (not udp_finished and ice and not ice->isRunning()) {
                return false;
            }
        }
    
        udp_finished = ice && ice->isRunning();
        if (udp_finished)
            JAMI_INFO("[call:%s] UDP negotiation is ready", call->getCallId().c_str());
        tcp_finished = ice_tcp && ice_tcp->isRunning();
        if (tcp_finished)
            JAMI_INFO("[call:%s] TCP negotiation is ready", call->getCallId().c_str());
        // If both transport are not running, the negotiation failed
        if (not udp_finished and not tcp_finished) {
            JAMI_ERR("[call:%s] Both ICE negotations failed", call->getCallId().c_str());
            call->onFailure();
            return true;
        }
    
        // Securize a SIP transport with TLS (on top of ICE tranport) and assign the call with it
        auto remote_device = pc.from;
        auto remote_account = pc.from_account;
        auto id = id_;
        if (not id.first or not id.second)
            throw std::runtime_error("No identity configured for this account.");
    
        std::weak_ptr<JamiAccount> waccount = weak();
        std::weak_ptr<SIPCall> wcall = call;
        tls::TlsParams tlsParams {
            /*.ca_list = */"",
            /*.ca = */pc.from_cert,
            /*.cert = */id.second,
            /*.cert_key = */id.first,
            /*.dh_params = */dhParams_,
            /*.timeout = */std::chrono::duration_cast<decltype(tls::TlsParams::timeout)>(TLS_TIMEOUT),
            /*.cert_check = */[waccount,wcall,remote_device,remote_account](unsigned status,
                                                                 const gnutls_datum_t* cert_list,
                                                                 unsigned cert_num) -> pj_status_t {
                try {
                    if (auto call = wcall.lock()) {
                        if (auto sthis = waccount.lock()) {
                            auto& this_ = *sthis;
                            std::shared_ptr<dht::crypto::Certificate> peer_cert;
                            auto ret = this_.checkPeerTlsCertificate(remote_device, remote_account, status, cert_list, cert_num, peer_cert);
                            if (ret == PJ_SUCCESS and peer_cert) {
                                std::lock_guard<std::mutex> lock(this_.callsMutex_);
                                for (auto& pscall : this_.pendingSipCalls_) {
                                    if (auto pcall = pscall.call.lock()) {
                                        if (pcall == call and not pscall.from_cert) {
                                            JAMI_DBG("[call:%s] got peer certificate from TLS negotiation", call->getCallId().c_str());
                                            tls::CertificateStore::instance().pinCertificate(peer_cert);
                                            pscall.from_cert = peer_cert;
                                            break;
                                        }
                                    }
                                }
                            }
                            return ret;
                        }
                    }
                    return PJ_SSL_CERT_EUNTRUSTED;
                } catch (const std::exception& e) {
                    JAMI_ERR("[peer:%s] TLS certificate check exception: %s",
                             remote_device.toString().c_str(), e.what());
                    return PJ_SSL_CERT_EUNKNOWN;
                }
            }
        };
    
        auto best_transport = pc.ice_tcp_sp;
        if (!tcp_finished) {
            JAMI_DBG("TCP not running, will use SIP over UDP");
            best_transport = pc.ice_sp;
            // Move the ice destruction in its own thread to avoid
            // slow operations on main thread
            dht::ThreadPool::io().run([ice = std::move(pc.ice_tcp_sp)] { });
        } else {
            dht::ThreadPool::io().run([ice = std::move(pc.ice_sp)] { });
        }
    
        // Following can create a transport that need to be negotiated (TLS).
        // This is a asynchronous task. So we're going to process the SIP after this negotiation.
        auto transport = link_.sipTransportBroker->getTlsIceTransport(best_transport,
                                                                       ICE_COMP_SIP_TRANSPORT,
                                                                       tlsParams);
        if (!transport)
            throw std::runtime_error("transport creation failed");
    
        call->setTransport(transport);
    
        if (incoming) {
            pendingSipCalls_.emplace_back(std::move(pc)); // copy of pc
        } else {
            // Be acknowledged on transport connection/disconnection
            auto lid = reinterpret_cast<uintptr_t>(this);
            auto remote_id = remote_device.toString();
            auto remote_addr = best_transport->getRemoteAddress(ICE_COMP_SIP_TRANSPORT);
            auto& tr_self = *transport;
    
            transport->addStateListener(lid,
                [&tr_self, lid, wcall, waccount, remote_id, remote_addr](pjsip_transport_state state,
                                                                         UNUSED const pjsip_transport_state_info* info) {
                    if (state == PJSIP_TP_STATE_CONNECTED) {
                        if (auto call = wcall.lock()) {
                            if (auto account = waccount.lock()) {
                                // Start SIP layer when TLS negotiation is successful
                                account->onConnectedOutgoingCall(*call, remote_id, remote_addr);
                                return;
                            }
                        }
                    } else if (state == PJSIP_TP_STATE_DISCONNECTED) {
                        tr_self.removeStateListener(lid);
                    }
                });
        }
    
        // Notify of fully available connection between peers
        call->setState(Call::ConnectionState::PROGRESSING);
    
        return true;
    }
    
    void
    JamiAccount::registerAsyncOps()
    {
        auto onLoad = [this, loaded = std::make_shared<std::atomic_uint>()]{
            if (++(*loaded) == 2u) {
                runOnMainThread([w = weak()]{
                    if (auto s = w.lock()) {
                        std::lock_guard<std::mutex> lock(s->configurationMutex_);
                        s->doRegister_();
                    }
                });
            }
        };
    
        loadCachedProxyServer([onLoad](const std::string&) {
            onLoad();
        });
    
        if (upnp_) {
            upnp_->requestMappingAdd([this, onLoad, update = std::make_shared<bool>(false)](uint16_t port_used, bool success) {
                auto oldPort = static_cast<in_port_t>(dhtPortUsed_);
                auto newPort = success ? port_used : dhtPort_;
                if (*update) {
                    if (oldPort != newPort or not dht_->isRunning()) {
                        JAMI_WARN("[Account %s] DHT port changed to %u: restarting network", getAccountID().c_str(), newPort);
                        dht_->connectivityChanged();
                    }
                } else {
                    *update = true;
                    if (success)
                        JAMI_WARN("[Account %s] Starting DHT on port %u", getAccountID().c_str(), newPort);
                    else
                        JAMI_WARN("[Account %s] Failed to open port %u: starting DHT anyways", getAccountID().c_str(), oldPort);
                    onLoad();
                }
            }, dhtPort_, upnp::PortType::UDP, false);
        } else
            onLoad();
    }
    
    void
    JamiAccount::doRegister()
    {
        std::lock_guard<std::mutex> lock(configurationMutex_);
        if (not isUsable()) {
            JAMI_WARN("Account must be enabled and active to register, ignoring");
            return;
        }
    
        JAMI_DBG("[Account %s] Starting account..", getAccountID().c_str());
    
        // invalid state transitions:
        // INITIALIZING: generating/loading certificates, can't register
        // NEED_MIGRATION: old account detected, user needs to migrate
        if (registrationState_ == RegistrationState::INITIALIZING
         || registrationState_ == RegistrationState::ERROR_NEED_MIGRATION)
            return;
    
        if (not dhParams_.valid()) {
            generateDhParams();
        }
    
        setRegistrationState(RegistrationState::TRYING);
        /* if UPnP is enabled, then wait for IGD to complete registration */
        if (upnp_ or proxyServerCached_.empty()) {
            JAMI_DBG("UPnP: Attempting to map ports for Jami account");
            registerAsyncOps();
        } else {
            doRegister_();
        }
    
        cacheTurnServers(); // reset cache for TURN servers
    }
    
    std::vector<std::string>
    JamiAccount::loadBootstrap() const
    {
        std::vector<std::string> bootstrap;
        if (!hostname_.empty()) {
            std::stringstream ss(hostname_);
            std::string node_addr;
            while (std::getline(ss, node_addr, ';'))
                bootstrap.emplace_back(std::move(node_addr));
            for (const auto& b : bootstrap)
                JAMI_DBG("Bootstrap node: %s", b.c_str());
        }
        return bootstrap;
    }
    
    void
    JamiAccount::trackBuddyPresence(const std::string& buddy_id, bool track)
    {
        std::string buddyUri;
    
        try {
            buddyUri = parseJamiUri(buddy_id);
        }
        catch (...) {
            JAMI_ERR("[Account %s] Failed to track a buddy due to an invalid URI %s", getAccountID().c_str(), buddy_id.c_str());
            return;
        }
        auto h = dht::InfoHash(buddyUri);
    
        if (!track && dht_ && dht_->isRunning()) {
            std::unique_lock<std::mutex> lk(sipConnectionsMtx_);
            std::set<std::string> devices;
            for (const auto& deviceConn: sipConnections_[buddy_id]) {
                devices.emplace(deviceConn.first);
            }
            sipConnections_.erase(buddy_id);
    
            for (auto pendingIt = pendingSipConnections_.begin(); pendingIt != pendingSipConnections_.end();) {
                if (buddy_id == pendingIt->first) {
                    devices.emplace(pendingIt->second);
                    pendingIt = pendingSipConnections_.erase(pendingIt);
                } else {
                    ++pendingIt;
                }
            }
    
            lk.unlock();
            for (const auto& device: devices) {
                if (connectionManager_)
                    connectionManager_->closeConnectionsWith(device);
            }
        }
    
        std::lock_guard<std::mutex> lock(buddyInfoMtx);
        if (track) {
            auto buddy = trackedBuddies_.emplace(h, BuddyInfo {h});
            if (buddy.second) {
                trackPresence(buddy.first->first, buddy.first->second);
            }
        } else {
            auto buddy = trackedBuddies_.find(h);
            if (buddy != trackedBuddies_.end()) {
                if (auto dht = dht_)
                    if (dht->isRunning())
                        dht->cancelListen(h, std::move(buddy->second.listenToken));
                trackedBuddies_.erase(buddy);
            }
        }
    }
    
    void
    JamiAccount::trackPresence(const dht::InfoHash& h, BuddyInfo& buddy)
    {
        auto dht = dht_;
        if (not dht or not dht->isRunning()) {
            return;
        }
        buddy.listenToken = dht->listen<DeviceAnnouncement>(h, [this, h](DeviceAnnouncement&&, bool expired){
            bool wasConnected, isConnected;
            {
                std::lock_guard<std::mutex> lock(buddyInfoMtx);
                auto buddy = trackedBuddies_.find(h);
                if (buddy == trackedBuddies_.end())
                    return true;
                wasConnected = buddy->second.devices_cnt > 0;
                if (expired)
                    --buddy->second.devices_cnt;
                else
                    ++buddy->second.devices_cnt;
                isConnected = buddy->second.devices_cnt > 0;
            }
            if (not expired) {
                // Retry messages every time a new device announce its presence
                messageEngine_.onPeerOnline(h.toString());
            }
            if (isConnected and not wasConnected) {
                onTrackedBuddyOnline(h);
            } else if (not isConnected and wasConnected) {
                onTrackedBuddyOffline(h);
            }
    
            return true;
        });
        JAMI_DBG("[Account %s] tracking buddy %s", getAccountID().c_str(), h.to_c_str());
    }
    
    std::map<std::string, bool>
    JamiAccount::getTrackedBuddyPresence() const
    {
        std::lock_guard<std::mutex> lock(buddyInfoMtx);
        std::map<std::string, bool> presence_info;
        for (const auto& buddy_info_p : trackedBuddies_)
            presence_info.emplace(buddy_info_p.first.toString(), buddy_info_p.second.devices_cnt > 0);
        return presence_info;
    }
    
    void
    JamiAccount::onTrackedBuddyOnline(const dht::InfoHash& contactId)
    {
        JAMI_DBG("Buddy %s online", contactId.toString().c_str());
        std::string id(contactId.toString());
        emitSignal<DRing::PresenceSignal::NewBuddyNotification>(getAccountID(), id, 1,  "");
    }
    
    void
    JamiAccount::onTrackedBuddyOffline(const dht::InfoHash& contactId)
    {
        JAMI_DBG("Buddy %s offline", contactId.toString().c_str());
        emitSignal<DRing::PresenceSignal::NewBuddyNotification>(getAccountID(), contactId.toString(), 0,  "");
    }
    
    void
    JamiAccount::doRegister_()
    {
        if (registrationState_ != RegistrationState::TRYING) {
            JAMI_ERR("[Account %s] already registered", getAccountID().c_str());
            return;
        }
    
        JAMI_DBG("[Account %s] Starting account...", getAccountID().c_str());
    
        try {
            if (not accountManager_ or not accountManager_->getInfo())
                throw std::runtime_error("No identity configured for this account.");
    
            loadTreatedCalls();
            loadTreatedMessages();
            if (dht_->isRunning()) {
                JAMI_ERR("[Account %s] DHT already running (stopping it first).", getAccountID().c_str());
                dht_->join();
            }
    
    #if HAVE_RINGNS
            // Look for registered name on the blockchain
            accountManager_->lookupAddress(accountManager_->getInfo()->accountId, [w=weak()](const std::string& result, const NameDirectory::Response& response) {
                if (auto this_ = w.lock()) {
                    if (response == NameDirectory::Response::found) {
                        if (this_->registeredName_ != result) {
                            this_->registeredName_ = result;
                            emitSignal<DRing::ConfigurationSignal::VolatileDetailsChanged>(this_->accountID_, this_->getVolatileAccountDetails());
                        }
                    } else if (response == NameDirectory::Response::notFound) {
                        if (not this_->registeredName_.empty()) {
                            this_->registeredName_.clear();
                            emitSignal<DRing::ConfigurationSignal::VolatileDetailsChanged>(this_->accountID_, this_->getVolatileAccountDetails());
                        }
                    }
                }
            });
    #endif
    
            dht::DhtRunner::Config config {};
            config.dht_config.node_config.network = 0;
            config.dht_config.node_config.maintain_storage = false;
            config.dht_config.node_config.persist_path = cachePath_+DIR_SEPARATOR_STR "dhtstate";
            config.dht_config.id = id_;
            config.dht_config.cert_cache_all = true;
            config.push_node_id = getAccountID();
            config.push_token = deviceKey_;
            config.threaded = true;
            config.peer_discovery = dhtPeerDiscovery_;
            config.peer_publish = dhtPeerDiscovery_;
            if (proxyEnabled_)
                config.proxy_server = proxyServerCached_;
    
            if (not config.proxy_server.empty()) {
                JAMI_INFO("[Account %s] using proxy server %s", getAccountID().c_str(), config.proxy_server.c_str());
                if (not config.push_token.empty()) {
                    JAMI_INFO("[Account %s] using push notifications", getAccountID().c_str());
                }
            }
    
            //check if dht peer service is enabled
            if (accountPeerDiscovery_ or accountPublish_) {
                peerDiscovery_ = std::make_shared<dht::PeerDiscovery>();
                if(accountPeerDiscovery_) {
                    JAMI_INFO("[Account %s] starting Jami account discovery...", getAccountID().c_str());
                    startAccountDiscovery();
                }
                if(accountPublish_)
                    startAccountPublish();
            }
            dht::DhtRunner::Context context {};
            context.peerDiscovery = peerDiscovery_;
    
            auto dht_log_level = Manager::instance().dhtLogLevel.load();
            if (dht_log_level > 0) {
                static auto silent = [](char const* /*m*/, va_list /*args*/) {};
                static auto log_error = [](char const* m, va_list args) { Logger::vlog(LOG_ERR, nullptr, 0, true, m, args); };
                static auto log_warn = [](char const* m, va_list args) { Logger::vlog(LOG_WARNING, nullptr, 0, true, m, args); };
                static auto log_debug = [](char const* m, va_list args) { Logger::vlog(LOG_DEBUG, nullptr, 0, true, m, args); };
    #ifndef _MSC_VER
                context.logger = std::make_shared<dht::Logger>(
                    log_error,
                    (dht_log_level > 1) ? log_warn : silent,
                    (dht_log_level > 2) ? log_debug : silent);
    #elif RING_UWP
                static auto log_all = [](char const* m, va_list args) {
                    char tmp[2048];
                    vsprintf(tmp, m, args);
                    auto now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
                    jami::emitSignal<DRing::DebugSignal::MessageSend>(std::to_string(now) + " " + std::string(tmp));
                };
                context.logger = std::make_shared<dht::Logger>(log_all, log_all, silent);
    #else
                if (dht_log_level > 2) {
                    context.logger = std::make_shared<dht::Logger>(log_error, log_warn, log_debug);
                } else if (dht_log_level > 1) {
                    context.logger = std::make_shared<dht::Logger>(log_error, log_warn, silent);
                } else {
                    context.logger = std::make_shared<dht::Logger>(log_error, silent, silent);
                }
    #endif
                //logger_ = std::make_shared<dht::Logger>(log_error, log_warn, log_debug);
            }
            context.certificateStore = [](const dht::InfoHash& pk_id) {
                std::vector<std::shared_ptr<dht::crypto::Certificate>> ret;
                if (auto cert = tls::CertificateStore::instance().getCertificate(pk_id.toString()))
                    ret.emplace_back(std::move(cert));
                JAMI_DBG("Query for local certificate store: %s: %zu found.", pk_id.toString().c_str(), ret.size());
                return ret;
            };
    
            auto currentDhtStatus = std::make_shared<dht::NodeStatus>(dht::NodeStatus::Disconnected);
            context.statusChangedCallback = [this, currentDhtStatus](dht::NodeStatus s4, dht::NodeStatus s6) {
                JAMI_DBG("[Account %s] Dht status : IPv4 %s; IPv6 %s", getAccountID().c_str(), dhtStatusStr(s4), dhtStatusStr(s6));
                RegistrationState state;
                auto newStatus = std::max(s4, s6);
                if (newStatus == *currentDhtStatus)
                    return;
                switch (newStatus) {
                    case dht::NodeStatus::Connecting:
                        JAMI_WARN("[Account %s] connecting to the DHT network...", getAccountID().c_str());
                        state = RegistrationState::TRYING;
                        break;
                    case dht::NodeStatus::Connected:
                        JAMI_WARN("[Account %s] connected to the DHT network", getAccountID().c_str());
                        state = RegistrationState::REGISTERED;
                        break;
                    case dht::NodeStatus::Disconnected:
                        JAMI_WARN("[Account %s] disconnected from the DHT network", getAccountID().c_str());
                        state = RegistrationState::UNREGISTERED;
                        break;
                    default:
                        state = RegistrationState::ERROR_GENERIC;
                        break;
                }
                *currentDhtStatus = newStatus;
                setRegistrationState(state);
            };
    
            setRegistrationState(RegistrationState::TRYING);
            dht_->run((in_port_t)dhtPortUsed_, config, std::move(context));
    
            for (const auto& bootstrap : loadBootstrap())
                dht_->bootstrap(bootstrap);
    
            accountManager_->setDht(dht_);
            accountManager_->startSync();
    
            // Init connection manager
            if (!connectionManager_)
                connectionManager_ = std::make_unique<ConnectionManager>(*this);
            connectionManager_->onDhtConnected(accountManager_->getInfo()->deviceId);
            connectionManager_->onICERequest([this](const std::string& deviceId) {
                std::promise<bool> accept;
                std::future<bool> fut = accept.get_future();
                accountManager_->findCertificate(dht::InfoHash(deviceId),
                    [this, &accept](const std::shared_ptr<dht::crypto::Certificate>& cert) {
                    dht::InfoHash peer_account_id;
                    auto res = accountManager_->onPeerCertificate(cert, dhtPublicInCalls_, peer_account_id);
                    if (res)
                        JAMI_INFO("Accepting ICE request from account %s", peer_account_id.toString().c_str());
                    else
                        JAMI_INFO("Discarding ICE request from account %s", peer_account_id.toString().c_str());
                    accept.set_value(res);
                });
                fut.wait();
                auto result = fut.get();
                return result;
            });
            connectionManager_->onChannelRequest([this](const std::string& /* deviceId */, const std::string& name) {
                auto isFile = name.substr(0, 7) == "file://";
                auto isVCard = name.substr(0, 8) == "vcard://";
                if (name == "sip") {
                    return true;
                } else if (isFile or isVCard) {
                    auto tid_str = isFile? name.substr(7) : name.substr(8);
                    uint64_t tid;
                    std::istringstream iss(tid_str);
                    iss >> tid;
                    if (dhtPeerConnector_->onIncomingChannelRequest(tid)) {
                        incomingFileTransfers_.emplace(tid_str);
                        return true;
                    }
                }
                return false;
            });
            connectionManager_->onConnectionReady([this](const std::string& deviceId, const std::string& name, std::shared_ptr<ChannelSocket> channel) {
                if (channel) {
                    auto cert = tls::CertificateStore::instance().getCertificate(deviceId);
                    if (!cert || !cert->issuer) return;
                    auto peerId = cert->issuer->getId().toString();
                    auto isFile = name.substr(0, 7) == "file://";
                    auto isVCard = name.substr(0, 8) == "vcard://";
                    if (name == "sip") {
                        cacheSIPConnection(std::move(channel), peerId, deviceId);
                    } else if (isFile or isVCard) {
                        auto tid_str = isFile? name.substr(7) : name.substr(8);
                        auto it = incomingFileTransfers_.find(tid_str);
                        // Note, outgoing file transfers are ignored.
                        if (it == incomingFileTransfers_.end()) return;
                        incomingFileTransfers_.erase(it);
                        uint64_t tid;
                        std::istringstream iss(tid_str);
                        iss >> tid;
                        std::function<void(const std::string&)> cb;
                        if (isVCard)
                            cb = [peerId, accountId=getAccountID()](const std::string& path) {
                                emitSignal<DRing::ConfigurationSignal::ProfileReceived>(accountId, peerId, path);
                            };
                        dhtPeerConnector_->onIncomingConnection(peerId, tid, std::move(channel), std::move(cb));
                    }
                }
            });
    
            // Listen for incoming calls
            callKey_ = dht::InfoHash::get("callto:"+accountManager_->getInfo()->deviceId);
            JAMI_DBG("[Account %s] Listening on callto:%s : %s", getAccountID().c_str(), accountManager_->getInfo()->deviceId.c_str(), callKey_.toString().c_str());
            dht_->listen<dht::IceCandidates>(
                callKey_,
                [this] (dht::IceCandidates&& msg) {
                    // callback for incoming call
                    auto from = msg.from;
                    if (from == dht_->getId())
                        return true;
    
                    auto res = treatedCalls_.insert(msg.id);
                    saveTreatedCalls();
                    if (!res.second)
                        return true;
    
                    JAMI_WARN("[Account %s] ICE candidate from %s.", getAccountID().c_str(), from.toString().c_str());
    
                    accountManager_->onPeerMessage(from, dhtPublicInCalls_, [this, msg=std::move(msg)](const std::shared_ptr<dht::crypto::Certificate>& cert,
                                                                   const dht::InfoHash& account) mutable
                    {
                        incomingCall(std::move(msg), cert, account);
                    });
                    return true;
                }
            );
    
            auto inboxDeviceKey = dht::InfoHash::get("inbox:"+accountManager_->getInfo()->deviceId);
            dht_->listen<dht::ImMessage>(
                inboxDeviceKey,
                [this,inboxDeviceKey](dht::ImMessage&& v) {
                    auto msgId = to_hex_string(v.id);
                    if (isMessageTreated(msgId)) return true;
                    accountManager_->onPeerMessage(v.from, dhtPublicInCalls_, [this, v, inboxDeviceKey, msgId](const std::shared_ptr<dht::crypto::Certificate>&,
                                                                            const dht::InfoHash& peer_account)
                    {
                        auto now = clock::to_time_t(clock::now());
                        std::string datatype = utf8_make_valid(v.datatype);
                        if (datatype.empty()) {
                            datatype = "text/plain";
                        }
                        std::map<std::string, std::string> payloads = {{datatype,
                                                                       utf8_make_valid(v.msg)}};
                        onTextMessage(msgId, peer_account.toString(), payloads);
                        JAMI_DBG() << "Sending message confirmation " << v.id;
                        dht_->putEncrypted(inboxDeviceKey,
                                  v.from,
                                  dht::ImMessage(v.id, std::string(), now));
                    });
                    return true;
                }
            );
    
            if (!dhtPeerConnector_)
                dhtPeerConnector_ = std::make_unique<DhtPeerConnector>(*this);
            dhtPeerConnector_->onDhtConnected(accountManager_->getInfo()->deviceId);
    
            std::lock_guard<std::mutex> bLock(buddyInfoMtx);
            for (auto& buddy : trackedBuddies_) {
                buddy.second.devices_cnt = 0;
                trackPresence(buddy.first, buddy.second);
            }
        }
        catch (const std::exception& e) {
            JAMI_ERR("Error registering DHT account: %s", e.what());
            setRegistrationState(RegistrationState::ERROR_GENERIC);
        }
    }
    
    void
    JamiAccount::onTextMessage(const std::string& id, const std::string& from,
                                  const std::map<std::string, std::string>& payloads)
    {
        try {
            const std::string fromUri = parseJamiUri(from);
            SIPAccountBase::onTextMessage(id, fromUri, payloads);
        } catch (...) {
        }
    }
    
    void
    JamiAccount::incomingCall(dht::IceCandidates&& msg, const std::shared_ptr<dht::crypto::Certificate>& from_cert, const dht::InfoHash& from)
    {
        auto call = Manager::instance().callFactory.newCall<SIPCall, JamiAccount>(*this, Manager::instance().getNewCallID(), Call::CallType::INCOMING);
        if (!call) {
            return;
        }
        auto callId = call->getCallId();
        auto onNegoDone = [callId, w=weak()](bool) {
            runOnMainThread([callId, w]() {
                if (auto shared = w.lock())
                    shared->checkPendingCall(callId);
            });
        };
        auto iceOptions = getIceOptions();
        iceOptions.onNegoDone = onNegoDone;
        auto ice = createIceTransport(("sip:"+call->getCallId()).c_str(), ICE_COMPONENTS, false, iceOptions);
        iceOptions.tcpEnable = true;
        auto ice_tcp = createIceTransport(("sip:" + call->getCallId()).c_str(), ICE_COMPONENTS, true, iceOptions);
    
        std::weak_ptr<SIPCall> wcall = call;
        Manager::instance().addTask([account=shared(), wcall, ice, ice_tcp, msg, from_cert, from] {
            auto call = wcall.lock();
    
            // call aborted?
            if (not call)
                return false;
    
            if (ice->isFailed()) {
                JAMI_ERR("[call:%s] ice init failed", call->getCallId().c_str());
                call->onFailure(EIO);
                return false;
            }
    
            // Loop until ICE transport is initialized.
            // Note: we suppose that ICE init routine has a an internal timeout (bounded in time)
            // and we let upper layers decide when the call shall be aborted (our first check upper).
            if ((not ice->isInitialized()) || (ice_tcp && !ice_tcp->isInitialized()))
                return true;
    
            account->replyToIncomingIceMsg(call, ice, ice_tcp, msg, from_cert, from);
            return false;
        });
    }
    
    void
    JamiAccount::replyToIncomingIceMsg(const std::shared_ptr<SIPCall>& call,
                                       const std::shared_ptr<IceTransport>& ice,
                                       const std::shared_ptr<IceTransport>& ice_tcp,
                                       const dht::IceCandidates& peer_ice_msg,
                                       const std::shared_ptr<dht::crypto::Certificate>& from_cert,
                                       const dht::InfoHash& from_id)
    {
        auto from = from_id.toString();
        call->setPeerUri(RING_URI_PREFIX + from);
        std::weak_ptr<SIPCall> wcall = call;
    #if HAVE_RINGNS
        accountManager_->lookupAddress(from, [wcall](const std::string& result, const NameDirectory::Response& response){
            if (response == NameDirectory::Response::found)
                if (auto call = wcall.lock()) {
                    call->setPeerRegistredName(result);
                    call->setPeerUri(RING_URI_PREFIX + result);
                }
        });
    #endif
        registerDhtAddress(*ice);
        if (ice_tcp) registerDhtAddress(*ice_tcp);
    
        auto blob = ice->packIceMsg();
        if (ice_tcp) {
            auto ice_tcp_msg = ice_tcp->packIceMsg(2);
            blob.insert(blob.end(), ice_tcp_msg.begin(), ice_tcp_msg.end());
        }
    
        // Asynchronous DHT put of our local ICE data
        dht_->putEncrypted(
            callKey_,
            peer_ice_msg.from,
            dht::Value {dht::IceCandidates(peer_ice_msg.id, blob)},
            [wcall](bool ok) {
                if (!ok) {
                    JAMI_WARN("Can't put ICE descriptor reply on DHT");
                    if (auto call = wcall.lock())
                        call->onFailure();
                } else
                    JAMI_DBG("Successfully put ICE descriptor reply on DHT");
            });
    
        auto started_time = std::chrono::steady_clock::now();
    
        auto sdp_list = IceTransport::parseSDPList(peer_ice_msg.ice_data);
        auto udp_failed = true, tcp_failed = true;
        initICE(peer_ice_msg.ice_data, ice, ice_tcp, udp_failed, tcp_failed);
    
        if (udp_failed && tcp_failed) {
            call->onFailure(EIO);
            return;
        }
    
        call->setPeerNumber(from);
    
        // Let the call handled by the PendingCall handler loop
        std::lock_guard<std::mutex> lock(callsMutex_);
        pendingCalls_.emplace(call->getCallId(),
            PendingCall{/*.start = */ started_time,
                        /*.ice_sp = */ udp_failed ? nullptr : ice,
                        /*.ice_tcp_sp = */ tcp_failed ? nullptr : ice_tcp,
                        /*.call = */ wcall,
                        /*.listen_key = */ {},
                        /*.call_key = */ {},
                        /*.from = */ peer_ice_msg.from,
                        /*.from_account = */ from_id,
                        /*.from_cert = */ from_cert});
    
        Manager::instance().scheduleTask([w=weak(), callId=call->getCallId()]() {
            if (auto shared = w.lock())
                shared->checkPendingCall(callId);
        }, std::chrono::steady_clock::now() + ICE_NEGOTIATION_TIMEOUT);
    }
    
    void
    JamiAccount::doUnregister(std::function<void(bool)> released_cb)
    {
        std::unique_lock<std::mutex> lock(configurationMutex_);
    
        if (registrationState_ == RegistrationState::INITIALIZING
         || registrationState_ == RegistrationState::ERROR_NEED_MIGRATION) {
            lock.unlock();
            if (released_cb) released_cb(false);
            return;
        }
    
        JAMI_WARN("[Account %s] unregistering account %p", getAccountID().c_str(), this);
        dht_->shutdown([this](){
            JAMI_WARN("[Account %s] dht shutdown complete", getAccountID().c_str());
        });
    
        {
            std::lock_guard<std::mutex> lock(callsMutex_);
            pendingCalls_.clear();
            pendingSipCalls_.clear();
        }
    
        dht_->join();
    
        if (upnp_) upnp_->requestMappingRemove(static_cast<in_port_t>(dhtPortUsed_), upnp::PortType::UDP);
    
        lock.unlock();
    
        // Stop all current p2p connections if account is disabled
        // Else, we let the system managing if the co is down or not
        if (not isEnabled())
            shutdownConnections();
    
        setRegistrationState(RegistrationState::UNREGISTERED);
    
        if (released_cb)
            released_cb(false);
    }
    
    void
    JamiAccount::connectivityChanged()
    {
        JAMI_WARN("connectivityChanged");
        if (not isUsable()) {
            // nothing to do
            return;
        }
    
        dht_->connectivityChanged();
        // reset cache
        setPublishedAddress({});
        cacheTurnServers();
    }
    
    bool
    JamiAccount::findCertificate(const dht::InfoHash& h, std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
    {
        if (accountManager_)
            return accountManager_->findCertificate(h, std::move(cb));
        return false;
    }
    
    bool
    JamiAccount::findCertificate(const std::string& crt_id)
    {
        if (accountManager_)
            return accountManager_->findCertificate(dht::InfoHash(crt_id));
        return false;
    }
    
    bool
    JamiAccount::setCertificateStatus(const std::string& cert_id, tls::TrustStore::PermissionStatus status)
    {
        bool done = accountManager_ ? accountManager_->setCertificateStatus(cert_id, status) : false;
        if (done) {
            findCertificate(cert_id);
            emitSignal<DRing::ConfigurationSignal::CertificateStateChanged>(getAccountID(), cert_id, tls::TrustStore::statusToStr(status));
        }
        return done;
    }
    
    std::vector<std::string>
    JamiAccount::getCertificatesByStatus(tls::TrustStore::PermissionStatus status)
    {
        if (accountManager_)
            return accountManager_->getCertificatesByStatus(status);
        return {};
    }
    
    template<typename ID=dht::Value::Id>
    std::set<ID>
    loadIdList(const std::string& path)
    {
        std::set<ID> ids;
        std::ifstream file = fileutils::ifstream(path);
        if (!file.is_open()) {
            JAMI_DBG("Could not load %s", path.c_str());
            return ids;
        }
        std::string line;
        while (std::getline(file, line)) {
            std::istringstream iss(line);
            ID vid;
            if (!(iss >> std::hex >> vid)) { break; }
            ids.insert(vid);
        }
        return ids;
    }
    
    template<typename ID=dht::Value::Id>
    void
    saveIdList(const std::string& path, const std::set<ID>& ids)
    {
        std::ofstream file = fileutils::ofstream(path, std::ios::trunc | std::ios::binary);
        if (!file.is_open()) {
            JAMI_ERR("Could not save to %s", path.c_str());
            return;
        }
        for (auto& c : ids)
            file << std::hex << c << "\n";
    }
    
    void
    JamiAccount::loadTreatedCalls()
    {
        treatedCalls_ = loadIdList(cachePath_+DIR_SEPARATOR_STR "treatedCalls");
    }
    
    void
    JamiAccount::saveTreatedCalls() const
    {
        fileutils::check_dir(cachePath_.c_str());
        saveIdList(cachePath_+DIR_SEPARATOR_STR "treatedCalls", treatedCalls_);
    }
    
    void
    JamiAccount::loadTreatedMessages()
    {
        std::lock_guard<std::mutex> lock(messageMutex_);
        auto path = cachePath_+DIR_SEPARATOR_STR "treatedMessages";
        treatedMessages_ = loadIdList<std::string>(path);
        if (treatedMessages_.empty()) {
            auto messages = loadIdList(path);
            for (const auto& m : messages)
                treatedMessages_.emplace(to_hex_string(m));
        }
    }
    
    void
    JamiAccount::saveTreatedMessages() const
    {
        dht::ThreadPool::io().run([w = weak()](){
            if (auto sthis = w.lock()) {
                auto& this_ = *sthis;
                std::lock_guard<std::mutex> lock(this_.messageMutex_);
                fileutils::check_dir(this_.cachePath_.c_str());
                saveIdList<std::string>(this_.cachePath_+DIR_SEPARATOR_STR "treatedMessages", this_.treatedMessages_);
            }
        });
    }
    
    bool
    JamiAccount::isMessageTreated(const std::string& id)
    {
        std::lock_guard<std::mutex> lock(messageMutex_);
        auto res = treatedMessages_.emplace(id);
        if (res.second) {
            saveTreatedMessages();
            return false;
        }
        return true;
    }
    
    std::map<std::string, std::string>
    JamiAccount::getKnownDevices() const
    {
        if (not accountManager_ or not accountManager_->getInfo())
            return {};
        std::map<std::string, std::string> ids;
        for (auto& d : accountManager_->getKnownDevices()) {
            auto id = d.first.toString();
            auto label = d.second.name.empty() ? id.substr(0, 8) : d.second.name;
            ids.emplace(std::move(id), std::move(label));
        }
        return ids;
    }
    
    tls::DhParams
    JamiAccount::loadDhParams(std::string path)
    {
        std::lock_guard<std::mutex> l(fileutils::getFileLock(path));
        try {
            // writeTime throw exception if file doesn't exist
            auto duration = clock::now() - fileutils::writeTime(path);
            if (duration >= std::chrono::hours(24 * 3)) // file is valid only 3 days
                throw std::runtime_error("file too old");
    
            JAMI_DBG("Loading DhParams from file '%s'", path.c_str());
            return {fileutils::loadFile(path)};
        } catch (const std::exception& e) {
            JAMI_DBG("Failed to load DhParams file '%s': %s", path.c_str(), e.what());
            if (auto params = tls::DhParams::generate()) {
                try {
                    fileutils::saveFile(path, params.serialize(), 0600);
                    JAMI_DBG("Saved DhParams to file '%s'", path.c_str());
                } catch (const std::exception& ex) {
                    JAMI_WARN("Failed to save DhParams in file '%s': %s", path.c_str(), ex.what());
                }
                return params;
            }
            JAMI_ERR("Can't generate DH params.");
            return {};
        }
    }
    
    void
    JamiAccount::loadCachedUrl(const std::string& url,
                    const std::string& cachePath,
                    const std::chrono::seconds& cacheDuration,
                    std::function<void(const dht::http::Response& response)> cb)
    {
        auto lock = std::make_shared<std::lock_guard<std::mutex>>(fileutils::getFileLock(cachePath));
        dht::ThreadPool::io().run([lock, cb, url, cachePath, cacheDuration, w=weak()]() {
            try {
                auto data = fileutils::loadCacheFile(cachePath, cacheDuration);
                dht::http::Response ret;
                ret.body = {data.begin(), data.end()};
                ret.status_code = 200;
                cb(ret);
            } catch (const std::exception& e) {
                JAMI_DBG("Failed to load '%.*s' from '%.*s': %s", (int)url.size(), url.c_str(), (int)cachePath.size(), cachePath.c_str(), e.what());
    
                if (auto sthis = w.lock()) {
                    auto req = std::make_shared<dht::http::Request>(*Manager::instance().ioContext(), url, [lock, cb, cachePath, w](const dht::http::Response& response) {
                        if (response.status_code == 200) {
                            try {
                                fileutils::saveFile(cachePath, (const uint8_t*)response.body.data(), response.body.size(), 0600);
                                JAMI_DBG("Cached result to '%.*s'", (int)cachePath.size(), cachePath.c_str());
                            } catch (const std::exception& ex) {
                                JAMI_WARN("Failed to save result to %.*s: %s", (int)cachePath.size(), cachePath.c_str(), ex.what());
                            }
                        } else {
                            JAMI_WARN("Failed to download url");
                        }
                        cb(response);
                        if (auto req = response.request.lock())
                            if (auto sthis = w.lock())
                                sthis->requests_.erase(req);
                    });
                    sthis->requests_.emplace(req);
                    req->send();
                }
            }
        });
    }
    
    void
    JamiAccount::loadCachedProxyServer(std::function<void(const std::string& proxy)> cb)
    {
        if (proxyEnabled_ and proxyServerCached_.empty()) {
            JAMI_DBG("[Account %s] loading DHT proxy URL: %s", getAccountID().c_str(), proxyListUrl_.c_str());
            if (proxyListUrl_.empty()) {
                cb(getDhtProxyServer(proxyServer_));
            } else {
                loadCachedUrl(proxyListUrl_,
                    cachePath_ + DIR_SEPARATOR_STR "dhtproxylist",
                    std::chrono::hours(24 * 3),
                    [w=weak(), cb=std::move(cb)](const dht::http::Response& response){
                        if (auto sthis = w.lock()) {
                            if (response.status_code == 200) {
                                cb(sthis->getDhtProxyServer(response.body));
                            } else {
                                cb(sthis->getDhtProxyServer(sthis->proxyServer_));
                            }
                        }
                    }
                );
            }
        } else {
            cb(proxyServerCached_);
        }
    }
    
    std::string
    JamiAccount::getDhtProxyServer(const std::string& serverList)
    {
        if (proxyServerCached_.empty()) {
            std::vector<std::string> proxys;
            // Split the list of servers
            std::sregex_iterator begin = {serverList.begin(), serverList.end(), PROXY_REGEX}, end;
            for (auto it = begin; it != end; ++it) {
                auto &match = *it;
                if (match[5].matched and match[6].matched) {
                    try {
                        auto start = std::stoi(match[5]), end = std::stoi(match[6]);
                        for (auto p = start; p <= end; p++)
                          proxys.emplace_back(match[1].str() + match[2].str() + ":" +
                                              std::to_string(p));
                    } catch (...) {
                        JAMI_WARN("Malformed proxy, ignore it");
                        continue;
                    }
                } else {
                    proxys.emplace_back(match[0].str());
                }
            }
            if (proxys.empty())
                return {};
            // Select one of the list as the current proxy.
            auto randIt = proxys.begin();
            std::advance(randIt, std::uniform_int_distribution<unsigned long>(0, proxys.size() - 1)(rand));
            proxyServerCached_ = *randIt;
            // Cache it!
            fileutils::check_dir(cachePath_.c_str(), 0700);
            std::string proxyCachePath = cachePath_ + DIR_SEPARATOR_STR "dhtproxy";
            std::ofstream file = fileutils::ofstream(proxyCachePath);
            JAMI_DBG("Cache DHT proxy server: %s", proxyServerCached_.c_str());
            if (file.is_open())
                file << proxyServerCached_;
            else
                JAMI_WARN("Cannot write into %s", proxyCachePath.c_str());
        }
        return proxyServerCached_;
    }
    
    void
    JamiAccount::generateDhParams()
    {
        //make sure cachePath_ is writable
        fileutils::check_dir(cachePath_.c_str(), 0700);
        dhParams_ = dht::ThreadPool::computation().get<tls::DhParams>(std::bind(loadDhParams, cachePath_ + DIR_SEPARATOR_STR "dhParams"));
    }
    
    MatchRank
    JamiAccount::matches(const std::string &userName, const std::string &server) const
    {
        if (not accountManager_ or not accountManager_->getInfo())
            return MatchRank::NONE;
    
        if (userName == accountManager_->getInfo()->accountId || server == accountManager_->getInfo()->accountId || userName == accountManager_->getInfo()->deviceId) {
            JAMI_DBG("Matching account id in request with username %s", userName.c_str());
            return MatchRank::FULL;
        } else {
            return MatchRank::NONE;
        }
    }
    
    std::string
    JamiAccount::getFromUri() const
    {
        const std::string uri = "<sip:" + accountManager_->getInfo()->accountId + "@ring.dht>";
        if (not displayName_.empty())
            return "\"" + displayName_ + "\" " + uri;
        return uri;
    }
    
    std::string
    JamiAccount::getToUri(const std::string& to) const
    {
        return "<sips:" + to + ";transport=tls>";
    }
    
    pj_str_t
    JamiAccount::getContactHeader(pjsip_transport* t)
    {
        std::string quotedDisplayName = "\"" + displayName_ + "\" " + (displayName_.empty() ? "" : " ");
        if (t) {
            auto* td = reinterpret_cast<tls::AbstractSIPTransport::TransportData*>(t);
            auto address = td->self->getLocalAddress().toString(true);
            bool reliable = t->flag & PJSIP_TRANSPORT_RELIABLE;
    
            contact_.slen = pj_ansi_snprintf(contact_.ptr, PJSIP_MAX_URL_SIZE,
                                             "%s<sips:%s%s%s;transport=%s>",
                                             quotedDisplayName.c_str(),
                                             id_.second->getId().toString().c_str(),
                                             (address.empty() ? "" : "@"),
                                             address.c_str(), reliable  ? "tls" : "dtls");
        } else {
            JAMI_ERR("getContactHeader: no SIP transport provided");
            contact_.slen = pj_ansi_snprintf(contact_.ptr, PJSIP_MAX_URL_SIZE,
                                             "%s<sips:%s@ring.dht>",
                                             quotedDisplayName.c_str(),
                                             id_.second->getId().toString().c_str());
        }
        return contact_;
    }
    
    /* contacts */
    
    void
    JamiAccount::addContact(const std::string& uri, bool confirmed)
    {
        if (accountManager_)
            accountManager_->addContact(uri, confirmed);
        else
            JAMI_WARN("[Account %s] addContact: account not loaded", getAccountID().c_str());
    }
    
    void
    JamiAccount::removeContact(const std::string& uri, bool ban)
    {
        if (accountManager_)
            accountManager_->removeContact(uri, ban);
        else
            JAMI_WARN("[Account %s] removeContact: account not loaded", getAccountID().c_str());
    
        // Remove current connections with contact
        dht::InfoHash peer_account(uri);
    
        std::unique_lock<std::mutex> lk(sipConnectionsMtx_);
        std::set<std::string> devices;
        for (const auto& deviceConn: sipConnections_[uri]) {
            devices.emplace(deviceConn.first);
        }
        sipConnections_.erase(uri);
    
        for (auto pendingIt = pendingSipConnections_.begin(); pendingIt != pendingSipConnections_.end();) {
            if (uri == pendingIt->first) {
                devices.emplace(pendingIt->second);
                pendingIt = pendingSipConnections_.erase(pendingIt);
            } else {
                ++pendingIt;
            }
        }
    
        lk.unlock();
        for (const auto& device: devices) {
            if (connectionManager_)
                connectionManager_->closeConnectionsWith(device);
        }
    }
    
    std::map<std::string, std::string>
    JamiAccount::getContactDetails(const std::string& uri) const
    {
        return (accountManager_ and accountManager_->getInfo()) ? accountManager_->getContactDetails(uri) : std::map<std::string, std::string>{};
    }
    
    std::vector<std::map<std::string, std::string>>
    JamiAccount::getContacts() const
    {
        if (not accountManager_)
            return {};
        return accountManager_->getContacts();
    }
    
    /* trust requests */
    
    std::vector<std::map<std::string, std::string>>
    JamiAccount::getTrustRequests() const
    {
        return accountManager_ ? accountManager_->getTrustRequests() : std::vector<std::map<std::string, std::string>>{};
    }
    
    bool
    JamiAccount::acceptTrustRequest(const std::string& from)
    {
        if (accountManager_)
            return accountManager_->acceptTrustRequest(from);
        JAMI_WARN("[Account %s] acceptTrustRequest: account not loaded", getAccountID().c_str());
        return false;
    }
    
    bool
    JamiAccount::discardTrustRequest(const std::string& from)
    {
        if (accountManager_)
            return accountManager_->discardTrustRequest(from);
        JAMI_WARN("[Account %s] discardTrustRequest: account not loaded", getAccountID().c_str());
        return false;
    }
    
    void
    JamiAccount::sendTrustRequest(const std::string& to, const std::vector<uint8_t>& payload)
    {
        if (accountManager_)
            accountManager_->sendTrustRequest(to, payload);
        else
            JAMI_WARN("[Account %s] sendTrustRequest: account not loaded", getAccountID().c_str());
    }
    
    void
    JamiAccount::sendTrustRequestConfirm(const std::string& to)
    {
        if (accountManager_)
            accountManager_->sendTrustRequestConfirm(dht::InfoHash(to));
        else
            JAMI_WARN("[Account %s] sendTrustRequestConfirm: account not loaded", getAccountID().c_str());
    }
    
    void
    JamiAccount::forEachDevice(const dht::InfoHash& to,
                               std::function<void(const dht::InfoHash&)>&& op,
                               std::function<void(bool)>&& end)
    {
        accountManager_->forEachDevice(to, std::move(op), std::move(end));
    }
    
    uint64_t
    JamiAccount::sendTextMessage(const std::string& to, const std::map<std::string, std::string>& payloads)
    {
        std::string toUri;
        try {
            toUri = parseJamiUri(to);
        } catch (...) {
            JAMI_ERR("Failed to send a text message due to an invalid URI %s", to.c_str());
            return 0;
        }
        if (payloads.size() != 1) {
            JAMI_ERR("Multi-part im is not supported yet by JamiAccount");
            return 0;
        }
        return SIPAccountBase::sendTextMessage(toUri, payloads);
    }
    
    void
    JamiAccount::sendTextMessage(const std::string& to, const std::map<std::string, std::string>& payloads, uint64_t token, bool retryOnTimeout)
    {
        std::string toUri;
        try {
            toUri = parseJamiUri(to);
        } catch (...) {
            JAMI_ERR("Failed to send a text message due to an invalid URI %s", to.c_str());
            messageEngine_.onMessageSent(to, token, false);
            return;
        }
        if (payloads.size() != 1) {
            // Multi-part message
            // TODO: not supported yet
            JAMI_ERR("Multi-part im is not supported yet by JamiAccount");
            messageEngine_.onMessageSent(toUri, token, false);
            return;
        }
    
        auto toH = dht::InfoHash(toUri);
        auto now = clock::to_time_t(clock::now());
    
        auto confirm = std::make_shared<PendingConfirmation>();
    
        std::set<std::string> devices;
        std::unique_lock<std::mutex> lk(sipConnectionsMtx_);
        sip_utils::register_thread();
        auto& sipConns = sipConnections_[to];
        auto deviceConnIt = sipConns.begin();
        while (deviceConnIt != sipConns.end()) {
            if (deviceConnIt->second.empty()) {
                ++deviceConnIt;
                continue;
            }
            auto& it = deviceConnIt->second.back();
    
            // Set input token into callback
            std::unique_ptr<TextMessageCtx> ctx{ std::make_unique<TextMessageCtx>() };
            ctx->acc = weak();
            ctx->to = to;
            ctx->deviceId = deviceConnIt->first;
            ctx->id = token;
            ctx->retryOnTimeout = retryOnTimeout;
            ctx->confirmation = confirm;
    
            try {
                auto res = sendSIPMessage(it, to, ctx.release(), token, payloads,
                    [](void *token, pjsip_event *event)
                    {
                        std::unique_ptr<TextMessageCtx> c{ (TextMessageCtx*)token };
                        auto code = event->body.tsx_state.tsx->status_code;
                        auto acc = c->acc.lock();
                        if (not acc) return;
    
                        if (code == PJSIP_SC_OK) {
                            std::unique_lock<std::mutex> l(c->confirmation->lock);
                            c->confirmation->replied = true;
                            l.unlock();
                            acc->messageEngine_.onMessageSent(c->to, c->id, true);
                        } else {
                            JAMI_WARN("Timeout when send a message, close current connection");
                            {
                                std::unique_lock<std::mutex> lk(acc->sipConnectionsMtx_);
                                acc->sipConnections_[c->to].erase(c->deviceId);
                            }
                            acc->connectionManager_->closeConnectionsWith(c->deviceId);
                            // This MUST be done after closing the connection to avoid race condition
                            // with messageEngine_
                            acc->messageEngine_.onMessageSent(c->to, c->id, false);
    
                            // In that case, the peer typically changed its connectivity.
                            // After closing sockets with that peer, we try to re-connect to
                            // that peer one time.
                            if (c->retryOnTimeout) acc->messageEngine_.onPeerOnline(c->to, false);
                        }
                    });
                if (!res) {
                    messageEngine_.onMessageSent(to, token, false);
                    ++deviceConnIt;
                    continue;
                }
            } catch (const std::runtime_error& ex) {
                JAMI_WARN("%s", ex.what());
                messageEngine_.onMessageSent(to, token, false);
                // Remove connection in incorrect state
                deviceConnIt = sipConns.erase(deviceConnIt);
                continue;
            }
    
            devices.emplace(deviceConnIt->first);
            ++deviceConnIt;
        }
        lk.unlock();
    
        // Find listening devices for this account
        accountManager_->forEachDevice(toH, [this,confirm,to,token,payloads,now, devices{std::move(devices)}](const dht::InfoHash& dev)
        {
            // Test if already sent
            if (devices.find(dev.toString()) != devices.end()) {
                return;
            }
    
            // Else, ask for a channel and send a DHT message
            requestSIPConnection(to, dev.toString());
            {
                std::lock_guard<std::mutex> lock(messageMutex_);
                sentMessages_[token].to.emplace(dev);
            }
    
            auto h = dht::InfoHash::get("inbox:"+dev.toString());
            std::lock_guard<std::mutex> l(confirm->lock);
            auto list_token = dht_->listen<dht::ImMessage>(h, [this, to, token, confirm](dht::ImMessage&& msg) {
                // check expected message confirmation
                if (msg.id != token)
                    return true;
    
                {
                    std::lock_guard<std::mutex> lock(messageMutex_);
                    auto e = sentMessages_.find(msg.id);
                    if (e == sentMessages_.end() or e->second.to.find(msg.from) == e->second.to.end()) {
                        JAMI_DBG() << "[Account " << getAccountID() << "] [message " << token << "] Message not found";
                        return true;
                    }
                    sentMessages_.erase(e);
                    JAMI_DBG() << "[Account " << getAccountID() << "] [message " << token << "] Received text message reply";
    
                    // add treated message
                    auto res = treatedMessages_.emplace(to_hex_string(msg.id));
                    if (!res.second)
                        return true;
                }
                saveTreatedMessages();
    
                // report message as confirmed received
                {
                    std::lock_guard<std::mutex> l(confirm->lock);
                    for (auto& t : confirm->listenTokens)
                        dht_->cancelListen(t.first, std::move(t.second));
                    confirm->listenTokens.clear();
                    confirm->replied = true;
                }
                messageEngine_.onMessageSent(to, token, true);
                return false;
            });
            confirm->listenTokens.emplace(h, std::move(list_token));
            dht_->putEncrypted(h, dev,
                dht::ImMessage(token, std::string(payloads.begin()->first), std::string(payloads.begin()->second), now),
                [this,to,token,confirm,h](bool ok) {
                    JAMI_DBG() << "[Account " << getAccountID() << "] [message " << token << "] Put encrypted " << (ok ? "ok" : "failed");
                    if (not ok) {
                        std::unique_lock<std::mutex> l(confirm->lock);
                        auto lt = confirm->listenTokens.find(h);
                        if (lt != confirm->listenTokens.end()) {
                            dht_->cancelListen(h, std::move(lt->second));
                            confirm->listenTokens.erase(lt);
                        }
                        if (confirm->listenTokens.empty() and not confirm->replied) {
                            l.unlock();
                            messageEngine_.onMessageSent(to, token, false);
                        }
                    }
                });
    
            JAMI_DBG() << "[Account " << getAccountID() << "] [message " << token << "] Sending message for device " << dev.toString();
        }, [this, to, token](bool ok) {
            if (not ok) {
                messageEngine_.onMessageSent(to, token, false);
            }
        });
    
        // Timeout cleanup
        Manager::instance().scheduleTask([w=weak(), confirm, to, token]() {
            std::unique_lock<std::mutex> l(confirm->lock);
            if (not confirm->replied) {
                if (auto this_ = w.lock()) {
                    JAMI_DBG() << "[Account " << this_->getAccountID() << "] [message " << token << "] Timeout";
                    for (auto& t : confirm->listenTokens)
                        this_->dht_->cancelListen(t.first, std::move(t.second));
                    confirm->listenTokens.clear();
                    confirm->replied = true;
                    l.unlock();
                    this_->messageEngine_.onMessageSent(to, token, false);
                }
            }
        }, std::chrono::steady_clock::now() + std::chrono::minutes(1));
    }
    
    void
    JamiAccount::onIsComposing(const std::string& peer, bool isWriting)
    {
        try {
            Account::onIsComposing(parseJamiUri(peer), isWriting);
        } catch (...) {
            JAMI_ERR("[Account %s] Can't parse URI: %s", getAccountID().c_str(), peer.c_str());
        }
    }
    
    void
    JamiAccount::registerDhtAddress(IceTransport& ice)
    {
        const auto reg_addr = [&](IceTransport& ice, const IpAddr& ip) {
                JAMI_DBG("[Account %s] using public IP: %s", getAccountID().c_str(), ip.toString().c_str());
                for (unsigned compId = 1; compId <= ice.getComponentCount(); ++compId)
                    ice.registerPublicIP(compId, ip);
                return ip;
            };
    
        auto ip = getPublishedAddress();
        if (ip.empty()) {
            // We need a public address in case of NAT'ed network
            // Trying to use one discovered by DHT service
    
            // IPv6 (sdp support only one IP, put IPv6 before IPv4 as this last has the priority over IPv6 less NAT'able)
            const auto& addr6 = dht_->getPublicAddress(AF_INET6);
            if (addr6.size())
                setPublishedAddress(reg_addr(ice, *addr6[0].get()));
    
            // IPv4
            const auto& addr4 = dht_->getPublicAddress(AF_INET);
            if (addr4.size())
                setPublishedAddress(reg_addr(ice, *addr4[0].get()));
        } else {
            reg_addr(ice, ip);
        }
    }
    
    std::vector<std::string>
    JamiAccount::publicAddresses()
    {
        std::vector<std::string> addresses;
        for (auto& addr : dht_->getPublicAddress(AF_INET)) {
            addresses.emplace_back(addr.toString());
        }
        for (auto& addr : dht_->getPublicAddress(AF_INET6)) {
            addresses.emplace_back(addr.toString());
        }
        return addresses;
    }
    
    void
    JamiAccount::requestPeerConnection(const std::string& peer_id, const DRing::DataTransferId& tid,
                                       bool isVCard,
                                       const std::function<void(PeerConnection*)>& connect_cb,
                                       const std::function<void(const std::shared_ptr<ChanneledOutgoingTransfer>&)>& channeledConnectedCb,
                                       const std::function<void()>& onChanneledCancelled)
    {
        dhtPeerConnector_->requestConnection(peer_id, tid, isVCard, connect_cb, channeledConnectedCb, onChanneledCancelled);
    }
    
    void
    JamiAccount::closePeerConnection(const std::string& peer, const DRing::DataTransferId& tid)
    {
        dhtPeerConnector_->closeConnection(peer, tid);
    }
    
    void
    JamiAccount::enableProxyClient(bool enable)
    {
        JAMI_WARN("[Account %s] DHT proxy client: %s", getAccountID().c_str(), enable ? "enable" : "disable");
        dht_->enableProxy(enable);
    }
    
    void JamiAccount::setPushNotificationToken(const std::string& token)
    {
        JAMI_WARN("[Account %s] setPushNotificationToken: %s", getAccountID().c_str(), token.c_str());
        deviceKey_ = token;
        dht_->setPushNotificationToken(deviceKey_);
    }
    
    /**
     * To be called by clients with relevant data when a push notification is received.
     */
    void JamiAccount::pushNotificationReceived(const std::string& from, const std::map<std::string, std::string>& data)
    {
        JAMI_WARN("[Account %s] pushNotificationReceived: %s", getAccountID().c_str(), from.c_str());
        dht_->pushNotificationReceived(data);
    }
    
    std::string
    JamiAccount::getUserUri() const
    {
    #ifdef HAVE_RINGNS
        if (not registeredName_.empty())
            return RING_URI_PREFIX + registeredName_;
    #endif
        return username_;
    }
    
    std::vector<DRing::Message>
    JamiAccount::getLastMessages(const uint64_t& base_timestamp)
    {
        return SIPAccountBase::getLastMessages(base_timestamp);
    }
    
    void
    JamiAccount::startAccountPublish()
    {
        AccountPeerInfo info_pub;
        info_pub.accountId = dht::InfoHash(accountManager_->getInfo()->accountId);
        info_pub.displayName = displayName_;
        peerDiscovery_->startPublish<AccountPeerInfo>(PEER_DISCOVERY_JAMI_SERVICE, info_pub);
    }
    
    void
    JamiAccount::startAccountDiscovery()
    {
        auto id = dht::InfoHash(accountManager_->getInfo()->accountId);
        peerDiscovery_->startDiscovery<AccountPeerInfo>(PEER_DISCOVERY_JAMI_SERVICE,[this,id](AccountPeerInfo&& v, dht::SockAddr&&){
    
            std::lock_guard<std::mutex> lc(discoveryMapMtx_);
            //Make sure that account itself will not be recorded
            if(v.accountId != id){
                //Create or Find the old one
                auto& dp = discoveredPeers_[v.accountId];
                dp.displayName = v.displayName;
                discoveredPeerMap_[v.accountId.toString()] = v.displayName;
                if (dp.cleanupTask) {
                    dp.cleanupTask->cancel();
                } else {
                    //Avoid Repeat Reception of Same peer
                    JAMI_INFO("Account discovered: %s: %s", v.displayName.c_str(), v.accountId.to_c_str());
                    //Send Added Peer and corrsponding accoundID
                    emitSignal<DRing::PresenceSignal::NearbyPeerNotification>(getAccountID(), v.accountId.toString(), 0, v.displayName);
                }
                dp.cleanupTask = Manager::instance().scheduler().scheduleIn([w = weak(), p = v.accountId, a = v.displayName]{
                    if (auto this_ = w.lock()){
                        {
                            std::lock_guard<std::mutex> lc(this_->discoveryMapMtx_);
                            this_->discoveredPeers_.erase(p);
                            this_->discoveredPeerMap_.erase(p.toString());
                        }
                        //Send Deleted Peer
                        emitSignal<DRing::PresenceSignal::NearbyPeerNotification>(this_->getAccountID(), p.toString(), 1, a);
                    }
                    JAMI_INFO("Account removed from discovery list: %s", a.c_str());
                }, PEER_DISCOVERY_EXPIRATION);
            }
        });
    }
    
    std::map<std::string, std::string>
    JamiAccount::getNearbyPeers() const
    {
        return discoveredPeerMap_;
    }
    
    void
    JamiAccount::setActiveCodecs(const std::vector<unsigned>& list)
    {
        Account::setActiveCodecs(list);
        if (!hasActiveCodec(MEDIA_AUDIO))
            setCodecActive(AV_CODEC_ID_OPUS);
        if (!hasActiveCodec(MEDIA_VIDEO)) {
            setCodecActive(AV_CODEC_ID_HEVC);
            setCodecActive(AV_CODEC_ID_H264);
            setCodecActive(AV_CODEC_ID_VP8);
        }
    }
    
    void
    JamiAccount::cacheTurnServers()
    {
        // The resolution of the TURN server can take quite some time (if timeout).
        // So, run this in its own io thread to avoid to block the main thread.
        dht::ThreadPool::io().run([w=weak()] {
            auto this_ = w.lock();
            if (not this_)
                return;
            // Avoid multiple refresh
            if (this_->isRefreshing_.exchange(true)) return;
            if (!this_->turnEnabled_) {
                // In this case, we do not use any TURN server
                std::lock_guard<std::mutex> lk(this_->cachedTurnMutex_);
                this_->cacheTurnV4_.reset();
                this_->cacheTurnV6_.reset();
                this_->isRefreshing_ = false;
                return;
            }
            JAMI_INFO("Refresh cache for TURN server resolution");
            // Retrieve old cached value if available.
            // This means that we directly get the correct value when launching the application on the same network
            std::string server = this_->turnServer_.empty() ? DEFAULT_TURN_SERVER : this_->turnServer_;
            fileutils::recursive_mkdir(this_->cachePath_ + DIR_SEPARATOR_STR + "domains", 0700);
            auto pathV4 = this_->cachePath_ + DIR_SEPARATOR_STR + "domains" + DIR_SEPARATOR_STR + "v4." + server;
            if (auto turnV4File = std::ifstream(pathV4)) {
                std::string content((std::istreambuf_iterator<char>(turnV4File)), std::istreambuf_iterator<char>());
                std::lock_guard<std::mutex> lk(this_->cachedTurnMutex_);
                this_->cacheTurnV4_ = std::make_unique<IpAddr>(content, AF_INET);
            }
            auto pathV6 = this_->cachePath_ + DIR_SEPARATOR_STR + "domains" + DIR_SEPARATOR_STR + "v6." + server;
            if (auto turnV6File = std::ifstream(pathV6)) {
                std::string content((std::istreambuf_iterator<char>(turnV6File)), std::istreambuf_iterator<char>());
                std::lock_guard<std::mutex> lk(this_->cachedTurnMutex_);
                this_->cacheTurnV6_ = std::make_unique<IpAddr>(content, AF_INET6);
            }
            // Resolve just in case. The user can have a different connectivity
            auto turnV4 = IpAddr {server, AF_INET};
            {
                if (turnV4) {
                    // Cache value to avoid a delay when starting up Jami
                    std::ofstream turnV4File(pathV4);
                    turnV4File << turnV4.toString();
                } else {
                    fileutils::remove(pathV4, true);
                }
                std::lock_guard<std::mutex> lk(this_->cachedTurnMutex_);
                // Update TURN
                this_->cacheTurnV4_ = std::make_unique<IpAddr>(std::move(turnV4));
            }
            auto turnV6 = IpAddr {server.empty() ? DEFAULT_TURN_SERVER : server, AF_INET6};
            {
                if (turnV6) {
                    // Cache value to avoid a delay when starting up Jami
                    std::ofstream turnV6File(pathV6);
                    turnV6File << turnV6.toString();
                } else {
                    fileutils::remove(pathV6, true);
                }
                std::lock_guard<std::mutex> lk(this_->cachedTurnMutex_);
                // Update TURN
                this_->cacheTurnV6_ = std::make_unique<IpAddr>(std::move(turnV6));
            }
            this_->isRefreshing_ = false;
            JAMI_INFO("Cache refreshed for TURN resolution");
        });
    }
    
    void
    JamiAccount::requestSIPConnection(const std::string& peerId, const std::string& deviceId)
    {
        // If a connection already exists or is in progress, no need to do this
        std::lock_guard<std::mutex> lk(sipConnectionsMtx_);
        auto id = std::make_pair<std::string, std::string>(std::string(peerId), std::string(deviceId));
        if (!sipConnections_[peerId][deviceId].empty() || pendingSipConnections_.find(id) != pendingSipConnections_.end()) {
            JAMI_DBG("A SIP connection with %s already exists", deviceId.c_str());
            return;
        }
        pendingSipConnections_.emplace(id);
        // If not present, create it
        JAMI_INFO("Ask %s for a new SIP channel", deviceId.c_str());
        if (!connectionManager_) return;
        connectionManager_->connectDevice(deviceId, "sip",
            [w=weak(), id](std::shared_ptr<ChannelSocket>) {
            auto shared = w.lock();
            if (!shared) return;
            // NOTE: No need to cache Connection there. OnConnectionReady
            // is called before this callback, so the socket is already
            // cached if succeed. We just need to remove the pending request.
            std::lock_guard<std::mutex> lk(shared->sipConnectionsMtx_);
            shared->pendingSipConnections_.erase(id);
        });
    }
    
    bool
    JamiAccount::needToSendProfile(const std::string& deviceId)
    {
        auto vCardMd5 = fileutils::md5File(idPath_ + DIR_SEPARATOR_STR + "profile.vcf");
        std::string currentMd5 {};
        auto vCardPath = cachePath_ + DIR_SEPARATOR_STR + "vcard";
        auto md5Path = vCardPath + DIR_SEPARATOR_STR + "md5";
        fileutils::check_dir(vCardPath.c_str(), 0700);
        try {
            currentMd5 = fileutils::loadTextFile(md5Path);
        } catch (...) {
            fileutils::saveFile(md5Path, {vCardMd5.begin(), vCardMd5.end()}, 0600);
            return true;
        }
        if (currentMd5 != vCardMd5) {
            // Incorrect md5 stored. Update it
            fileutils::removeAll(vCardPath, true);
            fileutils::check_dir(vCardPath.c_str(), 0700);
            fileutils::saveFile(md5Path, {vCardMd5.begin(), vCardMd5.end()}, 0600);
            return true;
        }
        return not fileutils::isFile(vCardPath + DIR_SEPARATOR_STR + deviceId);
    }
    
    bool
    JamiAccount::sendSIPMessage(SipConnection& conn, const std::string& to, void* ctx,
                                int token, const std::map<std::string, std::string>& data,
                                pjsip_endpt_send_callback cb)
    {
        auto transport = conn.transport;
        auto channel = conn.channel;
        if (!channel || !channel->underlyingICE())
            throw std::runtime_error("A SIP transport exists without Channel, this is a bug. Please report");
    
        // Build SIP Message
        // "deviceID@IP"
        auto toURI = getToUri(to + "@" + channel->underlyingICE()->getRemoteAddress(0).toString(true));
        std::string from = getFromUri();
        pjsip_tx_data* tdata;
    
        // Build SIP message
        constexpr pjsip_method msg_method = {PJSIP_OTHER_METHOD, jami::sip_utils::CONST_PJ_STR("MESSAGE")};
        pj_str_t pjFrom = pj_str((char*) from.c_str());
        pj_str_t pjTo = pj_str((char*) toURI.c_str());
    
        // Create request.
        pj_status_t status = pjsip_endpt_create_request(link_.getEndpoint(), &msg_method,
                                                        &pjTo, &pjFrom, &pjTo, nullptr, nullptr, -1,
                                                        nullptr, &tdata);
        if (status != PJ_SUCCESS) {
            JAMI_ERR("Unable to create request: %s", sip_utils::sip_strerror(status).c_str());
            return false;
        }
    
        // Add Date Header.
        pj_str_t date_str;
        constexpr auto key = sip_utils::CONST_PJ_STR("Date");
        pjsip_hdr *hdr;
        auto time = std::time(nullptr);
        auto date = std::ctime(&time);
        // the erase-remove idiom for a cstring, removes _all_ new lines with in date
        *std::remove(date, date+strlen(date), '\n') = '\0';
    
        // Add Header
        hdr = reinterpret_cast<pjsip_hdr*>(pjsip_date_hdr_create(tdata->pool, &key, pj_cstr(&date_str, date)));
        pjsip_msg_add_hdr(tdata->msg, hdr);
    
        // https://tools.ietf.org/html/rfc5438#section-6.3
        auto token_str = to_hex_string(token);
        auto pjMessageId = sip_utils::CONST_PJ_STR(token_str);
        hdr = reinterpret_cast<pjsip_hdr*>(pjsip_generic_string_hdr_create(tdata->pool, &STR_MESSAGE_ID, &pjMessageId));
        pjsip_msg_add_hdr(tdata->msg, hdr);
    
        // Add user agent header.
        pjsip_hdr *hdr_list;
        constexpr auto pJuseragent = sip_utils::CONST_PJ_STR("Jami");
        constexpr pj_str_t STR_USER_AGENT = jami::sip_utils::CONST_PJ_STR("User-Agent");
    
        // Add Header
        hdr_list = reinterpret_cast<pjsip_hdr*>(pjsip_user_agent_hdr_create(tdata->pool, &STR_USER_AGENT, &pJuseragent));
        pjsip_msg_add_hdr(tdata->msg, hdr_list);
    
        // Init tdata
        const pjsip_tpselector tp_sel = SIPVoIPLink::getTransportSelector(transport->get());
        status = pjsip_tx_data_set_transport(tdata, &tp_sel);
        if (status != PJ_SUCCESS) {
            JAMI_ERR("Unable to create request: %s", sip_utils::sip_strerror(status).c_str());
            return false;
        }
        im::fillPJSIPMessageBody(*tdata, data);
    
        // Because pjsip_endpt_send_request can take quite some time, move it in a io thread to avoid to block
        dht::ThreadPool::io().run([w=weak(), tdata, ctx=std::move(ctx), cb=std::move(cb)] {
            auto shared = w.lock();
            if (!shared) return;
            sip_utils::register_thread();
            auto status = pjsip_endpt_send_request(shared->link_.getEndpoint(), tdata, -1, ctx, cb);
            if (status != PJ_SUCCESS)
                JAMI_ERR("Unable to send request: %s", sip_utils::sip_strerror(status).c_str());
        });
        return true;
    }
    
    void
    JamiAccount::sendProfile(const std::string& deviceId)
    {
        try {
            if (not needToSendProfile(deviceId)) {
                JAMI_INFO() << "Peer " << deviceId << " already got an up-to-date vcard";
                return;
            }
    
            DRing::DataTransferInfo info;
            DRing::DataTransferId id {0};
            info.accountId = getAccountID();
            info.peer = deviceId;
            info.path = idPath_ + DIR_SEPARATOR_STR + "profile.vcf";
            info.displayName = "profile.vcf";
            info.bytesProgress = 0;
    
            Manager::instance().dataTransfers->sendFile(info, id, [info](const std::string&) {
                // Mark the VCard as sent
                auto path = fileutils::get_cache_dir() + DIR_SEPARATOR_STR
                            + info.accountId + DIR_SEPARATOR_STR
                            + "vcard" + DIR_SEPARATOR_STR + info.peer;
                fileutils::ofstream(path);
            });
        } catch (const std::exception& e) {
            JAMI_ERR() << e.what();
        }
    
    }
    
    void
    JamiAccount::cacheSIPConnection(std::shared_ptr<ChannelSocket>&& socket, const std::string& peerId, const std::string& deviceId)
    {
        std::unique_lock<std::mutex> lk(sipConnectionsMtx_);
        // Verify that the connection is not already cached
        auto& connections = sipConnections_[peerId][deviceId];
        auto conn = std::find_if(connections.begin(), connections.end(), [socket](auto v) {
            return v.channel == socket;
        });
        if (conn != connections.end()) {
            JAMI_WARN("Channel socket already cached with this peer");
            return;
        }
    
        // Convert to SIP transport
        sip_utils::register_thread();
        auto onShutdown = [w=weak(), peerId, deviceId, socket]() {
            auto shared = w.lock();
            if (!shared) return;
            std::lock_guard<std::mutex> lk(shared->sipConnectionsMtx_);
            auto& connections = shared->sipConnections_[peerId][deviceId];
            auto conn = connections.begin();
            while (conn != connections.end()) {
                if (conn->channel == socket)
                    conn = connections.erase(conn);
                else
                    conn++;
            }
        };
        auto sip_tr = link_.sipTransportBroker->getChanneledTransport(socket, std::move(onShutdown));
        // Store the connection
        sipConnections_[peerId][deviceId].emplace_back(SipConnection {
            std::move(sip_tr),
            socket
        });
        JAMI_WARN("New SIP channel opened with %s", deviceId.c_str());
        lk.unlock();
    
        sendProfile(deviceId);
    
        // Retry messages
        messageEngine_.onPeerOnline(peerId);
    }
    
    } // namespace jami