Skip to content
Snippets Groups Projects
binkit.js 24.10 KiB
/*
 * Intentionally simple "Advanced BinkleyTerm Style Outbound"
 * mailer.
 *
 * Known limitations:
 * 1) Does NOT support upper-case filenames in derived files/paths.
 *    This means that all .flo files must be in lower-case, all hex
 *    filename/path components must be in lower-case, and the ".pnt"
 *    directory extension must be in lower case.
 * 2) Will not check in a zone-specified directory for the default
 *    zone.  That is, if the default zone is zone 1, and the outbound
 *    is "/path/to/outbound", it will not correctly handle the case
 *    where there is a "/path/to/outbound.001" directory.
 * 3) Flow files are processed in alphabetical order, not the reccomended
 *    order from FTS-5005.
 * 
 * See FTS-5005 for details.
 */

load("binkp.js");
load("fidocfg.js");
load("freqit_common.js");

FREQIT.add_file = function(filename, bp, cfg)
{
	if (filename === undefined)
		return;
	if (FREQIT.files >= cfg.maxfiles)
		return;
	if (FREQIT.added[filename] !== undefined)
		return;
	bp.addFile(filename);
	FREQIT.files++;
	FREQIT.added[filename]='';
};

function lock_flow(file, csy)
{
	var ret = {
		bsy:new File(file.replace(/\.[^\.]*?$/, '.bsy')),
		csy:new File(file.replace(/\.[^\.]*?$/, '.csy'))
	};

	// Takes ownership of a lockfile if it's more than six hours old.
	/*
	 * Race-safe version...
	 * 1) If date is "too old", pick a random time in the future.
	 * 2) Set the file date to that random time.
	 * 3) Wait one second (there's still an unlikely race here)
	 * 4) If the file date is the random time you chose, set the date
	 *    to now and take "ownership" of the file.
	 */
	function take_lockfile(f)
	{
		var orig_date;
		var future;
		var remain;
		var now = time();

		// TODO: This is hacked for a signed 32-bit time_t... watch out in 2038!
		orig_date = f.date;
		if (orig_date > (now - 60*60*6))
			return false;
		remain = 0x80000000 - now;
		future = now + random(remain);
		f.date = future;
		mswait(1000);
		if (f.date != future)
			return false;
		if (!f.open("wb")) {
			f.date = orig_date;
			return false;
		}
		f.date = now;
		return true;
	}

	log(LOG_DEBUG, "Locking "+ret.bsy.name);
	if (!ret.bsy.open("web")) {
		if (!take_lockfile(ret.bsy)) {
			log(LOG_DEBUG, "Lock on "+ret.bsy.name+" failed.");
			return undefined;
		}
	}
	if (csy) {
		if (!ret.csy.open("web")) {
			if (!take_lockfile(ret.csy)) {
				ret.bsy.close();
				ret.bsy.remove();
				log(LOG_DEBUG, "Lock on "+ret.bsy.name+" failed on csy file.");
				return undefined;
			}
		}
	}
	ret.bsy.writeln("BinkIT");
	if (csy)
		ret.csy.writeln("BinkIT");
	log(LOG_DEBUG, "Lock successful.");
	return ret;
}

function unlock_flow(locks)
{
	log(LOG_DEBUG, "Unlocking "+locks.bsy.name+".");
	if (locks.csy !== undefined) {
		locks.csy.close();
		locks.csy.remove();
	}
	if (locks.bsy !== undefined) {
		locks.bsy.close();
		locks.bsy.remove();
	}
}

/*
 * TODO: Read the ftn_domains.ini and get the outbound from there,
 * falling back to the sbbsecho one.
 */
function outbound_root(addr, scfg, ftnd)
{
	if (ftnd.outboundMap[addr.domain] === undefined)
		return scfg.outbound.replace(/[\\\/]$/, '');
	return ftnd.outboundMap[addr.domain];
}

/*
 * Given a list of addresses to rescan, calls
 * bp.addFile() for any pending file transfers.
 * 
 * TODO: Call this after sending a M_EOB to rescan per FSP-1024?  We
 * 		 hold the lock files, so nothing should be changing the flow
 * 		 files (per FTS-5005) though.  This is mostly for REQ handling,
 * 		 so if we do integrate freqit.js, we should be fine to ignore
 * 		 the spec on this point.
 */
