/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsIPrefService.h"
#include "nsIPrefBranch.h"
#include "nsSmtpServer.h"
#include "nsNetUtil.h"
#include "nsIAuthPrompt.h"
#include "nsMsgUtils.h"
#include "nsIMsgAccountManager.h"
#include "nsMsgBaseCID.h"
#include "nsISmtpService.h"
#include "nsMsgCompCID.h"
#include "nsILoginInfo.h"
#include "nsILoginManager.h"
#include "nsIArray.h"
#include "nsArrayUtils.h"

NS_IMPL_ADDREF(nsSmtpServer)
NS_IMPL_RELEASE(nsSmtpServer)
NS_INTERFACE_MAP_BEGIN(nsSmtpServer)
    NS_INTERFACE_MAP_ENTRY(nsISmtpServer)
    NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
    NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISmtpServer)
NS_INTERFACE_MAP_END

nsSmtpServer::nsSmtpServer():
    mKey("")
{
    m_logonFailed = false;
    getPrefs();
}

nsSmtpServer::~nsSmtpServer()
{
}

NS_IMETHODIMP
nsSmtpServer::GetKey(char * *aKey)
{
    if (!aKey) return NS_ERROR_NULL_POINTER;
    if (mKey.IsEmpty())
        *aKey = nullptr;
    else
        *aKey = ToNewCString(mKey);
    return NS_OK;
}

NS_IMETHODIMP
nsSmtpServer::SetKey(const char * aKey)
{
    NS_ASSERTION(aKey, "Bad key pointer");
    mKey = aKey;
    return getPrefs();
}

nsresult nsSmtpServer::getPrefs()
{
    nsresult rv;
    nsCOMPtr<nsIPrefService> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
    if (NS_FAILED(rv))
        return rv;

    nsAutoCString branchName;
    branchName.AssignLiteral("mail.smtpserver.");
    branchName += mKey;
    branchName.Append('.');
    rv = prefs->GetBranch(branchName.get(), getter_AddRefs(mPrefBranch));
    if (NS_FAILED(rv))
        return rv;

    if(!mDefPrefBranch) {
        branchName.AssignLiteral("mail.smtpserver.default.");
        rv = prefs->GetBranch(branchName.get(), getter_AddRefs(mDefPrefBranch));
        if (NS_FAILED(rv))
            return rv;
    }

    return NS_OK;
}

NS_IMETHODIMP
nsSmtpServer::GetHostname(nsACString &aHostname)
{
  nsCString result;
  nsresult rv = mPrefBranch->GetCharPref("hostname", getter_Copies(result));
  if (NS_FAILED(rv))
    aHostname.Truncate();
  else
    aHostname = result;

  return NS_OK;
}

NS_IMETHODIMP
nsSmtpServer::SetHostname(const nsACString &aHostname)
{
  if (!aHostname.IsEmpty())
    return mPrefBranch->SetCharPref("hostname", PromiseFlatCString(aHostname).get());

  // If the pref value is already empty, ClearUserPref will return
  // NS_ERROR_UNEXPECTED, so don't check the rv here.
  mPrefBranch->ClearUserPref("hostname");
  return NS_OK;
}

NS_IMETHODIMP
nsSmtpServer::GetDescription(nsACString &aDescription)
{
    nsCString temp;
    mPrefBranch->GetCharPref("description", getter_Copies(temp));
    aDescription.Assign(temp);
    return NS_OK;
}

NS_IMETHODIMP
nsSmtpServer::SetDescription(const nsACString &aDescription)
{
    if (!aDescription.IsEmpty())
        return mPrefBranch->SetCharPref("description", PromiseFlatCString(aDescription).get());
    else
        mPrefBranch->ClearUserPref("description");
    return NS_OK;
}

// if GetPort returns 0, it means default port
NS_IMETHODIMP
nsSmtpServer::GetPort(int32_t *aPort)
{
  NS_ENSURE_ARG_POINTER(aPort);
  if (NS_FAILED(mPrefBranch->GetIntPref("port", aPort)))
    *aPort = 0;
  return NS_OK;
}

