#! /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

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')
		os.exit(1)
	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
			log('mddiff returned less messages')
			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
				log('added '..v.name)
			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
			log('skipping '..name..' already there')
			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 `mv '..name..' '..tmp_for(name,false)..'` should work.')
			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
				log('deleted '..name)
				return (trace(true)) -- removed successfully
			else
				log_error('Deletion of '..name..' failed.')
				log_error('It may be caused by bad directory permissions, please check.')
				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 smd-push so that this file is added to the remote mailbox')
			return (trace(false)) -- remove fails since local file is !=
		end
	end
	log('already deleted '..name)
	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
			log('skipping copy of '..name_src..' to '..name_tgt)
			return (trace(true)) -- skip copy, already there
		else
			log_error('Failed to copy '..name_src..' to '..name_tgt)
			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 `mv '..name_tgt..' '..
				tmp_for(name_tgt,false)..'` should work.')
			log_error('- run smd-push so that your changes to '..name_tgt..' are')
			log_error('  propagated to the remote mailbox')
			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
				log('copy '..name_src..' to '..name_tgt)
				mkdir_p(name_tgt)
				local ok = os.execute('cp '..name_src..' '..name_tgt)
				if ok == 0 then 
					return (trace(true)) -- copy successful
				else 
					log('copy '..name_src..' to '..name_tgt..' failed')
					log_error('Failed to copy '..name_src..' to '..name_tgt)
					log_error('It may be caused by bad directory permissions, '..
						'please check.')
					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
			log('skipping copy of non existent '..name_src..' to '..
				name_tgt ..' since target already there')
			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 `mv '..name_tgt..' '..
				tmp_for(name_tgt,false)..'` should work.')
			log_error('- run smd-push so that your changes to '..name_tgt..' are')
			log_error('  propagated to the remote mailbox')
			return (trace(false)) -- skip copy, already there and !=, no source
		end
	else
		log('add '..name_tgt..' as a copy of the not existing '..name_src)
		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)
		log('changed header of '..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.')
		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
			log('header already changed for '..name)
			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 `mv '..name..' '..tmp_for(name,false)..'` should work.')
			return (trace(false)) -- replace header fails, local header !=
		end
	else
		log('added '..name)
		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('cp '..name..' '..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('copy '..name..' to '..newname..' failed')
				log_error('Failed to copy '..name..' to '..newname)
				log_error('It may be caused by bad directory permissions, '..
					'please check.')
				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
			log('skipping copybody, already there')	
			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 `mv '..newname..' '..
				tmp_for(newname,false)..'` should work.')
			return (trace(false)) -- copybody 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
		local name1, hsha1, bsha1, name2, hsha2, bsha2 = 
		   cmd:match('^REPLACE (%S+) (%S+) (%S+) WITH (%S+) (%S+) (%S+)$')
		log_error('REPLACE command not implement, sorry')
		return (trace(false))

	else
		error('Unknown opcode '..opcode)
	end
end

function process_pending_queue()
	local rc = process_get_full_email_queue()
	if not rc then
		log('error getting full email')
		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(1)
	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)
	log('delta received')
	for i,cmd in ipairs(commands) do
		local rc = execute(cmd)
		if not rc then
			log('error executing command '..i..": "..cmd)
			io.write('ABORT\n')
			io.flush()
			os.exit(1)
		end
		if #get_full_email_queue > queue_max_len then
				process_pending_queue()
		end
	end
	process_pending_queue()
	
	log('committing')
	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(1)
	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(1)
	end
	os.remove(xdelta)
	io.write('DONE\n')
	io.flush()
	os.exit(0)
end

set_strict()

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

-- vim:set ts=4:
