/*
 *  XMail by Davide Libenzi ( Intranet and Internet mail server )
 *  Copyright (C) 1999,..,2004  Davide Libenzi
 *
 *  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 2 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 *  Davide Libenzi <davidel@xmailserver.org>
 *
 */

#include "SysInclude.h"
#include "SysDep.h"
#include "SvrDefines.h"
#include "ShBlocks.h"
#include "SList.h"
#include "BuffSock.h"
#include "ResLocks.h"
#include "MiscUtils.h"
#include "MailConfig.h"
#include "SvrUtils.h"
#include "UsrUtils.h"
#include "POP3Svr.h"
#include "POP3Utils.h"
#include "MessQueue.h"
#include "AppDefines.h"
#include "MailSvr.h"
#include "POP3GwLink.h"
#include "PSYNCSvr.h"

#define PSYNC_LOG_FILE              "psync"
#define PSYNC_TRIGGER_FILE          ".psync-trigger"
#define PSYNC_WAIT_SLEEP            2
#define MAX_CLIENTS_WAIT            300
#define PSYNC_WAKEUP_TIME           2
#define PSYNC_SERVER_NAME           "[" APP_NAME_VERSION_STR " PSYNC Server]"

struct PSYNCThreadData {
	PSYNCConfig *pPSYNCCfg;
	POP3Link *pPopLnk;
};

static bool PSYNCNeedSync(void);
static PSYNCConfig *PSYNCGetConfigCopy(SHB_HANDLE hShbPSYNC);
static int PSYNCThreadCountAdd(long lCount, SHB_HANDLE hShbPSYNC, PSYNCConfig * pPSYNCCfg = NULL);
static int PSYNCTimeToStop(SHB_HANDLE hShbPSYNC);
static SYS_THREAD PSYNCCreateSyncThread(SHB_HANDLE hShbPSYNC, POP3Link * pPopLnk);
static int PSYNCStartTransfer(SHB_HANDLE hShbPSYNC, PSYNCConfig * pPSYNCCfg);
unsigned int PSYNCThreadSyncProc(void *pThreadData);
static int PSYNCThreadNotifyExit(void);
static int PSYNCLogEnabled(PSYNCConfig * pPSYNCCfg);
static int PSYNCLogSession(POP3Link const *pPopLnk, PopSyncReport const *pSRep,
			   char const *pszStatus);

static bool PSYNCNeedSync(void)
{

	char szTriggerPath[SYS_MAX_PATH] = "";

	CfgGetRootPath(szTriggerPath, sizeof(szTriggerPath));

	StrNCat(szTriggerPath, PSYNC_TRIGGER_FILE, sizeof(szTriggerPath));

///////////////////////////////////////////////////////////////////////////////
//  Check for the presence of the trigger file
///////////////////////////////////////////////////////////////////////////////
	if (!SysExistFile(szTriggerPath))
		return (false);

	SysRemove(szTriggerPath);

	return (true);

}

static PSYNCConfig *PSYNCGetConfigCopy(SHB_HANDLE hShbPSYNC)
{

	PSYNCConfig *pPSYNCCfg = (PSYNCConfig *) ShbLock(hShbPSYNC);

	if (pPSYNCCfg == NULL)
		return (NULL);

	PSYNCConfig *pNewPSYNCCfg = (PSYNCConfig *) SysAlloc(sizeof(PSYNCConfig));

	if (pNewPSYNCCfg != NULL)
		memcpy(pNewPSYNCCfg, pPSYNCCfg, sizeof(PSYNCConfig));

	ShbUnlock(hShbPSYNC);

	return (pNewPSYNCCfg);

}

static int PSYNCThreadCountAdd(long lCount, SHB_HANDLE hShbPSYNC, PSYNCConfig * pPSYNCCfg)
{

	int iDoUnlock = 0;

	if (pPSYNCCfg == NULL) {
		if ((pPSYNCCfg = (PSYNCConfig *) ShbLock(hShbPSYNC)) == NULL)
			return (ErrGetErrorCode());

		++iDoUnlock;
	}

	pPSYNCCfg->lThreadCount += lCount;

	if (iDoUnlock)
		ShbUnlock(hShbPSYNC);

	return (0);

}

