#! /usr/bin/env lua5.1
-- 
-- Released under the terms of GPLv3 or at your option any later version.
-- No warranties.
-- Copyright 2009 Enrico Tassi <gares@fettunta.org>

require 'syncmaildir'

for k,v in pairs(syncmaildir) do _G[k] = v end

local queue_max_len = 50
local statistics = {
	added = 0,
	removed = 0,
}

function receive_delta(inf)
	local cmds = {}
	local line = ""

	repeat
		line = inf:read("*l")
		if line and line ~= "END" then cmds[#cmds+1] = line end
	until not line or line == "END"
	if line ~= "END" then
		log_error('Unable to receive a complete diff')
		log_tags("receive-delta","network",false,"retry")
		error("network error while receiving delta")
	end

	return cmds
end


local get_full_email_queue = {}

function process_get_full_email_queue()
	local command = {}
	for _,v in ipairs(get_full_email_queue) do
		command[#command+1] = 'GET ' .. v.name
	end
	command[#command+1] = ''
	io.write(table.concat(command,'\n'))
	command = nil
	io.flush()
	local tmp = {}
	for _,v in ipairs(get_full_email_queue) do
		tmp[#tmp+1] = tmp_for(v.name)
		v.tmp = tmp[#tmp]
		receive(io.stdin, tmp[#tmp])
	end
	local inf = io.popen(MDDIFF .. table.concat(tmp,' '))
	tmp = nil
	for _,v in ipairs(get_full_email_queue) do
		local hsha_l, bsha_l = inf:read('*l'):match('^(%S+) (%S+)$') 
		if hsha_l == nil or bsha_l == nil then
			error('mddiff incorrect behaviour')
		elseif hsha_l == v.hsha and bsha_l == v.bsha then
			mkdir_p(v.name)
			local rc = os.rename(v.tmp, v.name) 
			if rc then
				statistics.added = statistics.added + 1
			else
				log_error('Failed to rename '..v.tmp..' to '..v.name)
				log_error('It may be caused by bad directory permissions, please check.')
				os.remove(v.tmp)
				return (trace(false)) -- fail rename tmpfile to actual name
			end
		else
			log_error('The server sent a different email for '..v.name)
			log_error('This problem should be transient, please retry.')
			os.remove(v.tmp)
			return (trace(false)) -- get full email failed, received wrong mail
		end
	end
	inf:close()
	get_full_email_queue = {}
	return (trace(true)) -- get full email OK
end

function get_full_email(name,hsha,bsha)
	get_full_email_queue[#get_full_email_queue+1] = {
		name = name;
		hsha = hsha;
		bsha = bsha;
	}
	return true
end

function merge_mail(header,body,target)
	local h = io.open(header,"r")
	local b = io.open(body,"r")
	local t = io.open(target,"w")
	local l
	while true do
		l = h:read("*l")
		if l and l ~= "" then t:write(l,'\n') else break end
	end
	while true do
		l = b:read("*l")
		if not l or l == "" then break end
	end
	t:write('\n')
	while true do
		l = b:read("*l")
		if l then t:write(l,'\n') else break end
	end
	h:close()
	b:close()
	t:close()
end

function execute_add(cmd)
	local name, hsha, bsha = cmd:match('^ADD (%S+) (%S+) (%S+)$')
	local ex, hsha_l, bsha_l = exists_and_sha(name)
	if ex then
		if hsha == hsha_l and bsha == bsha_l then
			return (trace(true)) -- skipping add since already there
		else
			log_error('Failed to add '..name..
				' since a file with the same name')
			log_error('exists but its content is different.')
			log_error('To fix this problem you should rename '..name)
			log_error('Executing `cd; mv -n '..quote(name)..' '..
				quote(tmp_for(name,false))..'` should work.')

			log_tags("mail-addition","concurrent-mailbox-edit",true,
				"run(mv -n "..quote(homefy(name)).." "..
					quote(tmp_for(homefy(name),false)) ..")")
			return (trace(false)) -- skipping add since already there but !=
		end
	end
	return (get_full_email(name,hsha,bsha))
end

function execute_delete(cmd)
	local name, hsha, bsha = cmd:match('^DELETE (%S+) (%S+) (%S+)$')
	local ex, hsha_l, bsha_l = exists_and_sha(name)
	if ex then
		if hsha == hsha_l and bsha == bsha_l then
			local rc = os.remove(name) 
			if rc then
				statistics.removed = statistics.removed + 1
				return (trace(true)) -- removed successfully
			else
				log_error('Deletion of '..name..' failed.')
				log_error('It may be caused by bad directory permissions, '..
					'please check.')

				log_tags("delete-message","bad-directory-permission",true,
					"display-permissions("..quote(homefy(name))..")")
				return (trace(false)) -- os.remove failed
			end
		else
			log_error('Failed to delete '..name..
				' since the local copy of it has')
			log_error('modifications.')
			log_error('To fix this problem you have two options:')
			log_error('- delete '..name..' by hand')
			log_error('- run @@INVERSECOMMAND@@ so that this file is added '..
				'to the other mailbox')

			log_tags("delete-message", "concurrent-mailbox-edit",true,
				"display-mail("..quote(homefy(name))..")",
				"run(rm "..quote(homefy(name))..")",
				"run(@@INVERSECOMMAND@@ @@ENDPOINT@@)")
			return (trace(false)) -- remove fails since local file is !=
		end
	end
	return (trace(true)) -- already removed
end

function execute_copy(cmd)
	local name_src, hsha, bsha, name_tgt = 
		cmd:match('^COPY (%S+) (%S+) (%S+) TO (%S+)$')
	local ex_src, hsha_src, bsha_src = exists_and_sha(name_src)
	local ex_tgt, hsha_tgt, bsha_tgt = exists_and_sha(name_tgt)
	if ex_src and ex_tgt then
		if hsha_src == hsha_tgt and bsha_src == bsha_tgt then
			return (trace(true)) -- skip copy, already there
		else
			log_error('Failed to copy '..name_src..' to '..name_tgt)
			log_error('The destination already exists but its content differs.')
			log_error('To fix this problem you have two options:')
			log_error('- rename '..name_tgt..' by hand so that '..name_src)
			log_error('  can be copied without replacing it.')
			log_error('  Executing `cd; mv -n '..quote(name_tgt)..' '..
				quote(tmp_for(name_tgt,false))..'` should work.')
			log_error('- run @@INVERSECOMMAND@@ so that your changes to '..
				name_tgt)
			log_error('  are propagated to the other mailbox')

			log_tags("copy-message","concurrent-mailbox-edit",true,
				"run(mv -n "..quote(homefy(name_tgt)).." "..
					quote(tmp_for(homefy(name_tgt),false))..")",
				"run(@@INVERSECOMMAND@@ @@ENDPOINT@@)")
			return (trace(false)) -- fail copy, already there but !=
		end
	elseif ex_src and not ex_tgt then
		if hsha_src == hsha and bsha_src == bsha then
				mkdir_p(name_tgt)
				local ok = os.execute(CPN..' '..quote(name_src)..
					' '..  quote(name_tgt))
				if ok == 0 then 
					return (trace(true)) -- copy successful
				else 
					log_error('Failed to copy '..name_src..' to '..name_tgt)
					log_error('It may be caused by bad directory permissions, '
						..  'please check.')

					log_tags("delete-message","bad-directory-permission",true,
						"display-permissions("..quote(homefy(name_tgt))..")")
					return (trace(false)) -- copy failed (cp command failed)
				end
		else
				-- sub-optimal, we may reuse body or header 
				return (get_full_email(name_tgt,hsha,bsha))
		end
	elseif not ex_src and ex_tgt then
		if hsha == hsha_tgt and bsha == bsha_tgt then
			return (trace(true)) -- skip copy, already there (only the copy)
		else
			log_error('Failed to copy '..name_src..' to '..name_tgt)
			log_error('The source file has been locally removed.')
			log_error('The destination file already exists but its '..
				'content differs.')
			log_error('To fix this problem you have two options:')
			log_error('- rename '..name_tgt..' by hand so that '..
				name_src..' can be')
			log_error('  copied without replacing it.')
			log_error('  Executing `cd; mv -n '..quote(name_tgt)..' '..
				quote(tmp_for(name_tgt,false))..'` should work.')
			log_error('- run @@INVERSECOMMAND@@ so that your changes to '..
				name_tgt..' are')
			log_error('  propagated to the other mailbox')

			log_tags("copy-message","concurrent-mailbox-edit",true,
				"run(mv -n "..quote(homefy(name_tgt)).." "..
					quote(tmp_for(homefy(name_tgt),false))..")",
				"run(@@INVERSECOMMAND@@ @@ENDPOINT@@)")
			return (trace(false)) -- skip copy, already there and !=, no source
		end
	else
		return (get_full_email(name_tgt,hsha,bsha))
	end
end

function get_header_and_merge(name,hsha)
	local tmpfile = tmp_for(name)
	io.write('GETHEADER '..name..'\n')
	io.flush()
	receive(io.stdin, tmpfile)
	local hsha_l, _ = sha_file(tmpfile)
	if hsha_l == hsha then
		local tmpfile1 = tmp_for(name)
		merge_mail(tmpfile,name,tmpfile1)
		os.remove(tmpfile)
		os.rename(tmpfile1, name)
		return (trace(true)) -- get header OK
	else
		os.remove(tmpfile)
		log_error('The server sent a different email header for '..name)
		log_error('This problem should be transient, please retry.')

		log_tags("receive-header","modify-while-update",false,"retry")
		return (trace(false)) -- get header fails, got a different header
	end
end

function execute_replaceheader(cmd)
	local name, hsha, bsha, hsha_new = 
		cmd:match('^REPLACEHEADER (%S+) (%S+) (%S+) WITH (%S+)$')
	if exists(name) then
		local hsha_l, bsha_l = sha_file(name)
		if hsha == hsha_l and bsha == bsha_l then
			return (get_header_and_merge(name,hsha_new))
		elseif hsha_l == hsha_new and bsha == bsha_l then
			return (trace(true)) -- replace header ok, already changend
		else
			log_error('Failed to replace '..name..' header since it has local')
			log_error(' modifications.')
			log_error('To fix this problem you should rename '..name)
			log_error('Executing `cd; mv -n '..quote(name)..' '..
				quote(tmp_for(name,false))..'` should work.')
			log_tags("header-replacement","concurrent-mailbox-edit",true,
				"run(mv -n "..quote(homefy(name)).." "..
					quote(tmp_for(homefy(name),false))..")")
			return (trace(false)) -- replace header fails, local header !=
		end
	else
		return (get_full_email(name,hsha_new,bsha))
	end
end

function execute_copybody(cmd)
	local name, bsha, newname, hsha = 
		cmd:match('^COPYBODY (%S+) (%S+) TO (%S+) (%S+)$')
	local exn, hsha_ln, bsha_ln = exists_and_sha(newname)
	if not exn then
		local ex, _, bsha_l = exists_and_sha(name)
		if ex and bsha_l == bsha then
			local ok = os.execute(CPN..' '..quote(name)..
				' '..quote(newname))
			if ok == 0 then 
				ok = get_header_and_merge(newname,hsha)
				if ok then
					return (trace(true)) -- copybody OK
				else
					os.remove(newname)
					return (trace(false)) -- copybody failed, bad new header
				end
			else 
				log_error('Failed to copy '..name..' to '..newname)
				log_error('It may be caused by bad directory permissions, '..
					'please check.')
					
				log_tags("copy-message","bad-directory-permission",true,
					"display-permissions("..quote(homefy(newname))..")")
				return (trace(false)) -- copybody failed (cp command failed)
			end
		else
			return(get_full_email(newname,hsha,bsha))
		end
	else
		if bsha == bsha_ln and hsha == hsha_ln then
			return (trace(true)) -- copybody OK (already there)
		else
			log_error('Failed to copy body of '..name..' to '..newname)
			log_error('To fix this problem you should rename '..newname)
			log_error('Executing `cd; mv -n '..quote(newname)..' '..
				quote(tmp_for(newname,false))..'` should work.')

			log_tags("copy-body","concurrent-mailbox-edit",true,
				"run(mv -n "..quote(homefy(newname)).." "..
					quote(tmp_for(homefy(newname),false))..")")
			return (trace(false)) -- copybody failed (already there, != )
		end
	end
end

function execute_replace(cmd)
	local name1, hsha1, bsha1, hsha2, bsha2 = 
	   cmd:match('^REPLACE (%S+) (%S+) (%S+) WITH (%S+) (%S+)$')
	local exn, hsha_ln, bsha_ln = exists_and_sha(name1)
	if not exn then
		return(get_full_email(name1,hsha2,bsha2))
	else
		if bsha2 == bsha_ln and hsha2 == hsha_ln then
			return (trace(true)) -- replace OK (already there)
		elseif bsha1 == bsha_ln and hsha1 == hsha_ln then
			return(get_full_email(name1,hsha2,bsha2))
		else
			log_error('Failed to replace '..name1)
			log_error('To fix this problem you should rename '..name1)
			log_error('Executing `cd; mv -n '..quote(name1)..' '..
				quote(tmp_for(name1,false))..'` should work.')

			log_tags("replace","concurrent-mailbox-edit",true,
				"run(mv -n "..quote(homefy(name1)).." "..
					quote(tmp_for(homefy(name1),false))..")")
			return (trace(false)) -- replace failed (already there, != )
		end
	end
end

function execute(cmd)
	local opcode = cmd:match('^(%S+)')

	    if opcode == "ADD"           then return (execute_add(cmd))
	elseif opcode == "DELETE"        then return (execute_delete(cmd))
	elseif opcode == "COPY"          then return (execute_copy(cmd))
	elseif opcode == "REPLACEHEADER" then return (execute_replaceheader(cmd))
	elseif opcode == "COPYBODY"      then return (execute_copybody(cmd))
	elseif opcode == "REPLACE"       then return (execute_replace(cmd))
	else
		error('Unknown opcode '..opcode)
	end
end

function process_pending_queue()
	local rc = process_get_full_email_queue()
	if not rc then
		io.write('ABORT\n')
		io.flush()
		os.exit(1)
	end
end

-- ============================= MAIN =====================================

function main()
	if arg[1] == '-v' then
		set_verbose(true)
		table.remove(arg,1)
	end
	
	if #arg < 2 then
		io.stderr:write([[
Usage: ]]..arg[0]..[[ [-v] endpointname mailboxes...]],'\n')
		os.exit(2)
	end

	local endpoint = arg[1]
	table.remove(arg,1)
	local dbfile = dbfile_name(endpoint, arg)
	local xdelta = dbfile .. '.xdelta'
	local newdb = dbfile .. '.new'
	
	-- we check the protocol version and dbfile fingerprint
	handshake(dbfile)
	
	local commands = receive_delta(io.stdin)
	for i,cmd in ipairs(commands) do
		local rc = execute(cmd)
		if not rc then
			io.write('ABORT\n')
			io.flush()
			os.exit(3)
		end
		if #get_full_email_queue > queue_max_len then
				process_pending_queue()
		end
	end
	process_pending_queue()
	
	io.write('COMMIT\n')
	io.flush()
	receive(io.stdin, xdelta)
	local rc = os.execute(XDELTA..' patch '..xdelta..' '..dbfile..' '..newdb)
	if rc ~= 0 and rc ~= 256 then
		log_error('Unable to apply delta to dbfile.')
		io.write('ABORT\n')
		io.flush()
		os.exit(4)
	end
	rc = os.rename(newdb,dbfile)
	if not rc then
		log_error('Unable to rename '..newdb..' to '..dbfile)
		io.write('ABORT\n')
		io.flush()
		os.exit(5)
	end
	os.remove(xdelta)
	io.write('DONE\n')
	io.flush()
	log_tag('stats::new-mails('..  statistics.added..
		'), del-mails('..statistics.removed..')')
	os.exit(0)
end

set_strict()

xpcall(main,function(msg)
	log_error(tostring(msg))
	os.exit(6)
end)

-- vim:set ts=4:
