/* milter-callback - a sendmail milter to perform variuos sender verifications
 Copyright (C) 2008  Eugene M. Zheganin, milter-callback@norma.perm.ru
 
 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, see <http://www.gnu.org/licenses/>.
*/
	   
#include <milter-callback.h>
#include <commonprocs.h>

extern int logLevel;
extern int mcDebug;
extern struct mcCache *mcCacheStart;
extern time_t cacheTTL;

/* mutex signalling for nested calling */

#define CB_LOCKED	0	/* lock set */
#define CB_LOCKERR	1	/* lock set error */
#define CB_ALRLOCKED	2	/* lock setting skipped, set already */
#define CB_UNLOCKED	3	/* unlocked */
#define CB_UNLOCKERR	4	/* unlocking error */
#define CB_ALRUNLOCKED	5	/* unlock skipped, not locked */

int
initEmptyCache () {
/* inits cache, 0 - success, 1 - fail */
    int memSize;
    char tmpLog[MAXLOGLINE];
    
    logNow("entered initEmptyCache().", 3, 0);
    memSize = sizeof(struct mcCache);
    snprintf(tmpLog, MAXLOGLINE, "Trying to get %d bytes for the new cache element.", memSize);
    logNow(&tmpLog[0], 3, 0);
    mcCacheStart = malloc(memSize);
    if (mcCacheStart == NULL) {
        logNow("Cannot allocate memory.", 0, 0);
	return(1);
    } else {
	mcCacheStart -> nextNode = NULL;
	mcCacheStart -> prevNode = NULL;
	mcCacheStart -> expiresAt = 0;
	bzero(mcCacheStart -> relayPresIP, sizeof(mcCacheStart -> relayPresIP));
	mcCacheStart -> envFrom = NULL;
	mcCacheStart -> MXesThatReject = -1;
	mcCacheStart -> MXesThatAccept = -1;
	mcCacheStart -> MXesOutOfOrder = -1;
	mcCacheStart -> MXesThatTempfail = -1;
	mcCacheStart -> MXesNum = -1;
	mcCacheStart -> reverseRes = CB_UNKNOWN;
	mcCacheStart -> fakeRes = CB_UNKNOWN;
	mcCacheStart -> whitelisted = -1;
	mcCacheStart -> gracefulDNS = -1;
#ifdef WITH_SPF
	mcCacheStart -> SPFResult = -1;
#endif
	bzero(mcCacheStart -> resolvedRelayIP, sizeof(mcCacheStart -> resolvedRelayIP));
	return(0);
    }
};

