# $Id: mime.bro 4758 2007-08-10 06:49:23Z vern $

@load smtp

module MIME;

export {
	type mime_session_info: record {
		id: count;
		connection_id: conn_id;
		smtp_session: SMTP::smtp_session_info;
		level: count;
		data_offset: count;
		content_hash: string;
	};

	global get_session:
		function(c: connection, new_session_ok: bool): mime_session_info;
}

const mime_log = open_log_file("mime") &redef;

global mime_session_id = 0;
global mime_sessions: table[conn_id] of mime_session_info;

function mime_session_string(session: mime_session_info): string
	{
	return fmt("#%s %s +%d", prefixed_id(session$id),
			id_string(session$connection_id), session$level);
	}

function mime_log_warning(what: string)
	{
	print mime_log, fmt("%.6f warning: %s", network_time(), what);
	}

function mime_log_msg(session: mime_session_info, where: string, what: string)
	{
	print mime_log, fmt("%.6f %s: [%s] %s",
				network_time(),
				mime_session_string(session),
				where,
				what);
	}

type mime_header_handler_func:
	function(session: mime_session_info, name: string, arg: string);

type mime_header_handler_table: table[string] of mime_header_handler_func;

function mime_header_default_handler(session: mime_session_info,
					name: string, arg: string)
	{
	}

function mime_header_subject(session: mime_session_info,
				name: string, arg: string)
	{
	if ( session$level == 1 )
		session$smtp_session$subject = arg;
	}


### This is a bit clunky.  These are functions we call out to, defined
# elsewhere.  The way we really ought to do this is to have them passed
# in during initialization.  But for now, we presume knowledge of their
# names in global scope.
module GLOBAL;
global check_relay_3:
	function(session: MIME::mime_session_info, msg_id: string);
global check_relay_4:
	function(session: MIME::mime_session_info, content_hash: string);
module MIME;

function mime_header_message_id(session: mime_session_info, name: string, arg: string)
	{
	local s = arg;

	local t = split1(s, /</);
	if ( length(t) != 2 )
		{
		mime_log_msg(session, "event",
			fmt("message id does not contain '<': %s", arg));
		return;
		}

	s = t[2];

	t = split1(s, />/);
	if ( length(t) != 2 )
		{
		mime_log_msg(session, "event",
			fmt("message id does not contain '>': %s", arg));
		return;
		}

	s = t[1];

	if ( session$level == 1 && SMTP::process_smtp_relay )
		check_relay_3(session, s);
	}

global mime_header_handler: mime_header_handler_table = {
	["SUBJECT"] = mime_header_subject,
	["MESSAGE-ID"] = mime_header_message_id,
} &default =
	function(name: string): mime_header_handler_func
		{
		# This looks a little weird, but there is no other way
		# to specify a function as the default *value*
		return mime_header_default_handler;
		};

function new_mime_session(c: connection)
	{
	local id = c$id;
	local session_id = ++mime_session_id;
	local info: mime_session_info;

	info$id = session_id;
	info$connection_id = id;
	info$level = 0;
	info$data_offset = 0;
	info$content_hash = "";

	if ( id !in SMTP::smtp_sessions )
		SMTP::new_smtp_session(c);

	info$smtp_session = SMTP::smtp_sessions[id];

	mime_sessions[id] = info;
	mime_log_msg(info, "start", "");
	}

function get_session(c: connection, new_session_ok: bool): mime_session_info
	{
	local id = c$id;

	if ( id !in mime_sessions )
		{
		if ( ! new_session_ok )
			mime_log_warning(fmt("begin_entity missing for new MIME session %s", id_string(id)));

		new_mime_session(c);
		}

	return mime_sessions[id];
	}

function end_mime_session(session: mime_session_info)
	{
	mime_log_msg(session, "finish", "");
	delete mime_sessions[session$connection_id];
	}

event connection_state_remove(c: connection)
	{
	local id = c$id;

	if ( id in mime_sessions )
		{
		mime_log_msg(mime_sessions[id], "state remove", "");
		delete mime_sessions[id];
		}
	}

function do_mime_begin_entity(c: connection)
	{
	local session = get_session(c, T);

	++session$level;
	session$data_offset = 0;
	mime_log_msg(session, "begin entity", "");
	}

event mime_begin_entity(c: connection)
	{
	do_mime_begin_entity(c);
	}

function do_mime_end_entity(c: connection)
	{
	local session = get_session(c, T);

	mime_log_msg(session, "end entity", "");

	session$smtp_session$num_bytes_in_body =
		session$smtp_session$num_bytes_in_body + session$data_offset;

	if ( session$level > 0 )
		{
		--session$level;
		if ( session$level == 0 )
			end_mime_session(session);
		}
	else
		mime_log_warning(fmt("unmatched end_entity for MIME session %s",
					mime_session_string(session)));
	}

event mime_end_entity(c: connection)
	{
	do_mime_end_entity(c);
	}

event mime_next_entity(c: connection)
	{
	do_mime_end_entity(c);
	do_mime_begin_entity(c);
	}

# event mime_one_header(c: connection, h: mime_header_rec)
#	{
#	local session = get_session(c, T);
#	mime_log_msg(session, "header",
#			fmt("%s: \"%s\"", h$name, h$value));
#	mime_header_handler[h$name](session, h$name, h$value);
#	}

event mime_all_headers(c: connection, hlist: mime_header_list)
	{
	local session = get_session(c, T);
	local i = 0;

	for ( i in hlist )
		{
		local h = hlist[i];
		mime_log_msg(session, "header",
				fmt("%s: \"%s\"", h$name, h$value));
		mime_header_handler[h$name](session, h$name, h$value);
		}
	}

event mime_segment_data(c: connection, length: count, data: string)
	{
	local session = get_session(c, T);

	if ( session$data_offset < 256 )
		mime_log_msg(session, "data", fmt("%d: %s", length, data));

	session$data_offset = session$data_offset + length;
	}

# event mime_entity_data(c: connection, length: count, data: string)
# 	{
#  	local session = get_session(c, T);
#
# 	mime_log_msg(session, "data", fmt("%d: %s", length, sub_bytes(data, 0, 256)));
# 	}

event mime_event(c: connection, event_type: string, detail: string)
	{
	local session = get_session(c, T);
	mime_log_msg(session, "event", fmt("%s: %s", event_type, detail));
	}