function add_outbound_files(addrs, bp, is_callout)
{
	function has_lock(addr) {
		var bsy = outbound_root(addr, bp.cb_data.binkit_scfg, bp.cb_data.binkit_ftnd)+addr.flo_outbound(bp.default_zone, bp.default_domain)+'bsy';
		var i;

		for (i=0; i<bp.cb_data.binkit_locks.length; i++) {
			if (bp.cb_data.binkit_locks[i].bsy.name === bsy)
				return true;
		}
		return false;
	}

	addrs.forEach(function(addr) {
		var lock_files;

		log(LOG_DEBUG, "Adding outbound files for "+addr);
		// Find all possible flow files for the remote.
		var allfiles = directory(outbound_root(addr, bp.cb_data.binkit_scfg, bp.cb_data.binkit_ftnd)+addr.flo_outbound(bp.default_zone, bp.default_domain)+'*');
		// Parse flow files and call addFile() tracking what to do on success.
		if (allfiles.length > 0) {
			if (!has_lock(addr)) {
				lock_files = lock_flow(outbound_root(addr, bp.cb_data.binkit_scfg, bp.cb_data.binkit_ftnd)+addr.flo_outbound(bp.default_zone, bp.default_domain), is_callout);
				if (lock_files === undefined)
					return;
				bp.cb_data.binkit_locks.push(lock_files);
			}
			allfiles.forEach(function(file) {
				var flo;
				var line;
				var action;
				var i;
				var fnchars = '0123456789abcdefghijklmnopqrstuvwxyz';
				var fname;

				switch(file_getext(file).toLowerCase()) {
					case '.clo':
					case '.dlo':
					case '.flo':
					case '.hlo':
					case '.ilo':
						flo = new File(file);
						if (!flo.open("r")) {
							log(LOG_ERROR, "Unable to open FLO file '"+flo.name+"'.");
							break;
						}
						if (bp.cb_data.binkit_flow_contents[flo.name] === undefined)
							bp.cb_data.binkit_flow_contents[flo.name] = [];
						while((line = flo.readln(2048))) {
							switch(line.charAt(0)) {
								case '#':
									if (bp.addFile(line.substr(1)))
										bp.cb_data.binkit_file_actions[flo.name] = 'TRUNCATE';
									bp.cb_data.binkit_flow_contents[flo.name].push(line.substr(1));
									break;
								case '^':
								case '-':
									if (bp.addFile(line.substr(1)))
										bp.cb_data.binkit_file_actions[flo.name] = 'DELETE';
									bp.cb_data.binkit_flow_contents[flo.name].push(line.substr(1));
									break;
								case '~':
								case '!':
									break;
								case '@':
									line = line.substr(1);
									if (bp.addFile(line))
										bp.cb_data.binkit_flow_contents[flo.name].push(line.substr(1));
									break;
								default:
									if (bp.addFile(line))
										bp.cb_data.binkit_flow_contents[flo.name].push(line.substr(1));
									break;
							}
						}
						flo.close();
						break;
					case '.cut':
					case '.dut':
					case '.hut':
					case '.iut':
					case '.out':
						fname = '';
						for (i=0; i<8; i++)
							fname += fnchars[random(fnchars.length)];
						fname += '.pkt';
						if (bp.addFile(file, fname))
							bp.cb_data.binkit_file_actions[file] = 'DELETE';
						break;
					case '.req':
						fname = '';
						for (i=0; i<8; i++)
							fname += fnchars[random(fnchars.length)];
						fname += '.req';
						if (bp.addFile(file, fname))
							bp.cb_data.binkit_file_actions[file] = 'DELETE';
						break;
					case '.bsy':
					case '.csy':
					case '.try':
						break;
					default:
						log(LOG_WARNING, "Unsupported flow file type '"+file+"'.");
						break;
				}
			});
		}
	});
}

function callout_auth_cb(mode, bp)
{
	/*
	 * Loop through remote addresses, building a list of the ones with
	 * the same password (if we used an empty password, no other nodes
	 * are allowed)
	 */
	var addrs = [];

	if (bp.cb_data.binkitpw === undefined || bp.cb_data.binkitpw === '-')
		addrs.push(bp.cb_data.binkit_to_addr);
	else {
		bp.remote_addrs.forEach(function(addr) {
			if (bp.cb_data.binkitcfg.node[addr] !== undefined) {
				if (bp.cb_data.binkitcfg.node[addr].pass === bp.cb_data.binkitpw)
					addrs.push(addr);
			}
			else
				log(LOG_DEBUG, "Unconfigured address "+addr);
		});
	}

	add_outbound_files(addrs, bp, true);
}