static int PSYNCTimeToStop(SHB_HANDLE hShbPSYNC)
{

	PSYNCConfig *pPSYNCCfg = (PSYNCConfig *) ShbLock(hShbPSYNC);

	if (pPSYNCCfg == NULL)
		return (1);

	int iTimeToStop = (pPSYNCCfg->ulFlags & PSYNCF_STOP_SERVER) ? 1 : 0;

	ShbUnlock(hShbPSYNC);

	return (iTimeToStop);

}

unsigned int PSYNCThreadProc(void *pThreadData)
{

	SysLogMessage(LOG_LEV_MESSAGE, "%s started\n", PSYNC_SERVER_NAME);

	int iElapsedTime = 0;

	for (;;) {
		SysSleep(PSYNC_WAKEUP_TIME);

		iElapsedTime += PSYNC_WAKEUP_TIME;

		PSYNCConfig *pPSYNCCfg = PSYNCGetConfigCopy(hShbPSYNC);

		if (pPSYNCCfg == NULL)
			break;

		if (pPSYNCCfg->ulFlags & PSYNCF_STOP_SERVER) {
			SysFree(pPSYNCCfg);
			break;
		}

		if (((pPSYNCCfg->iSyncInterval == 0) || (iElapsedTime < pPSYNCCfg->iSyncInterval))
		    && !PSYNCNeedSync()) {
			SysFree(pPSYNCCfg);
			continue;
		}

		iElapsedTime = 0;

		PSYNCStartTransfer(hShbPSYNC, pPSYNCCfg);

		SysFree(pPSYNCCfg);
	}

///////////////////////////////////////////////////////////////////////////////
//  Wait for client completion
///////////////////////////////////////////////////////////////////////////////
	for (int iTotalWait = 0; (iTotalWait < MAX_CLIENTS_WAIT); iTotalWait += PSYNC_WAIT_SLEEP) {
		PSYNCConfig *pPSYNCCfg = (PSYNCConfig *) ShbLock(hShbPSYNC);

		if (pPSYNCCfg == NULL)
			break;

		long lThreadCount = pPSYNCCfg->lThreadCount;

		ShbUnlock(hShbPSYNC);

		if (lThreadCount == 0)
			break;

		SysSleep(PSYNC_WAIT_SLEEP);
	}

	SysLogMessage(LOG_LEV_MESSAGE, "%s stopped\n", PSYNC_SERVER_NAME);

	return (0);

}

static SYS_THREAD PSYNCCreateSyncThread(SHB_HANDLE hShbPSYNC, POP3Link * pPopLnk)
{

	PSYNCThreadData *pSTD = (PSYNCThreadData *) SysAlloc(sizeof(PSYNCThreadData));

	if (pSTD == NULL)
		return (SYS_INVALID_THREAD);

	if ((pSTD->pPSYNCCfg = PSYNCGetConfigCopy(hShbPSYNC)) == NULL) {
		SysFree(pSTD);
		return (SYS_INVALID_THREAD);
	}

	pSTD->pPopLnk = pPopLnk;

	SYS_THREAD hThread = SysCreateThread(PSYNCThreadSyncProc, pSTD);

	if (hThread == SYS_INVALID_THREAD) {
		SysFree(pSTD->pPSYNCCfg);
		SysFree(pSTD);
		return (SYS_INVALID_THREAD);
	}

	return (hThread);

}