NS_IMETHODIMP
nsSmtpServer::SetPort(int32_t aPort)
{
  if (aPort)
    return mPrefBranch->SetIntPref("port", aPort);

  mPrefBranch->ClearUserPref("port");
  return NS_OK;
}

NS_IMETHODIMP
nsSmtpServer::GetDisplayname(char * *aDisplayname)
{
    nsresult rv;
    NS_ENSURE_ARG_POINTER(aDisplayname);

    nsCString hostname;
    rv = mPrefBranch->GetCharPref("hostname", getter_Copies(hostname));
    if (NS_FAILED(rv)) {
        *aDisplayname=nullptr;
        return NS_OK;
    }
    int32_t port;
    rv = mPrefBranch->GetIntPref("port", &port);
    if (NS_FAILED(rv))
        port = 0;

    if (port) {
        hostname.Append(':');
        hostname.AppendInt(port);
    }

    *aDisplayname = ToNewCString(hostname);
    return NS_OK;
}

NS_IMETHODIMP
nsSmtpServer::GetSocketType(int32_t *socketType)
{
  NS_ENSURE_ARG_POINTER(socketType);
  getIntPrefWithDefault("try_ssl", socketType, 0);
  return NS_OK;
}

NS_IMETHODIMP
nsSmtpServer::SetSocketType(int32_t socketType)
{
    return mPrefBranch->SetIntPref("try_ssl", socketType);
}

NS_IMETHODIMP
nsSmtpServer::GetHelloArgument(char * *aHelloArgument)
{
    nsresult rv;
    NS_ENSURE_ARG_POINTER(aHelloArgument);
    rv = mPrefBranch->GetCharPref("hello_argument", aHelloArgument);
    if (NS_FAILED(rv))
    {
        rv = mDefPrefBranch->GetCharPref("hello_argument", aHelloArgument);
        if (NS_FAILED(rv))
            *aHelloArgument = nullptr;
    }
    return NS_OK;
}

NS_IMETHODIMP
nsSmtpServer::GetAuthMethod(int32_t *authMethod)
{
  NS_ENSURE_ARG_POINTER(authMethod);
  getIntPrefWithDefault("authMethod", authMethod, 3);
  return NS_OK;
}

void
nsSmtpServer::getIntPrefWithDefault(const char *prefName,
                                    int32_t *val,
                                    int32_t defVal)
{
  nsresult rv = mPrefBranch->GetIntPref(prefName, val);
  if (NS_SUCCEEDED(rv))
    return;

  rv = mDefPrefBranch->GetIntPref(prefName, val);
  if (NS_FAILED(rv))
    // last resort
    *val = defVal;
}

NS_IMETHODIMP
nsSmtpServer::SetAuthMethod(int32_t authMethod)
{
    return mPrefBranch->SetIntPref("authMethod", authMethod);
}

NS_IMETHODIMP
nsSmtpServer::GetUsername(nsACString &aUsername)
{
  nsCString result;
  nsresult rv = mPrefBranch->GetCharPref("username", getter_Copies(result));
  if (NS_FAILED(rv))
    aUsername.Truncate();
  else
    aUsername = result;
  return NS_OK;
}

NS_IMETHODIMP
nsSmtpServer::SetUsername(const nsACString &aUsername)
{
  if (!aUsername.IsEmpty())
    return mPrefBranch->SetCharPref("username", PromiseFlatCString(aUsername).get());

  // If the pref value is already empty, ClearUserPref will return
  // NS_ERROR_UNEXPECTED, so don't check the rv here.
  mPrefBranch->ClearUserPref("username");
  return NS_OK;
}

