ComputerCraft Archive

sysmail

computer networking LDDestroier github

Description

A collection of all my ComputerCraft programs and the APIs they use. This is mostly just to get them the fuck off of pastebin, and also to ensure that API owners don't change things to break my precious programs...!

Installation

Copy one of these commands into your ComputerCraft terminal:

wget:wget https://raw.githubusercontent.com/LDDestroier/CC/master/sysmail.lua sysmail
Archive:wget https://cc.shobie.xyz/cc/get/gh-LDDestroier-CC-sysmail sysmail
Quick Install: wget https://cc.shobie.xyz/cc/get/gh-LDDestroier-CC-sysmail sysmail

Usage

Run: sysmail

Tags

networking

Source

View Original Source

Code Preview

--[[
	sysMail
	 by LDDestroier
	 
	Make sure that the server has the keys of all clients!
	To do:
		+ update read prompt
		+ implement auto-update
--]]

local mainPath = ".sysmail"		-- where everything is based
local yourID = os.getComputerID()	-- duhhhhh
local onlyUseWiredModems = false		-- if true, will refuse to use wireless modems
local defaultTimer = 3			-- will wait this amount of seconds for a server response
local maximumMailLines = 16		-- will cap all emails to this amount of lines

local scr_x, scr_y = term.getSize()

local config = {
	channel = 1024,
	keyPath = fs.combine(mainPath, "keys"),
	mailPath = fs.combine(mainPath, "mail"),
	apiPath = fs.combine(mainPath, "api"),
	nameFile = fs.combine(mainPath, "names"),
	attachmentPath = "attachments"
}

-- 
local getTableLength = function(tbl)
	local output = 0
	for k,v in pairs(tbl) do
		output = output + 1
	end
	return output
end

-- used for picking attachments

local lddfm = {scroll = 0, ypaths = {}}

lddfm.scr_x, lddfm.scr_y = term.getSize()

lddfm.setPalate = function(_p)
	if type(_p) ~= "table" then _p = {} end
	lddfm.p = { --the DEFAULT color palate
		bg =        _p.bg or colors.gray,			-- whole background color
		d_txt =     _p.d_txt or colors.yellow,		-- directory text color
		d_bg =      _p.d_bg or colors.gray,			-- directory bg color
		f_txt =     _p.f_txt or colors.white,		-- file text color
		f_bg =      _p.f_bg or colors.gray,			-- file bg color
		p_txt =     _p.p_txt or colors.black,		-- path text color
		p_bg =      _p.p_bg or colors.lightGray,	-- path bg color
		close_txt = _p.close_txt or colors.gray,	-- close button text color
		close_bg =  _p.close_bg or colors.lightGray,-- close button bg color
		scr =       _p.scr or colors.lightGray,		-- scrollbar color
		scrbar =    _p.scrbar or colors.gray,		-- scroll tab color
	}
end

lddfm.setPalate()

lddfm.foldersOnTop = function(floop,path)
	local output = {}
	for a = 1, #floop do
		if fs.isDir(fs.combine(path,floop[a])) then
			table.insert(output,1,floop[a])
		else
			table.insert(output,floop[a])
		end
	end
	return output
end

lddfm.filterFileFolders = function(list,path,_noFiles,_noFolders,_noCD,_doHidden)
	local output = {}
	for a = 1, #list do
		local entry = fs.combine(path,list[a])
		if fs.isDir(entry) then
			if entry == ".." then
				if not (_noCD or _noFolders) then table.insert(output,list[a]) end
			else
				if not ((not _doHidden) and list[a]:sub(1,1) == ".") then
					if not _noFolders then table.insert(output,list[a]) end
				end
			end
		else
			if not ((not _doHidden) and list[a]:sub(1,1) == ".") then
				if not _noFiles then table.insert(output,list[a]) end
			end
		end
	end
	return output
end

lddfm.isColor = function(col)
	for k,v in pairs(colors) do
		if v == col then
			return true, k
		end
	end
	return false
end

lddfm.clearLine = function(x1,x2,_y,_bg,_char)
	local cbg, bg = term.getBackgroundColor()
	local x,y = term.getCursorPos()
	local sx,sy = term.getSize()
	if type(_char) == "string" then char = _char else char = " " end
	if type(_bg) == "number" then
		if lddfm.isColor(_bg) then bg = _bg
		else bg = cbg end
	else bg = cbg end
	term.setCursorPos(x1 or 1, _y or y)
	term.setBackgroundColor(bg)
	if x2 then --it pains me to add an if statement to something as simple as this
		term.write((char or " "):rep(x2-x1))
	else
		term.write((char or " "):rep(sx-(x1 or 0)))
	end
	term.setBackgroundColor(cbg)
	term.setCursorPos(x,y)
end