static int PSYNCStartTransfer(SHB_HANDLE hShbPSYNC, PSYNCConfig * pPSYNCCfg)
{

	GWLKF_HANDLE hLinksDB = GwLkOpenDB();

	if (hLinksDB == INVALID_GWLKF_HANDLE) {
		ErrorPush();
		SysLogMessage(LOG_LEV_ERROR, "%s\n", ErrGetErrorString(ErrorFetch()));
		return (ErrorPop());
	}

	POP3Link *pPopLnk = GwLkGetFirstUser(hLinksDB);

	for (; pPopLnk != NULL; pPopLnk = GwLkGetNextUser(hLinksDB)) {
///////////////////////////////////////////////////////////////////////////////
//  Check if link is enabled
///////////////////////////////////////////////////////////////////////////////
		if (GwLkCheckEnabled(pPopLnk) < 0) {
			GwLkFreePOP3Link(pPopLnk);
			continue;
		}

		if ((SysWaitSemaphore(hSyncSem, SYS_INFINITE_TIMEOUT) < 0) ||
		    PSYNCTimeToStop(hShbPSYNC)) {
			GwLkFreePOP3Link(pPopLnk);
			break;
		}

		SYS_THREAD hClientThread = PSYNCCreateSyncThread(hShbPSYNC, pPopLnk);

		if (hClientThread == SYS_INVALID_THREAD) {
			ErrorPush();
			GwLkFreePOP3Link(pPopLnk);
			SysReleaseSemaphore(hSyncSem, 1);
			GwLkCloseDB(hLinksDB);
			return (ErrorPop());
		}

		SysCloseThread(hClientThread, 0);
	}

	GwLkCloseDB(hLinksDB);

	return (0);

}

static int PSYNCThreadNotifyExit(void)
{

	SysReleaseSemaphore(hSyncSem, 1);

	return (0);

}