struct mcCache *
purgeCacheElem (struct mcCache *nodeToPurge, int *endMarker) {
/* deletes an element from cache, 0 - success, 1 - fail */
    struct mcCache *resNode;
    char tmpLog[MAXLOGLINE];

    logNow("entered purgeCacheElem().", 3, 0);
    snprintf(tmpLog, MAXLOGLINE, "Next node address: %p, previous node address: %p.", nodeToPurge -> nextNode, nodeToPurge -> prevNode);
    logNow(&tmpLog[0], 3, 0);
    if (nodeToPurge -> nextNode == NULL && nodeToPurge -> prevNode == NULL) {
	/* we got only one element */
	/* clearing it out and douing nothing */
	snprintf(tmpLog, MAXLOGLINE, "Got only one element in cache, resetting it instead of relinking.");
	logNow(&tmpLog[0], 3, 0);
	mcCacheStart -> nextNode = NULL;
	mcCacheStart -> prevNode = NULL;
	mcCacheStart -> expiresAt = 0;
	mcCacheStart -> relayPresIP[0] = '\0';
	mcCacheStart -> envFrom = NULL;
	mcCacheStart -> MXesThatReject = -1;
	mcCacheStart -> MXesThatAccept = -1;
	mcCacheStart -> MXesOutOfOrder = -1;
	mcCacheStart -> MXesThatTempfail = -1;
	mcCacheStart -> MXesNum = -1;
	mcCacheStart -> reverseRes = CB_UNKNOWN;
	mcCacheStart -> fakeRes = CB_UNKNOWN;
	mcCacheStart -> whitelisted = -1;
	mcCacheStart -> gracefulDNS = -1;
	mcCacheStart -> resolvedRelayIP[0] = '\0';
	logNow("Leaving purgeCacheElem().", 3, 0);
	*endMarker = 1;
	return(mcCacheStart);
    } else {
	if (nodeToPurge -> nextNode != NULL && nodeToPurge -> prevNode != NULL) {
	    /* we got a standard element */
	    logNow("Got standard cache element, relinking hooks.", 3, 0);
	    resNode = nodeToPurge -> nextNode;
	    nodeToPurge -> nextNode -> prevNode = nodeToPurge -> prevNode;
	    nodeToPurge -> prevNode -> nextNode = nodeToPurge -> nextNode;
	} else {
	    /* okay, we're at the beginning or in the end */
	    if (nodeToPurge -> nextNode != NULL) {
		/* seems more like beginning */
		logNow("Seems like we are at the beginning, relinking hooks.", 3, 0);
		resNode = nodeToPurge -> nextNode;
		snprintf(tmpLog, MAXLOGLINE, "I will return the node with address %p.", resNode);
		logNow(&tmpLog[0], 3, 0);
		snprintf(tmpLog, MAXLOGLINE, "Setting prevNode address of the node with address %p to NULL.", nodeToPurge -> nextNode);
		logNow(&tmpLog[0], 3, 0);
		nodeToPurge -> nextNode -> prevNode = NULL;
		/* now we need to relink mcCacheStart */
		snprintf(tmpLog, MAXLOGLINE, "Relinking mcCacheStart to address %p.", resNode);
		logNow(&tmpLog[0], 3, 0);
		mcCacheStart = resNode;
	    } else {
		/*
		 either we're at the end, or we completely fucked up 
		 with the logic, the code, and entire MOTHERFUCKERING 
		 LIFE
		*/
		logNow("Seems like we are at the end of cache, relinking hooks.", 3, 0);
		resNode = nodeToPurge -> prevNode;
		nodeToPurge -> prevNode -> nextNode = NULL;
		*endMarker = 1;
	    }
        }
	/* new we're freeing cache entry */
	/* the most THERMONUCLEAR PART HERE */
	logNow("Trying to free() envFrom element of purged cache entry.", 3, 0);
	free(nodeToPurge -> envFrom);
	logNow("Trying to free() purged cache entry.", 3, 0);
	free(nodeToPurge);
	logNow("Leaving purgeCacheElem().", 3, 0);
	return(resNode);
    }
};


void
purgeCache () {
    int i = 0;
    int dels = 0;
    struct mcCache *cacheMark;
    int thisIsTheEnd = 0;
    char tmpLog[MAXLOGLINE];

    logNow("entered purgeCache().", 3, 0);
    logNow("Displaying contents of the cache:", 3, 0);
    cacheMark = mcCacheStart;
    while (cacheMark != NULL && thisIsTheEnd == 0) {
	snprintf(tmpLog, MAXLOGLINE, "%s(%s)/%s.", cacheMark -> relayPresIP, cacheMark -> resolvedRelayIP, cacheMark -> envFrom);
	logNow(&tmpLog[0], 3, 0);
	if (cacheMark -> expiresAt > 0 && cacheMark -> expiresAt < time(NULL)) {
	    snprintf(tmpLog, MAXLOGLINE, "This entry is expired %d seconds ago, trying to clean it out.", time(NULL) - cacheMark -> expiresAt);
	    logNow(&tmpLog[0], 3, 0);
	    /* this will increment the mark on success */
	    snprintf(tmpLog, MAXLOGLINE, "Calling purgeCacheElem() with an address %p as an argument.", cacheMark );
	    logNow(&tmpLog[0], 3, 0);
	    snprintf(tmpLog, MAXLOGLINE, "Previous node of the argument is at %p, next node - at %p.", cacheMark -> prevNode, cacheMark -> nextNode);
	    logNow(&tmpLog[0], 3, 0);
	    dels++;
	    cacheMark = purgeCacheElem(cacheMark, &thisIsTheEnd);
	} else {
	    snprintf(tmpLog, MAXLOGLINE, "This entry will expire in %d seconds, skipping it for now.", cacheMark -> expiresAt - time(NULL));
	    logNow(&tmpLog[0], 3, 0);
	    cacheMark = cacheMark -> nextNode;
	}
	i++;
    };
    snprintf(tmpLog, MAXLOGLINE, "Cache: %d entries, %d deleted.", i, dels);
    logNow(&tmpLog[0], 3, 0);
}