/*
 * Delete completed flo files.
 */
function tx_callback(fname, bp)
{
	var j;

	// Remove flow files that have been completly processed.
	Object.keys(bp.cb_data.binkit_flow_contents).forEach(function(flo) {
		if (file_exists(flo)) {
			while ((j = bp.cb_data.binkit_flow_contents[flo].indexOf(fname)) !== -1)
				bp.cb_data.binkit_flow_contents[flo].splice(j, 1);
			if (bp.cb_data.binkit_flow_contents[flo].length == 0)
				file_remove(flo);
		}
	});
}

function handle_freq(reqfname, bp)
{
	var req=new File(reqfname);
	var m;
	var fname;
	var pw;
	var cfg = new FREQITCfg();

	if (!req.open("r"))
		return;
	FREQIT.reset();

	next_file:
	while((fname=req.readln())) {
		if ((m=fname.match(/^(.*) !(.*?)$/))!==null) {
			pw=m[2];
			fname=m[1];
		}
		// First, check for magic!
		for (m in cfg.magic) {
			if (m == fname.toLowerCase()) {
				FREQIT.handle_magic(cfg.magic[m], bp, bp.authenticated === 'secure', pw, cfg);
				continue next_file;
			}
		}

		// Now, check for the file...
		FREQIT.handle_regular(fname, bp, bp.authenticated === 'secure', pw, cfg);
	}
}

function rename_or_move(src, dst)
{
	var sf;
	var df;
	var buf;
	var remain;

	if (file_rename(src, dst))
		return true;
	sf = new File(src);
	if (!sf.open("rb"))
		return false;
	df = new File(dst);
	if (!df.open("web")) {
		sf.close();
		return false;
	}
	while (!sf.eof) {
		// Read 2MB at a time...
		remain = sf.length - sf.position;
		if (remain === 0)
			break;
		if (remain > 0x200000)
			remain = 0x200000;
		buf = sf.read(remain);
		if (!df.write(buf)) {
			df.close();
			df.remove();
			sf.close();
			return false;
		}
	}
	df.close();
	sf.close();
	df.date = sf.date;
	sf.remove();
	return true;
}

function rx_callback(fname, bp)
{
	var semname;
	var secure_inbound = bp.cb_data.binkit_scfg.secure_inbound;
	var inbound = bp.cb_data.binkit_scfg.inbound;
	if (secure_inbound == undefined)
		secure_inbound = inbound;

	if (fname.search(/\.(?:pkt|su.|mo.|tu.|we.|th.|fr.|sa.)$/i) !== -1) {
		semname = system.data_dir + 'fidoin.now';
		if (bp.cb_data.binkit_create_semaphores.indexOf(semname) == -1)
			bp.cb_data.binkit_create_semaphores.push(semname);
	}
	else if (fname.search(/\.tic$/i) !== -1) {
		semname = system.data_dir + 'tickit.now';
		if (bp.cb_data.binkit_create_semaphores.indexOf(semname) == -1)
			bp.cb_data.binkit_create_semaphores.push(semname);
	}

	if (fname.search(/\.req$/i) !== -1) {
		handle_freq(fname, bp);
		file_remove(fname);
	}
	else {
		if (bp.authenticated === 'secure') {
			if (secure_inbound === undefined)
				log(LOG_ERROR, "No secure inbound configured in sbbsecho!  Leaving secure file as '"+fname+"'.");
			else {
				log(LOG_INFO, "Moving '"+fname+"' to '"+bp.cb_data.binkit_scfg.secure_inbound+file_getname(fname)+"'.");
				if (!rename_or_move(fname, bp.cb_data.binkit_scfg.secure_inbound+file_getname(fname)))
					return false;
			}
		}
		else {
			if (secure_inbound === undefined)
				log(LOG_ERROR, "No inbound configured in sbbsecho!  Leaving inseucre file as '"+fname+"'.");
			else {
				log(LOG_INFO, "Moving '"+fname+"' to '"+bp.cb_data.binkit_scfg.inbound+file_getname(fname)+"'.");
				if (!rename_or_move(fname, bp.cb_data.binkit_scfg.inbound+file_getname(fname)))
					return false;
			}
		}
	}
	return true;
}