unsigned int PSYNCThreadSyncProc(void *pThreadData)
{

	PSYNCThreadData *pSTD = (PSYNCThreadData *) pThreadData;
	POP3Link *pPopLnk = pSTD->pPopLnk;
	PSYNCConfig *pPSYNCCfg = pSTD->pPSYNCCfg;

	SysFree(pSTD);

	SysLogMessage(LOG_LEV_MESSAGE, "[PSYNC] entry\n");

///////////////////////////////////////////////////////////////////////////////
//  Get configuration handle
///////////////////////////////////////////////////////////////////////////////
	SVRCFG_HANDLE hSvrConfig = SvrGetConfigHandle();

	if (hSvrConfig == INVALID_SVRCFG_HANDLE) {
		ErrorPush();
		SysLogMessage(LOG_LEV_MESSAGE, "%s\n", ErrGetErrorString(ErrorFetch()));

		GwLkFreePOP3Link(pPopLnk);
		SysFree(pPSYNCCfg);
///////////////////////////////////////////////////////////////////////////////
//  Notify thread exit semaphore
///////////////////////////////////////////////////////////////////////////////
		PSYNCThreadNotifyExit();
		return (ErrorPop());
	}
///////////////////////////////////////////////////////////////////////////////
//  Get the error account for email that the server is not able to deliver coz
//  it does not find information about where it has to deliver
///////////////////////////////////////////////////////////////////////////////
	char szErrorAccount[MAX_ADDR_NAME] = "";

	SvrConfigVar("Pop3SyncErrorAccount", szErrorAccount, sizeof(szErrorAccount) - 1,
		     hSvrConfig, "");

	char const *pszErrorAccount = (IsEmptyString(szErrorAccount)) ? NULL : szErrorAccount;

///////////////////////////////////////////////////////////////////////////////
//  Get headers tags that must be checked to extract recipients
///////////////////////////////////////////////////////////////////////////////
	char szFetchHdrTags[256] = "";

	SvrConfigVar("FetchHdrTags", szFetchHdrTags, sizeof(szFetchHdrTags) - 1,
		     hSvrConfig, "+X-Deliver-To,+Received,To,Cc");

///////////////////////////////////////////////////////////////////////////////
//  Lock the link
///////////////////////////////////////////////////////////////////////////////
	if (GwLkLinkLock(pPopLnk) < 0) {
		ErrorPush();
		SysLogMessage(LOG_LEV_MESSAGE, "%s\n", ErrGetErrorString(ErrorFetch()));

		SvrReleaseConfigHandle(hSvrConfig);
		GwLkFreePOP3Link(pPopLnk);
		SysFree(pPSYNCCfg);
///////////////////////////////////////////////////////////////////////////////
//  Notify thread exit semaphore
///////////////////////////////////////////////////////////////////////////////
		PSYNCThreadNotifyExit();
		return (ErrorPop());
	}
///////////////////////////////////////////////////////////////////////////////
//  Increase threads count
///////////////////////////////////////////////////////////////////////////////
	PSYNCThreadCountAdd(+1, hShbPSYNC);

///////////////////////////////////////////////////////////////////////////////
//  Sync for real internal account ?
///////////////////////////////////////////////////////////////////////////////
	PopSyncReport SRep;

	if (GwLkLocalDomain(pPopLnk)) {
///////////////////////////////////////////////////////////////////////////////
//  Verify user credentials
///////////////////////////////////////////////////////////////////////////////
		UserInfo *pUI = UsrGetUserByName(pPopLnk->pszDomain, pPopLnk->pszName);

		if (pUI != NULL) {
			SysLogMessage(LOG_LEV_MESSAGE,
				      "[PSYNC] User = \"%s\" - Domain = \"%s\"\n",
				      pPopLnk->pszName, pPopLnk->pszDomain);

///////////////////////////////////////////////////////////////////////////////
//  Sync
///////////////////////////////////////////////////////////////////////////////
			char szUserAddress[MAX_ADDR_NAME] = "";

			UsrGetAddress(pUI, szUserAddress);

			if (UPopSyncRemoteLink
			    (szUserAddress, pPopLnk->pszRmtDomain, pPopLnk->pszRmtName,
			     pPopLnk->pszRmtPassword, &SRep, szFetchHdrTags, pPopLnk->pszAuthType,
			     pszErrorAccount) < 0) {
				ErrLogMessage(LOG_LEV_MESSAGE,
					      "[PSYNC] User = \"%s\" - Domain = \"%s\" Failed !\n",
					      pPopLnk->pszName, pPopLnk->pszDomain);

				ZeroData(SRep);
				if (PSYNCLogEnabled(pPSYNCCfg))
					PSYNCLogSession(pPopLnk, &SRep, "SYNC=EFAIL");
			} else {
				if (PSYNCLogEnabled(pPSYNCCfg))
					PSYNCLogSession(pPopLnk, &SRep, "SYNC=OK");
			}

			UsrFreeUserInfo(pUI);
		} else {
			SysLogMessage(LOG_LEV_MESSAGE,
				      "[PSYNC] User = \"%s\" - Domain = \"%s\" Failed !\n"
				      "Error = %s\n", pPopLnk->pszName, pPopLnk->pszDomain,
				      ErrGetErrorString());

			ZeroData(SRep);
			if (PSYNCLogEnabled(pPSYNCCfg))
				PSYNCLogSession(pPopLnk, &SRep, "SYNC=ENOUSER");
		}
	} else if (GwLkMasqueradeDomain(pPopLnk)) {
		SysLogMessage(LOG_LEV_MESSAGE,
			      "[PSYNC/MASQ] MasqDomain = \"%s\" - RmtDomain = \"%s\" - RmtName = \"%s\"\n",
			      pPopLnk->pszDomain + 1, pPopLnk->pszRmtDomain, pPopLnk->pszRmtName);

///////////////////////////////////////////////////////////////////////////////
//  Sync ( "pszDomain" == "?" + masq-domain or "pszDomain" == "&" + add-domain )
///////////////////////////////////////////////////////////////////////////////
		if (UPopSyncRemoteLink
		    (pPopLnk->pszDomain, pPopLnk->pszRmtDomain, pPopLnk->pszRmtName,
		     pPopLnk->pszRmtPassword, &SRep, szFetchHdrTags, pPopLnk->pszAuthType,
		     pszErrorAccount) < 0) {
			ErrLogMessage(LOG_LEV_MESSAGE,
				      "[PSYNC/MASQ] MasqDomain = \"%s\" - RmtDomain = \"%s\" - RmtName = \"%s\" Failed !\n",
				      pPopLnk->pszDomain + 1, pPopLnk->pszRmtDomain,
				      pPopLnk->pszRmtName);

			ZeroData(SRep);
			if (PSYNCLogEnabled(pPSYNCCfg))
				PSYNCLogSession(pPopLnk, &SRep, "SYNC=EFAIL");
		} else {
			if (PSYNCLogEnabled(pPSYNCCfg))
				PSYNCLogSession(pPopLnk, &SRep, "SYNC=OK");
		}
	} else {
		char szSyncAddress[MAX_ADDR_NAME] = "";

		SysSNPrintf(szSyncAddress, sizeof(szSyncAddress) - 1, "%s%s", pPopLnk->pszName,
			    pPopLnk->pszDomain);

		SysLogMessage(LOG_LEV_MESSAGE,
			      "[PSYNC/EXT] Acount = \"%s\" - RmtDomain = \"%s\" - RmtName = \"%s\"\n",
			      szSyncAddress, pPopLnk->pszRmtDomain, pPopLnk->pszRmtName);

///////////////////////////////////////////////////////////////////////////////
//  Sync ( "pszDomain" == "@" + domain )
///////////////////////////////////////////////////////////////////////////////
		if (UPopSyncRemoteLink(szSyncAddress, pPopLnk->pszRmtDomain, pPopLnk->pszRmtName,
				       pPopLnk->pszRmtPassword, &SRep, szFetchHdrTags,
				       pPopLnk->pszAuthType, pszErrorAccount) < 0) {
			ErrLogMessage(LOG_LEV_MESSAGE,
				      "[PSYNC/EXT] Acount = \"%s\" - RmtDomain = \"%s\" - RmtName = \"%s\" Failed !\n",
				      szSyncAddress, pPopLnk->pszRmtDomain, pPopLnk->pszRmtName);

			ZeroData(SRep);
			if (PSYNCLogEnabled(pPSYNCCfg))
				PSYNCLogSession(pPopLnk, &SRep, "SYNC=EFAIL");
		} else {
			if (PSYNCLogEnabled(pPSYNCCfg))
				PSYNCLogSession(pPopLnk, &SRep, "SYNC=OK");
		}
	}

///////////////////////////////////////////////////////////////////////////////
//  Decrease threads count
///////////////////////////////////////////////////////////////////////////////
	PSYNCThreadCountAdd(-1, hShbPSYNC);

	GwLkLinkUnlock(pPopLnk);
	SvrReleaseConfigHandle(hSvrConfig);
	GwLkFreePOP3Link(pPopLnk);
	SysFree(pPSYNCCfg);

///////////////////////////////////////////////////////////////////////////////
//  Notify thread exit semaphore
///////////////////////////////////////////////////////////////////////////////
	PSYNCThreadNotifyExit();

	SysLogMessage(LOG_LEV_MESSAGE, "[PSYNC] exit\n");

	return (0);

}