void
getCache () {
    int i = 0;
    struct mcCache *cacheMark;
    char tmpLog[MAXLOGLINE];

    logNow("entered getCache().", 3, 0);
    logNow("Displaying contents of the cache:", 3, 0);
    cacheMark = mcCacheStart;
    while (cacheMark != NULL) {
	snprintf(tmpLog, MAXLOGLINE, "%s(%s)/%s.", cacheMark -> relayPresIP, cacheMark -> resolvedRelayIP, cacheMark -> envFrom);
	logNow(&tmpLog[0], 3, 0);
	cacheMark = cacheMark -> nextNode;
	i++;
    };
    snprintf(tmpLog, MAXLOGLINE, "Cache: %d entries.", i);
    logNow(&tmpLog[0], 3, 0);
}

int
cacheIsEmpty (struct mlfiPriv *privData) {
/* returns number of elements */
    struct mcCache *cacheMark;
    int i;
    char tmpLog[MAXLOGLINE];
    
    logNow("entered cacheIsEmpty().", 3, 0);
    if (mcCacheStart -> nextNode == NULL && strlen(mcCacheStart -> relayPresIP) == 0) {
        logNow("Cache is empty.", 3, privData);
        return(0);
    } else {
        cacheMark = mcCacheStart;
        for (i = 0; cacheMark != NULL; cacheMark = cacheMark -> nextNode) {
            i++;
        };
    }
    snprintf(tmpLog, MAXLOGLINE, "Cache is %d elements long.", i);
    logNow(&tmpLog[0], 3, privData);
    return(i);
};

int
tryCacheLock (struct mlfiPriv *privData) {
    char tmpLog[MAXLOGLINE];
    int mtxErr;

    if (privData -> cacheLockOwner == 0) {
	snprintf(tmpLog, MAXLOGLINE, "Trying to lock the cache.");
	logNow(&tmpLog[0], 3, privData);
	mtxErr = pthread_mutex_lock(mcCacheBusy);
        if (mtxErr == 0) {
	    privData -> cacheLockOwner = 1;
    	    logNow("Lock aquired.", 3, privData);
	    return(CB_LOCKED);
	} else {
	    snprintf(tmpLog, MAXLOGLINE, "Lock cannot be aquired, pthread_mutex_lock() signalled %d.", mtxErr);
	    logNow(&tmpLog[0], 0, privData);
	    return(CB_LOCKERR);
	}
    } else {
	logNow("Skipping cache locking, this thread already holds a lock on it.", 3, privData);
	return(CB_ALRLOCKED);
    }
};

int
tryCacheUnlock (struct mlfiPriv *privData) {
    char tmpLog[MAXLOGLINE];
    int mtxErr;
    
    if (privData -> cacheLockOwner == 1) {
	snprintf(tmpLog, MAXLOGLINE, "Trying to unlock the cache.");
	logNow(&tmpLog[0], 3, privData);
	mtxErr = pthread_mutex_unlock(mcCacheBusy);
	if (mtxErr == 0) {
	    privData -> cacheLockOwner = 0;
	    logNow("Cache unlocked.", 3, privData);
	    return(CB_UNLOCKED);
	} else {
    	    snprintf(tmpLog, MAXLOGLINE, "Cache cannot be unlocked, pthread_mutex_unlock() signalled %d.", mtxErr);
	    logNow(&tmpLog[0], 0, privData);
	    return(CB_UNLOCKERR);
        }
    } else {
	logNow("Skipping cache unlocking, this thread doesn't hold a lock on it.", 3, privData);
	return(CB_ALRUNLOCKED);
    }
};