function callout_want_callback(fobj, fsize, fdate, offset, bp)
{
	/*
	 * TODO: Currently a copy/paste from binkp.js...
	 * Likely we'll want magical handling for control files (*.TIC,
	 * *.REQ, and *.PKT, Bundle Files, and the default handling for the
	 * rest.
	 * 
	 * We should lower-case incoming filenames here too...
	 *
	 * Also, for partial files, we can cancel the remote and do an M_GET resume.
	 *
	 * Ok, so put active transfers into a temp directory somewhere with some way
	 * of tying it back to a set of address (we don't actually know which address it
	 * comes from, just a list of addresses it may come from) and store enough
	 * info to resume it.  This means the partially transferred file and some sort
	 * of bookkeeping info file need to be tied together somewhere... maybe resume
	 * support is simply "too hard" to bother with?
	 *
	 * Once the file is completely received, move it to the final resting place
	 * (in rx_callback) and do any required processing then.
	 */

	// Reject duplicate filenames... a more robust callback would rename them.
	// Or process the old ones first.
	if (this.received_files.indexOf(fobj.name) != -1)
		return this.file.REJECT;
	// Skip existing files.
	if (file_exists(fobj.name))
		return this.file.SKIP;
	// Accept everything else
	return this.file.ACCEPT;
}

function callout_done(bp, semaphores)
{
	var f;
	var lines;
	var semname;

	bp.sent_files.forEach(function(file) {
		if (bp.cb_data.binkit_file_actions[file] !== undefined) {
			switch(bp.cb_data.binkit_file_actions[file]) {
				case 'TRUNCATE':
					f = new File(file);
					if (f.truncate())
						log(LOG_INFO, "Truncated '"+f.name+"'.");
					else
						log(LOG_ERROR, "Unable to truncate '"+f.name+"'.");
					break;
				case 'DELETE':
					if (file_remove(file))
						log(LOG_INFO, "Removed '"+file+"'.");
					else
						log(LOG_ERROR, "Unable to remove '"+file+"'.");
			}
		}
	});

	Object.keys(bp.cb_data.binkit_flow_contents).forEach(function(key) {
		if (bp.cb_data.binkit_flow_contents[key].length > 0) {
			// We have some unsent files in here... re-write the flo file...
			f = new File(key);
			if (!f.open("r+")) {
				log(LOG_ERROR, "Unable to update flow file '"+key+"'.");
				return;
			}
			lines = f.readAll(2048);
			f.truncate(0);
			lines.forEach(function(line) {
				switch(line[0]) {
					case '#':
					case '^':
					case '-':
					case '@':
						if (bp.cb_data.binkit_flow_contents[key].indexOf(line.substr(1)) == -1)
							f.writeln('~'+line.substr(1));
						else
							f.writeln(line);
						break;
					// Already skipped...
					case '~':
					case '!':
						f.writeln(line);
						break;
					default:
						if (bp.cb_data.binkit_flow_contents[key].indexOf(line) == -1)
							f.writeln('~'+line);
						else
							f.writeln(line);
						break;
				}
			});
		}
	});

	// Remove flow files that have been completly processed.
	Object.keys(bp.cb_data.binkit_flow_contents).forEach(function(flo) {
		if (file_exists(flo)) {
			if (bp.cb_data.binkit_flow_contents[flo].length == 0)
				file_remove(flo);
		}
	});
}

function callout(addr, scfg, ftnd, semaphores, locks)
{
	var myaddr = FIDO.parse_addr(system.fido_addr_list[0], 1);
	var bp = new BinkP('BinkIT/'+("$Revision$".split(' ')[1]), undefined, rx_callback, tx_callback);
	var port;
	var f;
	var success = false;

	log(LOG_INFO, "Callout to "+addr+" started.");
	bp.cb_data = {
		binkitcfg:new BinkITCfg(),
		binkit_to_addr:addr,
		binkit_scfg:scfg,
		binkit_ftnd:ftnd,
		binkit_file_actions:{},
		binkit_flow_contents:{},
		binkit_create_semaphores:semaphores,
		binkit_locks:locks
	};
	if (bp.cb_data.binkitcfg.node[addr] !== undefined) {
		bp.cb_data.binkitpw = bp.cb_data.binkitcfg.node[addr].pass;
		port = bp.cb_data.binkitcfg.node[addr].port;
		bp.require_md5 = !(bp.cb_data.binkitcfg.node[addr].nomd5);
		bp.require_crypt = !(bp.cb_data.binkitcfg.node[addr].nocrypt);
	}
	// TODO: Force debug mode for now...
	bp.debug = true;
	bp.default_zone = myaddr.zone;
	bp.default_domain = myaddr.domain;
	bp.want_callback = callout_want_callback;

	/*
	 * We can't use the defaults since the defaults are only 4D addresses,
	 * and the address we're calling from needs to be first...
	 */
	bp.addr_list = [];
	system.fido_addr_list.forEach(function(faddr){
		bp.addr_list.push(FIDO.parse_addr(faddr, this.default_zone));
	}, this);
	/*
	 * TODO: Do we need a "SourceAddr" property in the config so we can
	 * ensure we use the password from the right callout address?
	 */
	// Sort by "distance" from callout address.
	bp.addr_list.sort(function(addr1, addr2) {
		var dist1;
		var dist2;

		if (addr1.domain !== addr2.domain) {
			if (addr1.domain === addr.domain)
				return -1;
			if (addr2.domain === addr2.domain)
				return 1;
			if (addr1.domain < addr2.domain)
				return -1;
			return 1;
		}
		if (addr1.zone !== addr2.zone) {
			dist1 = Math.abs(addr1.zone - addr.zone);
			dist2 = Math.abs(addr2.zone - addr.zone);
			return dist1-dist2;
		}
		if (addr1.net !== addr2.net) {
			dist1 = Math.abs(addr1.net - addr.net);
			dist2 = Math.abs(addr2.net - addr.net);
			return dist1-dist2;
		}
		if (addr1.node !== addr2.node) {
			dist1 = Math.abs(addr1.node - addr.node);
			dist2 = Math.abs(addr2.node - addr.node);
			return dist1-dist2;
		}
		if (addr1.point !== addr2.point) {
			dist1 = Math.abs(addr1.point - addr.point);
			dist2 = Math.abs(addr2.point - addr.point);
			return dist1-dist2;
		}
		return 0;
	});

	// We won't add files until the auth finishes...
	success = bp.connect(addr, bp.cb_data.binkitpw, callout_auth_cb, port);

	callout_done(bp, semaphores);

	f = new File(outbound_root(addr, bp.cb_data.binkit_scfg, bp.cb_data.binkit_ftnd)+addr.flo_outbound(bp.default_zone, bp.default_domain)+'try');
	if (f.open("w")) {
		f.writeln(success ? ('Success S/R: '+bp.sent_files.length+'/'+bp.received_files.length) :
			('Error S/R: '+bp.sent_files.length+'/'+bp.received_files.length+' Failed S/R: '+bp.failed_sent_files.length+'/'+bp.failed_received_files.length));
		f.close();
	}
	else {
		log(LOG_ERROR, "Unable to create .try file '"+f.name+"'.");
	}
}

function run_one_outbound_dir(dir, scfg, ftnd, semaphores)
{
	var myaddr = FIDO.parse_addr(system.fido_addr_list[0], 1);
	var ran = {};
	var locks = [];

	log(LOG_DEBUG, "Running outbound dir "+dir);

	function check_held(addr)
	{
		var until;
		var f = new File(outbound_root(addr, scfg, ftnd)+addr.flo_outbound(myaddr.zone)+'.hld');

		if (!f.exists)
			return false;
		if (!f.open("r")) {
			log(LOG_ERROR, "Unable to open hold file '"+f.name+"'");
			return true;
		}
		until = f.readln();
		if (until.search(/^[0-9]+$/) !== 0) {
			log(LOG_WARNING, "First line of '"+f.name+"' invalid ("+until+").  Should be a positive integer.");
			return false;
		}
		f.close();
		until = parseInt(until, 10);
		if (until < time()) {
			f.remove();
			log(LOG_INFO, "Removed stale ("+system.timestr(until)+") hold file '"+f.name+"'.");
			return false;
		}
		log(LOG_INFO, addr+" held until "+system.timestr(until)+".");
		return true;
	}

	function check_flavour(wildcard, typename)
	{
		var addr;
		var flow_files;
		var lock_files;
		var ext;
		var i;

		while (!js.terminated) {
			flow_files = directory(dir+wildcard);
			if (flow_files.length == 0)
				break;
			flow_file_loop:
			for (i=0; i<flow_files.length; i++) {
				try {
					addr = FIDO.parse_flo_file_path(flow_files[i], myaddr.zone);
				}
				catch(addr_e) {
					log(LOG_WARNING, addr_e+" when checking '"+flow_files[i]+"' (default zone: "+myaddr.zone+")");
					continue;
				}
				if (ran[addr] !== undefined)
					continue;
				ext = file_getext(flow_files[i]);

				// Ensure this is the "right" outbound (file case, etc)
				if (flow_files[i] !== outbound_root(addr, scfg, ftnd)+addr.flo_outbound(myaddr.zone)+ext.substr(1)) {
					log(LOG_WARNING, "Unexpected file path '"+flow_files[i]+"' expected '"+outbound_root(addr, scfg, ftnd)+addr.flo_outbound(myaddr.zone)+ext.substr(1)+"' (skipped)");
					continue;
				}

				switch(ext.substr(0, 2)) {
					case '.h':
						log(LOG_DEBUG, "Skipping hold flavoured flow file '"+flow_files[i]+"'.");
						continue;
					case '.c':
					case '.d':
					case '.i':
						break;
					case '.f':
						if (wildcard === '*.?lo')
							break;
						continue;
					case '.o':
						if (wildcard === '*.?ut')
							break;
						continue;
					default:
						log(LOG_WARNING, "Unknown flow file flavour '"+flow_files[i]+"'.");
						continue;
				}

				if ((lock_files = lock_flow(flow_files[i], true))!==undefined) {
					if (check_held(addr)) {
						unlock_flow(lock_files);
						continue;
					}
					break;
				}
			}
			if (i<flow_files.length) {
				log(LOG_INFO, "Attempting callout for file "+flow_files[i]);
				locks.push(lock_files);
				// Use a try/catch to ensure we clean up the lock files.
				callout(addr, scfg, ftnd, semaphores, locks);
				ran[addr] = true;
				locks.forEach(unlock_flow);
			}
			else {
				log(LOG_DEBUG, "No "+typename+" typed flow files to be processed.");
				break;
			}
		}
	}

	// First, look for any node with pending netmail and handle that node.
	check_flavour('*.?ut', "netmail");
	log(LOG_DEBUG, "Done checking netmail in "+dir+", checking file references.");

	// Now check for pending file reference
	check_flavour('*.?lo', "file reference");
	log(LOG_DEBUG, "Done checking file references in "+dir+".");
}

function run_outbound()
{
	var scfg;
	var ftnd;
	var outbound_base;
	var outbound_dirs=[];
	var outbound_roots=[];
	var semaphores = [];

	log(LOG_INFO, "Running outbound");
	scfg = new SBBSEchoCfg();
	ftnd = new FTNDomains();

	if (!scfg.is_flo) {
		log(LOG_ERROR, "sbbsecho not configured for FLO-style mailers.");
		return false;
	}
	outbound_roots.push(scfg.outbound.replace(/[\\\/]$/, ''));
	Object.keys(ftnd.outboundMap).forEach(function(key) {
		outbound_roots.push(ftnd.outboundMap[key]);
	});
	outbound_roots.forEach(function(oroot) {
		var dirs;

		function addDir(dir) {
			var bdir = backslash(dir);
			if (outbound_dirs.indexOf(bdir) == -1)
				outbound_dirs.push(bdir);
		}

		if (file_isdir(oroot))
			addDir(oroot);
		dirs = directory(oroot+'.*', 0);
		dirs.forEach(function(dir) {
			var pnts;

			if (file_getext(dir).search(/^\.[0-9a-f]+$/) == 0) {
				if (file_isdir(dir)) {
					addDir(dir);
					pnts = directory(backslash(dir)+'.pnt', false);
					pnts.forEach(function(pdir) {
						if (pdir.search(/[\\\/][0-9a-z]{8}.pnt$/) >= 0 && file_isdir(pdir))
							addDir(pdir);
						else
							log(LOG_WARNING, "Unhandled/Unexpected point path '"+pdir+"'.");
					});
				}
				else
					log(LOG_WARNING, "Unexpected file in outbound '"+dir+"'.");
			}
			else
				log(LOG_WARNING, "Unhandled outbound '"+dir+"'.");
		});
	});
	outbound_dirs.forEach(function(dir) {
		run_one_outbound_dir(dir, scfg, ftnd, semaphores);
	});

	semaphores.forEach(function(semname) {
		file_touch(semname);
	});
}