lddfm.render = function(_x1,_y1,_x2,_y2,_rlist,_path,_rscroll,_canClose,_scrbarY)
	local px,py = term.getCursorPos()
	local x1, x2, y1, y2 = _x1 or 1, _x2 or lddfm.scr_x, _y1 or 1, _y2 or lddfm.scr_y
	local rlist = _rlist or {"Invalid directory."}
	local path = _path or "And that's terrible."
	ypaths = {}
	local rscroll = _rscroll or 0
	for a = y1, y2 do
		lddfm.clearLine(x1,x2,a,lddfm.p.bg)
	end
	term.setCursorPos(x1,y1)
	term.setTextColor(lddfm.p.p_txt)
	lddfm.clearLine(x1,x2+1,y1,lddfm.p.p_bg)
	term.setBackgroundColor(lddfm.p.p_bg)
	term.write(("/"..path):sub(1,x2-x1))
	for a = 1,(y2-y1) do
		if rlist[a+rscroll] then
			term.setCursorPos(x1,a+(y1))
			if fs.isDir(fs.combine(path,rlist[a+rscroll])) then
				lddfm.clearLine(x1,x2,a+(y1),lddfm.p.d_bg)
				term.setTextColor(lddfm.p.d_txt)
				term.setBackgroundColor(lddfm.p.d_bg)
			else
				lddfm.clearLine(x1,x2,a+(y1),lddfm.p.f_bg)
				term.setTextColor(lddfm.p.f_txt)
				term.setBackgroundColor(lddfm.p.f_bg)
			end
			term.write(rlist[a+rscroll]:sub(1,x2-x1))
			ypaths[a+(y1)] = rlist[a+rscroll]
		else
			lddfm.clearLine(x1,x2,a+(y1),lddfm.p.bg)
		end
	end
	local scrbarY = _scrbarY or math.ceil( (y1+1)+( (_rscroll/(#_rlist-(y2-(y1+1))))*(y2-(y1+1)) ) )
	for a = y1+1, y2 do
		term.setCursorPos(x2,a)
		if a == scrbarY then
			term.setBackgroundColor(lddfm.p.scrbar)
		else
			term.setBackgroundColor(lddfm.p.scr)
		end
		term.write(" ")
	end
	if _canClose then
		term.setCursorPos(x2-4,y1)
		term.setTextColor(lddfm.p.close_txt)
		term.setBackgroundColor(lddfm.p.close_bg)
		term.write("close")
	end
	term.setCursorPos(px,py)
	return scrbarY
end

lddfm.coolOutro = function(x1,y1,x2,y2,_bg,_txt,char)
	local cx, cy = term.getCursorPos()
	local bg, txt = term.getBackgroundColor(), term.getTextColor()
	term.setTextColor(_txt or colors.white)
	term.setBackgroundColor(_bg or colors.black)
	local _uwah = 0
	for y = y1, y2 do
		for x = x1, x2 do
			_uwah = _uwah + 1
			term.setCursorPos(x,y)
			term.write(char or " ")
			if _uwah >= math.ceil((x2-x1)*1.63) then sleep(0) _uwah = 0 end
		end
	end
	term.setTextColor(txt)
	term.setBackgroundColor(bg)
	term.setCursorPos(cx,cy)
end

lddfm.scrollMenu = function(amount,list,y1,y2)
	if #list >= y2-y1 then
		lddfm.scroll = lddfm.scroll + amount
		if lddfm.scroll < 0 then
			lddfm.scroll = 0
		end
		if lddfm.scroll > #list-(y2-y1) then
			lddfm.scroll = #list-(y2-y1)
		end
	end
end

lddfm.makeMenu = function(_x1,_y1,_x2,_y2,_path,_noFiles,_noFolders,_noCD,_noSelectFolders,_doHidden,_p,_canClose)
	if _noFiles and _noFolders then
		return false, "C'mon, man..."
	end
	if _x1 == true then
		return false, "arguments: x1, y1, x2, y2, path, noFiles, noFolders, noCD, noSelectFolders, doHidden, palate, canClose" -- a little help
	end
	lddfm.setPalate(_p)
	local path, list = _path or ""
	lddfm.scroll = 0
	local _pbg, _ptxt = term.getBackgroundColor(), term.getTextColor()
	local x1, x2, y1, y2 = _x1 or 1, _x2 or lddfm.scr_x, _y1 or 1, _y2 or lddfm.scr_y
	local keysDown = {}
	local _barrY
	while true do
		list = lddfm.foldersOnTop(lddfm.filterFileFolders(fs.list(path),path,_noFiles,_noFolders,_noCD,_doHidden),path)
		if (fs.getDir(path) ~= "..") and not (_noCD or _noFolders) then
			table.insert(list,1,"..")
		end
		_res, _barrY = pcall( function() return lddfm.render(x1,y1,x2,y2,list,path,lddfm.scroll,_canClose) end)
		if not _res then
			error(_barrY)
		end
		local evt = {os.pullEvent()}
		if evt[1] == "mouse_scroll" then
			lddfm.scrollMenu(evt[2],list,y1,y2)
		elseif evt[1] == "mouse_click" then
			local butt,mx,my = evt[2],evt[3],evt[4]
			if (butt == 1 and my == y1 and mx <= x2 and mx >= x2-4) and _canClose then
				--lddfm.coolOutro(x1,y1,x2,y2)
				term.setTextColor(_ptxt) term.setBackgroundColor(_pbg)
				return false
			elseif ypaths[my] and (mx >= x1 and mx < x2) then --x2 is reserved for the scrollbar, breh
				if fs.isDir(fs.combine(path,ypaths[my])) then
					if _noCD or butt == 3 then
						if not _noSelectFolders or _noFolders then
							--lddfm.coolOutro(x1,y1,x2,y2)
							term.setTextColor(_ptxt) term.setBackgroundColor(_pbg)
							return fs.combine(path,ypaths[my])
						end
					else
						path = fs.combine(path,ypaths[my])
						lddfm.scroll = 0
					end
				else
					term.setTextColor(_ptxt) term.setBackgroundColor(_pbg)
					return fs.combine(path,ypaths[my])
				end
			end
		elseif evt[1] == "key" then
			keysDown[evt[2]] = true
			if evt[2] == keys.enter and not (_noFolders or _noCD or _noSelectFolders) then --the logic for _noCD being you'd normally need to go back a directory to select the current directory.
				--lddfm.coolOutro(x1,y1,x2,y2)
				term.setTextColor(_ptxt) term.setBackgroundColor(_pbg)
				return path
			end
			if evt[2] == keys.up then
				lddfm.scrollMenu(-1,list,y1,y2)
			elseif evt[2] == keys.down then
				lddfm.scrollMenu(1,list,y1,y2)
			end
			if evt[2] == keys.pageUp then
				lddfm.scrollMenu(y1-y2,list,y1,y2)
			elseif evt[2] == keys.pageDown then
				lddfm.scrollMenu(y2-y1,list,y1,y2)
			end
			if evt[2] == keys.home then
				lddfm.scroll = 0
			elseif evt[2] == keys["end"] then
				if #list > (y2-y1) then
					lddfm.scroll = #list-(y2-y1)
				end
			end
			if evt[2] == keys.h then
				if keysDown[keys.leftCtrl] or keysDown[keys.rightCtrl] then
					_doHidden = not _doHidden
				end
			elseif _canClose and (evt[2] == keys.x or evt[2] == keys.q or evt[2] == keys.leftCtrl) then
				--lddfm.coolOutro(x1,y1,x2,y2)
				term.setTextColor(_ptxt) term.setBackgroundColor(_pbg)
				return false
			end
		elseif evt[1] == "key_up" then
			keysDown[evt[2]] = false
		end
	end
end

local alphasort = function(tbl)
	table.sort(tbl, function(a,b)
		if type(a) == "table" then
			return string.lower(a.time) > string.lower(b.time)
		else
			return string.lower(a) > string.lower(b)
		end
	end)
	return tbl
end

local readFile = function(path)
	if fs.exists(path) then
		local file = fs.open(path, "r")
		local contents = file.readAll()
		file.close()
		return contents
	else
		return nil
	end
end

local writeFile = function(path, contents)
	if fs.isReadOnly(path) then
		return false
	else
		local file = fs.open(path, "w")
		file.write(contents)
		file.close()
		return true
	end
end

local cwrite = function(text, y)
	local cx, cy = term.getCursorPos()
	term.setCursorPos(math.floor(scr_x / 2 - #text / 2 + 1), y or cy)
	term.write(text)
end

local dialogueBox = function(msg, timeout)
	local height = 7
	local baseY = scr_y / 2 - height / 2
	term.setTextColor(colors.white)
	term.setBackgroundColor(colors.gray)
	for y = 1, height do
		term.setCursorPos(1, (scr_y / 2) - (baseY / 2) + (y - 1))
		term.clearLine()
	end
	cwrite(("="):rep(scr_x), baseY)
	cwrite(msg, baseY + height / 2)
	cwrite(("="):rep(scr_x), baseY + height - 1)
	local evt
	local tID = os.startTimer(timeout or 2)
	repeat
		evt = {os.pullEvent()}
	until (evt[1] == "key") or (evt[1] == "timer" and evt[2] == tID)
	term.setBackgroundColor(colors.black)
end

local keyList, names = {}, {}

local makeKey = function(ID, key)
	return writeFile(fs.combine(config.keyPath, ID), key)
end

local getKey = function(ID)
	return readFile(fs.combine(config.keyPath, ID))
end

local readNames = function()
	return textutils.unserialize(readFile(config.nameFile) or "{}") or {}
end

local writeNames = function(_names)
	return writeFile(config.nameFile, textutils.serialize(_names or names))
end

-- keyList[id] = key
-- names[id] = name

-- get personal key file
keyList[yourID] = ""
if fs.exists(fs.combine(config.keyPath, tostring(yourID))) then
	keyList[yourID] = readFile(fs.combine(config.keyPath, tostring(yourID)))
else
	for i = 1, 64 do
		keyList[yourID] = keyList[yourID] .. string.char(math.random(11, 255))
	end
	writeFile(fs.combine(config.keyPath, tostring(yourID)), keyList[yourID])
end

local getAllKeys = function()
	local list = fs.list(config.keyPath)
	local output = {}
	for i = 1, #list do
		if tonumber(list[i]) then
			output[tonumber(list[i])] = getKey(list[i])
		end
	end
	return output
end

names = readNames()
keyList = getAllKeys()

local apiData = {
	["aeslua"] = {
		path = "aeslua.lua",
		url = "https://raw.githubusercontent.com/LDDestroier/CC/master/API/aeslua.lua",	-- thanks SquidDev
		useLoadAPI = true,
	}
}

for name, data in pairs(apiData) do
	data.path = fs.combine(config.apiPath, data.path)
	if not fs.exists(data.path) then
		local net = http.get(data.url)
		if net then
			local file = fs.open(data.path, "w")
			file.write(net.readAll())
			file.close()
			net.close()
		else
			error("Could not download " .. name)
		end
	end
	if data.useLoadAPI then
		local res = os.loadAPI(data.path)
		--error(res)
	else
		_ENV[name] = dofile(data.path)
	end
end

local function interpretArgs(tInput, tArgs)
	local output = {}
	local errors = {}
	local usedEntries = {}
	for aName, aType in pairs(tArgs) do
		output[aName] = false
		for i = 1, #tInput do
			if not usedEntries[i] then
				if tInput[i] == aName and not output[aName] then
					if aType then
						usedEntries[i] = true
						if type(tInput[i+1]) == aType or type(tonumber(tInput[i+1])) == aType then
							usedEntries[i+1] = true
							if aType == "number" then
								output[aName] = tonumber(tInput[i+1])
							else
								output[aName] = tInput[i+1]
							end
						else
							output[aName] = nil
							errors[1] = errors[1] and (errors[1] + 1) or 1
							errors[aName] = "expected " .. aType .. ", got " .. type(tInput[i+1])
						end
					else
						usedEntries[i] = true
						output[aName] = true
					end
				end
			end
		end
	end
	for i = 1, #tInput do
		if not usedEntries[i] then
			output[#output+1] = tInput[i]
		end
	end
	return output, errors
end

local argList = {
	["--server"] = false
}

local argData, argErrors = interpretArgs({...}, argList)
local isServer = argData["--server"]
local serverName = argData[1] or "server"

if ccemux and (not peripheral.find("modem")) then
	ccemux.attach("top", "wireless_modem")
end

local modem
local getModem = function(doNotPickWireless)
	local output, periphList
	if not peripheral.find("modem") and ccemux then
		ccemux.attach("top", "wireless_modem")
	end
	for try = 1, 40 do
		periphList = peripheral.getNames()
		for i = 1, #periphList do
			if peripheral.getType(periphList[i]) == "modem" then
				output = peripheral.wrap(periphList[i])
				if not (doNotPickWireless and output.isWireless()) then
					output.open(config.channel)
					return output
				end
			end
		end
		if try == 1 then
			write("Looking for modem...")
		else
			write(".")
		end
		sleep(0.15)
	end
	error("No modems were found after 40 tries. That's as many as four tens. And that's terrible.")
end

-- allowed IDs
local userIDs = {}

-- all data recorded
local DATA = {}

local transmit = function(msg, msgID)
	modem = getModem(onlyUseWiredModems)
	modem.transmit(config.channel, config.channel, {
		msg = msg,
		encrypted = false,
		msgID = msgID
	})
end

local encTransmit = function(msg, msgID, recipient, encID)
	modem = getModem(onlyUseWiredModems)
	local key = keyList[encID or recipient]
	if not key then
		error("You do not possess the key of the recipient.")
	else
		modem.transmit(config.channel, config.channel, {
			msg = aeslua.encrypt(key, textutils.serialize(msg)),
			encrypted = true,
			msgID = msgID,
			recipient = recipient
		})
	end
end

local receive = function(msgID, specifyCommand, encID, timer)
	local evt, msg, tID
	if timer then
		tID = os.startTimer(timer)
	end
	modem = getModem()
	while true do
		evt = {os.pullEvent()}
		if evt[1] == "modem_message" then
			if type(evt[5]) == "table" then
				if evt[5].encrypted then
					if true then
						if encID then
							msg = aeslua.decrypt(keyList[encID], evt[5].msg)
						else
							for id, key in pairs(keyList) do
								if msg then break end
								if id ~= encID then
									msg = aeslua.decrypt(key, evt[5].msg)
								end
							end
						end
						if msg then
							msg = textutils.unserialize(msg)
						end
					end
				else
					msg = evt[5].msg
				end
				if (not msgID) or (evt[5].msgID == msgID) then
					if (not specifyCommand) or (msg.command == specifyCommand) then
						return msg, evt[5].encrypted, evt[5].msgID
					end
				end
			end
		elseif evt[1] == "timer" and evt[2] == tID then
			return nil, nil, nil
		end
	end
end

local getNameID = function(name)
	for k,v in pairs(names) do
		if v == name then
			return k
		end
	end
	return nil
end

local client = {}	-- all client-specific commands
local server = {}	-- all server-specific commands

----                       ----
----    CLIENT COMMANDS    ----
----                       ----

-- if you want a super duper secure network, manually enter the server ID into this
client.findServer = function(srv)
	local msgID = math.random(1, 2^30)
	srv = type(srv) == "number" and srv or getNameID(srv)
	assert(tonumber(srv) or (not srv), "invalid server")
	transmit({
		id = yourID,
		command = "find_server"
	}, msgID)
	local reply, isEncrypted = receive(msgID, "find_server_respond", srv, defaultTimer)
	if type(reply) == "table" then
		if reply.server then
			return reply.server
		end
	end
	return nil
end

-- Registers your ID to a name.
client.register = function(srv, username)
	local msgID = math.random(1, 2^30)
	assert(srv, "register( server, username )")
	srv = type(srv) == "number" and srv or getNameID(srv)
	assert(srv, "invalid server")
	encTransmit({
		id = yourID,
		command = "register",
		name = username
	}, msgID, srv, yourID)
	local reply, isEncrypted = receive(msgID, "register_respond", yourID, defaultTimer)
	if reply then
		return reply.result
	else
		return false
	end
end

-- Gets a list of all registered ID names
client.getNames = function(srv)
	local msgID = math.random(1, 2^30)
	assert(srv, "getNames( server )")
	srv = type(srv) == "number" and srv or getNameID(srv)
	assert(srv, "invalid server")
	encTransmit({
		id = yourID,
		command = "get_names"
	}, msgID, srv, yourID)
	local reply, isEncrypted = receive(msgID, "get_names_respond", yourID, defaultTimer)
	if type(reply) == "table" then
		return reply.names
	else
		return nil
	end
end

-- Sends an email to a recipient ID.
client.sendMail = function(srv, recipient, subject, message, attachments)
	assert(srv, "sendMail( server, recipient, subject, message, attachments )")
	srv = type(srv) == "number" and srv or getNameID(srv)
	assert(srv, "invalid server")
	assert(type(subject) == "string", "invalid subject")
	assert(type(message) == "string", "invalid message")
	local msgID = math.random(1, 2^30)
	if type(recipient) == "string" then
		recipient = getNameID(recipient)
	end
	assert(recipient, "invalid recipient")
	encTransmit({
		command = "send_mail",
		id = yourID,
		recipient = recipient,
		subject = subject,
		message = message,
		attachments = attachments
	}, msgID, srv, yourID)
	local reply, isEncrypted = receive(msgID, "send_mail_respond", yourID, defaultTimer)
	if (isEncrypted and type(reply) == "table") then
		return reply.result
	else
		return false
	end
end

client.getMail = function(srv)
	local msgID = math.random(1, 2^30)
	assert(srv, "getMail( server )")
	srv = type(srv) == "number" and srv or getNameID(srv)
	assert(srv, "invalid server")
	encTransmit({
		command = "get_mail",
		id = yourID,
	}, msgID, srv, yourID)
	local reply, isEncrypted = receive(msgID, "get_mail_respond", yourID, defaultTimer)
	if (isEncrypted and type(reply) == "table") then
		if reply.mail then
			return alphasort(reply.mail)
		end
	end
end

client.deleteMail = function(srv, mail)
	local msgID = math.random(1, 2^30)
	assert(srv, "deleteMail( server, mailEntryNumber )")
	srv = type(srv) == "number" and srv or getNameID(srv)
	assert(srv, "invalid server")
	assert(type(mail) == "number", "invalid mail entry")
	encTransmit({
		command = "delete_mail",
		id = yourID,
		mail = mail,
	}, msgID, srv, yourID)
	local reply, isEncrypted = receive(msgID, "delete_mail_respond", yourID, defaultTimer)
	if (isEncrypted and type(reply) == "table") then
		return reply.result
	else
		return false
	end
end

----                       ----
----    SERVER COMMANDS    ----
----                       ----

-- check whether or not a name is valid to be used
server.checkValidName = function(name)
	if type(name) == "string" then
		if #name >= 3 or #name <= 24 then
			return true
		end
	end
	return false
end

-- check whether or not an ID is registered
server.checkRegister = function(id)
	-- I make the code this stupid looking in case I add other stipulations
	if names[id] or getNameID(names[id]) then
		return true
	else
		return false
	end
end

server.registerID = function(id, name)
	local path = fs.combine(config.mailPath, tostring(id))
	if ((not server.checkRegister(name)) or getNameID(name) == id) then
		fs.makeDir(path)
		names[id] = name
		writeNames()
		return true, names[id]
	else
		return false, "name already exists"
	end
end

-- records a full email to file
server.recordMail = function(sender, _recipient, subject, message, attachments)
	local time = os.epoch("utc")
	local recipient

	if _recipient == "*" then
		recipient = fs.list(config.mailPath)
	elseif type(_recipient) ~= "table" then
		recipient = {tostring(_recipient)}
	end

	local msg = textutils.serialize({
		sender = sender,
		time = time,
		read = false,
		subject = subject,
		message = message,
		attachments = attachments
	})

	local requiredSpace = #msg + 2
	if fs.getFreeSpace(config.mailPath) < requiredSpace then
		return false, "Cannot write mail, not enough space!"
	end

	local path, file
	for i = 1, #recipient do
		server.registerID(recipient[i])
		path = fs.combine(config.mailPath, recipient[i])
		file = fs.open(fs.combine(path, tostring(time)), "w")
		file.write(msg)
		file.close()
	end
	return true
end

-- returns every email in an ID's inbox
server.getMail = function(id)
	local output, list = {}, {}
	local mails = fs.list(fs.combine(config.mailPath, tostring(id)))
	local file
	for k,v in pairs(mails) do
		list[v] = k
	end
	for k,v in pairs(list) do
		file = fs.open(fs.combine(config.mailPath, "/" .. id .. "/" .. k), "r")
		if file then
			output[#output + 1] = textutils.unserialize(file.readAll())
			file.close()
		end
	end
	return output
end

server.deleteMail = function(id, del)
	local mails = alphasort( fs.list(fs.combine(config.mailPath, tostring(id))) )
	if mails[del] then
		fs.delete(fs.combine(config.mailPath, tostring(id) .. "/" .. mails[del]))
		return true
	else
		return false
	end
end

server.setName = function(newName)
	if server.checkValidName(newName) then
		names[yourID] = newName
	end
end

-- receives messages and sends the appropriate response
server.makeServer = function(verbose)
	local msg, isEncrypted, msgID

	local say = function(text, id)
		if verbose then
			return print(text .. (id and (" (" .. id .. ")") or ""))
		end
	end

	if verbose then
		term.clear()
		term.setCursorPos(1,1)
		print("Make sure client keys are copied to key folder!")
	end

	say("SysMail server started.")

	while true do

		msg, isEncrypted, msgID = receive(nil, nil, nil, 5)

		if not msg then
			keyList = getAllKeys()
		else
			if not isEncrypted then
				if msg.command == "find_server" then
					transmit({
						command = msg.command .. "_respond",
						server = yourID,
					}, msgID, msg.id, yourID)
					say("find_server")
				end
			elseif type(msg.id) == "number" and type(msg.command) == "string" then
				if msg.command == "register" then
					if (
						type(msg.id) == "number" and
						type(msg.name) == "string"
					) then
						local reply
						local result, name = server.registerID(msg.id, msg.name)
						if result then
							reply = {
								command = msg.command .. "_respond",
								result = result,
								name = name,
							}
							say("user " .. tostring(msg.id) .. " registered as " .. name)
						else
							reply = {
								command = msg.command .. "_respond",
								result = result,
							}
							say("user " .. tostring(msg.id) .. " failed to register as " .. tostring(msg.name) .. ": " .. name)
						end
						encTransmit(reply, msgID, msg.id, msg.id)
					end
				elseif not server.checkRegister(msg.id) then
					encTransmit({
						command = msg.command .. "_respond",
						result = false,
						errorMsg = "not registered"
					}, msgID, msg.id, msg.id)
					say("unregistered user attempt to use")
				else

					-- all the real nice stuff

					if msg.command == "find_server" then
						encTransmit({
							command = msg.command .. "_respond",
							server = yourID,
							result = true
						}, msgID, msg.id, msg.id)
						say("find_server (aes)")
					elseif msg.command == "get_names" then
						encTransmit({
							command = msg.command .. "_respond",
							names = names,
							result = true,
						}, msgID, msg.id, msg.id)
						say("get_names", msg.id)
					elseif msg.command == "send_mail" then
						if (
							msg.recipient and
							type(msg.subject) == "string" and
							type(msg.message) == "string"
						) then
							local reply = {
								command = msg.command .. "_respond",
								result = server.recordMail(msg.id, msg.recipient, msg.subject, msg.message, msg.attachments)
							}
							encTransmit(reply, msgID, msg.id, msg.id)
							say("send_mail", msg.id)
						end
					elseif msg.command == "get_mail" then
						local mail = server.getMail(msg.id)
						local reply = {
							command = msg.command .. "_respond",
							result = true,
							mail = mail,
						}
						encTransmit(reply, msgID, msg.id, msg.id)
						say("get_mail", msg.id)
					elseif msg.command == "delete_mail" then
						local result = false
						if type(msg.mail) == "number" then
							result = server.deleteMail(msg.id, msg.mail, yourID)
						end
						encTransmit({
							command = msg.command .. "_respond",
							result = result,
						}, msgID, msg.id, msg.id)
						say("delete_mail", msg.id)
					end

				end
			end
		end

	end
end

local clientInterface = function(srv)
	local scr_x, scr_y = term.getSize()
	local inbox = {}
	local refresh = function()
		dialogueBox("Refreshing...", 0)
		inbox = client.getMail(srv)
	end
	local explode = function(div, str, replstr, includeDiv)
		if div == '' then
			return false
		end
		local pos, arr = 0, {}
		for st, sp in function() return string.find(str, div, pos, false) end do
			table.insert(arr, string.sub(replstr or str, pos, st - 1 + (includeDiv and #div or 0)))
			pos = sp + 1
		end
		table.insert(arr, string.sub(replstr or str, pos))
		return arr
	end
	srv = srv or tonumber( client.findServer(argData[1]) )
	if not srv then
		error("No server was found!")
	end

	if not names[yourID] then
		term.setBackgroundColor(colors.black)
		term.setTextColor(colors.white)
		term.clear()
		local attempt
		cwrite("Enter your name:", 3)
		while true do
			term.setCursorPos(2, 5)
			term.write(":")
			attempt = read()
			if server.checkValidName(attempt) then
				names[yourID] = attempt
				writeNames()
				break
			else
				term.clear()
				cwrite("Bad name! Enter your name:", 3)
			end
		end
	end
	client.register(srv, names[yourID])

	for k,v in pairs(client.getNames(srv) or {}) do
		names[k] = v
	end
	
	term.clear()
	refresh()

	local keyWrite = function(text, pos)
		local txcol = term.getTextColor()
		term.write(text:sub(1, pos - 1))
		term.setTextColor(colors.yellow)
		term.write(text:sub(pos, pos))
		term.setTextColor(txcol)
		term.write(text:sub(pos + 1))
	end

	local writeHeader = function(left, right, y)
		if y then
			term.setCursorPos(1, y)
		end
		term.setTextColor(colors.lightGray)
		term.write(left)
		term.setTextColor(colors.white)
		term.write(" " .. right)
	end

	local area_inbox = function()
		local scroll = 0
		local render = function(scroll)
			local y = 1
			term.setBackgroundColor(colors.black)
			term.clear()
			for i = 1 + scroll, scroll + scr_y - 1 do
				if inbox[i] then
					term.setCursorPos(1, y)
					term.setTextColor(colors.white)
					term.write(names[inbox[i].sender]:sub(1, 10))

					term.setCursorPos(11, y)
					term.setTextColor(colors.white)
					term.write(inbox[i].subject:sub(1, 18))

					term.setCursorPos(30, y)
					term.setTextColor(colors.gray)
					term.write(inbox[i].message:sub(1, scr_x - 30))
				end
				y = y + 1
			end
			term.setCursorPos(1, scr_y)
			term.setBackgroundColor(colors.gray)
			term.clearLine()
			term.setTextColor(colors.white)
			--term.write(names[yourID] .. ": ")
			keyWrite("Quit ", 1)
			keyWrite("New ", 1)
			keyWrite("Refresh ", 1)
			term.setCursorPos(scr_x - #names[yourID], scr_y)
			term.setTextColor(colors.lightGray)
			term.write(names[yourID])
		end

		-- logic(k)
		local barCommands = {
			[keys.q] = {1, scr_y, 4},
			[keys.n] = {6, scr_y, 3},
			[keys.r] = {10, scr_y, 7},
		}
		local evt, key, mx, my
		local adjY	-- mouse Y adjusted for scroll
		while true do
			render(scroll)
			inbox = alphasort(inbox)
			evt, key, mx, my = os.pullEvent()
			if evt == "mouse_click" then
				adjY = my + scroll
				if inbox[adjY] then
					return "view_mail", {adjY}
				else
					for key, data in pairs(barCommands) do
						if my == data[2] and mx >= data[1] and mx <= data[1] + data[3] - 1 then
							os.queueEvent("key", key)
							break
						end
					end
				end
			elseif evt == "mouse_scroll" then
				scroll = math.min(math.max(0, scroll + key), math.max(0, #inbox - (scr_y - 1)))
			elseif evt == "key" then
				if key == keys.n then
					return "new_mail"
				elseif key == keys.r then
					return "refresh"
				elseif key == keys.q then
					return "exit"
				end
			end
		end
	end

	local niftyRead = function(prebuffer, startX, startY, startCursorMX, startCursorMY, allowEnter, maxLines, maxLength, history)
		local cx, cy = term.getCursorPos()
		local histPos = 0
		startX, startY = startX or cx, startY or cy
		local scroll = 0
		local buffer = {{}}
		local unassemble = function(pBuffer)
			local output = {{""}}
			local y = 1
			local x = 1
			for i = 1, #pBuffer do
				if pBuffer:sub(i,i) == "\n" then
					x = 1
					y = y + 1
					output[y] = {""}
				else
					output[y][x] = pBuffer:sub(i,i)
					x = x + 1
				end
			end
			return output
		end
		if prebuffer then
			buffer = unassemble(prebuffer)
		end
		local curY = startCursorMY and math.max(1, math.min(startCursorMY - (startY - 1), #buffer)) or 1
		local curX = startCursorMX and math.max(1, math.min(startCursorMX - (startX - 1), #buffer[curY])) or 1
		local biggestHeight = math.max(1, #buffer)
		local getLength = function()
			local output = 0
			for ln = 1, #buffer do
				output = output + #buffer[ln]
				if ln ~= #buffer then	-- account for newline chars
					output = output + 1
				end
			end
			return output
		end
		local render = function()
			for y = startY, startY + (biggestHeight - 1) do
				term.setCursorPos(startX, y)
				term.write((" "):rep(maxLength))
			end
			term.setCursorPos(startX, startY)
			local x, y, words = startX, startY
			for ln = scroll + 1, #buffer + scroll do
				if buffer[ln] then
					words = explode(" ", table.concat(buffer[ln]), nil, true)
					for i = 1, #words do
						if x + #words[i] > scr_x and y < maxLines then
							x = startX
							y = y + 1
						end
						term.setCursorPos(x, y)
						term.write(words[i])
						x = x + #words[i]
					end
					term.write(" ")
					if ln ~= #buffer then
						y = y + 1
						x = startX
					end
				end
			end
			term.setCursorPos(curX + startX - 1, curY + startY - 1)
			biggestHeight = math.max(#buffer, biggestHeight)
		end

		local assemble = function(buffer)
			local output = ""
			for ln = 1, #buffer do
				output = output .. table.concat(buffer[ln])
				if ln ~= #buffer then
					output = output .. "\n"
				end
			end
			return output
		end

		if history then
			history[0] = assemble(buffer)
		end

		local evt, key, mx, my
		local keysDown = {}
		term.setCursorBlink(true)
		while true do
			render()
			evt, key, mx, my = os.pullEvent()
			if evt == "char" then
				if getLength() < maxLength then
					table.insert(buffer[curY], curX, key)
					curX = curX + 1
					if histPos == 0 and history then
						history[histPos] = assemble(buffer)
					end
				end
			elseif evt == "key_up" then
				keysDown[key] = nil
			elseif evt == "mouse_click" then
				if key == 1 then
					if my - (startY - 1) > maxLines or my < startY then
						term.setCursorBlink(false)
						return assemble(buffer), "mouse_click", mx, my
					else
						curY = math.max(1, math.min(my - (startY - 1), #buffer))
						curX = math.max(1, math.min(mx - (startX - 0), #buffer[curY]))
					end
				end
			elseif evt == "key" then
				keysDown[key] = true
				if key == keys.left then
					if curX == 1 then
						if curY > 1 then
							curY = curY - 1
							curX = #buffer[curY] + 1
						end
					elseif curX > 1 then
						curX = curX - 1
					end
				elseif key == keys.right then
					if curX == #buffer[curY] + 1 then
						if curY < #buffer then
							curY = curY + 1
							curX = 1
						end
					elseif curX < #buffer[curY] then
						curX = curX + 1
					end
				elseif key == keys.up then
					if history then
						if histPos < #history then
							histPos = histPos + 1
							buffer = unassemble(history[histPos])
							curY = #buffer
							curX = #buffer[curY] + 1
						end
					else
						if curY > 1 then
							curY = curY - 1
							curX = math.min(curX, #buffer[curY] + 1)
						else
							curX = 1
						end
					end
				elseif key == keys.down then
					if history then
						if histPos > 0 then
							histPos = histPos - 1
							buffer = unassemble(history[histPos])
							curY = #buffer
							curX = #buffer[curY] + 1
						end
					else
						if curY < #buffer then
							curY = curY + 1
							curX = math.min(curX, #buffer[curY] + 1)
						else
							curX = #buffer[curY] + 1
						end
					end
				elseif key == keys.enter then
					if allowEnter and not (keysDown[keys.leftAlt] or keysDown[keys.rightAlt]) and #buffer < maxLines then
						curY = curY + 1
						table.insert(buffer, curY, {})
						for i = curX, #buffer[curY - 1] do
							buffer[curY][#buffer[curY] + 1] = buffer[curY - 1][i]
							buffer[curY - 1][i] = nil
						end
						curX = 1
					elseif not allowEnter then
						term.setCursorBlink(false)
						return assemble(buffer), "key", keys.enter
					end
				elseif key == keys.tab or (key == keys.q and (keysDown[keys.leftAlt] or keysDown[keys.rightAlt])) then
					term.setCursorBlink(false)
					return assemble(buffer), "key", key
				elseif key == keys.backspace then
					if curX > 1 then
						table.remove(buffer[curY], curX - 1)
						curX = curX - 1
					elseif curY > 1 then
						curX = #buffer[curY - 1] + 1
						for i = 1, #buffer[curY] do
							buffer[curY - 1][#buffer[curY - 1] + 1] = buffer[curY][i]
						end
						table.remove(buffer, curY)
						curY = curY - 1
					end
				elseif key == keys.delete then
					if buffer[curY][curX] then
						table.remove(buffer[curY], curX)
					end
				end
			end
		end
	end

	local area_new_mail = function(recipient, subject, message)
		recipient = recipient or ""
		subject = subject or ""
		message = message or ""
		local attachments = {}
		sleep(0.05)
		local mode = "recipient"
		render = function()
			term.setTextColor(colors.white)
			term.setBackgroundColor(colors.black)
			term.clear()
			writeHeader("To:", recipient, 1)
			writeHeader("Subject:", subject, 2)
			writeHeader("Attachments:", "", 3)
			for name, contents in pairs(attachments) do
				term.write(name .. " ")
			end
			term.setCursorPos(1, 4)
			term.setBackgroundColor(colors.gray)
			term.setTextColor(colors.lightGray)
			term.clearLine()
			cwrite("(Alt+Enter = SEND, Alt+Q = QUIT)")
			term.setTextColor(colors.white)
			term.setBackgroundColor(colors.black)
			if mode ~= "message" then
				term.setCursorPos(1, 5)
				write(message)
			end
		end
		local mx, my, evt = 1, 1
		local _mx, _my, userList
		while true do
			render()
			if mode == "message" then
				message, evt, _mx, _my = niftyRead(message, 1, 5, mx, my, true, maximumMailLines, 512)
			elseif mode == "subject" then
				subject, evt, _mx, _my = niftyRead(subject, 10, 2, mx, 1, false, 1, 64)
			elseif mode == "recipient" then
				names = client.getNames(srv) or names
				userList = {}
				for k,v in pairs(names) do
					userList[#userList + 1] = v
				end
				recipient, evt, _mx, _my = niftyRead(recipient, 5, 1, mx, 1, false, 1, 24, userList)
			end
			if evt == "mouse_click" then
				mx, my = _mx, _my
				if my == 1 then
					mode = "recipient"
				elseif my == 2 then
					mode = "subject"
				elseif my == 3 then
					local newAttachment = lddfm.makeMenu(1, 4, scr_x, scr_y, "", false, false, false, true, false, nil, true)
					if newAttachment then
						local name = fs.getName(newAttachment)
						if attachments[name] then
							attachments[name] = nil
						else
							attachments[name] = readFile(newAttachment)
						end
					end
				elseif my >= 5 then
					mode = "message"
				end
			elseif evt == "key" then
				if _mx == keys.enter or _mx == keys.tab then
					if mode == "recipient" then
						mode = "subject"
					elseif mode == "subject" then
						mode = "message"
					elseif mode == "message" and _mx == keys.enter then
						local recip
						names = client.getNames(srv) or names
						if tonumber(recipient) then
							recip = tonumber(recipient)
							if not names[recip] then
								recip = nil
							end
						else
							recip = getNameID(recipient)
						end
						if recip then
							client.sendMail(srv, recip, subject, message, attachments)
							dialogueBox("Message sent!", 2)
							refresh()
							return
						else
							dialogueBox("There's no such recipient.", 2)
						end
					end
				elseif _mx == keys.q then
					return
				end
			end
		end
		niftyRead(nil, 1, 1, nil, true)
	end

	local area_view_mail = function(mailEntry)
		local scroll = 0
		local mail = inbox[mailEntry]
		local render = function(scroll)
			term.setBackgroundColor(colors.black)
			term.setTextColor(colors.lightGray)
			term.clear()
			local y
			writeHeader("From:", names[mail.sender], 1)
			writeHeader("Subject:", mail.subject, 2)
			if getTableLength(mail.attachments) > 0 then
				writeHeader("Attachments:","",3)
				for name, contents in pairs(mail.attachments) do
					term.write(name .. " ")
				end
				y = 5
			else
				y = 4
			end
			term.setTextColor(colors.gray)
			term.setCursorPos(1, y - 1)
			term.write(("="):rep(scr_x))
			term.setTextColor(colors.white)
			local words = {}
			local lines = explode("\n", mail.message, nil, true)
			for i = 1, #lines do
				local inWords = explode(" ", lines[i], nil, true)
				for ii = 1, #inWords do
					words[#words+1] = inWords[ii]
				end
				if i ~= #lines then
					words[#words+1] = "\n"
				end
			end
			local buffer = {""}
			for i = 1, #words do
				if words[i] == "\n" then
					buffer[#buffer+1] = ""
				elseif #buffer[#buffer] + #words[i] > scr_x then
					buffer[#buffer+1] = words[i]
				else
					buffer[#buffer] = buffer[#buffer] .. words[i]
				end
			end
			for i = scroll + 1, scroll + scr_y - y do
				if buffer[i] then
					term.setCursorPos(1, y)
					term.write(buffer[i])
				end
				y = y + 1
			end
			term.setCursorPos(1, scr_y)
			term.setBackgroundColor(colors.gray)
			term.setTextColor(colors.white)
			term.clearLine()
			keyWrite("Quit ", 1)
			keyWrite("Reply ", 1)
			if getTableLength(mail.attachments) > 0 then
				keyWrite("DL.Attachments ", 4)
			end
			keyWrite("Delete ", 1)
			return #buffer
		end

		local downloadAttachments = function()
			local path = fs.combine(config.attachmentPath, names[mail.sender])
			for name, contents in pairs(mail.attachments) do
				writeFile(fs.combine(path, name), contents)
			end
			return path
		end

		local barCommands = {
			[keys.q] = {1, scr_y, 4},
			[keys.r] = {6, scr_y, 5},
		}
		if getTableLength(mail.attachments) > 0 then
			barCommands[keys.a] = {12, scr_y, 14}
			barCommands[keys.d] = {27, scr_y, 6}
		else
			barCommands[keys.d] = {12, scr_y, 6}
		end
		local evt, key, mx, my, msgHeight
		while true do
			msgHeight = render(scroll)
			evt, key, mx, my = os.pullEvent()
			if evt == "key" then
				if key == keys.r then
					area_new_mail(names[mail.sender], "Re: " .. mail.subject, "\n\n~~~\nAt UTC epoch " .. mail.time .. ", " .. names[mail.sender] .. " wrote:\n\n" .. mail.message)
				elseif key == keys.d then
					client.deleteMail(srv, mailEntry)
					refresh()
					return
				elseif key == keys.a and getTableLength(mail.attachments) > 0 then
					local path = downloadAttachments()
					dialogueBox("DL'd to '" .. path .. "/'")
				elseif key == keys.q then
					return "exit"
				end
			elseif evt == "mouse_click" then
				if my == 3 and getTableLength(mail.attachments) > 0 then
					local path = downloadAttachments()
					dialogueBox("DL'd to '" .. path .. "/'")
				else
					for key, data in pairs(barCommands) do
						if my == data[2] and mx >= data[1] and mx <= data[1] + data[3] - 1 then
							os.queueEvent("key", key)
							break
						end
					end
				end
			elseif evt == "mouse_scroll" then
				scroll = math.min(math.max(0, scroll + key), math.max(0, msgHeight - (scr_y - 5)))
			end
		end
	end

	local res, output
	while true do
		res, output = area_inbox()
		if res == "exit" then
			term.setCursorPos(1, scr_y)
			term.setBackgroundColor(colors.black)
			term.clearLine()
			sleep(0.05)
			return
		elseif res == "refresh" then
			refresh()
		elseif res == "view_mail" then
			area_view_mail(table.unpack(output or {}))
		elseif res == "new_mail" then
			area_new_mail(table.unpack(output or {}))
		end
	end
end

if isServer then
	names[yourID] = names[yourID] or serverName
	writeNames()
	server.makeServer(true)
elseif shell then
	clientInterface()
end

return {client = client, server = server}