struct mcCache *
seekCache (struct mlfiPriv *privData, char *envFrom) {
/* returns address of the found element or null */
    struct mcCache *cacheMark;
    int matchFound = 0;
    int unlockNeeded, unlockResult;
    
    char tmpLog[MAXLOGLINE];
    
    logNow("entered seekCache().", 3, privData);
    snprintf(tmpLog, MAXLOGLINE, "Got my arguments - relayPresIP %s, envFrom %s.", privData -> relayPresIP, envFrom);
    logNow(&tmpLog[0], 3, privData);
    
    unlockNeeded = tryCacheLock(privData);
    snprintf(tmpLog, MAXLOGLINE, "tryCacheLock() returned %d.", unlockNeeded);
    logNow(&tmpLog[0], 3, privData);
    
    cacheMark = mcCacheStart;
    while (cacheMark != NULL && matchFound == 0 && strlen(cacheMark -> relayPresIP) > 0) {
	snprintf(tmpLog, MAXLOGLINE, "Comparing %s and %s against %s and %s.", privData -> relayPresIP, envFrom, cacheMark -> relayPresIP, cacheMark -> envFrom);
	logNow(&tmpLog[0], 3, privData);
	if (strncmp(cacheMark -> relayPresIP, privData -> relayPresIP, strlen(cacheMark -> relayPresIP)) == 0 && strncmp(cacheMark -> envFrom, envFrom, strlen(cacheMark -> envFrom)) == 0) {
	    snprintf(tmpLog, MAXLOGLINE, "Found a match between supplied pair %s/%s and cache entry %s/%s.", privData -> relayPresIP, envFrom, cacheMark -> relayPresIP, cacheMark -> envFrom);
	    logNow(&tmpLog[0], 3, privData);
	    snprintf(tmpLog, MAXLOGLINE, "Cache hit detected. Resetting this entry TTL.");
	    logNow(&tmpLog[0], 2, privData);
	    cacheMark -> expiresAt = time(NULL) + cacheTTL;
	    matchFound = 1;
	} else {
	    snprintf(tmpLog, MAXLOGLINE, "Supplied pair %s/%s and cache entry %s/%s don't match, skipping to next entry.", privData -> relayPresIP, envFrom, cacheMark -> relayPresIP, cacheMark -> envFrom);
	    logNow(&tmpLog[0], 3, privData);
	    cacheMark = cacheMark -> nextNode;
	}
    };
    
    if (unlockNeeded == CB_LOCKED) {
        logNow("Trying to clear the lock in seekCache(), looks like we need it.", 3, privData);
	unlockResult = tryCacheUnlock(privData);
    } else {
	if (unlockNeeded == CB_LOCKERR) {
    	    logNow("Skipping lock cleansing in seekCache(), due to previous lock set error.", 3, privData);
	} else {
	    logNow("Skipping lock cleansing in seekCache(), we're in the nested lock call.", 3, privData);
    	    logNow(&tmpLog[0], 3, privData);
	}
    };
    
    if (matchFound == 1) {
	return(cacheMark);
    } else {
	return(NULL);
    }
};

