Skip to content
Snippets Groups Projects
listserver.js 12.90 KiB
// listserver.js

// Mailing List Server module for Synchronet v3.12

// $Id$

load("sbbsdefs.js");

const REVISION = "$Revision$".split(' ')[1];
const user_list_ext = ".list.sub";

log(LOG_INFO,"ListServer " + REVISION);

js.auto_terminate=false;

var ini_fname = system.ctrl_dir + "listserver.ini";

ini_file = new File(ini_fname);
if(!ini_file.open("r")) {
	log(LOG_ERR,format("!ERROR %d opening ini_file: %s"
		,ini_file.error, ini_fname));
	exit();
}
listserver_address=ini_file.iniGetValue(null,"address","listserver@"+system.inet_addr);
listserver_name=ini_file.iniGetValue(null,"name","Synchronet ListServer");
subj_cmd=ini_file.iniGetValue(null,"SubjectCommand",false);
disabled=ini_file.iniGetValue(null,"disabled",false);
list_array=ini_file.iniGetAllObjects("name");
ini_file.close();
if(!list_array.length) {
	log(LOG_ERR,"!No lists defined in " + ini_fname);
	exit();
}

if(disabled) {
	log(LOG_WARNING,"Disabled in " + ini_fname);
	exit();
}

for(var l in list_array) {
	
	var list = list_array[l];

	/* Set default list addresses */
	if(!list.address)
		list.address = format("%s@%s", list.name, system.inet_addr);
	if(!msg_area.sub[list.sub]) {
		log(LOG_WARNING,"!Unrecognized sub-board internal code: " + list.sub);
		list.disabled=true;
		continue;
	}
	if(!list.description)
		list.description = msg_area.sub[list.sub].description;
	if(list.confirm==undefined)
		list.confirm=true;

	var msgbase = new MsgBase(list.sub);
	if(msgbase.open()==false) {
		log(LOG_ERR,format("%s !ERROR %s opening msgbase: %s"
			,list.name, msgbase.error, list.sub));
		continue;
	}
	list.msgbase_file = msgbase.file;

	/* Create the user list file if it doesn't exist */
	var user_fname = list.msgbase_file + user_list_ext;
	if(!file_exists(user_fname))
		file_touch(user_fname);
}
mailbase = new MsgBase("mail");
if(mailbase.open()==false) {
	log(LOG_ERR,"!ERROR " + mailbase.error + " opening mail database");
	exit();
}

/* Inbound message from SMTP Server? */
if(this.recipient_list_filename!=undefined) {	

	var error_file = new File(processing_error_filename);
	if(!error_file.open("w")) {
		log(LOG_ERR,format("!ERROR %d opening processing error file: %s"
			,error_file.error, processing_error_filename));
		exit();
	}

	var rcptlst_file = new File(recipient_list_filename);
	if(!rcptlst_file.open("r")) {
		error_file.writeln(log(LOG_ERR,format("!ERROR %d opening recipient list: %s"
			,rcptlst_file.error, recipient_list_filename)));
		exit();
	}
	var rcpt_list=rcptlst_file.iniGetAllObjects("number");
	rcptlst_file.close();

	var msgtxt_file = new File(message_text_filename);
	if(!msgtxt_file.open("r")) {
		error_file.writeln(log(LOG_ERR,format("!ERROR %d opening message text: %s"
			,msgtxt_file.error, message_text_filename)));
		exit();
	}
	var msgtxt = msgtxt_file.readAll()
	msgtxt_file.close();

	load("mailproc_util.js");	// import parse_msg_header() and get_msg_body()

	var header = parse_msg_header(msgtxt);
	header = convert_msg_header(header);
	var body = get_msg_body(msgtxt);

	var r;
	var handled=false;

	/* contribution to mailing list? */
	for(r=0;r<rcpt_list.length;r++) {
		var l;
		for(l=0;l<list_array.length;l++) {
			var list = list_array[l];
/** DEBUG
			for(var p in list)
				log("list_array["+l+"]."+p+" = "+list[p]);
**/
			if(rcpt_list[r].Recipient.toLowerCase()==list.address.toLowerCase()
				&& !list.disabled
				&& !list.readonly)
				break;
		}
		if(l<list_array.length) {	/* match found */
			log(LOG_INFO,format("ListServer Contribution message from %s to %s: %s"
				,header.from, rcpt_list[r].Recipient, header.subject));
			handled=true;
			if(!process_contribution(header, body, list))
				break;
		}
	}
	if(handled)
		exit();


	log(LOG_INFO,format("ListServer Control message from %s to %s: %s"
		,header.from, header.to, header.subject));

	file_remove(recipient_list_filename);

	if(subj_cmd)
		body.unshift(header.subject);	/* Process the subject as a command */

	var response = process_control_msg(body);
	var resp_hdr = {};

	resp_hdr.subject		= listserver_name + " Response";
	resp_hdr.to				= header.from;
	resp_hdr.to_net_addr	= header.from_net_addr;
	resp_hdr.to_net_type	= NET_INTERNET;
	resp_hdr.from			= listserver_name;
	resp_hdr.from_net_addr	= listserver_address;
	resp_hdr.from_net_type	= NET_INTERNET;
	resp_hdr.from_agent		= AGENT_PROCESS;
	resp_hdr.reply_id		= header.id;

	/* Write response to message */
	if(mailbase.save_msg(resp_hdr, response.body.join('\r\n')))
		log(LOG_INFO,"ListServer Response to control message created");
	else
		log(LOG_ERR,format("ListServer !ERROR %s saving response message to mail msgbase"
			,msgbase.error));

	exit();
}

for(var l in list_array) {

	if(js.terminated) {
		log(LOG_WARNING,"Terminated");
		break;
	}

	var list = list_array[l];

	if(list.disabled)
		continue;

	msgbase = new MsgBase(list.sub);
	if(msgbase.open()==false) {
		log(LOG_ERR,format("%s !ERROR %s opening msgbase: %s"
			,list.name, msgbase.error, list.sub));
		delete msgbase;
		continue;
	}

	/* Get subscriber list */
	var user_list = get_user_list(list);
	if(!user_list.length) {
		delete msgbase;
		continue;
	}

/***
	if(!user_list.length) {
		log(LOG_NOTICE,"No subscribers to list: " + list.name);
		delete msgbase;
		continue;
	}
***/

	/* Get export message pointer */
	ptr_fname = list.msgbase_file + ".list.ptr";
	if(!file_exists(ptr_fname))
		file_touch(ptr_fname);
	ptr_file = new File(ptr_fname);
	if(!ptr_file.open("r+")) {
		log(LOG_ERR,format("%s !ERROR %d opening/creating file: %s"
			,list.name, ptr_file.error, ptr_fname));
		delete msgbase;
		continue;
	}

	var last_msg = msgbase.last_msg;
	var ptr = Number(ptr_file.readln());

	log(LOG_DEBUG,format("%s pointer read: %u"
		,list.name, ptr));

	if(ptr < msgbase.first_msg)
		ptr = msgbase.first_msg;
	else
		ptr++;

	for(;ptr<=last_msg && !js.terminated; ptr++) {
		hdr = msgbase.get_msg_header(
			/* retrieve by offset? */	false,
			/* message number */		ptr
			);
		if(hdr == null) {
			/**
			log(LOG_WARNING,format("%s !ERROR %s getting msg header #%lu"
				,list.name, msgbase.error, ptr));
			**/
			continue;
		}
		if(hdr.attr&(MSG_DELETE|MSG_PRIVATE))	{ /* marked for deletion */
			log(LOG_NOTICE,format("%s Skipping %s message #%lu from %s: %s"
				,list.name, hdr.attr&MSG_DELETE ? "deleted":"private"
				,ptr, hdr.from, hdr.subject));
			continue;
		}
		if(hdr.attr&MSG_MODERATED && !(hdr.attr&MSG_VALIDATED)) {
			log(LOG_NOTICE,format("%s Stopping at unvalidated moderated message #%lu from %s: %s"
				,list.name, ptr, hdr.from, hdr.subject));
			ptr--;
			break;
		}

		body = msgbase.get_msg_body(
				 false	/* retrieve by offset */
				,ptr	/* message number */
				,true	/* remove ctrl-a codes */
				,false	/* rfc822 formatted text */
				,true	/* include tails */
				);
		if(body == null) {
			log(LOG_ERR,format("%s !ERROR %s reading text of message #%lu"
				,list.name, msgbase.error, ptr));
			continue;
		}

		rcpt_list = new Array();
		for(u in user_list) {
			if(js.terminated)
				break;
			if(user_list[u].disabled || !user_list[u].address)
				continue;
			log(LOG_DEBUG,format("%s Enqueing message #%lu for %s <%s>"
				,list.name, ptr, user_list[u].name, user_list[u].address));
			rcpt_list.push(	{	to:				user_list[u].name,
								to_net_addr:	user_list[u].address, 
								to_net_type:	NET_INTERNET 
							} );
		}
		if(js.terminated) {
			ptr--;
			break;
		}
		if(rcpt_list.length < 1) {
			log(LOG_NOTICE,format("%s No active subscriptions", list.name));
			continue;
		}

		log(LOG_INFO,format("%s Sending message #%lu from %s to %lu recipients: %s"
			,list.name, ptr, hdr.from, rcpt_list.length, hdr.subject));

		hdr.replyto_net_type = NET_INTERNET;
		hdr.replyto_net_addr = list.address;
		if(!mailbase.save_msg(hdr,body,rcpt_list))
			log(LOG_ERR,format("%s !ERROR %s saving mail message"
				,list.name, mailbase.error));
	}

	if(ptr > last_msg)
		ptr = last_msg;

	log(LOG_DEBUG,format("%s pointer written: %u"
		,list.name, ptr));

	ptr_file.rewind();
	ptr_file.length=0;		// truncate
	ptr_file.writeln(ptr);
	ptr_file.close();
}

/* clean-up */
mailbase.close();

/* End of Main */

/* Handle Mailing List Control Messages (e.g. subscribe/unsubscribe) here */
function process_control_msg(cmd_list)
{
	var response = { body: new Array(), subject: "" };
	
	response.body.push(listserver_name + " " +REVISION+ " Response:\r\n");

	for(var c in cmd_list) {
		var cmd=cmd_list[c];
		if(!cmd || !cmd.length)
			continue;
		var token=cmd.split(/\s+/);
		switch(token[0].toLowerCase()) {
			case "lists":
				response.body.push("List of lists:");
				for(var l in list_array) {
					var list = list_array[l];
					if(!list.disabled)
						response.body.push("\t"+list.name.toUpperCase()
										  +"\t\t"+list.description);
				}
				break;
			case "subscribe":
			case "unsubscribe":
				for(var l in list_array) {
					var list = list_array[l];
					if(list.disabled || list.closed)
						continue;
					if(list.name.toLowerCase()==token[1].toLowerCase()
						|| list.address.toLowerCase()==token[1].toLowerCase()) {
						response.body.push(subscription_control(token[0], list, token[2]));
						return(response);
					}
				}
				response.body.push("!List not found: " + token[1]);
				break;
			default:
				response.body.push("!Bad command: " + cmd);
			case "help":
				response.body.push("Available commands:");
				response.body.push("\tlists");
				response.body.push("\tsubscribe");
				response.body.push("\tunsubscribe");
				response.body.push("\thelp");
				response.body.push("\tend");
			case "end":
				return(response);
		}
	}

	return(response);
}

function open_user_list(list, mode)
{
	var user_fname = list.msgbase_file + user_list_ext;
	var user_file = new File(user_fname);
	if(!user_file.open(mode)) {
		log(LOG_ERR,format("%s !ERROR %d opening file: %s"
			,list.name, user_file.error, user_fname));
		return(null);
	}

	return(user_file);
}

function read_user_list(user_file)
{
	return user_file.iniGetAllObjects("address");
}

function get_user_list(list)
{
	var user_list = new Array();
	if((user_file = open_user_list(list,"r")) != null) {
		user_list = read_user_list(user_file);
		user_file.close();
	}
	return user_list;
}

function find_user(user_list, address)
{
	for(var u in user_list)
		if(user_list[u].address.toLowerCase()==address.toLowerCase())
			return(u);
	return(-1);
}
function remove_user(user_list, address)
{
	var u=find_user(user_list, address);
	if(u==-1)
		return(false);
	user_list.splice(u,1);
	return(true);
}

function write_user_list(user_list, user_file)
{
	user_file.rewind();
	user_file.length = 0;
	for(var u in user_list) {
		user_file.writeln("[" + user_list[u].address + "]");
		for(var p in user_list[u])
			if(p!="address")
				user_file.writeln(format("%-25s",p) + " = " + user_list[u][p]);
		user_file.writeln();
	}
}

function subscription_control(cmd, list, address)
{
	if(!address)
		address=sender_address;

	log(LOG_INFO,format("%s Subscription control command (%s) from %s"
		,list.name,cmd,address));

	/* Get subscriber list */
	if((user_file=open_user_list(list,"r+"))==null)
		return log(LOG_ERR,format("%s !ERROR opening subscriber list",list.name));

	user_list = read_user_list(user_file);
	
	switch(cmd.toLowerCase()) {
		case "unsubscribe":
			if(remove_user(user_list, address)) {
				write_user_list(user_list, user_file);
				return log(LOG_INFO,format("%s %s unsubscribed successfully"
					,list.name, address));
			}
			return log(LOG_WARNING,format("%s !subscriber not found: %s"
				,list.name, address));
		case "subscribe":
			if(find_user(user_list, address)!=-1)
				return log(LOG_WARNING,format("%s !%s is already subscribed"
					,list.name, address));
			var now=time();
			user_list.push({ 
				 name:					sender_name 
				,address:				address
				,created:				system.timestr(now)
				,last_activity:			system.timestr(now)
				,last_activity_time:	format("%08lx",now)
				});
			write_user_list(user_list, user_file);
			return log(LOG_INFO,format("%s %s subscription successful"
				,list.name, address));
	}
}

/* Handle Mailing List Contributions here */
function process_contribution(header, body, list)
{
	// ToDo: apply filtering here?

	var user_list = get_user_list(list);

	// verify author/sender is a list subscriber here

	if(find_user(user_list, sender_address)==-1) {
		error_file.writeln(log(LOG_WARNING,format("%s !ERROR %s is not a subscriber"
			,list.name, sender_address)));
//		error_file.writeln();
//		error_file.writeln("To subscribe to this list, send an e-mail to " 
//			+ listserver_address);
//		error_file.writeln("with \"subscribe " + list.name + "\" in the message body.");
		return(false);
	}

	var msgbase=new MsgBase(list.sub);
	if(!msgbase.open()) {
		error_file.writeln(log(LOG_ERR,format("%s !ERROR %s opening msgbase: %s"
			,list.name, msgbase.error, list.sub)));
		return(false);
	}

	// Convert from RFC822 to Synchronet-compatible
	header = convert_msg_header(header);

	if(!user.compare_ars(msgbase.cfg.moderated_ars))
		header.attr |= MSG_MODERATED;

	if(!msgbase.save_msg(header, body.join('\r\n'))) {
		log(LOG_ERR,format("%s !ERROR %s saving message to sub: %s"
			,list.name, msgbase.error, list.sub));
		return(false);
	}

	return(true);
}