NS_IMETHODIMP
nsSmtpServer::GetPassword(nsACString& aPassword)
{
    if (m_password.IsEmpty() && !m_logonFailed)
    {
      // try to avoid prompting the user for another password. If the user has set
      // the appropriate pref, we'll use the password from an incoming server, if
      // the user has already logged onto that server.

      // if this is set, we'll only use this, and not the other prefs
      // user_pref("mail.smtpserver.smtp1.incomingAccount", "server1");

      // if this is set, we'll accept an exact match of user name and server
      // user_pref("mail.smtp.useMatchingHostNameServer", true);

      // if this is set, and we don't find an exact match of user and host name,
      // we'll accept a match of username and domain, where domain
      // is everything after the first '.'
      // user_pref("mail.smtp.useMatchingDomainServer", true);

      nsCString accountKey;
      bool useMatchingHostNameServer = false;
      bool useMatchingDomainServer = false;
      mPrefBranch->GetCharPref("incomingAccount", getter_Copies(accountKey));

      nsCOMPtr<nsIMsgAccountManager> accountManager = do_GetService(NS_MSGACCOUNTMANAGER_CONTRACTID);
      nsCOMPtr<nsIMsgIncomingServer> incomingServerToUse;
      if (accountManager)
      {
        if (!accountKey.IsEmpty())
          accountManager->GetIncomingServer(accountKey, getter_AddRefs(incomingServerToUse));
        else
        {
          nsresult rv;
          nsCOMPtr<nsIPrefBranch> prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
          NS_ENSURE_SUCCESS(rv,rv);
          prefBranch->GetBoolPref("mail.smtp.useMatchingHostNameServer", &useMatchingHostNameServer);
          prefBranch->GetBoolPref("mail.smtp.useMatchingDomainServer", &useMatchingDomainServer);
          if (useMatchingHostNameServer || useMatchingDomainServer)
          {
            nsCString userName;
            nsCString hostName;
            GetHostname(hostName);
            GetUsername(userName);
            if (useMatchingHostNameServer)
              // pass in empty type and port=0, to match imap and pop3.
              accountManager->FindRealServer(userName, hostName, EmptyCString(), 0, getter_AddRefs(incomingServerToUse));
            int32_t dotPos = -1;
            if (!incomingServerToUse && useMatchingDomainServer
              && (dotPos = hostName.FindChar('.')) != kNotFound)
            {
              hostName.Cut(0, dotPos);
              nsCOMPtr<nsIArray> allServers;
              accountManager->GetAllServers(getter_AddRefs(allServers));
              if (allServers)
              {
                uint32_t count = 0;
                allServers->GetLength(&count);
                uint32_t i;
                for (i = 0; i < count; i++)
                {
                  nsCOMPtr<nsIMsgIncomingServer> server = do_QueryElementAt(allServers, i);
                  if (server)
                  {
                    nsCString serverUserName;
                    nsCString serverHostName;
                    server->GetRealUsername(serverUserName);
                    server->GetRealHostName(serverHostName);
                    if (serverUserName.Equals(userName))
                    {
                      int32_t serverDotPos = serverHostName.FindChar('.');
                      if (serverDotPos != kNotFound)
                      {
                        serverHostName.Cut(0, serverDotPos);
                        if (serverHostName.Equals(hostName))
                        {
                          incomingServerToUse = server;
                          break;
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
      if (incomingServerToUse)
        return incomingServerToUse->GetPassword(aPassword);
    }
    aPassword = m_password;
    return NS_OK;
}

NS_IMETHODIMP
nsSmtpServer::VerifyLogon(nsIUrlListener *aUrlListener, nsIMsgWindow *aMsgWindow,
                          nsIURI **aURL)
{
  nsresult rv;
  nsCOMPtr<nsISmtpService> smtpService(do_GetService(NS_SMTPSERVICE_CONTRACTID, &rv));
  NS_ENSURE_SUCCESS(rv, rv);
  return smtpService->VerifyLogon(this, aUrlListener, aMsgWindow, aURL);
}


NS_IMETHODIMP
nsSmtpServer::SetPassword(const nsACString& aPassword)
{
  m_password = aPassword;
  return NS_OK;
}

nsresult
nsSmtpServer::GetPasswordWithoutUI()
{
  nsresult rv;
  nsCOMPtr<nsILoginManager> loginMgr(do_GetService(NS_LOGINMANAGER_CONTRACTID,
                                                   &rv));
  NS_ENSURE_SUCCESS(rv, rv);

  NS_ConvertASCIItoUTF16 serverUri(GetServerURIInternal(false));

  uint32_t numLogins = 0;
  nsILoginInfo** logins = nullptr;
  rv = loginMgr->FindLogins(&numLogins, serverUri, EmptyString(),
                            serverUri, &logins);
  // Login manager can produce valid fails, e.g. NS_ERROR_ABORT when a user
  // cancels the master password dialog. Therefore handle that here, but don't
  // warn about it.
  if (NS_FAILED(rv))
    return rv;

  // Don't abort here, if we didn't find any or failed, then we'll just have
  // to prompt.
  if (numLogins > 0)
  {
    nsCString serverCUsername;
    rv = GetUsername(serverCUsername);
    NS_ConvertASCIItoUTF16 serverUsername(serverCUsername);

    nsString username;
    for (uint32_t i = 0; i < numLogins; ++i)
    {
      rv = logins[i]->GetUsername(username);
      NS_ENSURE_SUCCESS(rv, rv);

      if (username.Equals(serverUsername))
      {
        nsString password;
        rv = logins[i]->GetPassword(password);
        NS_ENSURE_SUCCESS(rv, rv);

        LossyCopyUTF16toASCII(password, m_password);
        break;
      }
    }
  }
  NS_FREE_XPCOM_ISUPPORTS_POINTER_ARRAY(numLogins, logins);
  return NS_OK;
}

NS_IMETHODIMP
nsSmtpServer::GetPasswordWithUI(const char16_t *aPromptMessage,
                                const char16_t *aPromptTitle,
                                nsIAuthPrompt* aDialog,
                                nsACString &aPassword)
{
  if (!m_password.IsEmpty())
    return GetPassword(aPassword);

  // We need to get a password, but see if we can get it from the password
  // manager without requiring a prompt.
  nsresult rv = GetPasswordWithoutUI();
  if (rv == NS_ERROR_ABORT)
    return NS_MSG_PASSWORD_PROMPT_CANCELLED;

  // Now re-check if we've got a password or not, if we have, then we
  // don't need to prompt the user.
  if (!m_password.IsEmpty())
  {
    aPassword = m_password;
    return NS_OK;
  }

  NS_ENSURE_ARG_POINTER(aDialog);

  // PromptPassword needs the username as well.
  nsCString serverUri(GetServerURIInternal(true));

  bool okayValue = true;
  nsString uniPassword;

  rv = aDialog->PromptPassword(aPromptTitle, aPromptMessage,
                               NS_ConvertASCIItoUTF16(serverUri).get(),
                               nsIAuthPrompt::SAVE_PASSWORD_PERMANENTLY,
                               getter_Copies(uniPassword), &okayValue);
  NS_ENSURE_SUCCESS(rv, rv);

  // If the user pressed cancel, just return an empty string.
  if (!okayValue)
  {
    aPassword.Truncate();
    return NS_MSG_PASSWORD_PROMPT_CANCELLED;
  }

  NS_LossyConvertUTF16toASCII password(uniPassword);

  rv = SetPassword(password);
  NS_ENSURE_SUCCESS(rv, rv);

  aPassword = password;
  return NS_OK;
}

NS_IMETHODIMP
nsSmtpServer::GetUsernamePasswordWithUI(const char16_t * aPromptMessage, const
                                char16_t *aPromptTitle,
                                nsIAuthPrompt* aDialog,
                                nsACString &aUsername,
                                nsACString &aPassword)
{
  nsresult rv;
  if (!m_password.IsEmpty())
  {
    rv = GetUsername(aUsername);
    NS_ENSURE_SUCCESS(rv, rv);

    return GetPassword(aPassword);
  }

  NS_ENSURE_ARG_POINTER(aDialog);

  nsCString serverUri;
  rv = GetServerURI(serverUri);
  NS_ENSURE_SUCCESS(rv, rv);

  nsString uniUsername;
  nsString uniPassword;
  bool okayValue = true;

  rv = aDialog->PromptUsernameAndPassword(aPromptTitle, aPromptMessage,
                                          NS_ConvertASCIItoUTF16(serverUri).get(),
                                          nsIAuthPrompt::SAVE_PASSWORD_PERMANENTLY,
                                          getter_Copies(uniUsername),
                                          getter_Copies(uniPassword),
                                          &okayValue);
  NS_ENSURE_SUCCESS(rv, rv);

  // If the user pressed cancel, just return emtpy strings.
  if (!okayValue)
  {
    aUsername.Truncate();
    aPassword.Truncate();
    return rv;
  }

  // We got a username and password back...so remember them.
  NS_LossyConvertUTF16toASCII username(uniUsername);

  rv = SetUsername(username);
  NS_ENSURE_SUCCESS(rv, rv);

  NS_LossyConvertUTF16toASCII password(uniPassword);

  rv = SetPassword(password);
  NS_ENSURE_SUCCESS(rv, rv);

  aUsername = username;
  aPassword = password;
  return NS_OK;
}

NS_IMETHODIMP
nsSmtpServer::ForgetPassword()
{
  nsresult rv;
  nsCOMPtr<nsILoginManager> loginMgr =
    do_GetService(NS_LOGINMANAGER_CONTRACTID, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  // Get the current server URI without the username
  nsAutoCString serverUri(NS_LITERAL_CSTRING("smtp://"));

  nsCString hostname;
  rv = GetHostname(hostname);

  if (NS_SUCCEEDED(rv) && !hostname.IsEmpty()) {
    nsCString escapedHostname;
    MsgEscapeString(hostname, nsINetUtil::ESCAPE_URL_PATH, escapedHostname);
    // not all servers have a hostname
    serverUri.Append(escapedHostname);
  }

  uint32_t count;
  nsILoginInfo** logins;

  NS_ConvertUTF8toUTF16 currServer(serverUri);

  nsCString serverCUsername;
  rv = GetUsername(serverCUsername);
  NS_ENSURE_SUCCESS(rv, rv);

  NS_ConvertUTF8toUTF16 serverUsername(serverCUsername);

  rv = loginMgr->FindLogins(&count, currServer, EmptyString(),
                            currServer, &logins);
  NS_ENSURE_SUCCESS(rv, rv);

  // There should only be one-login stored for this url, however just in case
  // there isn't.
  nsString username;
  for (uint32_t i = 0; i < count; ++i)
  {
    if (NS_SUCCEEDED(logins[i]->GetUsername(username)) &&
        username.Equals(serverUsername))
    {
      // If this fails, just continue, we'll still want to remove the password
      // from our local cache.
      loginMgr->RemoveLogin(logins[i]);
    }
  }
  NS_FREE_XPCOM_ISUPPORTS_POINTER_ARRAY(count, logins);

  rv = SetPassword(EmptyCString());
  m_logonFailed = true;
  return rv;
}

NS_IMETHODIMP
nsSmtpServer::GetServerURI(nsACString &aResult)
{
  aResult = GetServerURIInternal(true);
  return NS_OK;
}

nsCString
nsSmtpServer::GetServerURIInternal(const bool aIncludeUsername)
{
  nsCString uri(NS_LITERAL_CSTRING("smtp://"));
  nsresult rv;

  if (aIncludeUsername)
  {
    nsCString username;
    rv = GetUsername(username);

    if (NS_SUCCEEDED(rv) && !username.IsEmpty()) {
      nsCString escapedUsername;
      MsgEscapeString(username, nsINetUtil::ESCAPE_XALPHAS, escapedUsername);
      // not all servers have a username
      uri.Append(escapedUsername);
      uri.AppendLiteral("@");
    }
  }

  nsCString hostname;
  rv = GetHostname(hostname);

  if (NS_SUCCEEDED(rv) && !hostname.IsEmpty()) {
    nsCString escapedHostname;
    MsgEscapeString(hostname, nsINetUtil::ESCAPE_URL_PATH, escapedHostname);
    // not all servers have a hostname
    uri.Append(escapedHostname);
  }

  return uri;
}

NS_IMETHODIMP
nsSmtpServer::ClearAllValues()
{
  return mPrefBranch->DeleteBranch("");
}