int
extendCache (char *envFrom, struct mlfiPriv *privData, int action) {
/* adds element to cache, 0 - success, 1 - fail */

    struct mcCache *newCacheEntry;
    int memSize;
    char tmpLog[MAXLOGLINE];
    
    int
    initCacheElement () {
    	logNow("entered initCacheElement.", 3, privData);
	newCacheEntry -> expiresAt = time(NULL) + cacheTTL;
    	newCacheEntry -> action = action;
	newCacheEntry -> MXesThatReject = privData -> MXesThatReject;
	newCacheEntry -> MXesThatAccept  = privData -> MXesThatAccept;
	newCacheEntry -> MXesOutOfOrder = privData -> MXesOutOfOrder;
	newCacheEntry -> MXesThatTempfail = privData -> MXesThatTempfail;
	newCacheEntry -> MXesNum = privData -> MXesNum;
	newCacheEntry -> reverseRes = privData -> reverseRes;
	newCacheEntry -> fakeRes = privData -> fakeRes;
	newCacheEntry -> whitelisted = privData -> whitelisted;
	newCacheEntry -> gracefulDNS = privData -> gracefulDNS;
#ifdef WITH_SPF
	newCacheEntry -> SPFResult = privData -> SPFResult;
#endif
	snprintf(tmpLog, MAXLOGLINE, "Pushing %s to newCacheEntry->relayPresIP.", privData -> relayPresIP);
    	logNow(&tmpLog[0], 3, privData);
	strncpy(&newCacheEntry -> relayPresIP[0], privData -> relayPresIP, strlen(privData -> relayPresIP));
	snprintf(tmpLog, MAXLOGLINE, "Pushing %s to newCacheEntry->resolvedRelayIP.", privData -> resolvedRelayIP);
    	logNow(&tmpLog[0], 3, privData);
	strncpy(&newCacheEntry -> resolvedRelayIP[0], privData -> resolvedRelayIP, strlen(privData -> resolvedRelayIP));	
	newCacheEntry -> envFrom = malloc(strlen(envFrom) + 1);
	if (newCacheEntry -> envFrom == NULL) {
    	    logNow("Cannot allocate memory.", 0, privData);
	    return(1);
	} else {
	    bzero(newCacheEntry -> envFrom, strlen(envFrom) + 1);
	    snprintf(tmpLog, MAXLOGLINE, "And finally trying to push %s to newCacheEntry -> envFrom.", envFrom);
	    logNow(&tmpLog[0], 3, privData);
	    strncpy(newCacheEntry -> envFrom,  envFrom, strlen(envFrom));
	    /* while on debug - print out the cache */
	    if (logLevel > 2) {
		getCache();
	    }
	    return(0);
	}
    }
    
    logNow("entered extendCache().", 3, privData);
    snprintf(tmpLog, MAXLOGLINE, "got my env-from %s, my privData %p and my action %d.", envFrom, privData, action);
    logNow(&tmpLog[0], 3, privData);
    /* since we are in threaded environment, we need once more, with busyflag set, check for a dupe */
    if (cacheIsEmpty(privData) > 0 && seekCache(privData, envFrom) != NULL) {
	snprintf(tmpLog, MAXLOGLINE, "Supplied pair %s/%s already exists in cache, skipping addition.", privData -> relayPresIP, envFrom);
	logNow(&tmpLog[0], 2, privData);
	return(1);
    }
    snprintf(tmpLog, MAXLOGLINE, "After cache recheck the Supplied pair %s/%s wasn't found in cache, proceeding addition.", privData -> relayPresIP, envFrom);
    logNow(&tmpLog[0], 2, privData);
    /* first we will check - maby we're just with empty cache */
    if (cacheIsEmpty(privData) > 1) {
	/* we got non-empty cache */
    	logNow("Got non-empty cache, trying to add new first element.", 3, privData);
	memSize = sizeof(struct mcCache);
	snprintf(tmpLog, MAXLOGLINE, "Trying to get %d bytes for the new cache element.", memSize);
    	logNow(&tmpLog[0], 3, privData);
	newCacheEntry = malloc(memSize);
	if (newCacheEntry == NULL) {
    	    logNow("Cannot allocate memory.", 0, privData);
	    return(1);
	} else {
	    /* linking hooks of the new node */
	    mcCacheStart -> prevNode = newCacheEntry;
	    newCacheEntry -> nextNode = mcCacheStart;
	    newCacheEntry -> prevNode = NULL;
	    bzero(newCacheEntry -> relayPresIP, sizeof(newCacheEntry -> relayPresIP));
	    bzero(newCacheEntry -> resolvedRelayIP, sizeof(newCacheEntry -> resolvedRelayIP));
	    /* changing the pointer to the beginning of the cache */
	    mcCacheStart = newCacheEntry;
	    if (initCacheElement() == 0) {
		return(0);
	    } else {
		return(1);
	    }
	}
    } else {
	/* yeah, we got empty cache */
	/* just need to init it's only element */
    	logNow("Got empty cache, trying to initialize first element.", 3, privData);
	newCacheEntry = mcCacheStart;
	if (initCacheElement() == 0) {
	    return(0);
	} else {
	    return(1);
	}
    }
};

int
copyStuff (struct mlfiPriv *privData, struct mcCache *cacheEntry) {
    /* copies the attributes from the cache to the actual context-associated structure */
    logNow("entered copyStuff().", 3, privData);
    privData -> MXesThatReject = cacheEntry -> MXesThatReject;
    privData -> MXesThatAccept = cacheEntry -> MXesThatAccept;
    privData -> MXesOutOfOrder = cacheEntry -> MXesOutOfOrder;
    privData -> MXesThatTempfail = cacheEntry -> MXesThatTempfail;
    privData -> MXesNum = cacheEntry -> MXesNum;
    privData -> reverseRes = cacheEntry -> reverseRes;
    privData -> fakeRes = cacheEntry -> fakeRes;
    privData -> whitelisted = cacheEntry -> whitelisted;
    privData -> gracefulDNS = cacheEntry -> gracefulDNS;
    privData -> cached = 1;
#ifdef WITH_SPF
    privData -> SPFResult = cacheEntry -> SPFResult;
#endif
    /* these are strings of fixed length */
    strncpy(privData -> resolvedRelayIP, cacheEntry -> resolvedRelayIP, strlen(cacheEntry -> resolvedRelayIP));
    return(0);
}