static int PSYNCLogEnabled(PSYNCConfig * pPSYNCCfg)
{

	return ((pPSYNCCfg->ulFlags & PSYNCF_LOG_ENABLED) ? 1 : 0);

}

static int PSYNCLogSession(POP3Link const *pPopLnk, PopSyncReport const *pSRep,
			   char const *pszStatus)
{

	char szTime[256] = "";

	MscGetTimeNbrString(szTime, sizeof(szTime) - 1);

	RLCK_HANDLE hResLock = RLckLockEX(SVR_LOGS_DIR SYS_SLASH_STR PSYNC_LOG_FILE);

	if (hResLock == INVALID_RLCK_HANDLE)
		return (ErrGetErrorCode());

	MscFileLog(PSYNC_LOG_FILE,
		   "\"%s\""
		   "\t\"%s\""
		   "\t\"%s\""
		   "\t\"%s\""
		   "\t\"%s\""
		   "\t\"%s\""
		   "\t\"%s\""
		   "\t\"%d\""
		   "\t\"%lu\""
		   "\t\"%d\""
		   "\t\"%lu\""
		   "\n", szTime, pPopLnk->pszDomain, pPopLnk->pszName,
		   pPopLnk->pszRmtDomain, pPopLnk->pszRmtName, pPopLnk->pszAuthType, pszStatus,
		   pSRep->iMsgSync, pSRep->ulSizeSync, pSRep->iMsgErr, pSRep->ulSizeErr);

	RLckUnlockEX(hResLock);

	return (0);

}