function inbound_auth_cb(pwd, bp)
{
	/*
	 * Loop through remote addresses, building a list of the ones with
	 * the same password that we can send mail for.
	 */
	var addrs = [];
	var ret = '-';
	var nocrypt;

	function check_nocrypt(node) {
		if (node) {
			if (nocrypt === undefined)
				nocrypt = true;
		}
		else {
				nocrypt = false;
		}
	}

	bp.remote_addrs.forEach(function(addr) {
		var cpw;
		if (bp.cb_data.binkitcfg.node[addr] !== undefined) {
			cpw = bp.cb_data.binkitcfg.node[addr].pass;
			if (cpw === undefined)
				cpw = '-';
			if (pwd[0].substr(0, 9) === 'CRAM-MD5-') {
				if (bp.getCRAM('MD5', cpw) === pwd[0]) {
					addrs.push(addr);
					check_nocrypt(bp.cb_data.binkitcfg.node[addr]);
					ret = cpw;
				}
			}
			else {
				// TODO: Deal with arrays of passwords?
				if (bp.cb_data.binkitcfg.node[addr].nomd5 === false && bp.cb_data.binkitcfg.node[addr].pass === pwd[0]) {
					addrs.push(addr);
					check_nocrypt(bp.cb_data.binkitcfg.node[addr]);
					ret = cpw;
				}
			}
		}
		else
			log(LOG_DEBUG, "Unconfigured address "+addr);
	});
	if (addrs.length === 0) {
		// If we have NONE of their nodes configured, we can send them files for ALL of them.
		addrs = bp.remote_addrs;
		// And allow unencrypted sessions.
		nocrypt = true;
	}
	else {
		// If we have SOME of their nodes configured, only send them files for authenticated ones.
		bp.remote_addrs = addrs;
		// Require encryption unless all configured nodes say otherwise.
		if (nocrypt === undefined)
			nocrypt = false;
	}
	bp.require_crypt = !nocrypt;

	add_outbound_files(addrs, bp, false);
	return ret;
}

function run_inbound(sock)
{
	var myaddr = FIDO.parse_addr(system.fido_addr_list[0], 1);
	var bp = new BinkP('BinkIT/'+("$Revision$".split(' ')[1]), undefined, rx_callback, tx_callback);
	var port;
	var f;
	var success = false;
	var semaphores = [];
	var locks = [];

	log(LOG_INFO, "Got inbound call from "+sock.remote_ip_address+":"+sock.remote_port);
	bp.cb_data = {
		binkitcfg:new BinkITCfg(),
		binkit_scfg:new SBBSEchoCfg(),
		binkit_ftnd:new FTNDomains(),
		binkit_file_actions:{},
		binkit_flow_contents:{},
		binkit_create_semaphores:semaphores,
		binkit_locks:locks
	};

	// TODO: Force debug mode for now...
	bp.debug = true;
	bp.default_zone = myaddr.zone;
	bp.default_domain = myaddr.domain;
	bp.want_callback = callout_want_callback;

	// We can't use the defaults since the defaults are only 4D addresses.
	bp.addr_list = [];
	system.fido_addr_list.forEach(function(faddr){bp.addr_list.push(FIDO.parse_addr(faddr, this.default_zone));}, this);

	// We won't add files until the auth finishes...
	success = bp.accept(sock, inbound_auth_cb);

	callout_done(bp, semaphores);

	locks.forEach(function(lock) {
		unlock_flow(lock);
	});

	semaphores.forEach(function(semname) {
		file_touch(semname);
	});
}

var sock;
try {
	sock = client.socket;
}
catch(e) {}

// If we're running as a service, call run_inbound().
if (sock !== undefined && sock.descriptor !== -1)
	run_inbound(sock);
else
	run_outbound();